feat(banking C): credit cards + monthly statements

Final banking step. User-managed: each month, paste your bank statement
totals into the form. We don't auto-compute interest — bank's number is
authoritative.

Schema:
- credit_cards: bankName, cardName, last4, creditLimit, statementDay,
  dueDay, interestRate (monthly nominal %), bankAccountId (optional FK,
  payments expense from this account), archived, notes.
- credit_card_statements: cardId, period (YYYY-MM), statementDate,
  dueDate, totalDebt, minimumPayment, paidAmount, status
  (pending/partial/paid/overdue), financeEntryId (link to last payment).
- Indexes on (tenantId, archived) for cards and (tenantId, cardId) +
  (tenantId, status, dueDate) for statements.

Server (lib/appwrite/credit-card-actions.ts):
- create/update/archive(toggle)/delete for cards. Card delete cascades
  through statements + their finance_entries.
- createStatementAction: computes status from dueDate + paidAmount.
- payStatementAction: partial-payment friendly. Creates a single
  finance_entry expense for the pay amount, capped at remaining balance,
  bankAccountId carried from the card. Recomputes status: paid (full),
  partial (some + pre-due), overdue (past due with anything < total).
- deleteStatementAction: removes linked finance_entry too.
- All audit-logged.

UI (/finance/cards):
- 3 stat cards: aktif kart sayısı, bekleyen toplam, vadesi geçmiş ekstre.
- Per-card panel: bank/name/last4, limit + statement/due day + monthly
  interest, current outstanding. Statement table inside card with status
  badges, Öde button (opens partial-payment dialog), delete button.
- CardFormSheet for card CRUD, StatementFormSheet for statement creation
  with default period/dates derived from card's statementDay/dueDay.

Sidebar Finans submenu now functional: Banka hesapları → /finance/banks,
Krediler → /finance/loans, Kredi kartları → /finance/cards.
This commit is contained in:
kovakmedya
2026-04-30 07:36:31 +03:00
parent b632ae8a73
commit 121fbdba9d
10 changed files with 1719 additions and 0 deletions
@@ -0,0 +1,80 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { listBankAccounts } from "@/lib/appwrite/bank-account-queries";
import {
listCreditCards,
listStatements,
} from "@/lib/appwrite/credit-card-queries";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
import { CardsClient } from "./components/cards-client";
export const metadata: Metadata = {
title: "İşletmem — Kredi kartları",
};
export default async function CardsPage() {
let ctx;
try {
ctx = await requireTenant();
} catch {
redirect("/onboarding");
}
const [cards, statements, bankAccounts] = await Promise.all([
listCreditCards(ctx.tenantId),
listStatements(ctx.tenantId),
listBankAccounts(ctx.tenantId),
]);
const bankMap = new Map(
bankAccounts.map((b) => [b.$id, `${b.bankName}${b.accountName}`]),
);
return (
<div className="flex-1 space-y-6 px-6 pt-0">
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-sm">{ctx.settings?.companyName ?? "Çalışma alanı"}</p>
<h1 className="text-2xl font-bold tracking-tight">Kredi kartları</h1>
<p className="text-muted-foreground text-sm">
Kartlarınızı ve aylık ekstrelerinizi takip edin. Ekstre ödendiğinde otomatik gider kaydı oluşur.
</p>
</div>
<CardsClient
cards={cards.map((c) => ({
id: c.$id,
bankName: c.bankName,
cardName: c.cardName,
last4: c.last4 ?? "",
creditLimit: c.creditLimit ?? 0,
statementDay: c.statementDay ?? 1,
dueDay: c.dueDay ?? 10,
interestRate: c.interestRate ?? 4.25,
bankAccountId: c.bankAccountId ?? "",
bankAccountLabel: c.bankAccountId ? bankMap.get(c.bankAccountId) ?? "" : "",
archived: Boolean(c.archived),
notes: c.notes ?? "",
}))}
statements={statements.map((s) => ({
id: s.$id,
cardId: s.cardId,
period: s.period,
statementDate: s.statementDate,
dueDate: s.dueDate,
totalDebt: s.totalDebt,
minimumPayment: s.minimumPayment ?? 0,
paidAmount: s.paidAmount ?? 0,
status: s.status ?? "pending",
notes: s.notes ?? "",
}))}
bankAccounts={bankAccounts
.filter((b) => !b.archived)
.map((b) => ({
id: b.$id,
label: `${b.bankName}${b.accountName}`,
}))}
/>
</div>
);
}