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.
Reduced the patient record to the minimum a dental clinic actually needs:
just a code, optional first/last name and free-text notes. Phone and
date-of-birth fields are gone from the UI everywhere — Add form, edit
dialog inside the table, the Bağlantı Bilgileri block on job detail, and
the table column list. The patient list now surfaces 'Notlar' instead.
Backend
- DB: firstName and lastName columns set to required=false via Appwrite
MCP (tables_db_update_string_column). Existing rows untouched.
- schema.ts Patient interface: firstName/lastName now optional, phone
and dateOfBirth removed from the type entirely. The underlying columns
are still in the DB so legacy rows aren't broken — we just stop
referencing them in code.
- validation/patient.ts: firstName/lastName drop min(1), phone and dob
fields removed.
- patient-actions.ts: pickFields no longer reads phone/dob, create and
update payloads no longer write them.
UI fallbacks
- PatientsTable: header has 'Notlar' instead of Telefon/Doğum. Ad Soyad
cell shows the joined name or em-dash. Edit dialog mirrors the same
simplified form.
- jobs/[jobId] detail page: when patient row has neither name, the page
title falls back to 'Hasta {patientCode}' (same as before for jobs
without a linked patient). The Hasta Bilgileri card now shows Ad Soyad
and Patient Code side by side, with notes spanning both columns.
CSS grid tracks named '1fr' have an implicit 'min-width: auto' that defers
to the child's intrinsic minimum content size. With wide tables inside
those tracks (Ürünler, Hastalar, dashboard recent jobs) that minimum was
the full table width, so the column blew past its share of the row and
the table's overflow-x-auto wrapper never got a chance to do its job —
the whole page scrolled horizontally instead.
Switched the offending tracks to 'minmax(0,1fr)' which lets them collapse
to zero and lets the table primitive's own overflow handle horizontal
scroll inside the cell as designed.
Touched:
- /products grid-cols-[1fr_360px] → [minmax(0,1fr)_360px]
- /patients grid-cols-[1fr_360px] → [minmax(0,1fr)_360px]
- /dashboard grid-cols-[2fr_1fr] → [minmax(0,2fr)_minmax(0,1fr)]
Small inline forms ([1fr_auto], [1fr_100px]) were left as-is — they don't
host tables.
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.