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,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 };
|
||||
}
|
||||
Reference in New Issue
Block a user