import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { Query } from "node-appwrite"; import { Badge } from "@/components/ui/badge"; import { DueBadge } from "@/components/due-badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { listJobFiles } from "@/lib/appwrite/job-file-queries"; import { listJobHistory } from "@/lib/appwrite/job-history-queries"; import { getPatient } from "@/lib/appwrite/patient-queries"; import { toPlain } from "@/lib/appwrite/serialize"; import { JOB_LOCATION_LABELS, JOB_STATUS_LABELS, JOB_STEP_LABELS, JOB_STEP_ORDER, PROSTHETIC_TYPE_LABELS, } from "@/lib/appwrite/job-types"; import { createAdminClient } from "@/lib/appwrite/server"; import { DATABASE_ID, TABLES, type Job, type TenantSettings } from "@/lib/appwrite/schema"; import { requireTenant } from "@/lib/appwrite/tenant-guard"; import { JobActionsPanel } from "./components/job-actions-panel"; import { JobFilesPanel } from "./components/job-files-panel"; export const metadata = { title: "DLS — İş Detay", }; const dateFormatter = new Intl.DateTimeFormat("tr-TR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); function formatMoney(amount: number, currency: string) { try { return new Intl.NumberFormat("tr-TR", { style: "currency", currency }).format(amount); } catch { return `${amount.toFixed(2)} ${currency}`; } } export default async function JobDetailPage({ params, }: { params: Promise<{ jobId: string }>; }) { const { jobId } = await params; let ctx; try { ctx = await requireTenant(); } catch { redirect("/onboarding"); } const { tablesDB } = createAdminClient(); let job: Job; try { const row = await tablesDB.getRow(DATABASE_ID, TABLES.jobs, jobId); job = toPlain(row as unknown as Job); } catch { notFound(); } if (job.clinicTenantId !== ctx.tenantId && job.labTenantId !== ctx.tenantId) { notFound(); } const counterpartId = job.clinicTenantId === ctx.tenantId ? job.labTenantId : job.clinicTenantId; const counterpartLabel = job.clinicTenantId === ctx.tenantId ? "Laboratuvar" : "Klinik"; const counterpartRes = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.tenantSettings, queries: [Query.equal("tenantId", counterpartId), Query.limit(1)], }); const counterpart = counterpartRes.rows[0] as unknown as TenantSettings | undefined; const [history, files] = await Promise.all([ listJobHistory(jobId), listJobFiles(jobId), ]); const currentStepIdx = job.currentStep ? JOB_STEP_ORDER.indexOf(job.currentStep) : -1; const side = job.clinicTenantId === ctx.tenantId ? "clinic" : "lab"; // Patient record only resolves on the clinic side — labs see the code only. const patient = side === "clinic" && job.patientId ? await getPatient(job.patientId, ctx.tenantId) : null; return (

{counterpartLabel}: {counterpart?.companyName ?? "—"}

{(() => { const name = [patient?.firstName, patient?.lastName].filter(Boolean).join(" "); return name || `Hasta ${job.patientCode}`; })()}

{patient && (patient.firstName || patient.lastName) && ( <> {job.patientCode} ·{" "} )} {PROSTHETIC_TYPE_LABELS[job.prostheticType]} · {job.memberCount} üye

{JOB_STATUS_LABELS[job.status]}
İş Bilgileri {dateFormatter.format(new Date(job.$createdAt))} {job.color || "—"} {job.dueDate ? dateFormatter.format(new Date(job.dueDate)) : "—"} {typeof job.price === "number" ? formatMoney(job.price, job.currency || "TRY") : Lab tarafından belirlenecek} {job.currentStep ? JOB_STEP_LABELS[job.currentStep] : "—"} {job.status === "pending" ? "Klinikte (lab teslim alacak)" : job.status === "delivered" ? "Hasta'ya teslim edildi" : job.status === "cancelled" ? "İptal" : JOB_LOCATION_LABELS[job.location ?? "at_lab"]}

Dişler ({job.teeth?.length ?? job.memberCount})

{job.teeth && job.teeth.length > 0 ? job.teeth.join(", ") : `${job.memberCount} üye (diş listesi yok)`}

Açıklama

{job.description || "—"}

Aşamalar Ölçü → Alt Yapı → Üst Yapı → Cila/Bitim
    {JOB_STEP_ORDER.map((step, idx) => { const done = currentStepIdx > idx || job.status === "delivered"; const active = currentStepIdx === idx && job.status !== "delivered"; return (
  1. {idx + 1} {JOB_STEP_LABELS[step]}
  2. ); })}

Aşama güncelleme ve dosya yükleme sonraki sürümde.

{patient && ( Hasta Bilgileri Bu alan yalnızca kliniğinize görünür — laboratuvar hasta kodu dışında bir veri görmez. {[patient.firstName, patient.lastName].filter(Boolean).join(" ") || "—"} {patient.patientCode} {patient.notes && (

Notlar

{patient.notes}

)}
)} Taranan Dosyalar ve Görseller Hem klinik hem laboratuvar dosya yükleyip indirebilir. Akış Geçmişi İşin aşama transition'ları, kim yaptı ve hangi notla. {history.length === 0 ? (

Henüz aşama tamamlanmadı.

) : (
    {history.map((h) => { const isRevision = h.note?.startsWith("[Düzeltme talebi]"); return (
  1. {JOB_STEP_LABELS[h.step]} {isRevision && ( Düzeltme talebi )} {dateFormatter.format(new Date(h.completedAt))}
    {h.note && (

    {h.note.replace(/^\[Düzeltme talebi\]\s*/, "")}

    )}
  2. ); })}
)}
); } function Info({ label, children }: { label: string; children: React.ReactNode }) { return (

{label}

{children}

); }