feat(treatment): add voice detail entry #65
@@ -145,9 +145,25 @@ path**. `onAddDetail` is not called and not changed.
|
|||||||
`setEntryStep('treatment')` matters: `showChrome` is always on, so the control is visible
|
`setEntryStep('treatment')` matters: `showChrome` is always on, so the control is visible
|
||||||
during the **Lab** wizard step too. Confirming there returns to the treatment step.
|
during the **Lab** wizard step too. Confirming there returns to the treatment step.
|
||||||
|
|
||||||
|
**Confirm also saves.** The new detail is persisted immediately (`persistDraft({force:true})`),
|
||||||
|
and when the result carries a lab, a due date or a prosthesis map the lab case is saved with it
|
||||||
|
(`persistLabCases`). Not politeness — the autosave effect watches `details` only, so a lab draft
|
||||||
|
left in component state alone loses the destination lab, the due date and the whole prosthesis
|
||||||
|
map on the next reload. The detail survives, which is what makes that loss look like a
|
||||||
|
successful save.
|
||||||
|
|
||||||
|
One guard on it: `persistDraft` returns a **preview** treatment instead of saving when any
|
||||||
|
detail still lacks a treatment type — the blank chip the workspace opens with is enough — and a
|
||||||
|
preview's detail id falls back to the client id. Confirm therefore checks *what came back*, not
|
||||||
|
the precondition, and skips the lab-case save when it did not get a real id; posting a lab case
|
||||||
|
against an id the server has never seen fails the whole save. Checking the result rather than
|
||||||
|
the condition keeps this true for every early return `persistDraft` has.
|
||||||
|
|
||||||
> Accepted consequences:
|
> Accepted consequences:
|
||||||
> - Tapping Add and then 🎤 leaves behind the blank chip that Add created. It carries the
|
> - Tapping Add and then 🎤 leaves behind the blank chip that Add created. It carries the
|
||||||
> usual trash affordance.
|
> usual trash affordance.
|
||||||
|
> - That same blank chip blocks confirm's immediate lab-case save until it is given a type
|
||||||
|
> or removed; the lab rows stay in local state until the ordinary Lab-step save.
|
||||||
> - Dictating into an existing detail is not supported in v1 — voice always makes a new
|
> - Dictating into an existing detail is not supported in v1 — voice always makes a new
|
||||||
> one.
|
> one.
|
||||||
|
|
||||||
@@ -522,8 +538,8 @@ who can edit treatments, in every configured locale. `Plan.features.voiceTreatme
|
|||||||
and the availability API stay documented here as the intended gate, deferred rather than
|
and the availability API stay documented here as the intended gate, deferred rather than
|
||||||
dropped, so turning them on later is additive.
|
dropped, so turning them on later is additive.
|
||||||
|
|
||||||
Consequence to accept deliberately: with no plan gate and no duration cap (§2), the
|
Consequence to accept deliberately: with no plan gate, the per-user throttle and the 2-minute
|
||||||
per-user throttle is the **only** control on metered vendor spend. See open item 14.
|
recording cap are the **only** controls on metered vendor spend. See open item 14.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -535,15 +551,31 @@ English Nest exception for a user-facing failure.
|
|||||||
|
|
||||||
| Code | When |
|
| Code | When |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `VOICE_MIC_DENIED` | browser permission refused — **client-side only**: needs the `errors.X` key in all three message files, but no `ErrorCode` entry and no throw site |
|
| `VOICE_MIC_DENIED` | microphone permission actually refused, or no input device — **client-side only**: needs the `errors.X` key in all three message files, but no `ErrorCode` entry and no throw site. Reserved for a real permission failure: see the note below |
|
||||||
| `VOICE_CLIP_TOO_LONG` | over `maxMs` (server-side re-check), or over vendor limits |
|
| `VOICE_CLIP_TOO_LONG` | over `maxMs` (server-side re-check), over vendor limits, or a request body past the DTO's size cap |
|
||||||
| `VOICE_UNSUPPORTED_FORMAT` | recorder produced a container the profile rejects |
|
| `VOICE_UNSUPPORTED_FORMAT` | **the browser cannot record at all** — no `MediaRecorder`, no container both it and the API accept, or a recorder that throws after permission was granted; and server-side, a `format` outside `VOICE_AUDIO_FORMATS` |
|
||||||
| `VOICE_ASR_FAILED` | transcription stage failed |
|
| `VOICE_ASR_FAILED` | transcription stage failed |
|
||||||
| `VOICE_EXTRACT_FAILED` | transcript obtained, structuring failed |
|
| `VOICE_EXTRACT_FAILED` | transcript obtained, structuring failed |
|
||||||
| `VOICE_NOTHING_RECOGNIZED` | empty or unusable transcript |
|
| `VOICE_NOTHING_RECOGNIZED` | empty or unusable transcript |
|
||||||
| `VOICE_NOT_AVAILABLE` | no profile for locale (v1); plan flag off, once enforced |
|
| `VOICE_NOT_AVAILABLE` | no profile for locale (v1); plan flag off, once enforced |
|
||||||
| `VOICE_RATE_LIMITED` | throttle |
|
| `VOICE_RATE_LIMITED` | throttle |
|
||||||
|
|
||||||
|
**Two of these are raised by DTO validation, not by a throw site.**
|
||||||
|
`validationExceptionFactory` returns a constraint's `message` verbatim when the message is
|
||||||
|
itself a known `ErrorCode`, so the voice DTO names its own failures:
|
||||||
|
`@MaxLength(…, { message: ErrorCode.VOICE_CLIP_TOO_LONG })` and
|
||||||
|
`@IsIn(…, { message: ErrorCode.VOICE_UNSUPPORTED_FORMAT })`. Left to the shared constraint map
|
||||||
|
they fall through to `VALIDATION_FIELD_REQUIRED` and `VALIDATION_LANGUAGE_INVALID` — an
|
||||||
|
oversized recording telling the clinician a field is missing, and an unsupported container
|
||||||
|
telling them their language is invalid. Any new voice constraint should name its code the same
|
||||||
|
way.
|
||||||
|
|
||||||
|
**`VOICE_MIC_DENIED` is only for a real permission failure.** Three client paths used to report
|
||||||
|
it for something else entirely — no `MediaRecorder`, no acceptable container, and a recorder
|
||||||
|
that throws after permission was already granted. All three are "this browser cannot record"
|
||||||
|
and now report `VOICE_UNSUPPORTED_FORMAT`; blaming the microphone sends the clinician hunting
|
||||||
|
in site settings for a permission nothing ever asked for.
|
||||||
|
|
||||||
**Transcript salvage — specified, NOT built.** The backend half exists: `VOICE_EXTRACT_FAILED`
|
**Transcript salvage — specified, NOT built.** The backend half exists: `VOICE_EXTRACT_FAILED`
|
||||||
carries `details.transcript` and `HttpExceptionFilter` forwards it. The client half was
|
carries `details.transcript` and `HttpExceptionFilter` forwards it. The client half was
|
||||||
never written — `onError` only resolves a message through `getUserFacingError`, which never
|
never written — `onError` only resolves a message through `getUserFacingError`, which never
|
||||||
@@ -701,7 +733,18 @@ enabling this for real clinics.
|
|||||||
- no layout shift in the header row on record start, stop, or the 2:00 auto-stop;
|
- no layout shift in the header row on record start, stop, or the 2:00 auto-stop;
|
||||||
- **hold past 2:00** → auto-stops and proceeds to processing, not an error;
|
- **hold past 2:00** → auto-stops and proceeds to processing, not an error;
|
||||||
- **cancel during processing** → the vendor request is actually aborted;
|
- **cancel during processing** → the vendor request is actually aborted;
|
||||||
- **review sheet on mobile** → full-screen overlay; closing it leaves the draft intact.
|
- **review sheet on mobile** → full-screen overlay; closing it leaves the draft intact;
|
||||||
|
- **confirm with a lab, a due date or a prosthesis map, then reload** → all three are still
|
||||||
|
there. They live on the lab case, which the autosave effect does not watch, so this is
|
||||||
|
the check that catches a lab draft left unsaved in component state;
|
||||||
|
- **record straight after opening a visit**, while the blank chip is still untyped, and
|
||||||
|
confirm with a lab ticked → no error toast: confirm detects the preview treatment and
|
||||||
|
skips the lab-case save rather than posting an id the server has never seen;
|
||||||
|
- **dictate two different prosthesis types** ("۱۲ روکش PFM، ۱۳ روکش PFZ") → the form shows
|
||||||
|
both, and the dispatch panel's remembered "last type for this lab" does **not** overwrite
|
||||||
|
either. Also check the bulk «اعمال برای همه دندانها» select stays on its placeholder;
|
||||||
|
- **a hand-made prosthesis detail with an empty map** → still gets the remembered default
|
||||||
|
pre-filled. The exemption is for previewed cases only, not a removal of the convenience.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -733,6 +776,14 @@ Settled in a grilling session on 2026-08-20.
|
|||||||
| 28 | Review sheet | Modal on desktop, full-screen overlay (not a route) on mobile; candidate chips are its only interactive part |
|
| 28 | Review sheet | Modal on desktop, full-screen overlay (not a route) on mobile; candidate chips are its only interactive part |
|
||||||
| 29 | Cancel | Aborts the in-flight vendor call |
|
| 29 | Cancel | Aborts the in-flight vendor call |
|
||||||
| 30 | v1 gating | Open to everyone; `Plan.features` gate deferred, not dropped |
|
| 30 | v1 gating | Open to everyone; `Plan.features` gate deferred, not dropped |
|
||||||
|
|
||||||
|
Added while getting the first live recordings working (2026-08-21):
|
||||||
|
|
||||||
|
| # | Question | Decision |
|
||||||
|
|---|---|---|
|
||||||
|
| 31 | Tooth numbering | A spoken number **is** its FDI code, in all three locales. A lone digit stays unresolved and offers its candidate teeth as chips (§6, §7) |
|
||||||
|
| 32 | What confirm writes | Confirm persists the detail *and* its lab case, because autosave watches `details` only — but skips the lab-case save when it got a preview treatment back (§2) |
|
||||||
|
| 33 | Preview as contract | Applying a voice result fills exactly what the sheet showed. Per-detail conveniences that would add more are suppressed for that case (§7) |
|
||||||
| 15 | Gating | `Plan.features` flag — its first consumer |
|
| 15 | Gating | `Plan.features` flag — its first consumer |
|
||||||
|
|
||||||
UI placement settled in a second grilling session on 2026-08-20.
|
UI placement settled in a second grilling session on 2026-08-20.
|
||||||
|
|||||||
Reference in New Issue
Block a user