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:
egecankomur
2026-05-05 04:37:04 +03:00
commit 37679e83e6
383 changed files with 53525 additions and 0 deletions
+106
View File
@@ -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 };
}