Commit Graph

161 Commits

Author SHA1 Message Date
10408058c4 feat(voice): keep the transcript on the server
The review sheet rendered the full dictation at the top of the modal,
unconditionally. A raw transcript can carry the patient's spoken name — the
spec said so itself in §9, while §10 said telemetry must never contain it.

The transcript no longer reaches the browser by any route:

- removed from the success response (VoiceExtractionResponse is now plain
  ResolvedExtraction, which never had the field)
- removed from the error body. VOICE_EXTRACT_FAILED carried
  details.transcript for a salvage dialog that was never built, and
  ApiError['details'] is an array, so the shape never even matched — it was
  serialized onto the wire and dropped
- removed from the sheet, and from VoiceExtractionResult. tsc proves that
  <p> was the only reader in the whole frontend

It is logged instead: one info line per recording, written immediately after
the emptiness check so a failed extraction still records it, and deliberately
outside logTelemetry so that method's patient-free guarantee stays literally
true.

Accepted consequence, recorded in §10: patient words now persist in
production server logs at default level, so whatever retention and access
control applies to those logs applies to dictation. The repo's other
sensitive-text path (openrouter.provider.ts) uses debug level with
truncation; moving this line to debug is a one-word change.

Transcript salvage is dropped rather than deferred, which settles §11 open
item 16 by taking its second option. When extraction fails the clinician
re-dictates; an operator can read the words in the log, the person who spoke
them cannot.

Spec: §7, §9 and §10 rewritten, item 16 resolved, decisions 51-53 added, and
decisions 10 and 25 marked superseded so the log stops contradicting itself.

No automated coverage for the response shape or the log line: there is no
voice.service.spec.ts — the service is I/O orchestration and has never been
unit tested. Removing the type field is what proves no reader survives.

Gates: backend 216 tests, nest build, ESLint clean on the voice module;
frontend tsc --noEmit clean, 52 Vitest tests, next build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:39:27 +08:00
46c8a25139 fix(treatment): never keep a bridge across a tooth that left it
A bridge is a contiguous span, but every operation that filtered teeth out of
a group kept the `connected` kind as long as two teeth remained. Reduce a
12-13-14 span to 12 and 14 and you get a "bridge" with no pontic — and worse,
both teeth keep one selectionGroupId, so the lab receives them as a single
unit and task generation builds work that cannot be made.

linkedEdgesFromGroups already guarded the drawn marks with areArchNeighbors,
so the chart looked right while the data was wrong.

Adds splitDisconnectedRuns, which breaks what remains into contiguous runs: a
run of two or more stays connected, a run of one becomes a single. The first
run keeps the original groupId so lab rows pointing at it stay valid, and
pruneToothProsthesisForGroups re-maps the rest.

Applied at all three sites that filter a group's teeth, which each carried
their own copy of the length check:
- groupsFromFlatTeeth, when teeth are no longer selected
- applyShiftRange, when a shift-range takes teeth from an existing group
- removeTeethFromGroups, the path pruneDetailTeethToJobs uses

toggleToothInGroups already split into runs by hand for the single-removal
case; this is the same rule, shared.

Tests: a new toothSelectionGroups.spec.ts (13) plus two for
removeTeethFromGroups. The middle-tooth cases fail without the fix; the
end-tooth and contiguous cases pass either way and exist to prove it does not
over-reach. CLAUDE.md updated for the third Vitest file.

Not fixed: a pre-existing unused `leftIdx` warning in unlinkAdjacentTeeth,
unrelated to this change.

Gates: 52 Vitest tests, tsc --noEmit clean, next build clean, ESLint
unchanged at 1 pre-existing warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:26:23 +08:00
dc23c3dd1c fix(voice): prime the lab-drafts ref before the save that reads it
"برای دندون ۱۲ و ۱۴ یه فول متال کران" previewed correctly, then Apply failed
with TREATMENT_TOOTH_NOT_ON_DETAIL. The form still showed both teeth and both
crowns — that is React state. The database got a detail with no teeth.

