"use client"; import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { Plus, Trash2, Eye } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table"; import { Modal } from "@/components/ui/Modal"; import { AccountingNav } from "@/components/accounting/AccountingNav"; import { formatCurrency } from "@/lib/utils"; import api from "@/lib/api"; import type { Account, JournalEntry, JournalEntryLine } from "@/types/index"; function formatDate(iso: string) { return new Date(iso).toLocaleDateString("en-PH", { year: "numeric", month: "short", day: "numeric" }); } interface LineFormItem { accountId: string; debit: string; credit: string; memo: string; } const emptyLine = (): LineFormItem => ({ accountId: "", debit: "", credit: "", memo: "" }); export default function JournalEntriesPage() { const [showNew, setShowNew] = useState(false); const [viewEntry, setViewEntry] = useState(null); const [form, setForm] = useState({ date: "", description: "", reference: "" }); const [lines, setLines] = useState([emptyLine(), emptyLine()]); const { data: entries = [], isLoading, refetch } = useQuery({ queryKey: ["journal-entries"], queryFn: async () => { const res = await api.get("/api/v1/journal-entries"); const d = res.data; return Array.isArray(d) ? d : (d as { data: JournalEntry[] }).data ?? []; }, }); const { data: accounts = [] } = useQuery({ queryKey: ["accounts"], queryFn: async () => { const res = await api.get("/api/v1/accounts"); const d = res.data; return (Array.isArray(d) ? d : (d as { data: Account[] }).data ?? []).filter((a) => a.isActive); }, }); const totalDebit = lines.reduce((s, l) => s + (parseFloat(l.debit) || 0), 0); const totalCredit = lines.reduce((s, l) => s + (parseFloat(l.credit) || 0), 0); const isBalanced = Math.abs(totalDebit - totalCredit) < 0.01 && totalDebit > 0; const createMutation = useMutation({ mutationFn: async () => { const payload = { date: form.date, description: form.description, reference: form.reference || undefined, lines: lines .filter((l) => l.accountId) .map((l) => ({ accountId: l.accountId, debit: parseFloat(l.debit) || 0, credit: parseFloat(l.credit) || 0, memo: l.memo || undefined, })), }; await api.post("/api/v1/journal-entries", payload); }, onSuccess: () => { toast.success("Journal entry created"); setShowNew(false); setForm({ date: "", description: "", reference: "" }); setLines([emptyLine(), emptyLine()]); refetch(); }, onError: () => toast.error("Failed to create journal entry"), }); function setLine(i: number, field: keyof LineFormItem, value: string) { setLines((prev) => prev.map((l, idx) => idx === i ? { ...l, [field]: value } : l)); } function addLine() { setLines((prev) => [...prev, emptyLine()]); } function removeLine(i: number) { if (lines.length <= 2) return; setLines((prev) => prev.filter((_, idx) => idx !== i)); } const entryTotalDebit = (e: JournalEntry) => e.lines?.reduce((s, l) => s + Number(l.debit), 0) ?? e.totalDebit ?? 0; return (

Accounting

Journal entries and double-entry bookkeeping

Journal Entries ({entries.length}) {isLoading ? ( Array.from({ length: 4 }).map((_, i) => ( {[1,2,3,4,5,6,7].map((j) => ( ))} )) ) : entries.length === 0 ? ( ) : ( entries.map((e) => ( )) )}
Date Reference Description Source Lines Total Debit
{formatDate(e.date)} {e.reference ?? "—"} {e.description} {e.sourceType ?? "MANUAL"} {e.lines?.length ?? 0} {formatCurrency(entryTotalDebit(e))}
{/* New Entry Modal */} setShowNew(false)} title="New Journal Entry" className="max-w-3xl">
{ e.preventDefault(); createMutation.mutate(); }} className="space-y-4">
setForm((f) => ({ ...f, date: e.target.value }))} /> setForm((f) => ({ ...f, reference: e.target.value }))} placeholder="e.g. JE-001" />
setForm((f) => ({ ...f, description: e.target.value }))} placeholder="Describe this journal entry" /> {/* Lines */}

Line Items

{lines.map((line, i) => ( ))}
Account Debit Credit Memo
setLine(i, "debit", e.target.value)} placeholder="0.00" /> setLine(i, "credit", e.target.value)} placeholder="0.00" /> setLine(i, "memo", e.target.value)} placeholder="Optional" />
Totals {formatCurrency(totalDebit)} {formatCurrency(totalCredit)} {totalDebit > 0 && ( {isBalanced ? "✓ Balanced" : `Off by ${formatCurrency(Math.abs(totalDebit - totalCredit))}`} )}
{/* View Entry Modal */} {viewEntry && ( setViewEntry(null)} title="Journal Entry" className="max-w-2xl">

Date

{formatDate(viewEntry.date)}

Reference

{viewEntry.reference ?? "—"}

Source

{viewEntry.sourceType ?? "MANUAL"}

Description

{viewEntry.description}

{viewEntry.lines?.map((l, i) => ( ))}
Account Debit Credit Memo
{l.account ? `${l.account.code} — ${l.account.name}` : l.accountId} {Number(l.debit) > 0 ? formatCurrency(Number(l.debit)) : "—"} {Number(l.credit) > 0 ? formatCurrency(Number(l.credit)) : "—"} {l.memo ?? ""}
Total {formatCurrency(entryTotalDebit(viewEntry))} {formatCurrency(entryTotalDebit(viewEntry))}
)}
); }