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:
@@ -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>;
|
||||
Reference in New Issue
Block a user