Commit Graph

32 Commits

Author SHA1 Message Date
kovakmedya 12631cf9c5 perf+fix: file download proxy + drop awaits on audit/notifications/finance sync
Two problems reported by the user:

1. File downloads broken on the lab side.
   The link in JobFilesPanel pointed straight at Appwrite's
   /storage/.../view URL. Storage permissions are scoped to the job's two
   teams, but the browser only has a session cookie for our app domain,
   not for db.kovaksoft.com — so the cross-origin request hit Appwrite
   as a guest and 401'd.

   New /api/jobs/[jobId]/files/[fileId]/download route. requireTenant()
   first, then verify the caller's tenant is one of (clinicTenantId,
   labTenantId) on the parent job, then storage.getFileDownload via the
   admin SDK and stream the buffer back with Content-Disposition:
   attachment so the browser saves it under the original filename.
   listJobFiles now hands out that relative URL instead of the Appwrite
   one — same anchor in the panel, just routed through us.

2. Saves and edits feel slow whenever a notification is involved.
   Every mutation was awaiting logAudit, createNotification and
   syncFinanceForJob in sequence. None of these need to block the user
   response — audit is best-effort logging, notifications are async UX,
   and the finance sync is idempotent and re-runs on the next mutation
   anyway. Switched all 46 call sites across the action modules to
   void-fire-and-forget (matching the pattern we already used in
   clinic-pricing-actions). Net effect: each mutation drops ~3 sequential
   Appwrite roundtrips before the server action returns.
2026-05-22 01:05:25 +03:00
kovakmedya 97a6031992 feat(jobs/new): clinic picks a lab catalog product, not a raw type
What the clinic sees has changed from a fixed 6-item Protez Türü dropdown
to the actual products that lab has published in its catalog. Picking 'Premium
Zirkonyum (1500 TRY / diş)' is now an option; picking the generic
'Zirkonyum' bucket is not.

Wiring
  - DB: jobs.prostheticId string column (optional — legacy jobs stay valid).
    The denormalised prostheticType is still written, sourced server-side
    from the chosen catalog row, so reports/aggregates keep working.
  - validation/job.ts: prostheticId required, prostheticType removed from
    the form payload (computed instead).
  - job-actions.ts: looks up the catalog row, verifies tenantId match +
    not archived, then pulls .type and .unitPrice from it to drive
    calculateJobPriceForProsthetic.

Pricing helper
  - New calculateJobPriceForProsthetic(prosthetic, clinicTenantId,
    teethCount). Reuses the same clinic_pricing cascade
    (custom price wins, otherwise discountPercent off catalog) but skips
    the type-based catalog lookup entirely — it already has the row.
  - /api/pricing/quote rewritten to accept { prostheticId, teethCount },
    still gated on an approved connection between clinic and the lab that
    owns the product.

UI
  - jobs/new page server-loads each connected lab's active prosthetics
    once and hands them to NewJobForm as prostheticsByLab.
  - NewJobForm: 'Ürün *' Select replaces 'Protez Türü *'. The list filters
    by the currently selected lab; switching labs clears the chosen
    product. Each option shows name + 'unitPrice / diş' on the right.
    Once selected, a small caption surfaces the underlying category
    label ('Kategori: Zirkonyum') so the clinic still understands what
    bucket the product falls into.
  - PriceQuoteCard's hasInputs gating moved off prostheticType to
    prostheticId.
