feat(invoices): full invoice + line items module

The most complex module. Two-table model: invoices (header) +
invoice_items (lines). Auto-numbering via tenant_settings.invoiceCounter,
auto-totals on item changes.

Schema/validation:
- lib/validation/invoices.ts: invoiceSchema (header) + invoiceItemSchema
  (line). Both coerce comma decimals.
- lib/appwrite/invoice-actions.ts:
  * createInvoiceAction — fetches tenant_settings, increments
    invoiceCounter, formats number as '{prefix}-{year}-{0000}',
    persists totals as 0/0/0 (recomputed when items added).
  * updateInvoiceAction / deleteInvoiceAction — header CRUD; delete
    cascades to remove all items first then header.
  * addInvoiceItemAction / updateInvoiceItemAction /
    deleteInvoiceItemAction — line CRUD. Each computes lineTotal
    (qty*unit + vat) and triggers recomputeTotals(invoiceId) which
    re-sums all items and updates the header subtotal/vatTotal/total.
  All audit-logged.

Queries:
- listInvoices, getInvoice (with tenant cross-check), listInvoiceItems.

UI:
- /invoices index: 4 stat cards (Toplam / Tahsil edildi / Bekleyen /
  Gecikmiş), table with overdue-aware due date coloring, status badges,
  number is a Link to detail.
- InvoiceFormSheet: customer + dates (default issue=today, due=+30d) +
  status + notes. After create, redirects to /invoices/[id] for adding
  items.
- /invoices/[id] detail: header strip with status, dates, customer name;
  print/edit/delete actions; items editor card; subtotal/VAT/total card;
  notes card.
- InvoiceItemsEditor: rows are clickable to edit, X button to delete.
  ItemFormSheet for add/edit (description + qty + unitPrice + VAT %).

Print is just window.print() for now — relies on browser dialog. Detail
page deliberately uses tabular-nums for amounts.
This commit is contained in:
kovakmedya
2026-04-30 06:09:24 +03:00
parent 98ab73235f
commit d99daca3ca
11 changed files with 1777 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
import { z } from "zod";
export const invoiceSchema = z.object({
customerId: z.string().min(1, "Müşteri seçin."),
issueDate: z.string().min(1, "Düzenleme tarihi zorunlu."),
dueDate: z.string().min(1, "Vade tarihi zorunlu."),
status: z
.enum(["draft", "sent", "paid", "overdue", "cancelled"])
.optional()
.default("draft"),
notes: z.string().trim().max(2000).optional().transform((v) => (v ? v : undefined)),
});
export type InvoiceInput = z.infer<typeof invoiceSchema>;
export const invoiceItemSchema = z.object({
description: z.string().trim().min(1, "Açıklama zorunlu.").max(1000),
quantity: z
.union([z.number(), z.string()])
.transform((v) => (typeof v === "string" ? Number(v.replace(",", ".")) : v))
.pipe(z.number().positive("Miktar 0'dan büyük olmalı.")),
unitPrice: z
.union([z.number(), z.string()])
.transform((v) => (typeof v === "string" ? Number(v.replace(",", ".")) : v))
.pipe(z.number().nonnegative("Negatif olamaz.")),
vatRate: z
.union([z.number(), z.string()])
.optional()
.transform((v) => {
if (v === undefined || v === "") return 0;
const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
return Number.isFinite(n) ? n : 0;
}),
});
export type InvoiceItemInput = z.infer<typeof invoiceItemSchema>;