feat(invoices): auto income entry on 'paid' status

Marking an invoice as paid now creates a finance_entry (type=income) for
the customer with amount = invoice.total, linked via invoiceId. Reverting
status removes the entry. Idempotent: re-saving while already paid keeps
the existing entry (resyncs amount if invoice total changed in the
meantime).

- syncPaymentEntry(tenantId, userId, invoice) helper:
  * status === 'paid': create entry if none exists; otherwise update
    amount to match current invoice.total.
  * status !== 'paid': delete any income entries linked to the invoice.
  * Best-effort — failures are swallowed so the invoice mutation always
    succeeds even if Appwrite hiccups on the finance write.
  * Each create/delete writes an audit row tagged auto: 'invoice_paid' /
    'invoice_unpaid' so we can trace later.
- updateInvoiceAction now calls syncPaymentEntry after persisting.
- recomputeTotals (run on every item add/update/delete) also re-syncs
  the linked entry's amount when the invoice is currently paid.
- deleteInvoiceAction now cascade-deletes any linked finance_entries in
  addition to items.
- /invoices and /invoices/[id] both revalidate /finance after writes.

UI:
- Invoice form shows a hint under the status select explaining the
  finance side effect.
- Finance table tags rows with a 'Faturadan' badge when invoiceId is
  set, so users can tell auto-generated entries apart from manual ones.
This commit is contained in:
kovakmedya
2026-04-30 06:14:31 +03:00
parent d99daca3ca
commit 37777a71f9
5 changed files with 154 additions and 11 deletions
@@ -191,14 +191,18 @@ export function FinanceClient({ entries, customers }: Props) {
{
accessorKey: "description",
header: "Açıklama",
cell: ({ row }) =>
row.original.description ? (
<span className="text-muted-foreground line-clamp-1 max-w-[260px] text-sm">
{row.original.description}
cell: ({ row }) => (
<div className="flex max-w-[300px] items-center gap-2">
<span className="text-muted-foreground line-clamp-1 text-sm">
{row.original.description || "—"}
</span>
) : (
<span className="text-muted-foreground"></span>
),
{row.original.invoiceId && (
<Badge variant="outline" className="shrink-0 text-[10px]">
Faturadan
</Badge>
)}
</div>
),
},
{
id: "actions",
@@ -10,6 +10,7 @@ export type FinanceRow = {
customerId: string;
customerName: string;
paymentMethod: PaymentMethod;
invoiceId: string;
};
export type Customer = { id: string; name: string };
+1
View File
@@ -45,6 +45,7 @@ export default async function FinancePage() {
customerId: e.customerId ?? "",
customerName: e.customerId ? customerMap.get(e.customerId) ?? "" : "",
paymentMethod: e.paymentMethod ?? "",
invoiceId: e.invoiceId ?? "",
}))}
customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
/>
@@ -144,6 +144,10 @@ export function InvoiceFormSheet({ open, onOpenChange, invoice, customers }: Pro
<SelectItem value="cancelled">İptal</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs">
&ldquo;Ödendi&rdquo; seçildiğinde finans modülüne otomatik gelir kaydı düşer.
Durum geri alınırsa kayıt silinir.
</p>
</div>
<div className="grid gap-2">