Files
kovakemlak-crm/src/lib/appwrite/finance-actions.ts
T
egecankomur 37679e83e6 init: kovakemlak-crm project scaffold
- Next.js 16 + Appwrite multi-tenant emlak CRM
- Database: kovakemlak-db (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings)
- Same stack as isletmem-kovakcrm (shadcn/ui template base)
- Modules: portföy, müşteri takibi, arama kriterleri, otomatik eşleştirme, sunum linki, yatırımcı portalı
2026-05-05 04:37:04 +03:00

216 lines
5.8 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.
"use server";
import { revalidatePath } from "next/cache";
import { AppwriteException, ID } from "node-appwrite";
import { z } from "zod";
import { logAudit } from "./audit";
import {
isPlanLimitError,
planLimitMessage,
requirePlanCapacity,
} from "./plan-limits";
import { DATABASE_ID, TABLES, type FinanceEntry } from "./schema";
import { canAccessRow, scopedRowPermissions } from "./scope-permissions";
import { createAdminClient } from "./server";
import { requireTenant } from "./tenant-guard";
import type { FinanceActionState } from "./finance-types";
import { financeEntrySchema } from "@/lib/validation/finance";
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<string, string> {
const out: Record<string, string> = {};
for (const issue of err.issues) {
const key = issue.path.join(".");
if (key && !out[key]) out[key] = issue.message;
}
return out;
}
function pickFormFields(formData: FormData) {
return {
type: formData.get("type") as "income" | "expense" | "debt" | "receivable",
amount: String(formData.get("amount") ?? "0"),
date: String(formData.get("date") ?? ""),
description: String(formData.get("description") ?? "").trim(),
customerId: String(formData.get("customerId") ?? ""),
invoiceId: String(formData.get("invoiceId") ?? ""),
paymentMethod: formData.get("paymentMethod") as
| "cash"
| "transfer"
| "card"
| "check"
| "other"
| null,
bankAccountId: String(formData.get("bankAccountId") ?? ""),
scope: (formData.get("scope") as "company" | "personal" | null) ?? "company",
};
}
function toIso(v: string): string {
if (!v) return v;
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`;
return v;
}
export async function createFinanceEntryAction(
_prev: FinanceActionState,
formData: FormData,
): Promise<FinanceActionState> {
let ctx;
try {
ctx = await requireTenant();
} catch {
return { ok: false, error: "Yetkiniz yok." };
}
const parsed = financeEntrySchema.safeParse(pickFormFields(formData));
if (!parsed.success) {
return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
}
try {
await requirePlanCapacity(ctx, "financeEntries");
} catch (e) {
if (isPlanLimitError(e)) {
return {
ok: false,
error: planLimitMessage(e.resource, e.limit),
code: "PLAN_LIMIT_EXCEEDED",
};
}
throw e;
}
try {
const { tablesDB } = createAdminClient();
const data = { ...parsed.data, date: toIso(parsed.data.date) };
const row = await tablesDB.createRow(
DATABASE_ID,
TABLES.financeEntries,
ID.unique(),
{
tenantId: ctx.tenantId,
createdBy: ctx.user.id,
...data,
},
scopedRowPermissions(ctx.tenantId, ctx.user.id, parsed.data.scope),
);
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "create",
entityType: "finance_entry",
entityId: row.$id,
changes: data,
});
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/finance");
return { ok: true };
}
export async function updateFinanceEntryAction(
_prev: FinanceActionState,
formData: FormData,
): Promise<FinanceActionState> {
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 = financeEntrySchema.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.financeEntries,
id,
)) as unknown as FinanceEntry;
if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
return { ok: false, error: "Erişim engellendi." };
}
const data = { ...parsed.data, date: toIso(parsed.data.date) };
await tablesDB.updateRow(
DATABASE_ID,
TABLES.financeEntries,
id,
data,
scopedRowPermissions(ctx.tenantId, existing.createdBy, parsed.data.scope),
);
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "update",
entityType: "finance_entry",
entityId: id,
changes: data,
});
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/finance");
return { ok: true };
}
export async function deleteFinanceEntryAction(
formData: FormData,
): Promise<FinanceActionState> {
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.financeEntries,
id,
)) as unknown as FinanceEntry;
if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
return { ok: false, error: "Erişim engellendi." };
}
await tablesDB.deleteRow(DATABASE_ID, TABLES.financeEntries, id);
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "delete",
entityType: "finance_entry",
entityId: id,
changes: { type: existing.type, amount: existing.amount },
});
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/finance");
return { ok: true };
}