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>
31 lines
1.1 KiB
TypeScript
31 lines
1.1 KiB
TypeScript
import { apiClient } from './client';
|
|
import type { VoiceAvailability, VoiceExtractionResult } from '@/types/voice';
|
|
|
|
export interface ExtractVoicePayload {
|
|
/** Base64 audio, no data: prefix. */
|
|
audio: string;
|
|
format: string;
|
|
/** IANA zone — the server derives "today" from it for relative deadlines. */
|
|
timeZone: string;
|
|
durationMs: number;
|
|
/** Locale the clinician is speaking; the server uses it rather than the stored one. */
|
|
locale: string;
|
|
}
|
|
|
|
export const voiceApi = {
|
|
availability: async (): Promise<{ success: boolean; data: VoiceAvailability }> => {
|
|
const response = await apiClient.get('/voice/availability');
|
|
return response.data;
|
|
},
|
|
|
|
extract: async (
|
|
payload: ExtractVoicePayload,
|
|
signal?: AbortSignal,
|
|
): Promise<{ success: boolean; data: VoiceExtractionResult }> => {
|
|
// The signal is forwarded so cancelling closes the connection, which aborts the
|
|
// metered vendor call server-side rather than letting it settle unseen.
|
|
const response = await apiClient.post('/voice/extract', payload, { signal });
|
|
return response.data;
|
|
},
|
|
};
|