feat: emlak CRM iskelet kurulumu

- schema.ts tamamen yeniden yazıldı (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings)
- Sidebar emlak modüllerine güncellendi (İlanlar, Müşteriler, Yatırımcılar, Sunumlar, Aktiviteler)
- Eski CRM lib dosyaları temizlendi (finance, invoice, lead, task, software, vs.)
- Yeni modül dizinleri oluşturuldu (stub pages)
- command-search emlak navigasyonuna güncellendi
- site-header temizlendi
- Typecheck: 0 hata (chart.tsx template hariç)
This commit is contained in:
egecankomur
2026-05-05 11:43:29 +03:00
parent 37679e83e6
commit 2f17c342ca
172 changed files with 422 additions and 23862 deletions
-203
View File
@@ -1,203 +0,0 @@
"use server";
import { revalidatePath } from "next/cache";
import { AppwriteException, ID, Permission, Role } from "node-appwrite";
import { z } from "zod";
import { logAudit } from "./audit";
import {
isPlanLimitError,
planLimitMessage,
requirePlanCapacity,
} from "./plan-limits";
import { DATABASE_ID, TABLES, type Customer } from "./schema";
import { createAdminClient } from "./server";
import { requireTenant } from "./tenant-guard";
import type { CustomerActionState } from "./customer-types";
import { customerSchema } from "@/lib/validation/customers";
function appwriteError(e: unknown): string {
if (e instanceof AppwriteException) {
return e.message || "Beklenmeyen bir hata oluştu.";
}
return "Bağlantı hatası. Tekrar deneyin.";
}
function pickFormFields(formData: FormData) {
return {
name: String(formData.get("name") ?? "").trim(),
email: String(formData.get("email") ?? "").trim(),
phone: String(formData.get("phone") ?? "").trim(),
taxId: String(formData.get("taxId") ?? "").trim(),
address: String(formData.get("address") ?? "").trim(),
notes: String(formData.get("notes") ?? "").trim(),
status: (formData.get("status") as "active" | "passive" | null) ?? "active",
};
}
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 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")),
];
}
export async function createCustomerAction(
_prev: CustomerActionState,
formData: FormData,
): Promise<CustomerActionState> {
let ctx;
try {
ctx = await requireTenant();
} catch {
return { ok: false, error: "Yetkiniz yok." };
}
const parsed = customerSchema.safeParse(pickFormFields(formData));
if (!parsed.success) {
return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
}
try {
await requirePlanCapacity(ctx, "customers");
} 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 row = await tablesDB.createRow(
DATABASE_ID,
TABLES.customers,
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: "customer",
entityId: row.$id,
changes: parsed.data,
});
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/customers");
return { ok: true };
}
export async function updateCustomerAction(
_prev: CustomerActionState,
formData: FormData,
): Promise<CustomerActionState> {
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 = customerSchema.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.customers,
id,
)) as unknown as Customer;
if (existing.tenantId !== ctx.tenantId) {
return { ok: false, error: "Erişim engellendi." };
}
await tablesDB.updateRow(DATABASE_ID, TABLES.customers, id, parsed.data);
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "update",
entityType: "customer",
entityId: id,
changes: parsed.data,
});
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/customers");
return { ok: true };
}
export async function deleteCustomerAction(formData: FormData): Promise<CustomerActionState> {
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.customers,
id,
)) as unknown as Customer;
if (existing.tenantId !== ctx.tenantId) {
return { ok: false, error: "Erişim engellendi." };
}
await tablesDB.deleteRow(DATABASE_ID, TABLES.customers, id);
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "delete",
entityType: "customer",
entityId: id,
changes: { name: existing.name },
});
} catch (e) {
return { ok: false, error: appwriteError(e) };
}
revalidatePath("/customers");
return { ok: true };
}