2026-05-22 01:01:35 +03:00
kovakmedya dfd30ef239 fix(deploy): pin pnpm to 9.15.9 and regenerate lockfile in 9.x format
Last build hit ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING — the Nixpacks
image bundles corepack 0.24.1 which can't hydrate pnpm 11's binary on
Node 22 (known corepack/pnpm/Node interop bug). Rather than fight the
toolchain, downgrade to the version Coolify's Nixpacks already ships
natively: pnpm 9.15.9.

  packageManager: pnpm@9.15.9
  pnpm-lock.yaml regenerated under 9.15.9 (no patchedDependencies hash
  drift, no settings/* block, lockfile version 9.0)

Verified locally: 'pnpm build' produces the same Next 16 output as
before, and the node-fetch-native-with-agent patch is still applied in
node_modules (globalThis.fetch present in the patched agent.cjs). Coolify
should now do 'pnpm i --frozen-lockfile' without any version mismatch.
2026-05-21 23:44:27 +03:00
kovakmedya c746bc9ecb fix(deploy): pin packageManager to pnpm@11.1.2 so Coolify uses the right pnpm
Last build crashed with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on
patchedDependencies — Nixpacks bundles pnpm 9.15.9 by default, but our
lockfile is in pnpm 11's settings/patchedDependencies format. Adding the
'packageManager' field tells corepack-aware tooling (Nixpacks included)
to install pnpm 11.1.2 in the build container, matching what we use
locally. After this, 'pnpm i --frozen-lockfile' reads the same lockfile
format the project was developed with and the patched dependency
declaration lines up.
2026-05-21 23:41:29 +03:00
kovakmedya 3496ab1919 fix(deploy): gitignore pnpm-workspace.yaml so Coolify's pnpm 9 doesn't choke
pnpm 11 (local) keeps the build-approval list in pnpm-workspace.yaml and
will regenerate the file on every 'pnpm install'. Coolify ships pnpm
9.15.9 which treats any pnpm-workspace.yaml as a monorepo descriptor and
demands a 'packages:' field — without it the build fails immediately with
'packages field missing or empty'.

So the file has to be present locally (pnpm 11 needs it) and absent in
the build context (pnpm 9 must not see it). gitignoring it satisfies both
ends. The build-approval list is also mirrored in package.json's
'pnpm.onlyBuiltDependencies' which pnpm 9 understands, so Coolify still
gets the right behaviour for sharp / unrs-resolver.
2026-05-21 23:39:41 +03:00
kovakmedya 16f4dcfe66 fix(deploy): drop pnpm-workspace.yaml, move config into package.json
Coolify's Nixpacks pulls pnpm 9.15.9, which sees pnpm-workspace.yaml as a
monorepo descriptor and dies with 'packages field missing or empty' the
moment 'pnpm install' starts. The whole reason we kept that file around
was the 'allowBuilds' / 'patchedDependencies' blocks needed by pnpm 11
locally — both have a longstanding equivalent on the 'pnpm' key inside
package.json that pnpm 9 also understands. Consolidated them there and
removed the workspace file entirely.

  package.json
    "pnpm": {
      "onlyBuiltDependencies": ["sharp", "unrs-resolver"],
      "patchedDependencies": {
        "node-fetch-native-with-agent@1.7.2":
          "patches/node-fetch-native-with-agent@1.7.2.patch"
      }
    }

Lockfile regenerated under pnpm 11 locally; install succeeds. The
node-fetch-native-with-agent patch (the Node 26 / undici workaround) is
still applied so Coolify's pnpm install will replicate the fix in the
build container.
2026-05-21 23:39:01 +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 0dea028845 feat(jobs/new): live price quote with discount breakdown for the clinic
Clinics couldn't see what an order would cost them before publishing.
Added a server-priced quote box that updates as the lab, prosthetic type
and tooth selection change. Three rendered states:

  1. Discounted (custom unit price or discountPercent):
       Katalog (4 × 1.500 ₺)      6.000 ₺
       Klinik indirimi (%15)       -900 ₺
       ──────────────────────
       Toplam                    5.100 ₺

  2. Plain catalog match (no clinic override):
       4 × 1.500 ₺               6.000 ₺
       'Katalog fiyatı uygulanıyor'

  3. No catalog row for the chosen type:
       'Fiyat işe başlanırken laboratuvar tarafından belirlenecek'

Bits to make this work
  - calculateJobPrice now returns catalogUnitPrice, catalogTotal, savings
    and teethCount alongside the final amount, so the client can render
    a breakdown without reaching for any other table.
  - POST /api/pricing/quote endpoint guards on clinic kind + an approved
    connection before exposing pricing — no leaking catalog data to
    unconnected clinics, and no per-lab discount snooping.
  - NewJobForm's lab and prosthetic Selects became controlled (hidden
    inputs mirror state to the form action), and a debounced effect
    (250ms) re-fetches the quote when any of {lab, type, teeth.length}
    changes. AbortController cancels in-flight requests when inputs change
    again.
2026-05-21 22:52:31 +03:00
kovakmedya 067e4af440 fix(ui): connections pricing rules inline with commas instead of stacking
The Bağlantılarım table rendered the pricing summary cell as a vertical
<ul> — one rule per line — which doubled or tripled the row height for
labs that priced multiple prosthetic types. User asked for the rules to
sit on a single line, separated by commas.

Switched to an inline <span> with React.Fragment-joined rules, comma
separator, and the truncation suffix stays in the same line.

Truncation rule unchanged: show first 3, then ', +N kural'.
2026-05-21 22:48:22 +03:00
kovakmedya 48361792f0 perf(connections): collapse pricing N+1 into a single bulk query
The /connections page was firing one listClinicPricing call per approved
counterpart inside Promise.all. That meant a lab with 10 clinics paid 10
sequential Appwrite roundtrips on every load, and worse, every time the
ClinicPricingDialog saved a row revalidatePath('/connections') ran the
whole fan-out again — saving a single discount felt like the request had
hung.

Replaced the per-peer query with listAllPricingForLab /
listAllPricingForClinic (single Query.equal on the side-specific column,
limit 500) and group the result into a Map client-side. One roundtrip
regardless of how many connections you have.

Also flipped the audit-log calls in setClinicPricingAction /
clearClinicPricingAction from 'await logAudit(...)' to 'void logAudit(...)'
— audit is best-effort by design and never blocks the user-facing
mutation; awaiting it doubled the perceived latency for nothing.
2026-05-21 22:45:58 +03:00
kovakmedya 90abb398fa fix(ui): Card gets min-w-0 so children inside grid tracks can shrink
Even after switching the page-level grid columns to minmax(0,1fr), the
Card primitive itself was still bringing its intrinsic min-content into
the column — every Card defaults to 'min-width: auto', and a Card with
a wide table inside resolves min-content to the table width. That meant
the column still couldn't collapse and the table's overflow-x-auto
wrapper never saw a constrained parent, so the action buttons spilled
past the Card border.

Added 'min-w-0' to Card and CardContent so:
  - the Card collapses to whatever the grid track allows;
  - CardContent collapses inside the Card, letting the Table wrapper
    finally enforce its overflow-x-auto scroll.

Also fixed two inner form grids that had the same '1fr' overflow trap:
ProstheticForm and ProstheticsTable's edit dialog both use a
[price][currency] split, switched to minmax(0,1fr)_100px so the 'Para
birimi' label / TRY value no longer get pushed past the form edge.
2026-05-21 22:40:44 +03:00
kovakmedya 4f920e98fc fix(ui): tables no longer overflow their grid column
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.
2026-05-21 22:38:03 +03:00
kovakmedya dff1e8d1a7 fix(ui): Select trigger fills container, dropdown matches trigger width
The shadcn template ships SelectTrigger with 'w-fit' so the control sizes
to its content. That works for headless toolbar pickers but in our forms
the Select sits in a grid column next to Inputs of the same row — the
'w-fit' behaviour made labs/patients/products dropdowns visibly narrower
than their neighbouring fields. Switched the trigger default to 'w-full'
so column layouts stay tidy; anything that genuinely wants content-width
can still override via className.

Also fixed SelectContent: the Radix portal was capped at 'min-w-[8rem]'
which can be narrower than the trigger and the popper would dock to that
fixed 128px rather than the field. Replaced with
'min-w-[var(--radix-select-trigger-width)]' so the dropdown is always at
least as wide as the field it dropped from.
2026-05-21 22:09:49 +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 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 7c777a5b27 fix: move initialNotificationActionState out of 'use server' file
Memory [[feedback_use_server_only_async]]: 'use server' files can only
export async functions. notification-actions.ts also exported the
NotificationActionState type and an initialNotificationActionState const,
which Next 16 now flags hard at runtime ('A "use server" file can only
export async functions, found object.').

Moved both to a sibling notification-types.ts (same pattern we already
follow for connection-types / job-file-types / prosthetic-types). The
client component imports the const from -types and the actions from
-actions; no behaviour change.
2026-05-21 21:28:11 +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 6b1b44502a fix(upload): convert File to Buffer via InputFile.fromBuffer before sending to storage.createFile
node-appwrite's storage.createFile happily takes the Web File API today,
but Next.js's multipart parser had already consumed the request body by
the time the SDK tries to stream it again — the SDK's second pass dies
with 'Unexpected end of form'. isletmem-kovakcrm's logo-actions uses the
documented pattern: arrayBuffer → Buffer → InputFile.fromBuffer(buf,
name). Adopting the same approach in uploadJobFilesAction.

The middlewareClientMaxBodySize bump from the previous commit still
matters (lifts the 10MB cap so the body even reaches us), but on its own
it wasn't enough: the Web File handoff itself was broken.

InputFile is exported from 'node-appwrite/file' (separate entry point —
the helper isn't on the main package export).
2026-05-21 21:08:26 +03:00
kovakmedya 2bf130105e fix(upload): bump middlewareClientMaxBodySize to 100mb
Next 16 caps any request body that flows through middleware at 10MB by
default. Our auth middleware matches every path, so /jobs/:id POSTs from
the file upload form hit 'Request body exceeded 10MB / Unexpected end of
form' the moment a user picked anything bigger than ~10MB total — the
server action never even ran.

serverActions.bodySizeLimit alone isn't enough; the new
middlewareClientMaxBodySize knob (Next 16) is the one that gates
middleware-handled bodies. Set both to 100mb so the 30MB-per-file bucket
limit is what actually matters.

The key isn't in NextConfig's TS types yet (Next 16.1), so it's assigned
via a narrow cast on the side rather than dropped into the object literal.

Also added console.log/error breadcrumbs to uploadJobFilesAction so the
next mystery upload failure shows up in the dev server log immediately
instead of silently bouncing back as 'Bağlantı hatası'.
2026-05-21 21:05:33 +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 c980ce1d8d feat(dashboard): wire Anasayfa to live data
- getDashboardData aggregates open jobs, pending-action jobs, unread
  notifications, pending finance totals, approved connection count, recent
  jobs (up to 8) and recent notifications (up to 5) — single Promise.all so
  the dashboard renders in one round-trip.
- Four stat cards, each a Link to the relevant module; tone (positive /
  negative) flips between clinic (payable) and lab (receivable).
- Clinic users with zero approved connections see a 'Bağlantı Kur' prompt
  card so they don't get stuck on /jobs/new.
- Recent jobs table is role-aware: lab sees Klinik column + 'Son Gelen
  İşler' header, clinic sees Laboratuvar column + 'Son Giden İşler' header.
- Recent notifications panel with read/unread dot, clickable header arrow
  to /notifications.
- ActiveContext now carries 'kind' (mirror of TenantSettings.kind) so we no
  longer reach into ctx.settings?.kind in callers.
2026-05-21 20:41:39 +03:00
kovakmedya 97f397d2dd docs: Coolify deploy guide for lab.kovakcrm.com
Step-by-step instructions for the production deploy that has to happen in
the admin.kovaksoft.com UI (Coolify doesn't expose a clean CLI for creating
resources). Covers DNS A record, Coolify resource setup, ENV mapping,
domain + SSL, Gitea webhook for auto-deploy, and a verification cookbook.
Troubleshooting section includes the Node 26 / node-appwrite patch already
shipped in repo.
2026-05-21 20:19:13 +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 7fb8288f79 auth: route tenant-less sign-in to onboarding instead of erroring out
Previously, signing in with a kind toggle when the account had no
tenant_settings row in the lab database returned 'Bu hesap için kayıt
bulunamadı'. For a freshly-imported isletmem user that has never opened
DLS, the right behaviour is onboarding — not a dead end.

resolveTenantOnLogin now distinguishes three states:
  - no_tenants  → redirect /onboarding?kind=<pill>  (session stays)
  - mismatch    → real error naming the existing kind, session rolled back
  - matched     → existing tenant set as active, /dashboard

Onboarding page accepts ?kind= and the workspace form pre-selects it, so
the user keeps their login pill choice without re-picking. Also fixed the
'teams.total > 0' redirect — it now requires a tenant_settings row before
sending users off to /dashboard, otherwise users with cross-app teams but
no DLS workspace would bounce.
2026-05-21 19:39:29 +03:00
kovakmedya 9ea35e88cf fix: patch node-fetch-native-with-agent to bypass bundled undici on Node 26
node-appwrite 23.1.0 ships a bundled undici Agent via node-fetch-native-with-agent.
That bundle uses an older undici dispatcher API that crashes on Node 26 with
'invalid onError method' (UND_ERR_INVALID_ARG), making every Appwrite call
fail with 'fetch failed' / our user-facing 'Bağlantı hatası' fallback.

The patch replaces createAgent/createFetch with thin pass-throughs to
globalThis.fetch — Node native fetch handles HTTPS to db.kovaksoft.com
directly, no proxy/agent customization needed. Verified end-to-end via
users.listMemberships against the live project.

Also added dev-mode error surfacing in appwriteError so future SDK
exceptions show the real message instead of 'Bağlantı hatası'.
2026-05-21 19:31:16 +03:00
kovakmedya 1dd8627c30 fix(middleware): protect jobs/products/finance/connections routes
These DLS module routes were added in the previous bootstrap but the auth
middleware's PROTECTED_PREFIXES list still mirrored isletmem's CRM modules,
so /jobs/inbound etc. were returning 200 without a session and exposing the
placeholder shell. Build smoke test caught it; layout-level redirect alone
was not enforcing it for those paths.
2026-05-21 18:46:31 +03:00
kovakmedya 92c3b53e39 auth: login pill toggle for clinic/lab + server-side kind validation
Sign-in form now has a Klinik / Laboratuvar pill toggle (defaults to clinic).
The selected kind is posted alongside email+password; the server action
resolves the user's memberships, picks a tenant_settings row matching the
chosen kind, and sets it as active. If no matching tenant exists the session
is rolled back with a clear error.

Invite-flow logins skip the kind check — invite code drives team assignment.
2026-05-21 18:39:45 +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