improvement: all confirmed suggestions implemented

This commit is contained in:
2026-09-04 17:04:48 +03:30
parent 7f92e735fb
commit 72f885d9dd
23 changed files with 1070 additions and 679 deletions

View File

@@ -60,7 +60,7 @@ import {
toggleToothInGroups,
unlinkAdjacentTeeth,
} from '@/components/treatment/toothSelectionGroups';
import { hasArchJobs } from '@/components/treatment/prosthesisTree';
import { hasArchJobs, pruneDetailTeethToJobs } from '@/components/treatment/prosthesisTree';
import type { LabDispatchAttentionItem } from '@/components/treatment/labDispatchAttention';
import { collectLabDispatchAttention } from '@/components/treatment/labDispatchAttention';
import {
@@ -114,6 +114,19 @@ function labCaseDraftForDetail(
return rows.find((lc) => !lc.sentAt) ?? rows[0];
}
function pruneLabDependentDetailsToJobs(
details: TreatmentDetailDraft[],
drafts: LabCaseDraft[],
labDependentCodes: Set<string>,
): TreatmentDetailDraft[] {
if (labDependentCodes.size === 0) return details;
return details.map((detail) => {
if (!labDependentCodes.has(detail.treatmentType)) return detail;
const jobs = labCaseDraftForDetail(drafts, detail.clientId)?.toothProsthesis ?? [];
return pruneDetailTeethToJobs(detail, jobs);
});
}
function labCaseDraftsToPast(
labCaseDrafts: LabCaseDraft[],
details: TreatmentDetailDraft[],
@@ -422,6 +435,29 @@ interface TreatmentWorkspaceProps {
initialLabCaseId?: string | null;
}
function WholePlanToggle({
pressed,
label,
onToggle,
}: {
pressed: boolean;
label: string;
onToggle: () => void;
}) {
return (
<Button
type="button"
variant={pressed ? 'secondary' : 'outline'}
size="sm"
className="h-8"
aria-pressed={pressed}
onClick={onToggle}
>
{label}
</Button>
);
}
export function TreatmentWorkspace({
userId,
currentOrganization,
@@ -518,6 +554,7 @@ export function TreatmentWorkspace({
const pendingAppointmentIdRef = useRef<string | null>(initialAppointmentId);
const pendingLabCaseIdRef = useRef<string | null>(initialLabCaseId);
const labPanelRef = useRef<HTMLDivElement>(null);
const [labWorkspaceView, setLabWorkspaceView] = useState<'chart' | 'dispatch'>('chart');
const historyRequestRef = useRef(0);
/** When set, switching the active detail scrolls to the lab dispatch panel. */
const pendingScrollToLabRef = useRef(false);
@@ -704,6 +741,20 @@ export function TreatmentWorkspace({
[activeDetail, labDependentCodes],
);
useEffect(() => {
if (labDependentCodes.size === 0) return;
setDetails((prev) => {
const next = pruneLabDependentDetailsToJobs(
prev,
labCaseDraftsRef.current,
labDependentCodes,
);
if (serializeDetails(next) === serializeDetails(prev)) return prev;
detailsRef.current = next;
return next;
});
}, [labDependentCodes]);
const activeTypeSelected = Boolean(activeDetail && isDetailTypeSelected(activeDetail));
const activeLocked = Boolean(activeDetail && isDetailLocked(activeDetail));
const activeHasArchJobs = hasArchJobs(
@@ -846,7 +897,14 @@ export function TreatmentWorkspace({
treatment: PastTreatment,
options?: { seedBlankIfEmpty?: boolean },
) => {
const mapped = treatment.details.map(mapDetailFromApi);
const mappedLabCases = withoutEmptyLabCaseDrafts(
(treatment.labCases ?? []).map(mapLabCaseDraftFromApi),
);
const mapped = pruneLabDependentDetailsToJobs(
treatment.details.map(mapDetailFromApi),
mappedLabCases,
labDependentCodes,
);
const nextDetails =
mapped.length > 0
? mapped
@@ -859,9 +917,6 @@ export function TreatmentWorkspace({
return stillExists ? prev : (nextDetails[0]?.clientId ?? '');
});
setSavedSnapshot(serializeDetails(nextDetails));
const mappedLabCases = withoutEmptyLabCaseDrafts(
(treatment.labCases ?? []).map(mapLabCaseDraftFromApi),
);
const labSnap = serializeLabCases(mappedLabCases);
labCaseDraftsRef.current = mappedLabCases;
savedLabCasesSnapshotRef.current = labSnap;
@@ -870,7 +925,7 @@ export function TreatmentWorkspace({
setActiveLabCaseId(mappedLabCases[0]?.clientId ?? null);
setOrganizationSearch('');
setSaveStatus('idle');
}, []);
}, [labDependentCodes]);
const selectedTeethSet = useMemo(() => new Set(activeDetail?.teeth ?? []), [activeDetail?.teeth]);
const connectedSelectedTeeth = useMemo(
@@ -929,12 +984,19 @@ export function TreatmentWorkspace({
setOrganizationSearch('');
if (pendingScrollToLabRef.current) {
pendingScrollToLabRef.current = false;
setLabWorkspaceView('dispatch');
requestAnimationFrame(() => {
scrollWithinMainScrollContainer(labPanelRef.current);
});
} else {
setLabWorkspaceView('chart');
}
}, [activeDetailId]);
useEffect(() => {
if (!showLabDispatch) setLabWorkspaceView('chart');
}, [showLabDispatch]);
// Sync active lab shipment when the selected treatment detail changes.
useEffect(() => {
const match = labCaseDraftForDetail(labCaseDrafts, activeDetailId);
@@ -1201,8 +1263,16 @@ export function TreatmentWorkspace({
: await treatmentsApi.getDraftByTreatment(treatmentId!);
if (cancelled) return;
const mappedLabCases = withoutEmptyLabCaseDrafts(
(response.data?.labCases ?? []).map(mapLabCaseDraftFromApi),
);
if (response.data?.details?.length) {
const mapped = response.data.details.map(mapDetailFromApi);
const mapped = pruneLabDependentDetailsToJobs(
response.data.details.map(mapDetailFromApi),
mappedLabCases,
labDependentCodes,
);
setDetails(mapped);
setActiveDetailId((prev) => {
const stillExists = mapped.some((d) => d.clientId === prev);
@@ -1222,9 +1292,6 @@ export function TreatmentWorkspace({
setSavedSnapshot(serializeDetails([first]));
}
const mappedLabCases = withoutEmptyLabCaseDrafts(
(response.data?.labCases ?? []).map(mapLabCaseDraftFromApi),
);
const labSnap = serializeLabCases(mappedLabCases);
labCaseDraftsRef.current = mappedLabCases;
savedLabCasesSnapshotRef.current = labSnap;
@@ -1253,6 +1320,7 @@ export function TreatmentWorkspace({
selectedStandalone?.id,
workspaceMode,
treatmentCatalog,
labDependentCodes,
showError,
t,
tErrors,
@@ -1270,7 +1338,15 @@ export function TreatmentWorkspace({
const patientId = selectedAppointment?.patientId ?? selectedStandalone!.patientId;
const treatmentAt = selectedAppointment?.startAt ?? selectedStandalone!.treatmentAt;
const currentDetails = detailsRef.current;
const currentDetails = pruneLabDependentDetailsToJobs(
detailsRef.current,
labCaseDraftsRef.current,
labDependentCodes,
);
if (serializeDetails(currentDetails) !== serializeDetails(detailsRef.current)) {
detailsRef.current = currentDetails;
setDetails(currentDetails);
}
const dirty = isDetailsDirty(currentDetails, savedSnapshotRef.current);
if (!options?.force && !dirty) {
@@ -1333,7 +1409,7 @@ export function TreatmentWorkspace({
}
return response.data;
},
[selectedAppointment, selectedStandalone, t],
[labDependentCodes, selectedAppointment, selectedStandalone, t],
);
const persistLabCases = useCallback(
@@ -1881,8 +1957,11 @@ export function TreatmentWorkspace({
(item: LabDispatchAttentionItem) => {
if (item.isCurrentDraft) {
exitBrowse();
pendingScrollToLabRef.current = true;
setActiveDetailId(item.detailClientId);
setLabWorkspaceView('dispatch');
if (item.detailClientId !== activeDetailId) {
pendingScrollToLabRef.current = true;
setActiveDetailId(item.detailClientId);
}
const linked = labCaseDrafts.find(
(lc) => !lc.sentAt && lc.detailClientId === item.detailClientId,
);
@@ -1901,7 +1980,7 @@ export function TreatmentWorkspace({
if (!treatment) return;
void loadTreatmentIntoWorkspace(treatment, item.detailClientId);
},
[exitBrowse, history, historyPanelItems, labCaseDrafts, loadTreatmentIntoWorkspace],
[exitBrowse, activeDetailId, history, historyPanelItems, labCaseDrafts, loadTreatmentIntoWorkspace],
);
const handleSelectPatientLabCase = useCallback(
@@ -1949,8 +2028,11 @@ export function TreatmentWorkspace({
workspaceMode === 'live' &&
!isBrowsing
) {
pendingScrollToLabRef.current = true;
setActiveDetailId(item.detailClientId);
setLabWorkspaceView('dispatch');
if (item.detailClientId !== activeDetailId) {
pendingScrollToLabRef.current = true;
setActiveDetailId(item.detailClientId);
}
const matchingDraft = labCaseDrafts.find((lc) => lc.id === item.labCaseId);
if (matchingDraft) {
setActiveLabCaseId(matchingDraft.clientId);
@@ -1967,6 +2049,7 @@ export function TreatmentWorkspace({
})();
},
[
activeDetailId,
activePatientId,
currentDraftPreview,
handleLabCaseMarkedRead,
@@ -2244,6 +2327,7 @@ export function TreatmentWorkspace({
if (detail.treatmentType === nextType) return;
const leavingLab = labDependentCodes.has(detail.treatmentType);
const enteringLab = labDependentCodes.has(nextType);
const jobs =
labCaseDraftForDetail(labCaseDrafts, activeDetailId)?.toothProsthesis ?? [];
const hasWork =
@@ -2257,7 +2341,7 @@ export function TreatmentWorkspace({
const nextDetails = details.map((d) => {
if (d.clientId !== activeDetailId) return d;
if (leavingLab) {
if (leavingLab || enteringLab) {
return {
...d,
treatmentType: nextType,
@@ -2495,6 +2579,39 @@ export function TreatmentWorkspace({
],
);
const labDispatchPanel = (
<LabCasesDispatchPanel
details={details}
activeDetailId={activeDetailId}
labCases={labCaseDrafts}
labDependentCodes={labDependentCodes}
treatmentCatalog={treatmentCatalog}
prosthesisCatalog={prosthesisCatalog}
labCaseSummary={activeLabCaseSummary}
locale={locale}
onLabCaseSummaryChange={handleLabCaseSummaryChange}
onLabCaseMarkedRead={handleLabCaseMarkedRead}
onLabCaseActivityChange={() => {
if (historyPatientId) {
void refreshPatientLabCases(historyPatientId, { silent: true });
}
}}
activeLabCaseId={activeLabCaseId}
onLabCasesChange={handleLabCasesChange}
disabled={!canEditTreatmentForDay}
canEdit={canEdit}
orgs={orgs}
organizationSearch={organizationSearch}
onOrganizationSearchChange={setOrganizationSearch}
recentOrganizationIds={recentOrganizationIds}
sendBusyId={sendBusyId}
onSendLabCase={(lc, comment) => handleSendLabCase(lc, comment)}
onCommentError={showError}
canInviteLab={canAccessOrganizations}
onInviteLab={() => router.push('/organizations?action=invite-lab')}
/>
);
if (!canView) {
return (
<div className="surface-card p-6 max-w-xl">
@@ -2784,10 +2901,19 @@ export function TreatmentWorkspace({
showFields
hasArchJobs={activeHasArchJobs}
voice={voiceForEditor}
chartLocked={!activeTypeSelected && !showWholeTreatmentPlan}
chartLocked={!activeTypeSelected && !showWholeTreatmentPlan && labWorkspaceView !== 'dispatch'}
chartLockMessage={t('selectTypeBeforeTeeth')}
showLabDispatchToggle={showLabDispatch}
labWorkspaceView={labWorkspaceView}
onLabWorkspaceViewChange={(view) => {
setShowWholeTreatmentPlan(false);
setLabWorkspaceView(view);
}}
chart={
prosthesisAssignActive ? (
<div ref={labPanelRef} className="min-w-0 w-full">
{showLabDispatch && labWorkspaceView === 'dispatch' ? (
labDispatchPanel
) : prosthesisAssignActive ? (
<ProsthesisAssignChart
className="w-full"
groups={
@@ -2803,22 +2929,11 @@ export function TreatmentWorkspace({
catalog={prosthesisCatalog}
disabled={!canEditTreatmentForDay || activeLocked}
headerControl={
<button
type="button"
onClick={() => setShowWholeTreatmentPlan((open) => !open)}
aria-pressed={showWholeTreatmentPlan}
className={`
rounded-[var(--radius-md)] border px-2.5 py-1 text-[11px] leading-none transition-colors
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45
${
showWholeTreatmentPlan
? 'border-primary bg-primary-soft font-medium text-text-primary'
: 'border-border/70 text-text-secondary hover:border-border hover:bg-background-card/50'
}
`}
>
{t('toothChartWholePlan')}
</button>
<WholePlanToggle
pressed={showWholeTreatmentPlan}
label={t('toothChartWholePlan')}
onToggle={() => setShowWholeTreatmentPlan((open) => !open)}
/>
}
onToggleLink={(a, b) => {
if (
@@ -2868,35 +2983,34 @@ export function TreatmentWorkspace({
);
}}
onChange={({ groups, toothProsthesis }) => {
setDetails((prev) =>
prev.map((d) =>
d.clientId !== activeDetailId
? d
: {
...d,
toothSelectionGroups: groups,
teeth: deriveTeethFromGroups(groups),
},
),
const nextDetails = details.map((d) =>
d.clientId !== activeDetailId
? d
: {
...d,
toothSelectionGroups: groups,
teeth: deriveTeethFromGroups(groups),
},
);
setLabCaseDrafts((prev) => {
const existing = prev.find(
(lc) => lc.detailClientId === activeDetailId && !lc.sentAt,
);
if (existing) {
return prev.map((lc) =>
detailsRef.current = nextDetails;
setDetails(nextDetails);
const prev = labCaseDraftsRef.current;
const existing = prev.find(
(lc) => lc.detailClientId === activeDetailId && !lc.sentAt,
);
const nextDrafts = existing
? prev.map((lc) =>
lc.clientId === existing.clientId ? { ...lc, toothProsthesis } : lc,
);
}
return [
...prev,
{
...newLabCaseDraft(),
detailClientId: activeDetailId,
toothProsthesis,
},
];
});
)
: [
...prev,
{
...newLabCaseDraft(),
detailClientId: activeDetailId,
toothProsthesis,
},
];
handleLabCasesChange(nextDrafts);
}}
/>
) : (
@@ -2908,22 +3022,11 @@ export function TreatmentWorkspace({
toothColors={chartToothColors}
readOnly={showWholeTreatmentPlan}
headerControl={
<button
type="button"
onClick={() => setShowWholeTreatmentPlan((open) => !open)}
aria-pressed={showWholeTreatmentPlan}
className={`
rounded-[var(--radius-md)] border px-2.5 py-1 text-[11px] leading-none transition-colors
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45
${
showWholeTreatmentPlan
? 'border-primary bg-primary-soft font-medium text-text-primary'
: 'border-border/70 text-text-secondary hover:border-border hover:bg-background-card/50'
}
`}
>
{t('toothChartWholePlan')}
</button>
<WholePlanToggle
pressed={showWholeTreatmentPlan}
label={t('toothChartWholePlan')}
onToggle={() => setShowWholeTreatmentPlan((open) => !open)}
/>
}
onToggle={(fdi, event) => {
if (
@@ -3084,43 +3187,10 @@ export function TreatmentWorkspace({
}}
/>
)
}
</div>
}
/>
{showLabDispatch ? (
<div ref={labPanelRef} className="space-y-3">
<LabCasesDispatchPanel
details={details}
activeDetailId={activeDetailId}
labCases={labCaseDrafts}
labDependentCodes={labDependentCodes}
treatmentCatalog={treatmentCatalog}
prosthesisCatalog={prosthesisCatalog}
labCaseSummary={activeLabCaseSummary}
locale={locale}
onLabCaseSummaryChange={handleLabCaseSummaryChange}
onLabCaseMarkedRead={handleLabCaseMarkedRead}
onLabCaseActivityChange={() => {
if (historyPatientId) {
void refreshPatientLabCases(historyPatientId, { silent: true });
}
}}
activeLabCaseId={activeLabCaseId}
onLabCasesChange={handleLabCasesChange}
disabled={!canEditTreatmentForDay}
canEdit={canEdit}
orgs={orgs}
organizationSearch={organizationSearch}
onOrganizationSearchChange={setOrganizationSearch}
recentOrganizationIds={recentOrganizationIds}
sendBusyId={sendBusyId}
onSendLabCase={(lc, comment) => handleSendLabCase(lc, comment)}
onCommentError={showError}
canInviteLab={canAccessOrganizations}
onInviteLab={() => router.push('/organizations?action=invite-lab')}
/>
</div>
) : null}
</>
)}
</div>