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ı
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"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 };
|
||||
}
|
||||
Reference in New Issue
Block a user