Files
lab/src/app/(dashboard)/patients/[patientId]/page.tsx
T
kovakmedya 88a42c9d06 feat(patients): detail page with full job history
Clinics had no way to look up 'what have we made for this patient
before'. The patient row showed in the list and edit dialog, but no
deeper page. Added /patients/[id] with the patient header, notes card
and a 'İş Geçmişi' table that's chronological.

  - listPatientJobs(patientId, patientCode, clinicTenantId) merges two
    queries — explicit patientId match (new jobs) and patientCode
    match (legacy rows from before we had the relation). Dedupes by
    $id and sorts createdAt desc. Returns plain.
  - /patients/[patientId]/page.tsx (clinic-only via requireTenantKind):
    notFound on missing/foreign rows, header shows code + full name +
    Arşivlenmiş badge, 'Bu hastaya yeni iş' shortcut into /jobs/new,
    history table with date + type + member count + status badge +
    due badge + a 'Aç' button per row.
  - Patient list rows now link both the protocol code and the name
    cells to /patients/[$id] so clinics can click straight in. The
    edit/archive controls stay on the row trailing edge as before.
2026-05-22 16:10:20 +03:00

169 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className="flex-1 space-y-6 px-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<p className="text-muted-foreground text-sm font-mono">
{patient.patientCode}
</p>
<h1 className="text-2xl font-bold tracking-tight">{fullName}</h1>
{patient.archived && (
<Badge variant="outline" className="w-fit">
Arşivlenmiş
</Badge>
)}
</div>
<Button asChild>
<Link href="/jobs/new">
<Plus className="size-4" />
Bu hastaya yeni
</Link>
</Button>
</div>
{patient.notes && (
<Card>
<CardHeader>
<CardTitle>Notlar</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground whitespace-pre-wrap text-sm">
{patient.notes}
</p>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>İş Geçmişi</CardTitle>
<CardDescription>
{jobs.length === 0
? "Bu hastaya ait iş kaydı yok."
: `${jobs.length}`}
</CardDescription>
</CardHeader>
<CardContent>
{jobs.length === 0 ? (
<p className="text-muted-foreground py-6 text-center text-sm">
Henüz bu hasta için gönderilmemiş.
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Tarih</TableHead>
<TableHead>Tür</TableHead>
<TableHead>Üye</TableHead>
<TableHead>Durum</TableHead>
<TableHead>Termin</TableHead>
<TableHead className="text-right">Detay</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{jobs.map((j) => (
<TableRow key={j.$id}>
<TableCell className="text-muted-foreground text-xs">
{dateFormatter.format(new Date(j.$createdAt))}
</TableCell>
<TableCell className="text-muted-foreground">
{PROSTHETIC_TYPE_LABELS[j.prostheticType] ?? j.prostheticType}
</TableCell>
<TableCell className="tabular-nums">{j.memberCount}</TableCell>
<TableCell>
<Badge variant={statusVariant(j.status)}>
{JOB_STATUS_LABELS[j.status]}
</Badge>
</TableCell>
<TableCell>
<DueBadge job={j} />
</TableCell>
<TableCell className="text-right">
<Button asChild size="sm" variant="outline">
<Link href={`/jobs/${j.$id}`}></Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
<div>
<Button asChild variant="outline">
<Link href="/patients">
<ArrowLeft className="size-4" />
Hasta listesine dön
</Link>
</Button>
</div>
</div>
);
}