Commit Graph

9 Commits

Author SHA1 Message Date
kovakmedya 94e9dffaef feat(jobs): step-by-step timeline on the detail page
The job_status_history table was already being populated on every
transition; the detail page just rendered a flat list with date only.
Replaced it with a proper vertical timeline:

  - Card title moved from 'Aşama Geçmişi' to 'Akış Geçmişi' since we
    now include side-trips (revision requests), not just forward steps.
  - Vertical guide line with a coloured node per entry: emerald for
    a normal step completion, rose for a revision request. Spotting a
    bounced prova in the history is a glance.
  - Revision rows get an inline 'Düzeltme talebi' pill; the '[Düzeltme
    talebi]' prefix is stripped from the visible note so the actual
    feedback text reads cleanly.
  - Always rendered (with an empty-state line) so the card position
    doesn't move around as the case progresses.
2026-05-22 16:05:07 +03:00
kovakmedya d7d2ac557b feat(jobs): due-date awareness — DueBadge + dashboard 'Geciken İşler' widget
dueDate was sitting on every job but the UI never warned anyone about
it. Added a small badge primitive and surfaced it everywhere a job is
listed plus a dedicated dashboard card.

  - lib/appwrite/due-date.ts: dueState() buckets a job into overdue /
    today / soon (1-3 days) / future / none, with delivered + cancelled
    jobs always resolving to none. dueLabel() returns the Turkish text.
  - components/due-badge.tsx: renders nothing for future/none, a
    secondary badge for today/soon, a destructive badge for overdue.
    Drop-in (job, className).
  - JobsTable (inbound + outbound): new 'Termin' column shows the date
    and the DueBadge stacked. Sorting still on createdAt for now —
    explicit ordering by dueDate will come with the filter task.
  - Job detail header: badge stack now has the DueBadge before the
    status pill so a glance at the page shows whether the case is
    behind schedule.
  - Dashboard: getDashboardData fetches up to 10 overdue jobs in
    parallel (lessThan('dueDate', now), excluding delivered/cancelled).
    Added a destructive-tinted 'Geciken İşler' card above the rest of
    the widgets when the list is non-empty, with quick links into each
    job's detail page.
2026-05-22 16:02:13 +03:00
kovakmedya 479972e9a9 feat(workflow): split job step from location, model back-and-forth between lab and clinic
Real prosthetic production isn't a one-way pipeline — the work moves
between lab and clinic multiple times. After substructure is produced
the lab hands it to the clinic for a fitting, the clinic approves it
back to the lab, the lab builds the superstructure, hands it back for
a second fitting, the clinic approves again, the lab does cila/bitim,
and finally delivers it to the clinic for handover to the patient.

Previously we only had a single 'advance step' action callable by the
lab, which collapsed all of that into a linear forward push and didn't
capture who physically had the work at any given moment.

DB
  - New jobs.location enum (at_clinic | at_lab, default at_clinic).
  - Existing jobs keep working via a 'location ?? at_lab' fallback in
    code; no manual backfill required for the four test rows.

State machine
  - acceptJobAction (lab): pending → in_progress, currentStep=alt_yapi_prova,
    location=at_lab. Skips the implicit 'olcu' production step now that
    accepting the job means the lab has the impression in hand.
  - handToClinicAction (lab, NEW): at_lab → at_clinic, step stays the
    same. If step is cila_bitim, status becomes 'sent' (final delivery)
    and finance sync fires.
  - approveAtClinicAction (clinic, NEW): at_clinic → at_lab, step
    advances to the next stage so the lab knows what to produce next.
  - markDeliveredAction unchanged — clinic confirms the final handoff.
  - advanceStepAction removed; its single forward push doesn't fit the
    new bidirectional flow.

