feat(banking A): bank accounts module + finance integration
First of 3-step banking expansion. Banks tracked separately from
customer/supplier debts so we can compute real cash position later.
Schema:
- New bank_accounts table: bankName, accountName, iban, openingBalance,
notes, archived. Indexes on (tenantId, archived).
- New column finance_entries.bankAccountId (FK, optional). Index on
(tenantId, bankAccountId).
- schema.ts: TABLES.bankAccounts, BankAccount type, FinanceEntry gains
bankAccountId.
Server side:
- lib/validation/bank-accounts.ts (Zod): IBAN normalized to upper-case
no-spaces; openingBalance defaults to 0.
- lib/appwrite/bank-account-actions.ts: create/update/archive(toggle)/
delete with audit. Delete refuses if any finance_entry still references
the account; archive toggle replaces it for safe disable.
- lib/appwrite/bank-account-queries.ts:
* listBankAccounts
* getBankAccountBalances — computes opening + Σ(income) − Σ(expense)
per account by scanning up to 5000 entries with bankAccountId set.
Pure cash flow; debt/receivable don't move balance.
* listEntriesForAccount
UI:
- /finance/banks server page renders BanksClient with computed balances.
- BanksClient: card grid for active accounts, collapsed details for
archived. Sum card on top showing total active balance (color-coded by
sign). Each card shows bank, account name, formatted IBAN, current
balance + opening (if drifted). Dropdown: Düzenle / Arşivle / Sil.
- BankFormSheet: bank/account/IBAN/openingBalance/notes form.
- Finance form gets a bank-account Select (sentinel-stripped). Existing
finance entries get a 'bankAccountLabel' subtitle in their row.
Sidebar: Finans group expanded with Bankalar submenu (Banka hesapları
/ Krediler / Kredi kartları). The latter two land in B and C.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const bankAccountSchema = z.object({
|
||||
bankName: z.string().trim().min(1, "Banka adı zorunlu.").max(100),
|
||||
accountName: z.string().trim().min(1, "Hesap adı zorunlu.").max(100),
|
||||
iban: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(50)
|
||||
.optional()
|
||||
.transform((v) => (v ? v.replace(/\s+/g, "").toUpperCase() : undefined)),
|
||||
openingBalance: 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;
|
||||
}),
|
||||
notes: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(500)
|
||||
.optional()
|
||||
.transform((v) => (v ? v : undefined)),
|
||||
});
|
||||
|
||||
export type BankAccountInput = z.infer<typeof bankAccountSchema>;
|
||||
@@ -14,6 +14,7 @@ export const financeEntrySchema = z.object({
|
||||
.enum(["cash", "transfer", "card", "check", "other"])
|
||||
.optional()
|
||||
.transform((v) => v || undefined),
|
||||
bankAccountId: z.string().optional().transform((v) => (v ? v : undefined)),
|
||||
});
|
||||
|
||||
export type FinanceEntryInput = z.infer<typeof financeEntrySchema>;
|
||||
|
||||
Reference in New Issue
Block a user