applyVoiceResult called setLabCaseDrafts but never wrote
labCaseDraftsRef.current, which is only refreshed in the render body.
persistDraft runs in the same tick and starts by pruning each lab-dependent
detail down to the teeth its jobs cover, reading that stale ref. It found no
draft for the brand-new detail, so jobs were empty, so the detail was saved
with teeth: []. persistLabCases then posted rows for 12 and 14 against it,
and the server — which re-reads the detail's teeth from the database, because
the lab-case endpoint carries no teeth field — correctly refused.

detailsRef was already written by hand two lines above for exactly this
reason (3911477). This is the other half of the same mistake.

Two sibling handlers had the same gap and are fixed with it: adopting an
orphan lab case, and creating a new lab draft. Both call setLabCaseDrafts and
then persistDraft in the same tick. handleLabCasesChange was the only site
that already primed the ref, and it is the pattern the others now follow.

Adds a comment at the ref declaration, because the coupling between this ref
and persistDraft's prune is invisible at the call sites and has now cost two
bugs.

No automated test: this is state-and-ref ordering inside a React component,
and the frontend's Vitest scope is pure helpers only — no React, no DOM.
Mirroring the server's tooth rule on the client to make it testable would
duplicate a rule across layers, which decision 38 rules out.

Gates: tsc --noEmit clean, 37 Vitest tests, next build clean, ESLint warnings
unchanged at 10 (all pre-existing, count verified against HEAD).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 17:18:02 +08:00
f1a4594a0a fix(voice): validate a code's region against its target properly
Second correctness pass on e271858 found three ways an arch-only appliance
could be written as a per-tooth job, plus a gap in the previous repair.

1. The deferred region check was never completed. A mixed-region category
   resolved with no region recorded, and nothing re-validated the leaf the
   clinician then picked — "پروتز متحرک برای دندون ۱۲" wrote a complete
   denture onto tooth 12, which the manual chart cannot produce and which
   task generation would expand as denture steps. Targets now resolve BEFORE
   types, so the target kind narrows a category's candidates and an
   impossible leaf is never offered. The deferral disappears with it.

2. `some` over the stack's regions let one legal code admit every other.
   `types: ['pfm_crown','night_guard_soft']` on tooth 12 passed, because
   `crown` suited the tooth, and wrote a night guard onto that tooth. Now
   every named code must suit the target.

3. `regions.size === 1` also deferred `implant`, whose leaves span root and
   crown — both tooth regions, so not a tooth/arch category at all. An
   implant aimed at a jaw resolved as a UA target. Narrowing by target kind
   rejects it instead.

4. The e271858 type-row lock ticked the row and the payload but not the
   count, so the sheet read "Apply 1 field" while two landed. One
   `effectiveSelection` now drives the count and the payload.

Also narrows a `filter(Boolean)` in the disjointness test that left two
implicit-any errors under the full tsconfig (nest build excludes specs, so
the repo gate never saw them). Pre-existing at 15ddb9a.

Adds four resolver tests: candidate narrowing per target kind, a category
with no usable leaf, a stack where only one code suits, and a legal stack.
All three defects passed the previous 209 tests.

Gates: backend 212 tests, nest build, ESLint clean on the voice module;
frontend 37 Vitest tests, tsc --noEmit, ESLint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:42:06 +08:00
e271858a33 fix(voice): repair the two defects the correctness critic found
Both found by the /orchestrate correctness critic against 15ddb9a, and both
verified by hand before fixing.

1. A picked tooth chip was dropped on Apply. pickCandidate ticked the teeth
   row from `available`, which is memoised from `effective` — the value the
   same handler is in the middle of changing. On "ترمیم برای دندون دو" the
   backend returns no teeth, so `available.teeth` was false at pick time and
   `prev.teeth` was false too; `false || false` stuck permanently. A variant
   of the original live-test failure. Each branch now ticks the row it feeds
   and returns, so nothing reads the stale memo.

2. Decision 41's type-row lock was missing. The treatmentType row was a plain
   toggle, and applyVoiceResult derived `labDependent` from
   `result.treatmentType` rather than the `detail.treatmentType` it writes —
   so unticking the type row saved prosthesis lab rows on whatever type the
   appointment purpose had seeded. Row gains a `locked` state, the type row
   locks while the prosthesis row is ticked, Apply sends the forced tick, and
   `labDependent` now comes from the detail being written.

