37679e83e6
- 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ı
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
"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 TenantSettings } from "./schema";
|
||
import { createAdminClient } from "./server";
|
||
import { requireRole, requireTenant } from "./tenant-guard";
|
||
import type { WorkspaceSettingsState } from "./workspace-types";
|
||
import { workspaceSettingsSchema } from "@/lib/validation/workspace";
|
||
|
||
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 {
|
||
companyName: String(formData.get("companyName") ?? "").trim(),
|
||
companyTaxId: String(formData.get("companyTaxId") ?? "").trim(),
|
||
companyAddress: String(formData.get("companyAddress") ?? "").trim(),
|
||
companyEmail: String(formData.get("companyEmail") ?? "").trim(),
|
||
companyPhone: String(formData.get("companyPhone") ?? "").trim(),
|
||
defaultVatRate: String(formData.get("defaultVatRate") ?? "20"),
|
||
invoicePrefix: String(formData.get("invoicePrefix") ?? "").trim(),
|
||
};
|
||
}
|
||
|
||
export async function updateWorkspaceSettingsAction(
|
||
_prev: WorkspaceSettingsState,
|
||
formData: FormData,
|
||
): Promise<WorkspaceSettingsState> {
|
||
let ctx;
|
||
try {
|
||
ctx = await requireTenant();
|
||
requireRole(ctx, ["owner", "admin"]);
|
||
} catch {
|
||
return { ok: false, error: "Düzenleme yetkiniz yok." };
|
||
}
|
||
|
||
const parsed = workspaceSettingsSchema.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.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.tenantSettings,
|
||
queries: [Query.equal("tenantId", ctx.tenantId), Query.limit(1)],
|
||
});
|
||
const row = existing.rows[0] as unknown as TenantSettings | undefined;
|
||
|
||
if (row) {
|
||
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, row.$id, parsed.data);
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "update",
|
||
entityType: "tenant_settings",
|
||
entityId: row.$id,
|
||
changes: parsed.data,
|
||
});
|
||
} else {
|
||
const created = await tablesDB.createRow(
|
||
DATABASE_ID,
|
||
TABLES.tenantSettings,
|
||
ID.unique(),
|
||
{ tenantId: ctx.tenantId, ...parsed.data },
|
||
[
|
||
Permission.read(Role.team(ctx.tenantId)),
|
||
Permission.update(Role.team(ctx.tenantId, "owner")),
|
||
Permission.update(Role.team(ctx.tenantId, "admin")),
|
||
Permission.delete(Role.team(ctx.tenantId, "owner")),
|
||
],
|
||
);
|
||
await logAudit({
|
||
tenantId: ctx.tenantId,
|
||
userId: ctx.user.id,
|
||
action: "create",
|
||
entityType: "tenant_settings",
|
||
entityId: created.$id,
|
||
changes: parsed.data,
|
||
});
|
||
}
|
||
} catch (e) {
|
||
return { ok: false, error: appwriteError(e) };
|
||
}
|
||
|
||
// Tenant name shows up everywhere — revalidate broadly
|
||
revalidatePath("/", "layout");
|
||
return { ok: true };
|
||
}
|