"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 { isPlanLimitError, planLimitMessage, requirePlanCapacity, } from "./plan-limits"; import { DATABASE_ID, TABLES, type CustomerSoftware, type Software, } from "./schema"; import { createAdminClient } from "./server"; import { requireTenant } from "./tenant-guard"; import type { SoftwareActionState } from "./software-types"; import { customerSoftwareSchema, softwareSchema } from "@/lib/validation/software"; function appwriteError(e: unknown): string { if (e instanceof AppwriteException) { return e.message || "Beklenmeyen bir hata oluştu."; } return "Bağlantı hatası. Tekrar deneyin."; } function flattenErrors(err: z.ZodError): Record { const out: Record = {}; 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")), ]; } // -------------------- Software (catalog) -------------------- function pickSoftwareFields(formData: FormData) { return { name: String(formData.get("name") ?? "").trim(), version: String(formData.get("version") ?? "").trim(), description: String(formData.get("description") ?? "").trim(), defaultFee: String(formData.get("defaultFee") ?? ""), }; } export async function createSoftwareAction( _prev: SoftwareActionState, formData: FormData, ): Promise { let ctx; try { ctx = await requireTenant(); } catch { return { ok: false, error: "Yetkiniz yok." }; } const parsed = softwareSchema.safeParse(pickSoftwareFields(formData)); if (!parsed.success) { return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) }; } try { await requirePlanCapacity(ctx, "software"); } 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.software, 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: "software", entityId: row.$id, changes: parsed.data, }); } catch (e) { return { ok: false, error: appwriteError(e) }; } revalidatePath("/software"); return { ok: true }; } export async function updateSoftwareAction( _prev: SoftwareActionState, formData: FormData, ): Promise { 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 = softwareSchema.safeParse(pickSoftwareFields(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.software, id, )) as unknown as Software; if (existing.tenantId !== ctx.tenantId) { return { ok: false, error: "Erişim engellendi." }; } await tablesDB.updateRow(DATABASE_ID, TABLES.software, id, parsed.data); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "software", entityId: id, changes: parsed.data, }); } catch (e) { return { ok: false, error: appwriteError(e) }; } revalidatePath("/software"); return { ok: true }; } export async function deleteSoftwareAction(formData: FormData): Promise { 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.software, id, )) as unknown as Software; if (existing.tenantId !== ctx.tenantId) { return { ok: false, error: "Erişim engellendi." }; } // Detach from all customer_software rows first const assignments = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.customerSoftware, queries: [ Query.equal("tenantId", ctx.tenantId), Query.equal("softwareId", id), Query.limit(500), ], }); for (const row of assignments.rows) { await tablesDB.deleteRow(DATABASE_ID, TABLES.customerSoftware, row.$id); } await tablesDB.deleteRow(DATABASE_ID, TABLES.software, id); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "delete", entityType: "software", entityId: id, changes: { name: existing.name, detachedAssignments: assignments.rows.length }, }); } catch (e) { return { ok: false, error: appwriteError(e) }; } revalidatePath("/software"); return { ok: true }; } // -------------------- customer_software (assignments) -------------------- function pickAssignmentFields(formData: FormData) { return { customerId: String(formData.get("customerId") ?? ""), softwareId: String(formData.get("softwareId") ?? ""), startDate: String(formData.get("startDate") ?? ""), endDate: String(formData.get("endDate") ?? ""), fee: String(formData.get("fee") ?? ""), billingPeriod: (formData.get("billingPeriod") as "monthly" | "yearly" | "onetime" | null) ?? "monthly", notes: String(formData.get("notes") ?? "").trim(), }; } function toIsoDate(v?: string): string | undefined { if (!v) return undefined; // input type=date sends YYYY-MM-DD; Appwrite expects ISO with timezone if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`; return v; } export async function createAssignmentAction( _prev: SoftwareActionState, formData: FormData, ): Promise { let ctx; try { ctx = await requireTenant(); } catch { return { ok: false, error: "Yetkiniz yok." }; } const parsed = customerSoftwareSchema.safeParse(pickAssignmentFields(formData)); if (!parsed.success) { return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) }; } try { const { tablesDB } = createAdminClient(); const data = { ...parsed.data, startDate: toIsoDate(parsed.data.startDate), endDate: toIsoDate(parsed.data.endDate), }; const row = await tablesDB.createRow( DATABASE_ID, TABLES.customerSoftware, ID.unique(), { tenantId: ctx.tenantId, createdBy: ctx.user.id, ...data, }, teamRowPermissions(ctx.tenantId), ); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "create", entityType: "customer_software", entityId: row.$id, changes: data, }); } catch (e) { return { ok: false, error: appwriteError(e) }; } revalidatePath("/software"); return { ok: true }; } export async function updateAssignmentAction( _prev: SoftwareActionState, formData: FormData, ): Promise { 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 = customerSoftwareSchema.safeParse(pickAssignmentFields(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.customerSoftware, id, )) as unknown as CustomerSoftware; if (existing.tenantId !== ctx.tenantId) { return { ok: false, error: "Erişim engellendi." }; } const data = { ...parsed.data, startDate: toIsoDate(parsed.data.startDate), endDate: toIsoDate(parsed.data.endDate), }; await tablesDB.updateRow(DATABASE_ID, TABLES.customerSoftware, id, data); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "customer_software", entityId: id, changes: data, }); } catch (e) { return { ok: false, error: appwriteError(e) }; } revalidatePath("/software"); return { ok: true }; } export async function deleteAssignmentAction(formData: FormData): Promise { 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.customerSoftware, id, )) as unknown as CustomerSoftware; if (existing.tenantId !== ctx.tenantId) { return { ok: false, error: "Erişim engellendi." }; } await tablesDB.deleteRow(DATABASE_ID, TABLES.customerSoftware, id); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "delete", entityType: "customer_software", entityId: id, }); } catch (e) { return { ok: false, error: appwriteError(e) }; } revalidatePath("/software"); return { ok: true }; }