UI
  - JobActionsPanel now picks the right button from the role + status +
    location matrix:
      * Lab + pending          → 'İşleme Al'
      * Lab + in_progress + at_lab + cila_bitim → 'Cila Bitim — Nihai Teslime Gönder'
      * Lab + in_progress + at_lab + other      → '{stage} Provaya Gönder'
      * Clinic + in_progress + at_clinic → '{stage} Provası Tamam'
      * Clinic + sent          → 'Teslim Aldım'
      * Both + pending         → 'İptal Et'
  - Job detail surfaces a new 'Şu An' info row that resolves to a
    human-readable location ('Klinikte', 'Laboratuvarda', 'Hasta'ya
    teslim edildi', ...) so anyone glancing at the page can tell where
    the work physically is.
2026-05-22 01:31:49 +03:00
kovakmedya ca4ea87d37 feat(patients): drop phone/dateOfBirth, name fields optional
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.
2026-05-21 23:01:52 +03:00
kovakmedya 95f2d065b4 feat(pricing): tooth-based selection, lab-owned pricing, clinic-specific overrides
What changed
  - jobs.teeth (FDI string[]). memberCount becomes a derived field (teeth.length).
    A new TeethChart component renders the full permanent dentition as a
    16-column grid for each arch with click-toggle selection.
  - /jobs/new: removed the price + currency inputs and the manual memberCount
    field. Clinics now pick teeth via the chart; the form blocks submission
    until at least one tooth is selected.
  - createJobAction calls a new calculateJobPrice() helper that walks the
    pricing cascade and writes price + currency on the job server-side. A
    clinic-supplied price hidden field would now be ignored — the field
    isn't even in the schema.

Pricing cascade (calculateJobPrice, lib/appwrite/pricing.ts)
  1. clinic_pricing row matching (lab, clinic, type) with customUnitPrice
     → use that flat unit price.
  2. clinic_pricing row with discountPercent → catalog unitPrice × (1-d).
  3. lab's prosthetics catalog row matching type (not archived).
  4. nothing → price stays null; lab can still set it manually later.

Clinic-specific overrides (clinic_pricing table)
  - Unique on (labTenantId, clinicTenantId, prostheticType) so each
    combination has at most one rule.
  - Row permissions: read by both teams (transparency for clinic), write
    only by lab — clinic can see the discount they're getting but cannot
    edit it.
  - setClinicPricingAction validates an approved connection exists before
    creating/updating, and rejects requests where neither customUnitPrice
    nor discountPercent is set.
  - clearClinicPricingAction wipes a rule (catalog price re-applies).

UI
  - /connections 'Bağlantılarım' table gets a new column showing the active
    pricing rules per counterpart. Lab side has a 'Fiyatlandırma' button
    that opens a dialog (PROSTHETIC_TYPE × customPrice|discountPercent form
    + list of active rules with delete). Clinic side is read-only.
  - Job detail: 'Fiyat' field now shows 'Lab tarafından belirlenecek' when
    null, instead of a literal —. Adds a 'Dişler' info block listing the
    selected FDI numbers.
2026-05-21 22:04:26 +03:00
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
kovakmedya f34630de62 fix: serialize Appwrite rows before sending to client components
Next 16's server-to-client serializer rejects values whose prototype is
not plain Object. node-appwrite returns row objects carrying internal
helpers (toString etc.), so every <ClientComponent prop={row}> crashed with
'Only plain objects, and a few built-ins, can be passed to Client
Components from Server Components.'

Added a tiny toPlain helper that JSON-roundtrips any value and applied it
at the boundary of every query that returns rows consumed by 'use client'
files:
  - connection-queries (enrich)
  - job-queries (inbound, outbound, approved labs)
  - job-file-queries (listJobFiles)
  - job-history-queries (listJobHistory)
  - prosthetic-queries (listProsthetics, listActiveProsthetics)
  - finance-queries (listFinanceEntries)
  - notification-helpers (listNotifications)
  - dashboard-queries (getDashboardData)
  - jobs/[jobId] page (direct getRow for the job prop on JobActionsPanel)

Internal Maps inside queries stay live — only the data crossing the
server/client boundary is normalised.
2026-05-21 20:57:59 +03:00
kovakmedya 2c6c074a06 feat: job status/step flow, file upload, finance sync, notifications
Job lifecycle
  - acceptJobAction (lab): pending → in_progress + currentStep=olcu
  - advanceStepAction (lab): step ilerletir, son adım sonrası status=sent
  - markDeliveredAction (clinic): sent → delivered
  - cancelJobAction: pending iş iptali (her iki taraf)
  - job_status_history her step transition'da idempotent kayıt
  - Detay sayfası interactive panel + Aşama Geçmişi kartı

Job files (Appwrite Storage job-files bucket, 30MB/file)
  - uploadJobFilesAction: çoklu dosya, mimeType'tan kind sınıflandırma
    (scan/image/document), her iki team'e read permission, partial-fail
    rollback (storage + row temizliği)
  - deleteJobFileAction: yetkilendirilmiş silme, file + row birlikte
  - JobFilesPanel: client-side select + upload + liste + indir + sil
  - next.config bodySizeLimit 3mb → 100mb (toplu yükleme için)

Finance sync (idempotent)
  - syncFinanceForJob helper: sent/delivered transition'larında klinik
    payable + lab receivable rows (jobId+tenantId+type unique kontrolü,
    her tarafta tek satır garanti)
  - markFinancePaidAction / reopenFinanceAction: manuel ödendi/geri al
  - /finance sayfası: stat kartlar (bekleyen alacak/borç, aylık gelir/gider)
    + hareketler tablosu, role-aware kopyalar
  - Memory rule [[feedback_cross_entity_sync_helpers]]: best-effort, never
    re-throws

Notifications
  - createNotification helper, connection (request/approve) ve job
    (create/accept/sent/delivered) eventlerinde tetikleniyor
  - /notifications sayfası + tek tek / hepsi okundu işaretle
  - Header'a Bell ikonu + okunmamış count badge (layout SSR'de besler)
  - Middleware PROTECTED_PREFIXES'e /notifications ekli
2026-05-21 20:17:33 +03:00
kovakmedya 76e02754b8 feat(modules): connections, products, jobs (list/form/detail-placeholder)
Connections (clinic ↔ lab)
  - request via member number, approve/reject (counterparty), cancel pending,
    delete approved
  - permission rows opened to both teams; audit log for every mutation
  - /connections page: own code card, request form, pending inbound/outbound
    tables, approved connections table with delete confirm

Products (lab catalog)
  - createProstheticAction + update + archive/restore + delete (lab-only)
  - zod validation, dev-mode error surfacing
  - /products page: catalog table + add form + edit dialog. Hidden from
    clinic accounts via requireTenantKind.

Jobs (work orders)
  - createJobAction (clinic-only) — checks approved connection before write,
    permissions opened to both clinic and lab teams
  - listInboundJobs (lab perspective), listOutboundJobs (clinic perspective),
    listApprovedLabsForClinic for the new-job form
  - /jobs/inbound + /jobs/outbound tables with role-aware copy
  - /jobs/new full form (lab select, patient code, prosthetic type, member
    count, color, due date, price/currency, description)
  - /jobs/[jobId] placeholder detail page with stepper visualisation;
    status/step updates and file upload come next session

All new mutations follow the memory rules: schema-checked row payloads,
admin client behind requireTenant + requireRole/requireTenantKind, audit
log calls best-effort, no empty-string Radix Select values.
2026-05-21 19:59:23 +03:00