Files
lab/src/app/(dashboard)/jobs/new/page.tsx
T
kovakmedya ee9c0015a5 feat(patients): clinic-side patient registry
Clinics get a real patient ledger. Labs see only patientCode — no name,
phone, date of birth, or notes ever cross the team boundary.

Data model
  - New table 'patients' (clinicTenantId, patientCode, firstName, lastName,
    phone?, dateOfBirth?, notes?, archived). Unique index on
    (clinicTenantId, patientCode) so each clinic gets its own code space.
    Fulltext index on (firstName, lastName) for future patient search.
    Row permissions Role.team(clinicTenantId) only — labs literally cannot
    read the rows.
  - jobs.patientId attribute (optional) + key index, references the
    patient row when one exists. patientCode stays denormalised on jobs so
    labs keep a stable identifier without joining patients.

Server
  - createPatientAction: clinic-only, requireTenantKind guard. Protocol no
    is optional; if absent we generate a 6-char unique code (re-roll on
    collision, 8 attempts). Duplicate protocol no within a clinic is
    rejected with a friendly error.
  - updatePatientAction: edits name/phone/dob/notes. patientCode is
    explicitly NOT mutable — re-keying historical jobs would be confusing.
  - archivePatientAction: toggle, preserves history.
  - listPatients / getPatient queries return plain objects via toPlain.

UI
  - /patients page (clinic-only, sidebar nav 'Hastalar', middleware
    protected): table + add form + edit dialog + archive.
  - /jobs/new: patient Select replaces the bare patientCode input. Picking
    a patient locks the patientCode field to that patient's code; falling
    back to 'Hasta listesinde yok — kodu manuel gir' keeps the old free-
    text flow.
  - createJobAction validates patientId ownership and overwrites
    patientCode with the patient's code on the server, so a manipulated
    form can't desync the two.
  - /jobs/[jobId] (clinic side only): adds a 'Hasta Bilgileri' card with
    name/phone/dob/notes and uses the patient's full name as the page
    title. Lab side is unchanged — code only.

The protocol-no / generated-code split matches what the user asked for:
existing patient management software's protocol number flows in directly,
otherwise the system mints one.
2026-05-21 21:54:35 +03:00

77 lines
2.4 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 { redirect } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { listApprovedLabsForClinic } from "@/lib/appwrite/job-queries";
import { listPatients } from "@/lib/appwrite/patient-queries";
import { requireTenant, requireTenantKind } from "@/lib/appwrite/tenant-guard";
import { NewJobForm } from "./components/new-job-form";
export const metadata = {
title: "DLS — Yeni İş Yayınla",
};
export default async function NewJobPage() {
let ctx;
try {
ctx = await requireTenant();
requireTenantKind(ctx, ["clinic"]);
} catch {
redirect("/dashboard");
}
const [labs, patients] = await Promise.all([
listApprovedLabsForClinic(ctx.tenantId),
listPatients(ctx.tenantId, { includeArchived: false }),
]);
const defaultCurrency = ctx.settings?.defaultCurrency ?? "TRY";
return (
<div className="flex-1 space-y-6 px-6">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold tracking-tight">Yeni İş Yayınla</h1>
<p className="text-muted-foreground text-sm">
Bağlı laboratuvarınıza yeni bir protez işi gönderin.
</p>
</div>
{labs.length === 0 ? (
<Card>
<CardHeader>
<CardTitle>Önce bir laboratuvarla bağlantı kurun</CardTitle>
<CardDescription>
İş gönderebilmeniz için onaylanmış bir laboratuvar bağlantınız olmalı.
</CardDescription>
</CardHeader>
<CardContent>
<Button asChild>
<Link href="/connections">Bağlantı Kur</Link>
</Button>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle>İş Bilgileri</CardTitle>
<CardDescription>
Hasta kodu, protez türü ve diğer detayları girin. Dosya yüklemesi sonraki sürümde.
</CardDescription>
</CardHeader>
<CardContent>
<NewJobForm
labs={labs}
patients={patients.map((p) => ({
id: p.$id,
code: p.patientCode,
label: `${p.firstName} ${p.lastName}`,
}))}
defaultCurrency={defaultCurrency}
/>
</CardContent>
</Card>
)}
</div>
);
}