121fbdba9d
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.
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
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>
|
||
);
|
||
}
|