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);
+}