diff --git a/src/app/(dashboard)/patients/[patientId]/page.tsx b/src/app/(dashboard)/patients/[patientId]/page.tsx new file mode 100644 index 0000000..7faae1c --- /dev/null +++ b/src/app/(dashboard)/patients/[patientId]/page.tsx @@ -0,0 +1,168 @@ +import Link from "next/link"; +import { notFound, redirect } from "next/navigation"; +import { ArrowLeft, Plus } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DueBadge } from "@/components/due-badge"; +import { + JOB_STATUS_LABELS, + PROSTHETIC_TYPE_LABELS, +} from "@/lib/appwrite/job-types"; +import { getPatient, listPatientJobs } from "@/lib/appwrite/patient-queries"; +import type { JobStatus } from "@/lib/appwrite/schema"; +import { requireTenant, requireTenantKind } from "@/lib/appwrite/tenant-guard"; + +export const metadata = { + title: "DLS — Hasta", +}; + +const dateFormatter = new Intl.DateTimeFormat("tr-TR", { + day: "2-digit", + month: "2-digit", + year: "numeric", +}); + +function statusVariant(s: JobStatus): "default" | "secondary" | "outline" | "destructive" { + if (s === "delivered") return "default"; + if (s === "sent" || s === "in_progress") return "secondary"; + if (s === "cancelled") return "destructive"; + return "outline"; +} + +export default async function PatientDetailPage({ + params, +}: { + params: Promise<{ patientId: string }>; +}) { + const { patientId } = await params; + + let ctx; + try { + ctx = await requireTenant(); + requireTenantKind(ctx, ["clinic"]); + } catch { + redirect("/dashboard"); + } + + const patient = await getPatient(patientId, ctx.tenantId); + if (!patient) notFound(); + + const jobs = await listPatientJobs(patient.$id, patient.patientCode, ctx.tenantId); + + const fullName = + [patient.firstName, patient.lastName].filter(Boolean).join(" ") || + `Hasta ${patient.patientCode}`; + + return ( +
+
+
+

+ {patient.patientCode} +

+

{fullName}

+ {patient.archived && ( + + Arşivlenmiş + + )} +
+ +
+ + {patient.notes && ( + + + Notlar + + +

+ {patient.notes} +

+
+
+ )} + + + + İş Geçmişi + + {jobs.length === 0 + ? "Bu hastaya ait iş kaydı yok." + : `${jobs.length} iş`} + + + + {jobs.length === 0 ? ( +

+ Henüz bu hasta için iş gönderilmemiş. +

+ ) : ( + + + + Tarih + Tür + Üye + Durum + Termin + Detay + + + + {jobs.map((j) => ( + + + {dateFormatter.format(new Date(j.$createdAt))} + + + {PROSTHETIC_TYPE_LABELS[j.prostheticType] ?? j.prostheticType} + + {j.memberCount} + + + {JOB_STATUS_LABELS[j.status]} + + + + + + + + + + ))} + +
+ )} +
+
+ +
+ +
+
+ ); +} diff --git a/src/app/(dashboard)/patients/components/patients-table.tsx b/src/app/(dashboard)/patients/components/patients-table.tsx index 19ee04e..ba0c419 100644 --- a/src/app/(dashboard)/patients/components/patients-table.tsx +++ b/src/app/(dashboard)/patients/components/patients-table.tsx @@ -1,6 +1,7 @@ "use client"; import { useActionState, useEffect, useState, useTransition } from "react"; +import Link from "next/link"; import { Archive, ArchiveRestore, Loader2, Pencil } from "lucide-react"; import { toast } from "sonner"; @@ -83,11 +84,17 @@ function PatientRow({ row }: { row: Patient }) { return ( - {row.patientCode} + + + {row.patientCode} + + - {[row.firstName, row.lastName].filter(Boolean).join(" ") || ( - - )} + + {[row.firstName, row.lastName].filter(Boolean).join(" ") || ( + + )} + {row.notes || "—"} diff --git a/src/lib/appwrite/patient-queries.ts b/src/lib/appwrite/patient-queries.ts index 7d33450..e9e41dc 100644 --- a/src/lib/appwrite/patient-queries.ts +++ b/src/lib/appwrite/patient-queries.ts @@ -2,7 +2,7 @@ import "server-only"; import { Query } from "node-appwrite"; -import { DATABASE_ID, TABLES, type Patient } from "./schema"; +import { DATABASE_ID, TABLES, type Job, type Patient } from "./schema"; import { createAdminClient } from "./server"; import { toPlain } from "./serialize"; @@ -44,3 +44,47 @@ export async function getPatient( return null; } } + +/** + * Every job linked to this patient — by explicit patientId on newer jobs, + * or by matching patientCode on legacy rows that pre-date the relation + * (we still want to surface that history). + */ +export async function listPatientJobs( + patientId: string, + patientCode: string, + clinicTenantId: string, +): Promise { + const { tablesDB } = createAdminClient(); + const [byId, byCode] = await Promise.all([ + tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.jobs, + queries: [ + Query.equal("clinicTenantId", clinicTenantId), + Query.equal("patientId", patientId), + Query.orderDesc("$createdAt"), + Query.limit(200), + ], + }), + tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.jobs, + queries: [ + Query.equal("clinicTenantId", clinicTenantId), + Query.equal("patientCode", patientCode), + Query.orderDesc("$createdAt"), + Query.limit(200), + ], + }), + ]); + const seen = new Set(); + const merged: Job[] = []; + for (const row of [...byId.rows, ...byCode.rows] as unknown as Job[]) { + if (seen.has(row.$id)) continue; + seen.add(row.$id); + merged.push(row); + } + merged.sort((a, b) => (a.$createdAt < b.$createdAt ? 1 : -1)); + return toPlain(merged); +}