Gates: backend 16 suites / 209 tests, frontend 37 Vitest tests,
tsc --noEmit clean, next build clean, ESLint 0 new warnings
(VoiceReviewSheet.tsx 0 issues; the 10 in TreatmentWorkspace.tsx are
pre-existing and unchanged in count).

Not covered by a test: both fixes live in component state logic, which the
chosen Vitest scope — pure helpers, no React, no DOM — cannot reach. §12's
manual checks cover them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:27:43 +08:00
15ddb9aac2 feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.

Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.

Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().

Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.

Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.

KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
  type-row lock is missing, so unticking it saves prosthesis lab rows on a
  non-prosthesis detail

Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.

Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 12:23:58 +08:00
1ca8e6178e improvement: Nothing done is related to subscriptions!!! 2026-09-05 20:44:16 +03:30
5d3597f973 improvement: icons added to prosthesis types catalog.
Some checks failed
Production — tag build, push, deploy / build-and-push (push) Failing after 23s
Production — tag build, push, deploy / deploy (push) Has been skipped
2026-09-05 01:29:53 +03:30
72f885d9dd improvement: all confirmed suggestions implemented 2026-09-04 17:04:48 +03:30
7f92e735fb improvement: Some improvements done. some bugs fixed. 2026-09-02 17:24:01 +03:30
a3c14a18c1 improvement: new prosthesis type data structure implemented and finally working! 2026-09-01 21:03:04 +03:30
50eda34e8e The app is now wired to GlitchTip with the Sentry SDKs
Some checks failed
Production — tag build, push, deploy / build-and-push (push) Successful in 1h13m36s
Production — tag build, push, deploy / deploy (push) Failing after 1m1s
2026-08-31 07:34:07 +03:30
eee2506adf ASR + LLM routinely exceeds the default 10s axios timeout; match slow upload endpoints. 2026-08-25 10:21:55 +03:30
dc10d8dbe3 docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.

Removed by genre rather than by taste:

- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
  map that literally shows it, and a docblock on startOfWeek explaining that
  it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
  recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
  denied microphone" reason appeared three times in one file, and the
  "aborting stops a per-minute metered call" reason across three files. Each
  now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
  own module;
- over-explaining defensive branches, three separate comments to distinguish
  null from missing-kind from unrecognised-kind.

What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.

Comments only. The diff contains no non-comment line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:14:15 +03:30
8f2d3f97ba fix: stop voice entry posting an unsaved detail id, and name failures right
Review findings on this branch.

The lab-case save could be posted against a detail the server has never
seen. persistDraft returns a *preview* treatment instead of saving when any
detail lacks a treatment type — the blank one the workspace opens with is
enough — and a preview's detail id falls back to the client id. Recording
straight after opening a visit and confirming a result with a lab or due
date would send that id and fail the whole save. It now checks what came
back rather than the precondition, so it holds for every early return
persistDraft has.

stop() optional-chained into a no-op when the recorder was already gone,
leaving the bar recording forever with a live timer and only Cancel as a
way out.

Three "this browser cannot record" paths reported VOICE_MIC_DENIED — no
MediaRecorder at all, no container the API accepts, and a recorder that
throws after permission was already granted. Telling clinicians their
microphone was denied sends them hunting for a permission nothing asked
for; they now report VOICE_UNSUPPORTED_FORMAT.

The voice route's large-body match stripped every trailing slash while
Express ignores exactly one, so '/api/voice/extract//' bought a 10 MB
buffer for a request that then 404s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:07:53 +03:30
6a4c0cb1bb fix(frontend): stop the level meter re-rendering the whole workspace
The meter wrote React state from a requestAnimationFrame loop, and the hook
lives in TreatmentWorkspace — so every frame re-rendered the details editor,
the FDI chart, the lab panel and the history rail. About 7,200 whole-tree
renders across a two-minute recording, while the user is dictating.

