     1→'use client'
     2→
     3→import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
     4→import { toast } from 'sonner'
     5→import {
     6→  BookCopy,
     7→  ChevronDown,
     8→  ChevronRight,
     9→  FolderPlus,
    10→  MoreHorizontal,
    11→  Pencil,
    12→  Plus,
    13→  Search,
    14→  Trash2,
    15→  X,
    16→} from 'lucide-react'
    17→
    18→import { PageHeader } from '../page-header'
    19→import { Button } from '@/components/ui/button'
    20→import { Input } from '@/components/ui/input'
    21→import { Label } from '@/components/ui/label'
    22→import { Badge } from '@/components/ui/badge'
    23→import { Switch } from '@/components/ui/switch'
    24→import { Skeleton } from '@/components/ui/skeleton'
    25→import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
    26→import {
    27→  Select,
    28→  SelectContent,
    29→  SelectItem,
    30→  SelectTrigger,
    31→  SelectValue,
    32→} from '@/components/ui/select'
    33→import {
    34→  Dialog,
    35→  DialogContent,
    36→  DialogDescription,
    37→  DialogFooter,
    38→  DialogHeader,
    39→  DialogTitle,
    40→} from '@/components/ui/dialog'
    41→import {
    42→  DropdownMenu,
    43→  DropdownMenuContent,
    44→  DropdownMenuItem,
    45→  DropdownMenuLabel,
    46→  DropdownMenuSeparator,
    47→  DropdownMenuTrigger,
    48→} from '@/components/ui/dropdown-menu'
    49→import {
    50→  AlertDialog,
    51→  AlertDialogAction,
    52→  AlertDialogCancel,
    53→  AlertDialogContent,
    54→  AlertDialogDescription,
    55→  AlertDialogFooter,
    56→  AlertDialogHeader,
    57→  AlertDialogTitle,
    58→} from '@/components/ui/alert-dialog'
    59→import { formatCurrency } from '@/lib/format'
    60→import { cn } from '@/lib/utils'
    61→
    62→// ---------- Types ----------
    63→
    64→type CoaNode = {
    65→  id: string
    66→  kodeAkun: string
    67→  namaAkun: string
    68→  parentId: string | null
    69→  level: number
    70→  jenisAkun: string
    71→  tipeAkun: string // 'Posting' | 'Total'
    72→  kodeAkunPajak: string | null
    73→  namaAkunPajak: string | null
    74→  saldoAwal: number
    75→  posisi: string // 'Debit' | 'Kredit'
    76→  statusAktif: boolean
    77→}
    78→
    79→type CoaTree = CoaNode & { children: CoaTree[] }
    80→
    81→// ---------- Constants ----------
    82→
    83→const JENIS_AKUN_OPTIONS = [
    84→  'Aset',
    85→  'Kewajiban',
    86→  'Modal',
    87→  'Pendapatan',
    88→  'HPP',
    89→  'Biaya Operasional',
    90→  'Pendapatan Non Operasional',
    91→  'Biaya Non Operasional',
    92→] as const
    93→
    94→const JENIS_AKUN_BADGE: Record<string, string> = {
    95→  Aset: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/60 dark:text-emerald-300',
    96→  Kewajiban: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/60 dark:text-amber-300',
    97→  Modal: 'border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-900/60 dark:bg-purple-950/60 dark:text-purple-300',
    98→  Pendapatan: 'border-teal-200 bg-teal-50 text-teal-700 dark:border-teal-900/60 dark:bg-teal-950/60 dark:text-teal-300',
    99→  HPP: 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/60 dark:text-rose-300',
   100→  'Biaya Operasional': 'border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-900/60 dark:bg-orange-950/60 dark:text-orange-300',
   101→  'Pendapatan Non Operasional': 'border-fuchsia-200 bg-fuchsia-50 text-fuchsia-700 dark:border-fuchsia-900/60 dark:bg-fuchsia-950/60 dark:text-fuchsia-300',
   102→  'Biaya Non Operasional': 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/60 dark:bg-red-950/60 dark:text-red-300',
   103→}
   104→
   105→const ROOT_PARENT_VALUE = '__root__'
   106→
   107→// ---------- Helpers ----------
   108→
   109→function buildTree(flat: CoaNode[]): CoaTree[] {
   110→  const map = new Map<string, CoaTree>()
   111→  flat.forEach((n) => map.set(n.id, { ...n, children: [] }))
   112→  const roots: CoaTree[] = []
   113→  flat.forEach((n) => {
   114→    const node = map.get(n.id)!
   115→    if (n.parentId && map.has(n.parentId)) {
   116→      map.get(n.parentId)!.children.push(node)
   117→    } else {
   118→      roots.push(node)
   119→    }
   120→  })
   121→  const sortRecursive = (nodes: CoaTree[]) => {
   122→    nodes.sort((a, b) => a.kodeAkun.localeCompare(b.kodeAkun))
   123→    nodes.forEach((n) => sortRecursive(n.children))
   124→  }
   125→  sortRecursive(roots)
   126→  return roots
   127→}
   128→
   129→function getDescendantIds(
   130→  id: string,
   131→  childrenMap: Map<string, string[]>
   132→): Set<string> {
   133→  const result = new Set<string>([id])
   134→  const queue = [id]
   135→  while (queue.length) {
   136→    const cur = queue.pop()!
   137→    const kids = childrenMap.get(cur) || []
   138→    for (const k of kids) {
   139→      if (!result.has(k)) {
   140→        result.add(k)
   141→        queue.push(k)
   142→      }
   143→    }
   144→  }
   145→  return result
   146→}
   147→
   148→function countVisibleNodes(roots: CoaTree[]): number {
   149→  let n = 0
   150→  const walk = (nodes: CoaTree[]) => {
   151→    for (const node of nodes) {
   152→      n += 1
   153→      walk(node.children)
   154→    }
   155→  }
   156→  walk(roots)
   157→  return n
   158→}
   159→
   160→// ---------- Form state ----------
   161→
   162→type CoaForm = {
   163→  id: string | null
   164→  kodeAkun: string
   165→  namaAkun: string
   166→  parentId: string // ROOT_PARENT_VALUE for root
   167→  jenisAkun: string
   168→  tipeAkun: string
   169→  posisi: string
   170→  kodeAkunPajak: string
   171→  namaAkunPajak: string
   172→  saldoAwal: number
   173→  statusAktif: boolean
   174→}
   175→
   176→const emptyForm: CoaForm = {
   177→  id: null,
   178→  kodeAkun: '',
   179→  namaAkun: '',
   180→  parentId: ROOT_PARENT_VALUE,
   181→  jenisAkun: 'Aset',
   182→  tipeAkun: 'Posting',
   183→  posisi: 'Debit',
   184→  kodeAkunPajak: '',
   185→  namaAkunPajak: '',
   186→  saldoAwal: 0,
   187→  statusAktif: true,
   188→}
   189→
   190→// ---------- Main view ----------
   191→
   192→export default function CoaView() {
   193→  const [accounts, setAccounts] = useState<CoaNode[]>([])
   194→  const [loading, setLoading] = useState(true)
   195→
   196→  // filters
   197→  const [search, setSearch] = useState('')
   198→  const [jenisAkunFilter, setJenisAkunFilter] = useState<string>('all')
   199→  const [statusFilter, setStatusFilter] = useState<string>('all')
   200→
   201→  // tree expand state
   202→  const [expanded, setExpanded] = useState<Set<string>>(new Set())
   203→  const initRef = useRef(false)
   204→
   205→  // dialog
   206→  const [dialogOpen, setDialogOpen] = useState(false)
   207→  const [form, setForm] = useState<CoaForm>(emptyForm)
   208→
   209→  // delete confirm
   210→  const [deleteTarget, setDeleteTarget] = useState<CoaNode | null>(null)
   211→
   212→  const [submitting, setSubmitting] = useState(false)
   213→
   214→  const refresh = useCallback(async () => {
   215→    setLoading(true)
   216→    try {
   217→      const res = await fetch('/api/coa', { cache: 'no-store' })
   218→      const data = await res.json()
   219→      if (!res.ok) throw new Error(data?.error || 'Gagal memuat data')
   220→      setAccounts(Array.isArray(data) ? data : [])
   221→    } catch (e: any) {
   222→      toast.error(e?.message ?? 'Gagal memuat daftar akun')
   223→    } finally {
   224→      setLoading(false)
   225→    }
   226→  }, [])
   227→
   228→  useEffect(() => {
   229→    refresh()
   230→  }, [refresh])
   231→
   232→  // Initialize default expand (level 1 & 2) once after first load
   233→  useEffect(() => {
   234→    if (accounts.length && !initRef.current) {
   235→      initRef.current = true
   236→      const s = new Set<string>()
   237→      accounts.forEach((a) => {
   238→        if (a.level <= 2) s.add(a.id)
   239→      })
   240→      setExpanded(s)
   241→    }
   242→  }, [accounts])
   243→
   244→  // children map + byId map for filtering / parent select restrictions
   245→  const childrenMap = useMemo(() => {
   246→    const m = new Map<string, string[]>()
   247→    accounts.forEach((a) => {
   248→      if (a.parentId) {
   249→        const arr = m.get(a.parentId) || []
   250→        arr.push(a.id)
   251→        m.set(a.parentId, arr)
   252→      }
   253→    })
   254→    return m
   255→  }, [accounts])
   256→
   257→  const byId = useMemo(() => {
   258→    const m = new Map<string, CoaNode>()
   259→    accounts.forEach((a) => m.set(a.id, a))
   260→    return m
   261→  }, [accounts])
   262→
   263→  // Apply filters: keep matching nodes + their ancestors
   264→  const filteredFlat = useMemo(() => {
   265→    const hasFilter =
   266→      !!search.trim() ||
   267→      jenisAkunFilter !== 'all' ||
   268→      statusFilter !== 'all'
   269→    if (!hasFilter) return accounts
   270→
   271→    const q = search.trim().toLowerCase()
   272→    const keep = new Set<string>()
   273→    accounts.forEach((a) => {
   274→      let match = true
   275→      if (jenisAkunFilter !== 'all' && a.jenisAkun !== jenisAkunFilter) match = false
   276→      if (statusFilter === 'aktif' && !a.statusAktif) match = false
   277→      if (statusFilter === 'nonaktif' && a.statusAktif) match = false
   278→      if (q) {
   279→        if (
   280→          !a.kodeAkun.toLowerCase().includes(q) &&
   281→          !a.namaAkun.toLowerCase().includes(q)
   282→        )
   283→          match = false
   284→      }
   285→      if (match) {
   286→        keep.add(a.id)
   287→        let cur: CoaNode | undefined = a
   288→        while (cur && cur.parentId) {
   289→          if (keep.has(cur.parentId)) break
   290→          keep.add(cur.parentId)
   291→          cur = byId.get(cur.parentId)
   292→        }
   293→      }
   294→    })
   295→    return accounts.filter((a) => keep.has(a.id))
   296→  }, [accounts, search, jenisAkunFilter, statusFilter, byId])
   297→
   298→  const tree = useMemo(() => buildTree(filteredFlat), [filteredFlat])
   299→
   300→  // When any filter active, auto-expand everything visible
   301→  const effectiveExpanded = useMemo(() => {
   302→    const hasFilter =
   303→      !!search.trim() ||
   304→      jenisAkunFilter !== 'all' ||
   305→      statusFilter !== 'all'
   306→    if (hasFilter) return new Set(filteredFlat.map((a) => a.id))
   307→    return expanded
   308→  }, [expanded, search, jenisAkunFilter, statusFilter, filteredFlat])
   309→
   310→  const toggleExpand = (id: string) => {
   311→    setExpanded((prev) => {
   312→      const next = new Set(prev)
   313→      if (next.has(id)) next.delete(id)
   314→      else next.add(id)
   315→      return next
   316→    })
   317→  }
   318→
   319→  const hasActiveFilters =
   320→    !!search.trim() ||
   321→    jenisAkunFilter !== 'all' ||
   322→    statusFilter !== 'all'
   323→
   324→  const clearFilters = () => {
   325→    setSearch('')
   326→    setJenisAkunFilter('all')
   327→    setStatusFilter('all')
   328→  }
   329→
   330→  // ---------- Dialog handlers ----------
   331→
   332→  const openAdd = () => {
   333→    setForm({ ...emptyForm })
   334→    setDialogOpen(true)
   335→  }
   336→
   337→  const openAddChild = (parent: CoaNode) => {
   338→    setForm({
   339→      ...emptyForm,
   340→      parentId: parent.id,
   341→      jenisAkun: parent.jenisAkun,
   342→      posisi: parent.posisi,
   343→    })
   344→    setDialogOpen(true)
   345→  }
   346→
   347→  const openEdit = (node: CoaNode) => {
   348→    setForm({
   349→      id: node.id,
   350→      kodeAkun: node.kodeAkun,
   351→      namaAkun: node.namaAkun,
   352→      parentId: node.parentId ?? ROOT_PARENT_VALUE,
   353→      jenisAkun: node.jenisAkun,
   354→      tipeAkun: node.tipeAkun,
   355→      posisi: node.posisi,
   356→      kodeAkunPajak: node.kodeAkunPajak ?? '',
   357→      namaAkunPajak: node.namaAkunPajak ?? '',
   358→      saldoAwal: node.saldoAwal,
   359→      statusAktif: node.statusAktif,
   360→    })
   361→    setDialogOpen(true)
   362→  }
   363→
   364→  const handleSubmit = async () => {
   365→    const kodeAkun = form.kodeAkun.trim()
   366→    const namaAkun = form.namaAkun.trim()
   367→    if (!kodeAkun || !namaAkun) {
   368→      toast.error('Kode Akun dan Nama Akun wajib diisi')
   369→      return
   370→    }
   371→    setSubmitting(true)
   372→    try {
   373→      const isEdit = !!form.id
   374→      const payload = {
   375→        kodeAkun,
   376→        namaAkun,
   377→        parentId: form.parentId === ROOT_PARENT_VALUE ? null : form.parentId,
   378→        jenisAkun: form.jenisAkun,
   379→        tipeAkun: form.tipeAkun,
   380→        posisi: form.posisi,
   381→        kodeAkunPajak: form.kodeAkunPajak.trim() || null,
   382→        namaAkunPajak: form.namaAkunPajak.trim() || null,
   383→        saldoAwal: form.tipeAkun === 'Total' ? 0 : Number(form.saldoAwal) || 0,
   384→        statusAktif: form.statusAktif,
   385→      }
   386→      const url = isEdit ? `/api/coa/${form.id}` : '/api/coa'
   387→      const method = isEdit ? 'PUT' : 'POST'
   388→      const res = await fetch(url, {
   389→        method,
   390→        headers: { 'Content-Type': 'application/json' },
   391→        body: JSON.stringify(payload),
   392→      })
   393→      const data = await res.json().catch(() => ({}))
   394→      if (!res.ok) {
   395→        throw new Error(data?.error || 'Gagal menyimpan akun')
   396→      }
   397→      toast.success(isEdit ? 'Akun berhasil diperbarui' : 'Akun berhasil ditambahkan')
   398→      setDialogOpen(false)
   399→      await refresh()
   400→    } catch (e: any) {
   401→      toast.error(e?.message ?? 'Gagal menyimpan akun')
   402→    } finally {
   403→      setSubmitting(false)
   404→    }
   405→  }
   406→
   407→  const handleDelete = async () => {
   408→    if (!deleteTarget) return
   409→    setSubmitting(true)
   410→    try {
   411→      const res = await fetch(`/api/coa/${deleteTarget.id}`, {
   412→        method: 'DELETE',
   413→      })
   414→      const data = await res.json().catch(() => ({}))
   415→      if (!res.ok) {
   416→        throw new Error(data?.error || 'Gagal menghapus akun')
   417→      }
   418→      toast.success('Akun berhasil dihapus')
   419→      setDeleteTarget(null)
   420→      await refresh()
   421→    } catch (e: any) {
   422→      toast.error(e?.message ?? 'Gagal menghapus akun')
   423→    } finally {
   424→      setSubmitting(false)
   425→    }
   426→  }
   427→
   428→  // Parent select options — exclude self + descendants when editing
   429→  const parentOptions = useMemo(() => {
   430→    const excluded = form.id
   431→      ? getDescendantIds(form.id, childrenMap)
   432→      : new Set<string>()
   433→    const opts = accounts
   434→      .filter((a) => !excluded.has(a.id))
   435→      .map((a) => ({
   436→        id: a.id,
   437→        label: `${a.kodeAkun} - ${a.namaAkun}`,
   438→        level: a.level,
   439→      }))
   440→    opts.sort((a, b) => a.label.localeCompare(b.label))
   441→    return opts
   442→  }, [accounts, form.id, childrenMap])
   443→
   444→  const visibleCount = countVisibleNodes(tree)
   445→  const totalActive = accounts.filter((a) => a.statusAktif).length
   446→
   447→  return (
   448→    <div className="space-y-4">
   449→      <PageHeader
   450→        title="Daftar Akun"
   451→        subtitle="Chart of Accounts — struktur pohon akun perusahaan"
   452→        icon={<BookCopy className="h-5 w-5" />}
   453→        actions={
   454→          <Button
   455→            onClick={openAdd}
   456→            className="neu-sm neu-hover neu-pressable"
   457→          >
   458→            <Plus className="h-4 w-4" /> Tambah Akun
   459→          </Button>
   460→        }
   461→      />
   462→
   463→      {/* Filters bar */}
   464→      <div className="rounded-2xl neu p-3 sm:p-4">
   465→        <div className="flex flex-col md:flex-row gap-2 md:items-center">
   466→          <div className="relative flex-1 min-w-0">
   467→            <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
   468→            <Input
   469→              value={search}
   470→              onChange={(e) => setSearch(e.target.value)}
   471→              placeholder="Cari kode atau nama akun…"
   472→              className="neu-inset-sm pl-9 border-0 rounded-xl h-9"
   473→            />
   474→            {search && (
   475→              <button
   476→                onClick={() => setSearch('')}
   477→                className="absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1 hover:bg-accent text-muted-foreground"
   478→                aria-label="Bersihkan pencarian"
   479→              >
   480→                <X className="h-3.5 w-3.5" />
   481→              </button>
   482→            )}
   483→          </div>
   484→          <div className="flex gap-2 flex-wrap">
   485→            <Select value={jenisAkunFilter} onValueChange={setJenisAkunFilter}>
   486→              <SelectTrigger className="neu-inset-sm border-0 rounded-xl w-[160px] sm:w-[180px] h-9">
   487→                <SelectValue placeholder="Jenis Akun" />
   488→              </SelectTrigger>
   489→              <SelectContent>
   490→                <SelectItem value="all">Semua Jenis</SelectItem>
   491→                {JENIS_AKUN_OPTIONS.map((j) => (
   492→                  <SelectItem key={j} value={j}>
   493→                    {j}
   494→                  </SelectItem>
   495→                ))}
   496→              </SelectContent>
   497→            </Select>
   498→            <Select value={statusFilter} onValueChange={setStatusFilter}>
   499→              <SelectTrigger className="neu-inset-sm border-0 rounded-xl w-[130px] sm:w-[150px] h-9">
   500→                <SelectValue placeholder="Status" />
   501→              </SelectTrigger>
   502→              <SelectContent>
   503→                <SelectItem value="all">Semua Status</SelectItem>
   504→                <SelectItem value="aktif">Aktif</SelectItem>
   505→                <SelectItem value="nonaktif">Non-aktif</SelectItem>
   506→              </SelectContent>
   507→            </Select>
   508→            {hasActiveFilters && (
   509→              <Button
   510→                variant="outline"
   511→                size="sm"
   512→                onClick={clearFilters}
   513→                className="neu-sm neu-hover h-9"
   514→              >
   515→                <X className="h-3.5 w-3.5" /> Reset
   516→              </Button>
   517→            )}
   518→          </div>
   519→        </div>
   520→      </div>
   521→
   522→      {/* Summary strip */}
   523→      <div className="flex items-center justify-between px-1 text-xs text-muted-foreground">
   524→        <span>
   525→          Menampilkan <span className="font-semibold text-foreground">{visibleCount}</span>{' '}
   526→          akun dari total {accounts.length}
   527→        </span>
   528→        <span className="hidden sm:inline">
   529→          Aktif: <span className="font-semibold text-foreground">{totalActive}</span> ·
   530→          Non-aktif: <span className="font-semibold text-foreground">{accounts.length - totalActive}</span>
   531→        </span>
   532→      </div>
   533→
   534→      {/* Tree / Loading / Empty */}
   535→      {loading ? (
   536→        <div className="space-y-2">
   537→          {Array.from({ length: 6 }).map((_, i) => (
   538→            <div key={i} className="rounded-2xl neu p-3 flex items-center gap-3">
   539→              <Skeleton className="h-4 w-4 rounded" />
   540→              <Skeleton className="h-5 w-20 rounded" />
   541→              <Skeleton className="h-5 flex-1 rounded" />
   542→              <Skeleton className="h-5 w-24 rounded" />
   543→            </div>
   544→          ))}
   545→        </div>
   546→      ) : accounts.length === 0 ? (
   547→        <div className="rounded-2xl neu p-10 text-center">
   548→          <div className="mx-auto h-14 w-14 rounded-2xl neu-sm flex items-center justify-center text-primary mb-4">
   549→            <BookCopy className="h-7 w-7" />
   550→          </div>
   551→          <h3 className="text-base font-semibold text-foreground">Belum ada akun</h3>
   552→          <p className="text-sm text-muted-foreground mt-1 max-w-sm mx-auto">
   553→            Mulai bangun Chart of Accounts perusahaan Anda dengan menambahkan akun pertama.
   554→          </p>
   555→          <Button onClick={openAdd} className="mt-4 neu-sm neu-hover neu-pressable">
   556→            <Plus className="h-4 w-4" /> Tambah Akun
   557→          </Button>
   558→        </div>
   559→      ) : visibleCount === 0 ? (
   560→        <div className="rounded-2xl neu p-10 text-center">
   561→          <div className="mx-auto h-14 w-14 rounded-2xl neu-sm flex items-center justify-center text-muted-foreground mb-4">
   562→            <Search className="h-7 w-7" />
   563→          </div>
   564→          <h3 className="text-base font-semibold text-foreground">Tidak ada akun yang cocok</h3>
   565→          <p className="text-sm text-muted-foreground mt-1">
   566→            Coba ubah kata kunci pencarian atau filter.
   567→          </p>
   568→          <Button variant="outline" onClick={clearFilters} className="mt-4 neu-sm neu-hover">
   569→            <X className="h-4 w-4" /> Reset Filter
   570→          </Button>
   571→        </div>
   572→      ) : (
   573→        <div className="space-y-1.5">
   574→          {tree.map((node) => (
   575→            <CoaTreeRow
   576→              key={node.id}
   577→              node={node}
   578→              depth={0}
   579→              expanded={effectiveExpanded}
   580→              onToggle={toggleExpand}
   581→              onEdit={openEdit}
   582→              onAddChild={openAddChild}
   583→              onDelete={setDeleteTarget}
   584→            />
   585→          ))}
   586→        </div>
   587→      )}
   588→
   589→      {/* Add / Edit dialog */}
   590→      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
   591→        <DialogContent className="sm:max-w-[560px] max-h-[90vh] overflow-y-auto neu-scroll">
   592→          <DialogHeader>
   593→            <DialogTitle>
   594→              {form.id ? 'Edit Akun' : 'Tambah Akun'}
   595→            </DialogTitle>
   596→            <DialogDescription>
   597→              Lengkapi data akun di bawah ini. Field bertanda * wajib diisi.
   598→            </DialogDescription>
   599→          </DialogHeader>
   600→
   601→          <div className="grid gap-4 py-1">
   602→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   603→              <Label htmlFor="kodeAkun">Kode Akun *</Label>
   604→              <Input
   605→                id="kodeAkun"
   606→                value={form.kodeAkun}
   607→                onChange={(e) => setForm({ ...form, kodeAkun: e.target.value })}
   608→                placeholder="cth. 1.1.1.01"
   609→                className="neu-inset-sm border-0 rounded-xl font-mono"
   610→              />
   611→            </div>
   612→
   613→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   614→              <Label htmlFor="namaAkun">Nama Akun *</Label>
   615→              <Input
   616→                id="namaAkun"
   617→                value={form.namaAkun}
   618→                onChange={(e) => setForm({ ...form, namaAkun: e.target.value })}
   619→                placeholder="cth. Kas Kecil"
   620→                className="neu-inset-sm border-0 rounded-xl"
   621→              />
   622→            </div>
   623→
   624→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   625→              <Label>Akun Induk</Label>
   626→              <Select
   627→                value={form.parentId}
   628→                onValueChange={(v) => setForm({ ...form, parentId: v })}
   629→              >
   630→                <SelectTrigger className="neu-inset-sm border-0 rounded-xl w-full h-9">
   631→                  <SelectValue placeholder="— Akun Induk —" />
   632→                </SelectTrigger>
   633→                <SelectContent className="max-h-72 neu-scroll">
   634→                  <SelectItem value={ROOT_PARENT_VALUE}>
   635→                    — Akun Induk (Root) —
   636→                  </SelectItem>
   637→                  {parentOptions.map((o) => (
   638→                    <SelectItem key={o.id} value={o.id}>
   639→                      <span style={{ paddingLeft: (o.level - 1) * 10 }} className="font-mono">
   640→                        {o.label}
   641→                      </span>
   642→                    </SelectItem>
   643→                  ))}
   644→                </SelectContent>
   645→              </Select>
   646→            </div>
   647→
   648→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   649→              <Label>Jenis Akun</Label>
   650→              <Select
   651→                value={form.jenisAkun}
   652→                onValueChange={(v) => setForm({ ...form, jenisAkun: v })}
   653→              >
   654→                <SelectTrigger className="neu-inset-sm border-0 rounded-xl w-full h-9">
   655→                  <SelectValue placeholder="Pilih jenis akun" />
   656→                </SelectTrigger>
   657→                <SelectContent className="max-h-72 neu-scroll">
   658→                  {JENIS_AKUN_OPTIONS.map((j) => (
   659→                    <SelectItem key={j} value={j}>
   660→                      {j}
   661→                    </SelectItem>
   662→                  ))}
   663→                </SelectContent>
   664→              </Select>
   665→            </div>
   666→
   667→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-start">
   668→              <Label className="mt-2">Tipe Akun</Label>
   669→              <RadioGroup
   670→                value={form.tipeAkun}
   671→                onValueChange={(v) =>
   672→                  setForm({
   673→                    ...form,
   674→                    tipeAkun: v,
   675→                    saldoAwal: v === 'Total' ? 0 : form.saldoAwal,
   676→                  })
   677→                }
   678→                className="flex gap-4"
   679→              >
   680→                <Label
   681→                  htmlFor="tipe-posting"
   682→                  className="flex items-center gap-2 rounded-xl neu-sm neu-pressable px-3 py-2 cursor-pointer has-[:checked]:neu-inset-sm has-[:checked]:text-primary font-medium text-sm"
   683→                >
   684→                  <RadioGroupItem id="tipe-posting" value="Posting" />
   685→                  Posting
   686→                </Label>
   687→                <Label
   688→                  htmlFor="tipe-total"
   689→                  className="flex items-center gap-2 rounded-xl neu-sm neu-pressable px-3 py-2 cursor-pointer has-[:checked]:neu-inset-sm has-[:checked]:text-primary font-medium text-sm"
   690→                >
   691→                  <RadioGroupItem id="tipe-total" value="Total" />
   692→                  Total
   693→                </Label>
   694→              </RadioGroup>
   695→            </div>
   696→
   697→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-start">
   698→              <Label className="mt-2">Posisi Saldo</Label>
   699→              <RadioGroup
   700→                value={form.posisi}
   701→                onValueChange={(v) => setForm({ ...form, posisi: v })}
   702→                className="flex gap-4"
   703→              >
   704→                <Label
   705→                  htmlFor="pos-debit"
   706→                  className="flex items-center gap-2 rounded-xl neu-sm neu-pressable px-3 py-2 cursor-pointer has-[:checked]:neu-inset-sm has-[:checked]:text-primary font-medium text-sm"
   707→                >
   708→                  <RadioGroupItem id="pos-debit" value="Debit" />
   709→                  Debit
   710→                </Label>
   711→                <Label
   712→                  htmlFor="pos-kredit"
   713→                  className="flex items-center gap-2 rounded-xl neu-sm neu-pressable px-3 py-2 cursor-pointer has-[:checked]:neu-inset-sm has-[:checked]:text-primary font-medium text-sm"
   714→                >
   715→                  <RadioGroupItem id="pos-kredit" value="Kredit" />
   716→                  Kredit
   717→                </Label>
   718→              </RadioGroup>
   719→            </div>
   720→
   721→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   722→              <Label htmlFor="saldoAwal">Saldo Awal</Label>
   723→              <Input
   724→                id="saldoAwal"
   725→                type="number"
   726→                inputMode="numeric"
   727→                value={form.saldoAwal}
   728→                onChange={(e) =>
   729→                  setForm({ ...form, saldoAwal: Number(e.target.value) || 0 })
   730→                }
   731→                disabled={form.tipeAkun === 'Total'}
   732→                placeholder="0"
   733→                className="neu-inset-sm border-0 rounded-xl tnum disabled:opacity-60"
   734→              />
   735→            </div>
   736→
   737→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   738→              <Label htmlFor="kodeAkunPajak">Kode Akun Pajak</Label>
   739→              <Input
   740→                id="kodeAkunPajak"
   741→                value={form.kodeAkunPajak}
   742→                onChange={(e) =>
   743→                  setForm({ ...form, kodeAkunPajak: e.target.value })
   744→                }
   745→                placeholder="opsional — cth. 110100"
   746→                className="neu-inset-sm border-0 rounded-xl font-mono"
   747→              />
   748→            </div>
   749→
   750→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   751→              <Label htmlFor="namaAkunPajak">Nama Akun Pajak</Label>
   752→              <Input
   753→                id="namaAkunPajak"
   754→                value={form.namaAkunPajak}
   755→                onChange={(e) =>
   756→                  setForm({ ...form, namaAkunPajak: e.target.value })
   757→                }
   758→                placeholder="opsional — nama sesuai Coretax"
   759→                className="neu-inset-sm border-0 rounded-xl"
   760→              />
   761→            </div>
   762→
   763→            <div className="grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-2 sm:gap-4 sm:items-center">
   764→              <Label htmlFor="statusAktif">Status</Label>
   765→              <div className="flex items-center gap-3">
   766→                <Switch
   767→                  id="statusAktif"
   768→                  checked={form.statusAktif}
   769→                  onCheckedChange={(v) => setForm({ ...form, statusAktif: v })}
   770→                />
   771→                <span className="text-sm text-muted-foreground">
   772→                  {form.statusAktif ? 'Aktif' : 'Non-aktif'}
   773→                </span>
   774→              </div>
   775→            </div>
   776→          </div>
   777→
   778→          <DialogFooter>
   779→            <Button
   780→              variant="outline"
   781→              onClick={() => setDialogOpen(false)}
   782→              disabled={submitting}
   783→              className="neu-sm neu-hover"
   784→            >
   785→              Batal
   786→            </Button>
   787→            <Button
   788→              onClick={handleSubmit}
   789→              disabled={submitting}
   790→              className="neu-sm neu-hover neu-pressable"
   791→            >
   792→              {submitting && (
   793→                <span className="h-4 w-4 rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground animate-spin" />
   794→              )}
   795→              {form.id ? 'Simpan Perubahan' : 'Tambah Akun'}
   796→            </Button>
   797→          </DialogFooter>
   798→        </DialogContent>
   799→      </Dialog>
   800→
   801→      {/* Delete confirm */}
   802→      <AlertDialog
   803→        open={!!deleteTarget}
   804→        onOpenChange={(o) => !o && setDeleteTarget(null)}
   805→      >
   806→        <AlertDialogContent className="sm:max-w-md">
   807→          <AlertDialogHeader>
   808→            <AlertDialogTitle>Hapus akun ini?</AlertDialogTitle>
   809→            <AlertDialogDescription>
   810→              Anda akan menghapus akun{' '}
   811→              <span className="font-mono font-semibold text-foreground">
   812→                {deleteTarget?.kodeAkun}
   813→              </span>{' '}
   814→              — {deleteTarget?.namaAkun}. Tindakan ini tidak dapat dibatalkan.
   815→            </AlertDialogDescription>
   816→          </AlertDialogHeader>
   817→          <AlertDialogFooter>
   818→            <AlertDialogCancel disabled={submitting} className="neu-sm neu-hover">
   819→              Batal
   820→            </AlertDialogCancel>
   821→            <AlertDialogAction
   822→              onClick={handleDelete}
   823→              disabled={submitting}
   824→              className="bg-destructive text-white hover:bg-destructive/90"
   825→            >
   826→              {submitting ? 'Menghapus…' : 'Hapus'}
   827→            </AlertDialogAction>
   828→          </AlertDialogFooter>
   829→        </AlertDialogContent>
   830→      </AlertDialog>
   831→    </div>
   832→  )
   833→}
   834→
   835→// ---------- Tree row component ----------
   836→
   837→interface CoaTreeRowProps {
   838→  node: CoaTree
   839→  depth: number
   840→  expanded: Set<string>
   841→  onToggle: (id: string) => void
   842→  onEdit: (n: CoaNode) => void
   843→  onAddChild: (n: CoaNode) => void
   844→  onDelete: (n: CoaNode) => void
   845→}
   846→
   847→function CoaTreeRow({
   848→  node,
   849→  depth,
   850→  expanded,
   851→  onToggle,
   852→  onEdit,
   853→  onAddChild,
   854→  onDelete,
   855→}: CoaTreeRowProps) {
   856→  const hasChildren = node.children.length > 0
   857→  const isExpanded = expanded.has(node.id)
   858→  const isTotal = node.tipeAkun === 'Total'
   859→  const isPosting = node.tipeAkun === 'Posting'
   860→
   861→  return (
   862→    <div>
   863→      <div
   864→        className={cn(
   865→          'rounded-2xl p-2 sm:p-3 flex items-start sm:items-center gap-2 sm:gap-3 transition-colors',
   866→          isTotal ? 'neu-inset-sm bg-muted/40' : 'neu neu-hover'
   867→        )}
   868→        style={{ marginLeft: depth * 20 }}
   869→      >
   870→        {/* Chevron toggle */}
   871→        <button
   872→          type="button"
   873→          onClick={() => hasChildren && onToggle(node.id)}
   874→          className={cn(
   875→            'shrink-0 rounded-md p-1 transition-colors',
   876→            hasChildren
   877→              ? 'hover:bg-accent text-foreground cursor-pointer'
   878→              : 'cursor-default text-transparent'
   879→          )}
   880→          aria-label={isExpanded ? 'Lipat akun' : 'Bentangkan akun'}
   881→          tabIndex={hasChildren ? 0 : -1}
   882→        >
   883→          {hasChildren ? (
   884→            isExpanded ? (
   885→              <ChevronDown className="h-4 w-4" />
   886→            ) : (
   887→              <ChevronRight className="h-4 w-4" />
   888→            )
   889→          ) : (
   890→            <span className="block h-4 w-4" />
   891→          )}
   892→        </button>
   893→
   894→        {/* Kode akun chip */}
   895→        <span className="shrink-0 rounded-md neu-inset-sm px-2 py-0.5 font-mono text-xs text-foreground self-center">
   896→          {node.kodeAkun}
   897→        </span>
   898→
   899→        {/* Main info + mobile badges */}
   900→        <div className="flex-1 min-w-0 self-center">
   901→          <div className="flex items-center gap-2 flex-wrap">
   902→            <span
   903→              className={cn(
   904→                'truncate',
   905→                isTotal ? 'font-bold text-foreground' : 'font-medium text-foreground'
   906→              )}
   907→            >
   908→              {node.namaAkun}
   909→            </span>
   910→            <span
   911→              className={cn(
   912→                'h-2 w-2 rounded-full shrink-0',
   913→                node.statusAktif ? 'bg-emerald-500' : 'bg-muted-foreground/40'
   914→              )}
   915→              title={node.statusAktif ? 'Aktif' : 'Non-aktif'}
   916→              aria-label={node.statusAktif ? 'Aktif' : 'Non-aktif'}
   917→            />
   918→          </div>
   919→          {/* Badges — stacked below name on mobile */}
   920→          <div className="flex items-center gap-1.5 mt-1 flex-wrap sm:hidden">
   921→            <Badge
   922→              variant="outline"
   923→              className={cn('text-[10px] px-1.5 py-0', JENIS_AKUN_BADGE[node.jenisAkun])}
   924→            >
   925→              {node.jenisAkun}
   926→            </Badge>
   927→            <Badge
   928→              variant={isPosting ? 'default' : 'secondary'}
   929→              className="text-[10px] px-1.5 py-0"
   930→            >
   931→              {node.tipeAkun}
   932→            </Badge>
   933→            <Badge variant="outline" className="text-[10px] px-1.5 py-0">
   934→              {node.posisi}
   935→            </Badge>
   936→            {isPosting && (
   937→              <span className="tnum text-[11px] font-medium text-muted-foreground ml-auto">
   938→                {formatCurrency(node.saldoAwal)}
   939→              </span>
   940→            )}
   941→          </div>
   942→        </div>
   943→
   944→        {/* Desktop inline badges */}
   945→        <div className="hidden sm:flex items-center gap-1.5 shrink-0 self-center">
   946→          <Badge
   947→            variant="outline"
   948→            className={cn('text-[10px] px-1.5 py-0', JENIS_AKUN_BADGE[node.jenisAkun])}
   949→          >
   950→            {node.jenisAkun}
   951→          </Badge>
   952→          <Badge
   953→            variant={isPosting ? 'default' : 'secondary'}
   954→            className="text-[10px] px-1.5 py-0"
   955→          >
   956→            {node.tipeAkun}
   957→          </Badge>
   958→          <Badge variant="outline" className="text-[10px] px-1.5 py-0">
   959→            {node.posisi}
   960→          </Badge>
   961→        </div>
   962→
   963→        {/* Saldo — desktop only */}
   964→        <div className="hidden md:block w-32 text-right shrink-0 self-center">
   965→          {isPosting ? (
   966→            <span className="tnum text-sm font-medium text-foreground">
   967→              {formatCurrency(node.saldoAwal)}
   968→            </span>
   969→          ) : (
   970→            <span className="text-muted-foreground/50 text-xs">—</span>
   971→          )}
   972→        </div>
   973→
   974→        {/* Actions menu */}
   975→        <div className="shrink-0 self-center">
   976→          <DropdownMenu>
   977→            <DropdownMenuTrigger asChild>
   978→              <Button
   979→                variant="ghost"
   980→                size="icon"
   981→                className="h-8 w-8 neu-sm neu-hover neu-pressable"
   982→                aria-label="Aksi akun"
   983→              >
   984→                <MoreHorizontal className="h-4 w-4" />
   985→              </Button>
   986→            </DropdownMenuTrigger>
   987→            <DropdownMenuContent align="end" className="w-44">
   988→              <DropdownMenuLabel>Aksi Akun</DropdownMenuLabel>
   989→              <DropdownMenuItem onClick={() => onEdit(node)}>
   990→                <Pencil className="h-4 w-4" /> Edit
   991→              </DropdownMenuItem>
   992→              <DropdownMenuItem onClick={() => onAddChild(node)}>
   993→                <FolderPlus className="h-4 w-4" /> Tambah Sub-akun
   994→              </DropdownMenuItem>
   995→              <DropdownMenuSeparator />
   996→              <DropdownMenuItem
   997→                variant="destructive"
   998→                onClick={() => onDelete(node)}
   999→              >
  1000→                <Trash2 className="h-4 w-4" /> Hapus
  1001→              </DropdownMenuItem>
  1002→            </DropdownMenuContent>
  1003→          </DropdownMenu>
  1004→        </div>
  1005→      </div>
  1006→
  1007→      {hasChildren && isExpanded && (
  1008→        <div className="mt-1.5 space-y-1.5">
  1009→          {node.children.map((c) => (
  1010→            <CoaTreeRow
  1011→              key={c.id}
  1012→              node={c}
  1013→              depth={depth + 1}
  1014→              expanded={expanded}
  1015→              onToggle={onToggle}
  1016→              onEdit={onEdit}
  1017→              onAddChild={onAddChild}
  1018→              onDelete={onDelete}
  1019→            />
  1020→          ))}
  1021→        </div>
  1022→      )}
  1023→    </div>
  1024→  )
  1025→}
  1026→