+
+
+
+
+
+
+
+
diff --git a/src/app/(dashboard)/finance/components/types.ts b/src/app/(dashboard)/finance/components/types.ts
index f8bdf6f..b8dc669 100644
--- a/src/app/(dashboard)/finance/components/types.ts
+++ b/src/app/(dashboard)/finance/components/types.ts
@@ -11,9 +11,12 @@ export type FinanceRow = {
customerName: string;
paymentMethod: PaymentMethod;
invoiceId: string;
+ bankAccountId: string;
+ bankAccountLabel: string;
};
export type Customer = { id: string; name: string };
+export type BankAccountOption = { id: string; label: string };
export const TYPE_LABEL: Record
= {
income: "Gelir",
diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx
index 9811549..24e8f5b 100644
--- a/src/app/(dashboard)/finance/page.tsx
+++ b/src/app/(dashboard)/finance/page.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
+import { listBankAccounts } from "@/lib/appwrite/bank-account-queries";
import { listCustomers } from "@/lib/appwrite/customer-queries";
import { listFinanceEntries } from "@/lib/appwrite/finance-queries";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
@@ -18,12 +19,16 @@ export default async function FinancePage() {
redirect("/onboarding");
}
- const [entries, customers] = await Promise.all([
+ const [entries, customers, bankAccounts] = await Promise.all([
listFinanceEntries(ctx.tenantId),
listCustomers(ctx.tenantId),
+ listBankAccounts(ctx.tenantId),
]);
const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
+ const bankMap = new Map(
+ bankAccounts.map((b) => [b.$id, `${b.bankName} — ${b.accountName}`]),
+ );
return (
@@ -46,8 +51,16 @@ export default async function FinancePage() {
customerName: e.customerId ? customerMap.get(e.customerId) ?? "" : "",
paymentMethod: e.paymentMethod ?? "",
invoiceId: e.invoiceId ?? "",
+ bankAccountId: e.bankAccountId ?? "",
+ bankAccountLabel: e.bankAccountId ? bankMap.get(e.bankAccountId) ?? "" : "",
}))}
customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
+ bankAccounts={bankAccounts
+ .filter((b) => !b.archived)
+ .map((b) => ({
+ id: b.$id,
+ label: `${b.bankName} — ${b.accountName}`,
+ }))}
/>
);
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index 47242a9..4be05bf 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -85,6 +85,16 @@ const navGroups = [
url: "/finance",
icon: Wallet,
},
+ {
+ title: "Bankalar",
+ url: "/finance/banks",
+ icon: Briefcase,
+ items: [
+ { title: "Banka hesapları", url: "/finance/banks" },
+ { title: "Krediler", url: "/finance/loans" },
+ { title: "Kredi kartları", url: "/finance/cards" },
+ ],
+ },
{
title: "Faturalar",
url: "/invoices",
diff --git a/src/lib/appwrite/bank-account-actions.ts b/src/lib/appwrite/bank-account-actions.ts
new file mode 100644
index 0000000..8ec1cc4
--- /dev/null
+++ b/src/lib/appwrite/bank-account-actions.ts
@@ -0,0 +1,244 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
+import { z } from "zod";
+
+import { logAudit } from "./audit";
+import { DATABASE_ID, TABLES, type BankAccount } from "./schema";
+import { createAdminClient } from "./server";
+import { requireTenant } from "./tenant-guard";
+import type { BankAccountActionState } from "./bank-account-types";
+import { bankAccountSchema } from "@/lib/validation/bank-accounts";
+
+function appwriteError(e: unknown): string {
+ if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
+ return "Bağlantı hatası. Tekrar deneyin.";
+}
+
+function flattenErrors(err: z.ZodError): Record {
+ const out: Record = {};
+ for (const issue of err.issues) {
+ const key = issue.path.join(".");
+ if (key && !out[key]) out[key] = issue.message;
+ }
+ return out;
+}
+
+function teamRowPermissions(tenantId: string) {
+ return [
+ Permission.read(Role.team(tenantId)),
+ Permission.update(Role.team(tenantId)),
+ Permission.delete(Role.team(tenantId, "owner")),
+ Permission.delete(Role.team(tenantId, "admin")),
+ ];
+}
+
+function pickFormFields(formData: FormData) {
+ return {
+ bankName: String(formData.get("bankName") ?? "").trim(),
+ accountName: String(formData.get("accountName") ?? "").trim(),
+ iban: String(formData.get("iban") ?? "").trim(),
+ openingBalance: String(formData.get("openingBalance") ?? "0"),
+ notes: String(formData.get("notes") ?? "").trim(),
+ };
+}
+
+export async function createBankAccountAction(
+ _prev: BankAccountActionState,
+ formData: FormData,
+): Promise {
+ let ctx;
+ try {
+ ctx = await requireTenant();
+ } catch {
+ return { ok: false, error: "Yetkiniz yok." };
+ }
+
+ const parsed = bankAccountSchema.safeParse(pickFormFields(formData));
+ if (!parsed.success) {
+ return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
+ }
+
+ try {
+ const { tablesDB } = createAdminClient();
+ const row = await tablesDB.createRow(
+ DATABASE_ID,
+ TABLES.bankAccounts,
+ ID.unique(),
+ {
+ tenantId: ctx.tenantId,
+ createdBy: ctx.user.id,
+ ...parsed.data,
+ },
+ teamRowPermissions(ctx.tenantId),
+ );
+
+ await logAudit({
+ tenantId: ctx.tenantId,
+ userId: ctx.user.id,
+ action: "create",
+ entityType: "bank_account",
+ entityId: row.$id,
+ changes: parsed.data,
+ });
+ } catch (e) {
+ return { ok: false, error: appwriteError(e) };
+ }
+
+ revalidatePath("/finance/banks");
+ return { ok: true };
+}
+
+export async function updateBankAccountAction(
+ _prev: BankAccountActionState,
+ formData: FormData,
+): Promise {
+ const id = String(formData.get("id") ?? "");
+ if (!id) return { ok: false, error: "ID eksik." };
+
+ let ctx;
+ try {
+ ctx = await requireTenant();
+ } catch {
+ return { ok: false, error: "Yetkiniz yok." };
+ }
+
+ const parsed = bankAccountSchema.safeParse(pickFormFields(formData));
+ if (!parsed.success) {
+ return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
+ }
+
+ try {
+ const { tablesDB } = createAdminClient();
+ const existing = (await tablesDB.getRow(
+ DATABASE_ID,
+ TABLES.bankAccounts,
+ id,
+ )) as unknown as BankAccount;
+ if (existing.tenantId !== ctx.tenantId) {
+ return { ok: false, error: "Erişim engellendi." };
+ }
+
+ await tablesDB.updateRow(DATABASE_ID, TABLES.bankAccounts, id, parsed.data);
+
+ await logAudit({
+ tenantId: ctx.tenantId,
+ userId: ctx.user.id,
+ action: "update",
+ entityType: "bank_account",
+ entityId: id,
+ changes: parsed.data,
+ });
+ } catch (e) {
+ return { ok: false, error: appwriteError(e) };
+ }
+
+ revalidatePath("/finance/banks");
+ return { ok: true };
+}
+
+export async function archiveBankAccountAction(
+ formData: FormData,
+): Promise {
+ const id = String(formData.get("id") ?? "");
+ if (!id) return { ok: false, error: "ID eksik." };
+
+ let ctx;
+ try {
+ ctx = await requireTenant();
+ } catch {
+ return { ok: false, error: "Yetkiniz yok." };
+ }
+
+ try {
+ const { tablesDB } = createAdminClient();
+ const existing = (await tablesDB.getRow(
+ DATABASE_ID,
+ TABLES.bankAccounts,
+ id,
+ )) as unknown as BankAccount;
+ if (existing.tenantId !== ctx.tenantId) {
+ return { ok: false, error: "Erişim engellendi." };
+ }
+
+ const newArchivedState = !existing.archived;
+ await tablesDB.updateRow(DATABASE_ID, TABLES.bankAccounts, id, {
+ archived: newArchivedState,
+ });
+
+ await logAudit({
+ tenantId: ctx.tenantId,
+ userId: ctx.user.id,
+ action: "update",
+ entityType: "bank_account",
+ entityId: id,
+ changes: { archived: newArchivedState },
+ });
+ } catch (e) {
+ return { ok: false, error: appwriteError(e) };
+ }
+
+ revalidatePath("/finance/banks");
+ return { ok: true };
+}
+
+export async function deleteBankAccountAction(
+ formData: FormData,
+): Promise {
+ const id = String(formData.get("id") ?? "");
+ if (!id) return { ok: false, error: "ID eksik." };
+
+ let ctx;
+ try {
+ ctx = await requireTenant();
+ } catch {
+ return { ok: false, error: "Yetkiniz yok." };
+ }
+
+ try {
+ const { tablesDB } = createAdminClient();
+ const existing = (await tablesDB.getRow(
+ DATABASE_ID,
+ TABLES.bankAccounts,
+ id,
+ )) as unknown as BankAccount;
+ if (existing.tenantId !== ctx.tenantId) {
+ return { ok: false, error: "Erişim engellendi." };
+ }
+
+ // Block delete if any finance_entry still references this account.
+ const linked = await tablesDB.listRows({
+ databaseId: DATABASE_ID,
+ tableId: TABLES.financeEntries,
+ queries: [
+ Query.equal("tenantId", ctx.tenantId),
+ Query.equal("bankAccountId", id),
+ Query.limit(1),
+ ],
+ });
+ if (linked.rows.length > 0) {
+ return {
+ ok: false,
+ error:
+ "Bu hesaba bağlı finans hareketleri var. Önce hesabı arşivleyin veya hareketleri başka bir hesaba taşıyın.",
+ };
+ }
+
+ await tablesDB.deleteRow(DATABASE_ID, TABLES.bankAccounts, id);
+
+ await logAudit({
+ tenantId: ctx.tenantId,
+ userId: ctx.user.id,
+ action: "delete",
+ entityType: "bank_account",
+ entityId: id,
+ changes: { bankName: existing.bankName, accountName: existing.accountName },
+ });
+ } catch (e) {
+ return { ok: false, error: appwriteError(e) };
+ }
+
+ revalidatePath("/finance/banks");
+ return { ok: true };
+}
diff --git a/src/lib/appwrite/bank-account-queries.ts b/src/lib/appwrite/bank-account-queries.ts
new file mode 100644
index 0000000..3c14e35
--- /dev/null
+++ b/src/lib/appwrite/bank-account-queries.ts
@@ -0,0 +1,93 @@
+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 {
+ 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