Now samples every frame but publishes at LEVEL_POLL_MS, the rate the
elapsed timer already used. Peaks between publishes are carried forward, so
the meter stays responsive to transients rather than sampling at 10 Hz.

Also adds the catch the start path never had: new MediaRecorder() and
recorder.start() both throw on some browsers, and by then the stream is
live. The rejection went unhandled, the UI sat at 'idle' showing nothing,
and the browser's recording indicator stayed lit until unmount.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
3911477e42 fix(frontend): persist the lab case a voice result creates
applyVoiceResult put the lab draft in state and stopped there. Every other
path that creates a LabCaseDraft — handleContinueToLab, handleLabCasesChange
— immediately runs persistDraft + persistLabCases, and the autosave effect
only watches `details`. So applying a voice result carrying a lab, a due
date and a prosthesis map, then reloading, kept the detail and silently
dropped all three: the surviving detail made it look like the save worked.

applyVoiceResult moves below persistDraft/persistLabCases so it can call
them, and writes detailsRef itself before persisting — persistDraft reads
that ref, and setDetails has not rendered by the time the save runs. The
ref is already written imperatively elsewhere for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
62deff0523 fix(frontend): untick prosthesis when a picked tooth breaks its map
initialVoiceSelection deliberately never auto-ticks an incomplete
prosthesis map, because a detail with an untyped tooth cannot ship — it
fails at dispatch instead. Picking a candidate tooth walked straight
through that rule: the tick was seeded once, so a map that was complete at
extraction stayed ticked after a tooth with no prosthesis type joined it,
and Apply attached a map assertCompleteToothProsthesisMap rejects.

Recomputed on each pick, and only ever downwards — re-ticking is the
clinician's call, not a side effect of un-picking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
754efdee09 feat(frontend): let the clinician pick the tooth from the candidates
An under-specified tooth was a dead end: the sheet said what was missing
and the clinician had to leave and hunt for it on the chart. The readings
are enumerable, so the review sheet now renders them as chips — the one
interactive part of an otherwise read-only confirmation step.

A pick is folded into the result by withChosenTeeth() rather than tracked
alongside it, so the rows, the mini chart, the prosthesis warning and
applyVoiceResult all keep reading a single VoiceExtractionResult and none
of them has to know the chips exist. It unions rather than toggles: a
candidate can coincidentally be a tooth the recording already produced, and
tapping it must not deselect that one.

Two things that would otherwise make the chips look functional while
applying nothing: the teeth row is ticked on the first pick (it starts
unticked when the recording produced no teeth of its own), and the apply
count is now intersected with row availability so it cannot promise to
apply a row with nothing in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
1be735a7ab fix(voice): say the quadrant is missing instead of "could not be read"
"ترمیم برای دندون دو" set the treatment type but reported the tooth as
unreadable. Nothing was misheard: position 2 arrived intact, with no
quadrant, because none was spoken — four teeth carry position 2 and the
resolver correctly refused to pick one. Only the label was wrong, and it
sent the clinician looking for a transcription fault.

Adds a tooth_missing_quadrant reason that names what is missing and shows
how to say it ("دو بالا راست"), and tells the model explicitly to report a
quadrant-less number with arch and side null rather than guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
3f97940a16 feat: wire voice entry into the treatment workspace
Makes the feature reachable end to end: availability is fetched alongside the
catalogs, the capture hook drives the segmented control, and confirming the
review sheet appends a new detail.

Confirm always appends — it never edits an existing detail and never calls
onAddDetail. Ticked rows land on top of the seeded defaults, so unticking the
type row leaves the appointment-purpose default rather than a blank. Lab-side
rows ride on a lab case draft keyed by the detail's *client* id, so a brand-new
unsaved detail can carry a lab, due date and per-tooth prosthesis map.

Availability comes from the API rather than a NEXT_PUBLIC_* var, since those are
baked in at build time; a failure fetching it degrades to no microphone rather
than taking the treatment tab down.

From review of this commit:

