Files
isletmem-kovakcrm/src/lib/appwrite/bank-account-queries.ts
T
kovakmedya 7b6be623ae 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.
2026-04-30 07:22:51 +03:00

94 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 [];
}
}