improvement: fdi tooth chart selection modes polished. bugs related to teeth selecyion fixed.
This commit is contained in:
@@ -41,7 +41,6 @@ import {
|
||||
isLabDependentDetailMissingTeeth,
|
||||
} from '@/components/treatment/treatmentDetailRules';
|
||||
import {
|
||||
applyCtrlRange,
|
||||
applyShiftRange,
|
||||
connectedTeethSet,
|
||||
deriveTeethFromGroups,
|
||||
@@ -267,6 +266,26 @@ function isDetailsDirty(
|
||||
return serializeDetails(details) !== savedSnapshot;
|
||||
}
|
||||
|
||||
/** Apply server ids/attachments onto local rows without replacing newer local edits. */
|
||||
function mergeServerIdsIntoDetails(
|
||||
local: TreatmentDetailDraft[],
|
||||
fromServer: TreatmentDetailDraft[],
|
||||
): TreatmentDetailDraft[] {
|
||||
const byClientId = new Map(fromServer.map((d) => [d.clientId, d]));
|
||||
return local.map((d) => {
|
||||
const s = byClientId.get(d.clientId);
|
||||
if (!s) return d;
|
||||
return {
|
||||
...d,
|
||||
id: s.id ?? d.id,
|
||||
labCaseId: s.labCaseId ?? d.labCaseId,
|
||||
taskProgress: s.taskProgress ?? d.taskProgress,
|
||||
attachmentMetas:
|
||||
d.attachmentMetas.length > 0 ? d.attachmentMetas : s.attachmentMetas,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function detailsToPreviewTreatment(
|
||||
details: TreatmentDetailDraft[],
|
||||
meta: { title: string; patientId: string; treatmentAt: string; id?: string },
|
||||
@@ -387,6 +406,8 @@ export function TreatmentWorkspace({
|
||||
const pendingAppointmentIdRef = useRef<string | null>(initialAppointmentId);
|
||||
const labPanelRef = useRef<HTMLDivElement>(null);
|
||||
const historyRequestRef = useRef(0);
|
||||
/** When set, activeDetailId effect opens this wizard step instead of resetting to teeth. */
|
||||
const pendingEntryStepRef = useRef<EntryStep | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
pendingAppointmentIdRef.current = initialAppointmentId;
|
||||
@@ -613,10 +634,13 @@ export function TreatmentWorkspace({
|
||||
const chartToothColors = showWholeTreatmentPlan ? wholePlanToothColors : undefined;
|
||||
|
||||
// Reset whole-plan overview when switching details.
|
||||
// Prefer pendingEntryStepRef (e.g. Lab shipments → Lab step) over defaulting to teeth.
|
||||
useEffect(() => {
|
||||
setShowWholeTreatmentPlan(false);
|
||||
rangeAnchorRef.current = null;
|
||||
setEntryStep('teeth');
|
||||
const pending = pendingEntryStepRef.current;
|
||||
pendingEntryStepRef.current = null;
|
||||
setEntryStep(pending ?? 'teeth');
|
||||
}, [activeDetailId]);
|
||||
|
||||
// Leave Lab step if the active detail is no longer prosthesis / lab-dependent.
|
||||
@@ -927,24 +951,40 @@ export function TreatmentWorkspace({
|
||||
),
|
||||
});
|
||||
const mapped = response.data.details.map(mapDetailFromApi);
|
||||
setDetails(mapped);
|
||||
setActiveDetailId((prev) => {
|
||||
const stillExists = mapped.some((d) => d.clientId === prev);
|
||||
return stillExists ? prev : mapped[0]?.clientId ?? prev;
|
||||
});
|
||||
setSavedSnapshot(serializeDetails(mapped));
|
||||
const sentSnapshot = serializeDetails(currentDetails);
|
||||
const localNow = detailsRef.current;
|
||||
|
||||
// If the user changed details while this save was in flight (e.g. removed a
|
||||
// detail), do not clobber local state with the stale response.
|
||||
if (serializeDetails(localNow) === sentSnapshot) {
|
||||
setDetails(mapped);
|
||||
setActiveDetailId((prev) => {
|
||||
const stillExists = mapped.some((d) => d.clientId === prev);
|
||||
return stillExists ? prev : mapped[0]?.clientId ?? prev;
|
||||
});
|
||||
setSavedSnapshot(serializeDetails(mapped));
|
||||
} else {
|
||||
const merged = mergeServerIdsIntoDetails(localNow, mapped);
|
||||
detailsRef.current = merged;
|
||||
setDetails(merged);
|
||||
setActiveDetailId((prev) => {
|
||||
const stillExists = merged.some((d) => d.clientId === prev);
|
||||
return stillExists ? prev : merged[0]?.clientId ?? prev;
|
||||
});
|
||||
// Keep dirty so the queued autosave persists the newer local state.
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
[selectedAppointment, t],
|
||||
);
|
||||
|
||||
const refreshHistory = useCallback(async (patientId: string) => {
|
||||
const refreshHistory = useCallback(async (patientId: string, options?: { silentLabCases?: boolean }) => {
|
||||
const requestId = ++historyRequestRef.current;
|
||||
try {
|
||||
const response = await treatmentsApi.listPatientHistory(patientId, 50);
|
||||
if (requestId !== historyRequestRef.current) return;
|
||||
setHistory(response.data);
|
||||
void refreshPatientLabCases(patientId);
|
||||
void refreshPatientLabCases(patientId, { silent: options?.silentLabCases ?? false });
|
||||
} catch (error: unknown) {
|
||||
if (requestId !== historyRequestRef.current) return;
|
||||
showError(getUserFacingError(error, tErrors, t('errorLoadHistory')));
|
||||
@@ -1119,10 +1159,16 @@ export function TreatmentWorkspace({
|
||||
|
||||
skipNextGetDraftRef.current = true;
|
||||
draftHydratingRef.current = true;
|
||||
if (focusDetailClientId && options?.scrollToLabPanel !== false) {
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
}
|
||||
hydrateFromTreatment(treatmentToLoad);
|
||||
draftHydratingRef.current = false;
|
||||
|
||||
if (focusDetailClientId) {
|
||||
if (options?.scrollToLabPanel !== false) {
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
}
|
||||
setActiveDetailId(focusDetailClientId);
|
||||
const mappedLabCases = withoutEmptyLabCaseDrafts(
|
||||
(treatmentToLoad.labCases ?? []).map(mapLabCaseDraftFromApi),
|
||||
@@ -1187,6 +1233,7 @@ export function TreatmentWorkspace({
|
||||
(item: LabDispatchAttentionItem) => {
|
||||
if (item.isCurrentDraft) {
|
||||
exitBrowse();
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
setActiveDetailId(item.detailClientId);
|
||||
const linked = labCaseDrafts.find(
|
||||
(lc) => !lc.sentAt && lc.detailClientId === item.detailClientId,
|
||||
@@ -1268,12 +1315,21 @@ export function TreatmentWorkspace({
|
||||
workspaceMode === 'live' &&
|
||||
!isBrowsing
|
||||
) {
|
||||
pendingEntryStepRef.current = 'lab';
|
||||
setActiveDetailId(item.detailClientId);
|
||||
const matchingDraft = labCaseDrafts.find((lc) => lc.id === item.labCaseId);
|
||||
if (matchingDraft) {
|
||||
setActiveLabCaseId(matchingDraft.clientId);
|
||||
}
|
||||
setEntryStep('lab');
|
||||
requestAnimationFrame(() => {
|
||||
scrollWithinMainScrollContainer(labPanelRef.current);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await loadTreatmentIntoWorkspace(treatment, item.detailClientId, {
|
||||
scrollToLabPanel: false,
|
||||
scrollToLabPanel: true,
|
||||
});
|
||||
})();
|
||||
},
|
||||
@@ -1284,6 +1340,7 @@ export function TreatmentWorkspace({
|
||||
history,
|
||||
historyPanelItems,
|
||||
isBrowsing,
|
||||
labCaseDrafts,
|
||||
loadTreatmentIntoWorkspace,
|
||||
selectedAppointment?.id,
|
||||
selectedAppointmentId,
|
||||
@@ -1456,6 +1513,9 @@ export function TreatmentWorkspace({
|
||||
? (nextDetails[Math.min(idx, nextDetails.length - 1)]?.clientId ??
|
||||
nextDetails[0]?.clientId)
|
||||
: activeDetailId;
|
||||
|
||||
// Sync ref before any persist triggered by lab-case cleanup (same tick).
|
||||
detailsRef.current = nextDetails;
|
||||
setDetails(nextDetails);
|
||||
if (nextActive) setActiveDetailId(nextActive);
|
||||
|
||||
@@ -1554,6 +1614,9 @@ export function TreatmentWorkspace({
|
||||
const activeDetail = details.find((d) => d.clientId === activeDetailId);
|
||||
if (!activeDetail || !isDetailReadyForLabDispatch(activeDetail, labDependentCodes)) return;
|
||||
if (isLabDependentDetailMissingTeeth(activeDetail, labDependentCodes)) return;
|
||||
// Already shipped (or locked) — do not create another draft (avoids post-send flicker).
|
||||
if (isDetailLocked(activeDetail)) return;
|
||||
if (labCaseDrafts.some((lc) => lc.detailClientId === activeDetailId && lc.sentAt)) return;
|
||||
const hasDraft = labCaseDrafts.some((lc) => lc.detailClientId === activeDetailId);
|
||||
if (hasDraft) return;
|
||||
void handleAddLabCase();
|
||||
@@ -1563,6 +1626,7 @@ export function TreatmentWorkspace({
|
||||
details,
|
||||
entryStep,
|
||||
handleAddLabCase,
|
||||
isDetailLocked,
|
||||
labCaseDrafts,
|
||||
labDependentCodes,
|
||||
selectedAppointment,
|
||||
@@ -1608,12 +1672,17 @@ export function TreatmentWorkspace({
|
||||
const draftResponse = await treatmentsApi.getDraft(selectedAppointment.id);
|
||||
if (draftResponse.data?.details?.length) {
|
||||
const mapped = draftResponse.data.details.map(mapDetailFromApi);
|
||||
detailsRef.current = mapped;
|
||||
setDetails(mapped);
|
||||
setActiveDetailId((prev) => {
|
||||
const stillExists = mapped.some((d) => d.clientId === prev);
|
||||
return stillExists ? prev : mapped[0]?.clientId ?? prev;
|
||||
});
|
||||
setSavedSnapshot(serializeDetails(mapped));
|
||||
} else {
|
||||
const sentDetailClientId = labCase.detailClientId;
|
||||
setDetails((prev) =>
|
||||
prev.map((detail) => {
|
||||
setDetails((prev) => {
|
||||
const next = prev.map((detail) => {
|
||||
if (detail.clientId !== sentDetailClientId) return detail;
|
||||
return {
|
||||
...detail,
|
||||
@@ -1624,8 +1693,10 @@ export function TreatmentWorkspace({
|
||||
? [response.data.destinationOrganizationId]
|
||||
: detail.sendToOrganizationIds,
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
detailsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
setLabCaseDrafts((prev) =>
|
||||
@@ -1637,6 +1708,7 @@ export function TreatmentWorkspace({
|
||||
sentAt: response.data.sentAt,
|
||||
destinationOrganizationId: response.data.destinationOrganizationId,
|
||||
sends: response.data.sends,
|
||||
taskProgress: response.data.taskProgress ?? lc.taskProgress,
|
||||
}
|
||||
: lc,
|
||||
),
|
||||
@@ -1649,8 +1721,8 @@ export function TreatmentWorkspace({
|
||||
showSuccess(t('successCaseSent'));
|
||||
notifyTabBadgesChanged();
|
||||
if (selectedAppointment.patientId) {
|
||||
void refreshPatientLabCases(selectedAppointment.patientId);
|
||||
void refreshHistory(selectedAppointment.patientId);
|
||||
// Silent: avoid rail/history loading flicker while staying on Lab step.
|
||||
void refreshHistory(selectedAppointment.patientId, { silentLabCases: true });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
showError(getUserFacingError(error, tErrors, t('errorSendCase')));
|
||||
@@ -1663,7 +1735,6 @@ export function TreatmentWorkspace({
|
||||
selectedAppointment,
|
||||
persistDraft,
|
||||
persistLabCases,
|
||||
refreshPatientLabCases,
|
||||
refreshHistory,
|
||||
showSuccess,
|
||||
showError,
|
||||
@@ -1959,17 +2030,14 @@ export function TreatmentWorkspace({
|
||||
|
||||
let nextGroups: ReturnType<typeof toggleToothInGroups> | null = null;
|
||||
|
||||
// Shift wins if both modifiers are held (connected span).
|
||||
if (event.shiftKey || event.ctrlKey) {
|
||||
if (event.shiftKey) {
|
||||
const anchor = rangeAnchorRef.current;
|
||||
// Need a prior click as range start; modifier alone on one tooth does nothing.
|
||||
// Need a prior click as range start; shift alone on one tooth does nothing.
|
||||
if (!anchor || anchor === fdi) {
|
||||
rangeAnchorRef.current = fdi;
|
||||
return;
|
||||
}
|
||||
nextGroups = event.shiftKey
|
||||
? applyShiftRange(currentGroups, anchor, fdi)
|
||||
: applyCtrlRange(currentGroups, anchor, fdi);
|
||||
nextGroups = applyShiftRange(currentGroups, anchor, fdi);
|
||||
rangeAnchorRef.current = fdi;
|
||||
if (!nextGroups) return;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user