"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 Patient } from "./schema"; import { createAdminClient } from "./server"; import { requireRole, requireTenant, requireTenantKind } from "./tenant-guard"; import type { PatientActionState, PatientFormState, } from "./patient-types"; import { patientSchema } from "@/lib/validation/patient"; const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; function appwriteError(e: unknown, fallback = "Beklenmeyen bir hata oluştu."): string { if (e instanceof AppwriteException) return e.message || fallback; return process.env.NODE_ENV !== "production" && e instanceof Error ? `${fallback} (${e.message})` : fallback; } 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 pickFields(formData: FormData) { return { patientCode: String(formData.get("patientCode") ?? "").trim(), firstName: String(formData.get("firstName") ?? "").trim(), lastName: String(formData.get("lastName") ?? "").trim(), phone: String(formData.get("phone") ?? "").trim(), dateOfBirth: String(formData.get("dateOfBirth") ?? "").trim(), notes: String(formData.get("notes") ?? "").trim(), }; } function patientPermissions(clinicTenantId: string): string[] { return [ Permission.read(Role.team(clinicTenantId)), Permission.update(Role.team(clinicTenantId, "owner")), Permission.update(Role.team(clinicTenantId, "admin")), Permission.update(Role.team(clinicTenantId, "member")), Permission.delete(Role.team(clinicTenantId, "owner")), Permission.delete(Role.team(clinicTenantId, "admin")), ]; } function generateCode(): string { let out = ""; for (let i = 0; i < 6; i++) { out += CODE_ALPHABET[Math.floor(Math.random() * CODE_ALPHABET.length)]; } return out; } async function reserveUniqueCode(clinicTenantId: string): Promise { const { tablesDB } = createAdminClient(); for (let attempt = 0; attempt < 8; attempt++) { const candidate = generateCode(); const existing = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.patients, queries: [ Query.equal("clinicTenantId", clinicTenantId), Query.equal("patientCode", candidate), Query.limit(1), ], }); if (existing.total === 0) return candidate; } throw new Error("PATIENT_CODE_GENERATION_FAILED"); } export async function createPatientAction( _prev: PatientFormState, formData: FormData, ): Promise { let ctx; try { ctx = await requireTenant(); requireRole(ctx, ["owner", "admin", "member"]); requireTenantKind(ctx, ["clinic"]); } catch { return { ok: false, error: "Hasta kaydı yalnızca klinik hesapları için." }; } const parsed = patientSchema.safeParse(pickFields(formData)); if (!parsed.success) { return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) }; } const { tablesDB } = createAdminClient(); // If user supplied a code, make sure it isn't already in use within this clinic. let code = parsed.data.patientCode; if (code) { const dup = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.patients, queries: [ Query.equal("clinicTenantId", ctx.tenantId), Query.equal("patientCode", code), Query.limit(1), ], }); if (dup.total > 0) { return { ok: false, error: "Bu protokol no kayıtlı.", fieldErrors: { patientCode: "Bu kod başka bir hasta için kullanılmış." }, }; } } else { code = await reserveUniqueCode(ctx.tenantId); } try { const created = await tablesDB.createRow( DATABASE_ID, TABLES.patients, ID.unique(), { clinicTenantId: ctx.tenantId, createdBy: ctx.user.id, patientCode: code, firstName: parsed.data.firstName, lastName: parsed.data.lastName, phone: parsed.data.phone, dateOfBirth: parsed.data.dateOfBirth, notes: parsed.data.notes, archived: false, }, patientPermissions(ctx.tenantId), ); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "create", entityType: "patient", entityId: created.$id, changes: { patientCode: code, firstName: parsed.data.firstName }, }); revalidatePath("/patients"); return { ok: true, patientId: created.$id }; } catch (e) { return { ok: false, error: appwriteError(e, "Hasta eklenemedi.") }; } } export async function updatePatientAction( _prev: PatientFormState, formData: FormData, ): Promise { const id = String(formData.get("id") ?? "").trim(); if (!id) return { ok: false, error: "Hasta bulunamadı." }; let ctx; try { ctx = await requireTenant(); requireRole(ctx, ["owner", "admin", "member"]); requireTenantKind(ctx, ["clinic"]); } catch { return { ok: false, error: "Yetkiniz yok." }; } const parsed = patientSchema.safeParse(pickFields(formData)); if (!parsed.success) { return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) }; } try { const { tablesDB } = createAdminClient(); const row = (await tablesDB.getRow( DATABASE_ID, TABLES.patients, id, )) as unknown as Patient; if (row.clinicTenantId !== ctx.tenantId) { return { ok: false, error: "Bu hastayı düzenleme yetkiniz yok." }; } // patientCode is intentionally NOT updatable here — re-keying historical // jobs would be confusing. Only firstName/lastName/phone/dateOfBirth/notes. await tablesDB.updateRow(DATABASE_ID, TABLES.patients, id, { firstName: parsed.data.firstName, lastName: parsed.data.lastName, phone: parsed.data.phone, dateOfBirth: parsed.data.dateOfBirth, notes: parsed.data.notes, }); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "patient", entityId: id, changes: parsed.data, }); } catch (e) { return { ok: false, error: appwriteError(e, "Güncellenemedi.") }; } revalidatePath("/patients"); return { ok: true, patientId: id }; } export async function archivePatientAction( _prev: PatientActionState, formData: FormData, ): Promise { const id = String(formData.get("id") ?? "").trim(); if (!id) return { ok: false, error: "Hasta bulunamadı." }; let ctx; try { ctx = await requireTenant(); requireRole(ctx, ["owner", "admin"]); requireTenantKind(ctx, ["clinic"]); } catch { return { ok: false, error: "Yetkiniz yok." }; } try { const { tablesDB } = createAdminClient(); const row = (await tablesDB.getRow( DATABASE_ID, TABLES.patients, id, )) as unknown as Patient; if (row.clinicTenantId !== ctx.tenantId) { return { ok: false, error: "Yetkiniz yok." }; } await tablesDB.updateRow(DATABASE_ID, TABLES.patients, id, { archived: !row.archived, }); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "patient", entityId: id, changes: { archived: !row.archived }, }); } catch (e) { return { ok: false, error: appwriteError(e, "İşlem başarısız.") }; } revalidatePath("/patients"); return { ok: true }; }