Commit Graph

10 Commits

Author SHA1 Message Date
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 5fbc0a3c95 fix(upload): two-phase UI — uploading bar then 'processing' spinner
XHR's upload.progress event only tracks browser→server byte transfer. It
fires 100% the moment the multipart body has been pushed, but at that
point our route handler hasn't even started streaming the files into
Appwrite Storage yet. A 200MB upload completes the network phase fast,
then sits 30-60 seconds while the server does sequential storage.createFile
+ tablesDB.createRow per file. The UI was stuck on '100%' the whole time,
making it look frozen.

Switched the form to a discriminated phase state:
  idle → uploading (real bytes %) → processing (full bar + spinner)
       → idle (success/fail)

Listening on xhr.upload's own load event flips us to 'processing' the
instant the body is done. The outer xhr.load fires when the route handler
responds with JSON — that's when we toast + router.refresh().

User now sees: bar fills, then 'Sunucu Appwrite'a yazıyor — büyük dosyalar
30-60 sn sürebilir' message until the response comes back. No more
mystery wait.
2026-05-21 21:38:54 +03:00
kovakmedya 4186d95447 feat(upload): bump per-file cap to 200MB end-to-end
Cluster: Appwrite container _APP_STORAGE_LIMIT 30000000 → 209715200
  (200MB) in /root/services/appwrite/.env on kovaksoft-coolify, then
  docker compose up -d to roll the worker pool with the new value.
  Backup of the .env left at .env.bak.<date>.

Bucket: job-files maximumFileSize updated to 209715200 via Appwrite MCP
  (storage_update_bucket).

App: MAX_FILE_BYTES in both the upload API route and the original server
  action raised to 200MB. Client-side panel guard relaxed accordingly —
  one large file is now allowed to fill the entire batch (the 200MB
  proxy/serverActions cap is the bottleneck, not the per-file rule).
  Error copy updated.

isletmem and any other tenants on the cluster also get the new limit,
which is the desired behaviour — old 30MB ceiling was a relic of an
Appwrite default that no DLS workflow can actually live with.
2026-05-21 21:24:11 +03:00
kovakmedya c990a177eb feat(upload): 200mb cap + API route with XHR progress
- next.config: serverActions.bodySizeLimit + experimental.proxyClientMaxBodySize
  bumped from 500mb back down to 200mb. Batch ceiling (client side) is 180mb
  to stay comfortably under the proxy cap.
- New POST /api/jobs/[jobId]/files endpoint replaces the server action for
  upload. Same auth/permissions/rollback semantics, but Returns JSON so the
  client can read the response. Server action is retained for delete only.
- JobFilesPanel switched from useActionState to XMLHttpRequest.upload —
  xhr.upload.onprogress feeds a Progress bar (real bytes, not a fake
  ticker). Cancel button aborts the in-flight request. Successful upload
  triggers router.refresh() to repopulate the file list.

Server actions can't expose upload progress (no streaming feedback in the
RSC protocol yet), so any progress UX needs to go through fetch/XHR
against a route handler. Trade-off accepted.
2026-05-21 21:18:51 +03:00
kovakmedya ad6de29115 fix(upload): use correct experimental.proxyClientMaxBodySize key + client-side size guard
The previous attempt put 'middlewareClientMaxBodySize' at the top level of
NextConfig — Next 16.1 rejected it as an unrecognised key. Inspecting the
shipped config schema (node_modules/next/dist/server/config-schema.js)
revealed the option lives under experimental and was renamed to
'proxyClientMaxBodySize' when middleware.ts → proxy.ts; the old name is
still accepted but deprecated. Switched to the new name and confirmed Next
now lists it in the Experiments banner at boot.

While we were at it the cap was bumped to 500MB (was 100MB) so batch
uploads have headroom over the 30MB-per-file bucket limit. Added a
client-side pre-flight in JobFilesPanel: rejects individual files >30MB
and total batches >400MB *before* hitting the server, with inline error
messaging instead of a cryptic 'Unexpected end of form' bounce.

Also raised serverActions.bodySizeLimit to 500mb to match.
2026-05-21 21:15:36 +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
kovakmedya cb150f7a24 init: lab project bootstrapped from isletmem-kovakcrm
- CRM domain modules removed (customers, services, software, calendar, tasks, invoices, leads, finance, etc.)
- DLS branding: package name=lab, logo wordmark, sidebar nav, header CTA
- Tenant layer extended with kind dimension (lab|clinic) + requireTenantKind helper
- Schema rewritten for DLS domain: jobs, job_files, job_status_history, prosthetics, connections, finance_entries, notifications
- Onboarding form: clinic/lab account-type selection + auto-generated memberNumber
- Placeholder routes for jobs/{inbound,outbound,new}, products, finance, connections
- PDF spec + spec.md under belgeler/
- db: lab database + 13 collections + indexes + storage bucket (job-files) provisioned via Appwrite MCP

Ref: belgeler/dls-ui-tasarim.pdf
2026-05-21 18:28:38 +03:00