7b6be623ae
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.
94 lines
2.6 KiB
TypeScript
94 lines
2.6 KiB
TypeScript
import "server-only";
|
||
|
||
import { Query } from "node-appwrite";
|
||
|
||
import { createAdminClient } from "./server";
|
||
import {
|
||
DATABASE_ID,
|
||
TABLES,
|
||
type BankAccount,
|
||
type FinanceEntry,
|
||
} from "./schema";
|
||
|
||
export async function listBankAccounts(tenantId: string): Promise<BankAccount[]> {
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const result = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.bankAccounts,
|
||
queries: [
|
||
Query.equal("tenantId", tenantId),
|
||
Query.orderAsc("bankName"),
|
||
Query.limit(200),
|
||
],
|
||
});
|
||
return result.rows as unknown as BankAccount[];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Computes a current balance for each account: openingBalance + Σ(income/receivable) − Σ(expense/debt).
|
||
*/
|
||
export async function getBankAccountBalances(
|
||
tenantId: string,
|
||
): Promise<Map<string, number>> {
|
||
const balances = new Map<string, number>();
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const accounts = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.bankAccounts,
|
||
queries: [Query.equal("tenantId", tenantId), Query.limit(200)],
|
||
});
|
||
for (const a of accounts.rows as unknown as BankAccount[]) {
|
||
balances.set(a.$id, a.openingBalance ?? 0);
|
||
}
|
||
|
||
const entries = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.financeEntries,
|
||
queries: [
|
||
Query.equal("tenantId", tenantId),
|
||
Query.isNotNull("bankAccountId"),
|
||
Query.limit(5000),
|
||
],
|
||
});
|
||
for (const e of entries.rows as unknown as FinanceEntry[]) {
|
||
if (!e.bankAccountId) continue;
|
||
const cur = balances.get(e.bankAccountId);
|
||
if (cur === undefined) continue;
|
||
if (e.type === "income") balances.set(e.bankAccountId, cur + e.amount);
|
||
else if (e.type === "expense") balances.set(e.bankAccountId, cur - e.amount);
|
||
// debt/receivable don't affect cash balance
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return balances;
|
||
}
|
||
|
||
export async function listEntriesForAccount(
|
||
tenantId: string,
|
||
bankAccountId: string,
|
||
limit = 25,
|
||
): Promise<FinanceEntry[]> {
|
||
try {
|
||
const { tablesDB } = createAdminClient();
|
||
const result = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.financeEntries,
|
||
queries: [
|
||
Query.equal("tenantId", tenantId),
|
||
Query.equal("bankAccountId", bankAccountId),
|
||
Query.orderDesc("date"),
|
||
Query.limit(limit),
|
||
],
|
||
});
|
||
return result.rows as unknown as FinanceEntry[];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|