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
+171
View File
@@ -0,0 +1,171 @@
"use server";
import { revalidatePath } from "next/cache";
import { ID, Permission, Role } from "node-appwrite";
import { InputFile } from "node-appwrite/file";
import { logAudit } from "./audit";
import type { LogoActionState } from "./logo-types";
import { BUCKETS, DATABASE_ID, TABLES } from "./schema";
import { createAdminClient } from "./server";
import { requireRole, requireTenant } from "./tenant-guard";
const MAX_BYTES = 2 * 1024 * 1024;
const ALLOWED_TYPES = new Set([
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"image/svg+xml",
]);
function teamLogoPermissions(tenantId: string) {
return [
Permission.read(Role.any()),
Permission.update(Role.team(tenantId, "owner")),
Permission.update(Role.team(tenantId, "admin")),
Permission.delete(Role.team(tenantId, "owner")),
Permission.delete(Role.team(tenantId, "admin")),
];
}
export async function uploadLogoAction(
_prev: LogoActionState,
formData: FormData,
): Promise<LogoActionState> {
let ctx;
try {
ctx = await requireTenant();
requireRole(ctx, ["owner", "admin"]);
} catch {
return { ok: false, error: "Logo yüklemek için yönetici yetkisi gerekli." };
}
const file = formData.get("logo");
if (!(file instanceof File) || file.size === 0) {
return { ok: false, error: "Dosya seçin." };
}
if (file.size > MAX_BYTES) {
return { ok: false, error: "Dosya 2MB'dan büyük olamaz." };
}
if (!ALLOWED_TYPES.has(file.type)) {
return { ok: false, error: "Sadece PNG, JPG, WebP veya SVG yükleyebilirsiniz." };
}
if (!ctx.settings) {
return { ok: false, error: "Çalışma alanı ayarları bulunamadı." };
}
const { storage, tablesDB } = createAdminClient();
const previousLogoId = ctx.settings.logo;
let newFileId: string | null = null;
try {
const buffer = Buffer.from(await file.arrayBuffer());
const inputFile = InputFile.fromBuffer(buffer, file.name);
const created = await storage.createFile({
bucketId: BUCKETS.tenantLogos,
fileId: ID.unique(),
file: inputFile,
permissions: teamLogoPermissions(ctx.tenantId),
});
newFileId = created.$id;
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, {
logo: newFileId,
});
if (previousLogoId && previousLogoId !== newFileId) {
try {
await storage.deleteFile({
bucketId: BUCKETS.tenantLogos,
fileId: previousLogoId,
});
} catch {
// best-effort — orphaned file is acceptable, won't block the new logo
}
}
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "update",
entityType: "tenant_logo",
entityId: newFileId,
changes: { previous: previousLogoId ?? null },
});
} catch (e) {
if (newFileId) {
try {
await storage.deleteFile({
bucketId: BUCKETS.tenantLogos,
fileId: newFileId,
});
} catch {
/* ignore cleanup error */
}
}
return {
ok: false,
error: e instanceof Error ? e.message : "Logo yüklenemedi.",
};
}
revalidatePath("/settings/workspace");
revalidatePath("/", "layout");
return { ok: true };
}
export async function removeLogoAction(): Promise<LogoActionState> {
let ctx;
try {
ctx = await requireTenant();
requireRole(ctx, ["owner", "admin"]);
} catch {
return { ok: false, error: "Logo silmek için yönetici yetkisi gerekli." };
}
if (!ctx.settings) {
return { ok: false, error: "Çalışma alanı ayarları bulunamadı." };
}
const previousLogoId = ctx.settings.logo;
if (!previousLogoId) {
return { ok: true };
}
try {
const { storage, tablesDB } = createAdminClient();
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, {
logo: null,
});
try {
await storage.deleteFile({
bucketId: BUCKETS.tenantLogos,
fileId: previousLogoId,
});
} catch {
/* file already gone, fine */
}
await logAudit({
tenantId: ctx.tenantId,
userId: ctx.user.id,
action: "delete",
entityType: "tenant_logo",
entityId: previousLogoId,
});
} catch (e) {
return {
ok: false,
error: e instanceof Error ? e.message : "Logo silinemedi.",
};
}
revalidatePath("/settings/workspace");
revalidatePath("/", "layout");
return { ok: true };
}