- Unticking "teeth" while leaving "prosthesis" ticked attached prosthesis rows
  for teeth the detail does not contain. Nothing downstream filters them —
  assertCompleteToothProsthesisMap only checks detail-teeth ⊆ map, never the
  reverse — so they would have reached task generation as lab work for teeth
  nobody is treating. The map is now filtered to the detail's own teeth.
- The microphone was gated on the URL locale while the server resolved
  everything from req.user.language. Those diverge (a bookmarked /fa/ URL, a
  language toggle whose save failed), which would transcribe Persian with an
  English hint and anchor "next Thursday" to a Monday week instead of a Saturday
  one — or 403 from a visibly-enabled button. The client now sends the locale the
  microphone was offered in, so the gate and the request agree by construction.

Also fixed from the previous review: a civil YYYY-MM-DD date rendered a day
early west of Greenwich (parsed as UTC midnight); the missing-teeth list
hardcoded the Arabic comma for all locales; and voiceApply had no ICU plural, so
the common single-field case read "Apply 1 fields".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
c07f550e00 feat(frontend): split Add detail into a segmented control with voice
The microphone becomes the second segment of the Add detail button, built like
the detail chip's trash affordance in the same file — an overflow-hidden rounded
wrapper holding two raw <button>s divided by border-s — rather than two shared
Buttons, which each hardcode their own rounding and would fight a segmented
control. border-s puts the mic at the logical end: visually right in en/nl,
visually left in fa, on the same side as the chip's trash in both directions.

The two halves share a wrapper and nothing else. Add keeps its exact behaviour.

The control never changes size while recording; the timer and level meter live
in a bar between the header row and the chip strip, because the header is
sm:justify-between and growing the button would shove the row on every start and
stop. The meter exists to prove the microphone is actually hearing something —
silence and a dead mic look identical otherwise.

Voice reaches the editor as one optional `voice` prop, so its absence *is* the
unavailable state and the two cannot disagree.

Fixes from review of this commit:

- mountedRef was set false on unmount and never re-armed, so under StrictMode
  the hook was permanently "unmounted" in dev and recording silently never
  started.
- onStart guarded only on `phase`, which does not change until getUserMedia
  resolves; a second click during the permission prompt orphaned the first
  MediaStream, leaving the mic indicator lit.
- Week start is now per locale. "Next Thursday" is week-relative, and hardcoding
  Saturday put an en/nl clinician's deadline a week out.
- A missing `which` on a weekday intent is read as "this" rather than failing —
  a bare weekday carries no qualifier, and rejecting it discarded a real
  deadline.
- durationMs is client-reported and so is a claim, not enforcement; the cap is
  now also checked against the vendor's own usage.seconds.
- Blob type falls back to the recorder's actual mimeType before webm, so old
  Safari's mp4/aac clips are not mislabelled.

Two review findings were rejected as incorrect, both re-verified against live
sources: google/gemini-3.7-flash does exist on OpenRouter (1M context,
$0.375/$1.875 per M), and base64 JSON input_audio is the documented primary
path for /audio/transcriptions, with multipart as the OpenAI-compatible
alternative. The spec's stale "unverified" note is corrected, and the provider
now has unit tests covering the request shape, usage parsing, and that a vendor
error body never reaches the thrown message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
93c6513df6 feat(frontend): voice capture hook, API client and types
MediaRecorder handling and the API call live in lib/, not in ui/, so
TreatmentDetailsEditor can stay presentational and take only a `voice` prop.

Container choice is made at record time and needs no transcode: Chrome and
Android give webm/opus, Safari and iPad give mp4/aac, and the transcription
endpoint accepts both. Safari's `audio/mp4` is sent as `m4a`, the name the
vendor's container list actually uses, so iPad recordings do not fail while
Chrome works. Older Safari shipped MediaRecorder without isTypeSupported, so
that path lets the browser choose rather than refusing outright.

From review of this commit:

- The auto-stop at maxMs guaranteed a 413. The client measures the final length
  after the recorder has stopped, so a recording that runs to the cap always
  reports slightly over it, and the server rejected exactly the recording the
  auto-stop existed to save. The server now allows a documented 2s tolerance and
  the client keeps reporting the true length, so telemetry stays honest.
