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:
kovakmedya
2026-04-30 07:22:51 +03:00
parent c848531326
commit 7b6be623ae
16 changed files with 972 additions and 19 deletions
+28
View File
@@ -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>;
+1
View File
@@ -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>;