improvement: all confirmed suggestions implemented
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user