- getUserMedia is async, so a permission granted after unmount installed a live
  stream the cleanup effect had already run past — leaving the browser's
  recording indicator lit with nothing listening. Guarded with a mounted ref.
- Client-side failures are now ApiError-shaped ({code, statusCode}) rather than
  bare Errors, because getUserFacingError only resolves that shape; without it
  errors.VOICE_MIC_DENIED was dead in all three locales.

Cancelling aborts the request, which closes the connection and aborts the
metered vendor call server-side rather than letting it settle unseen. The level
meter is best-effort: a blocked AudioContext costs the meter, not the recording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
8d34def001 improvement: the last flow improved a little. 2026-08-22 20:33:11 +03:30
7f02a84ebf improvement: a flow added to create treatment plan for searched patients with no treatment history. 2026-08-22 20:25:11 +03:30
b1405b22b1 improvement: patient search moved to the top of treatment feature. 2026-08-22 20:11:43 +03:30
50725b7b3f bugfix: no-detail treatment ui made some confusion. it is improved. at least to a degree. 2026-08-22 19:36:37 +03:30
f52ad6b84a bugfix: Treatment detail auto-fill bug fixed. 2026-08-22 18:51:57 +03:30
de46da216f bugfix: FDI tooth chart is now compatible with RTL direction. 2026-08-22 17:49:38 +03:30
d2f07c0ed3 improvement: treatments UI/UX updated again to minimize clicking and scrolling. 2026-08-19 23:59:37 +03:30
29c639f5a1 improvement: logo and name svgs updated and replaced. 2026-08-19 19:39:27 +03:30
96f698be98 improvement: UI/UX improved for v1 standalone treatments/cases feature. 2026-08-19 16:07:32 +03:30
8bfa8c88fe improvement: v1 standalone treatment/case creation made possible. 2026-08-19 15:05:58 +03:30
e5c39c6b7e bugfix: site renamed. Name component created. 2026-08-19 02:41:05 +03:30
80167c622c bugfix: appointment hours now use the client timezone on UTC servers.
Logical API errors throw stable codes so users see translated messages instead of a generic bad request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 01:43:50 +03:30
d6958b2e48 bugfix: the flow for trial and accept invitation (org + staff)is now unified. all navigate to dshboard if succesfull. 2026-08-19 00:39:31 +03:30
6d90787c13 improvement: FDI tooth chart overhauled. 2026-08-19 00:06:21 +03:30
db515f9630 improvement: time period dropdown added to appropriate dashboard charts. 2026-07-19 00:37:59 +03:30
538121d653 improvement: pdf generation added to cases feature. 2026-07-18 22:00:44 +03:30
408b2c3329 improvement: task assignment notif added to notifications feature. 2026-07-18 17:23:47 +03:30
9941dca849 improvement: notification feature polished. feature tabs following the same notification socket emmited data to be updated. 2026-07-18 16:57:13 +03:30
9f6ec193d2 feature: version one notification feature implemented. 2026-07-18 01:09:54 +03:30
0740493384 improvement: fdi tooth chart selection modes polished. bugs related to teeth selecyion fixed. 2026-07-18 00:02:40 +03:30
4b2349228a improvement: treatment preview wizard optimized. comments component updated and unified accross the app. 2026-07-17 12:30:43 +03:30
58676f702e improvement: FDI chart tool selection modes updated. shift+ and crtl+ both are assigned with a selection mode. 2026-07-17 11:36:02 +03:30
1b63bfff00 improvement: some improvement done for appointment and treatment shared components. 2026-07-17 11:21:08 +03:30
d2ad1fd7b6 improvement: loading state added to buttons who interact with backend. 2026-07-17 10:58:04 +03:30
68fc9d5d6d improvement: treatment plan turned into a wizard. shift+click control added to FDI tooth chart for cunnected prosthesises. 2026-07-17 00:39:32 +03:30
cf07b4d8a8 improvement: delete action added for unsent treatment details. some edit controls added to details and shipments. 2026-07-16 22:45:37 +03:30
334b7841ae improvement: treatment exits contoll adde to appoinment remove action. 2026-07-16 22:35:02 +03:30