Compare commits

...

2 Commits

7 changed files with 49 additions and 43 deletions

View File

@@ -70,7 +70,8 @@ Thin route: `app/[locale]/(dashboard)/treatment/page.tsx` (supports `?appointmen
- **Not shipped to lab** — show treatments that have at least one lab-dependent detail (prosthesis via `labDependentCodes`) with `!sentAt`. - **Not shipped to lab** — show treatments that have at least one lab-dependent detail (prosthesis via `labDependentCodes`) with `!sentAt`.
- **Date** — filter on `treatmentAt` matching that local calendar day. - **Date** — filter on `treatmentAt` matching that local calendar day.
- When **not shipped** is on and workspace is live (not browsing), prepend a synthetic **current draft** row (`id: 'current-draft'`) if it has pending lab-dependent details. - When **not shipped** is on, filters **already-fetched** history only (excludes the active appointment row).
- **Previous treatments rail** excludes the active appointment; live draft stays in **Current draft** preview only. Refresh history after lab send.
Helpers: `frontend/src/components/treatment/treatmentHistoryFilters.ts`. Helpers: `frontend/src/components/treatment/treatmentHistoryFilters.ts`.

View File

@@ -25,17 +25,13 @@ export function treatmentMatchesHistoryDate(treatmentAt: string, date: string):
export function filterTreatmentHistoryItems( export function filterTreatmentHistoryItems(
history: PastTreatment[], history: PastTreatment[],
currentDraft: PastTreatment | null,
labDependentCodes: Set<string>, labDependentCodes: Set<string>,
filters: TreatmentHistoryFilters, filters: TreatmentHistoryFilters,
excludeAppointmentId?: string | null,
): PastTreatment[] { ): PastTreatment[] {
const source = (() => { const source = excludeAppointmentId
if (!currentDraft) return history; ? history.filter((item) => item.appointmentId !== excludeAppointmentId)
const withoutCurrentAppointment = currentDraft.appointmentId
? history.filter((item) => item.appointmentId !== currentDraft.appointmentId)
: history; : history;
return [currentDraft, ...withoutCurrentAppointment];
})();
return source.filter((treatment) => { return source.filter((treatment) => {
if (!treatmentMatchesHistoryDate(treatment.treatmentAt, filters.date)) { if (!treatmentMatchesHistoryDate(treatment.treatmentAt, filters.date)) {

View File

@@ -156,7 +156,7 @@ export function AppDateInput({
<div <div
id={`${panelId}-parts`} id={`${panelId}-parts`}
role="dialog" role="dialog"
className="absolute left-0 right-0 top-full z-50 mt-1 rounded-[var(--radius-md)] border border-border bg-background-secondary p-3 shadow-lg" className="absolute start-0 top-full z-50 mt-1 min-w-[17.5rem] w-max max-w-[calc(100vw-1.5rem)] rounded-[var(--radius-md)] border border-border bg-background-secondary p-3 shadow-lg"
> >
<CalendarDayPartsPanel <CalendarDayPartsPanel
panelId={panelId} panelId={panelId}

View File

@@ -27,6 +27,8 @@ const MONTH_KEYS = [
'monthDecember', 'monthDecember',
] as const; ] as const;
const COMPACT_SELECT_CLASS = 'text-xs ps-2 pe-8 py-1';
function daysInMonth(year: number, month: number): number { function daysInMonth(year: number, month: number): number {
return new Date(year, month + 1, 0).getDate(); return new Date(year, month + 1, 0).getDate();
} }
@@ -102,14 +104,15 @@ export function CalendarDayPartsPanel({
} }
return ( return (
<div className="grid grid-cols-3 gap-2"> <div className="grid min-w-[17rem] grid-cols-[minmax(4.25rem,1fr)_minmax(6.75rem,1.35fr)_minmax(3.25rem,0.85fr)] gap-1.5">
<div> <div className="min-w-0">
<label htmlFor={`${panelId}-year`} className="mb-1 block text-xs font-medium text-text-muted"> <label htmlFor={`${panelId}-year`} className="mb-1 block text-xs font-medium text-text-muted">
{t('year')} {t('year')}
</label> </label>
<CompactSelect <CompactSelect
id={`${panelId}-year`} id={`${panelId}-year`}
value={selectedYear} value={selectedYear}
className={COMPACT_SELECT_CLASS}
onChange={(e) => applyParts(Number(e.target.value), selectedMonth, selectedDay)} onChange={(e) => applyParts(Number(e.target.value), selectedMonth, selectedDay)}
> >
{years.map((year) => ( {years.map((year) => (
@@ -120,13 +123,14 @@ export function CalendarDayPartsPanel({
</CompactSelect> </CompactSelect>
</div> </div>
<div> <div className="min-w-0">
<label htmlFor={`${panelId}-month`} className="mb-1 block text-xs font-medium text-text-muted"> <label htmlFor={`${panelId}-month`} className="mb-1 block text-xs font-medium text-text-muted">
{t('month')} {t('month')}
</label> </label>
<CompactSelect <CompactSelect
id={`${panelId}-month`} id={`${panelId}-month`}
value={selectedMonth} value={selectedMonth}
className={COMPACT_SELECT_CLASS}
onChange={(e) => applyParts(selectedYear, Number(e.target.value), selectedDay)} onChange={(e) => applyParts(selectedYear, Number(e.target.value), selectedDay)}
> >
{persian {persian
@@ -143,13 +147,14 @@ export function CalendarDayPartsPanel({
</CompactSelect> </CompactSelect>
</div> </div>
<div> <div className="min-w-0">
<label htmlFor={`${panelId}-day`} className="mb-1 block text-xs font-medium text-text-muted"> <label htmlFor={`${panelId}-day`} className="mb-1 block text-xs font-medium text-text-muted">
{t('day')} {t('day')}
</label> </label>
<CompactSelect <CompactSelect
id={`${panelId}-day`} id={`${panelId}-day`}
value={selectedDay} value={selectedDay}
className={COMPACT_SELECT_CLASS}
onChange={(e) => onChange={(e) =>
applyParts(selectedYear, selectedMonth, Number(e.target.value), closePanelOnDaySelect) applyParts(selectedYear, selectedMonth, Number(e.target.value), closePanelOnDaySelect)
} }

View File

@@ -6,7 +6,7 @@ type CompactSelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
export function CompactSelect({ className = '', children, ...props }: CompactSelectProps) { export function CompactSelect({ className = '', children, ...props }: CompactSelectProps) {
return ( return (
<select <select
className={`form-select w-full appearance-none rounded-[var(--radius-sm)] border border-border bg-background-card/90 text-text-primary text-sm ps-3 pe-10 py-1.5 focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong disabled:cursor-not-allowed disabled:opacity-60 ${className}`} className={`form-select w-full min-w-0 appearance-none rounded-[var(--radius-sm)] border border-border bg-background-card/90 text-text-primary text-sm ps-3 pe-10 py-1.5 focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong disabled:cursor-not-allowed disabled:opacity-60 ${className}`}
{...props} {...props}
> >
{children} {children}

View File

@@ -15,7 +15,6 @@ import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
interface PastTreatmentsPanelProps { interface PastTreatmentsPanelProps {
items: PastTreatment[]; items: PastTreatment[];
currentDraft?: PastTreatment | null;
patientName?: string; patientName?: string;
currentAppointmentId?: string | null; currentAppointmentId?: string | null;
treatmentCatalog: TreatmentCatalogEntry[]; treatmentCatalog: TreatmentCatalogEntry[];
@@ -33,7 +32,6 @@ function formatHistoryTimestamp(iso: string, locale: string): string {
export function PastTreatmentsPanel({ export function PastTreatmentsPanel({
items, items,
currentDraft = null,
patientName, patientName,
currentAppointmentId, currentAppointmentId,
treatmentCatalog, treatmentCatalog,
@@ -54,11 +52,11 @@ export function PastTreatmentsPanel({
const displayedItems = useMemo( const displayedItems = useMemo(
() => () =>
filterTreatmentHistoryItems(items, currentDraft, labDependentCodes, { filterTreatmentHistoryItems(items, labDependentCodes, {
notShippedOnly, notShippedOnly,
date: filterDate, date: filterDate,
}), }, currentAppointmentId),
[items, currentDraft, labDependentCodes, notShippedOnly, filterDate], [items, currentAppointmentId, labDependentCodes, notShippedOnly, filterDate],
); );
function clearFilters() { function clearFilters() {
@@ -119,9 +117,6 @@ export function PastTreatmentsPanel({
> >
{displayedItems.map((treatment) => { {displayedItems.map((treatment) => {
const isSelected = selectedPreviewId === treatment.id; const isSelected = selectedPreviewId === treatment.id;
const isCurrentAppointment =
Boolean(currentAppointmentId) && treatment.appointmentId === currentAppointmentId;
const isLiveDraft = treatment.id === 'current-draft';
return ( return (
<article <article
@@ -152,15 +147,6 @@ export function PastTreatmentsPanel({
> >
{formatHistoryTimestamp(treatment.treatmentAt, locale)} {formatHistoryTimestamp(treatment.treatmentAt, locale)}
</time> </time>
{isLiveDraft ? (
<span className="text-[10px] font-medium uppercase tracking-wide text-primary">
{t('labAttentionCurrentDraft')}
</span>
) : isCurrentAppointment ? (
<span className="text-[10px] font-medium uppercase tracking-wide text-primary">
{t('historyCurrentAppointment')}
</span>
) : null}
</div> </div>
{treatment.details.length === 0 ? ( {treatment.details.length === 0 ? (

View File

@@ -501,12 +501,19 @@ export function TreatmentWorkspace({
const previewTreatment = useMemo(() => { const previewTreatment = useMemo(() => {
if (!selectedPreviewId) return currentDraftPreview; if (!selectedPreviewId) return currentDraftPreview;
return ( const fromHistory =
history.find((item) => item.id === selectedPreviewId) ?? history.find((item) => item.id === selectedPreviewId) ??
historyPanelItems.find((item) => item.id === selectedPreviewId) ?? historyPanelItems.find((item) => item.id === selectedPreviewId);
currentDraftPreview if (
); workspaceMode === 'live' &&
}, [selectedPreviewId, history, historyPanelItems, currentDraftPreview]); currentDraftPreview?.appointmentId &&
(selectedPreviewId === currentDraftPreview.id ||
fromHistory?.appointmentId === currentDraftPreview.appointmentId)
) {
return currentDraftPreview;
}
return fromHistory ?? currentDraftPreview;
}, [selectedPreviewId, history, historyPanelItems, currentDraftPreview, workspaceMode]);
const isBrowsing = selectedPreviewId !== null; const isBrowsing = selectedPreviewId !== null;
@@ -1039,23 +1046,33 @@ export function TreatmentWorkspace({
const ok = workspaceModeRef.current === 'live' ? await flushDraftSave() : true; const ok = workspaceModeRef.current === 'live' ? await flushDraftSave() : true;
if (!ok) return false; if (!ok) return false;
const isHistorical = isTreatmentDayHistorical(treatment.treatmentAt, todayStart); let treatmentToLoad = treatment;
try {
const draftResponse = await treatmentsApi.getDraft(treatment.appointmentId);
if (draftResponse.data) {
treatmentToLoad = draftResponse.data;
}
} catch {
// Fall back to the history snapshot when draft cannot be loaded.
}
const isHistorical = isTreatmentDayHistorical(treatmentToLoad.treatmentAt, todayStart);
setWorkspaceMode(isHistorical ? 'historical' : 'live'); setWorkspaceMode(isHistorical ? 'historical' : 'live');
setSelectedPreviewId(null); setSelectedPreviewId(null);
const nextDay = startOfLocalDay(new Date(treatment.treatmentAt)); const nextDay = startOfLocalDay(new Date(treatmentToLoad.treatmentAt));
setSelectedDay((prev) => (compareLocalDayStart(prev, nextDay) === 0 ? prev : nextDay)); setSelectedDay((prev) => (compareLocalDayStart(prev, nextDay) === 0 ? prev : nextDay));
setSelectionLocked(true); setSelectionLocked(true);
setSelectedAppointmentId(treatment.appointmentId); setSelectedAppointmentId(treatmentToLoad.appointmentId ?? treatment.appointmentId);
skipNextGetDraftRef.current = true; skipNextGetDraftRef.current = true;
draftHydratingRef.current = true; draftHydratingRef.current = true;
hydrateFromTreatment(treatment); hydrateFromTreatment(treatmentToLoad);
draftHydratingRef.current = false; draftHydratingRef.current = false;
if (focusDetailClientId) { if (focusDetailClientId) {
setActiveDetailId(focusDetailClientId); setActiveDetailId(focusDetailClientId);
const mappedLabCases = withoutEmptyLabCaseDrafts( const mappedLabCases = withoutEmptyLabCaseDrafts(
(treatment.labCases ?? []).map(mapLabCaseDraftFromApi), (treatmentToLoad.labCases ?? []).map(mapLabCaseDraftFromApi),
); );
const linked = mappedLabCases.find( const linked = mappedLabCases.find(
(lc) => !lc.sentAt && lc.detailClientId === focusDetailClientId, (lc) => !lc.sentAt && lc.detailClientId === focusDetailClientId,
@@ -1510,6 +1527,7 @@ export function TreatmentWorkspace({
notifyTabBadgesChanged(); notifyTabBadgesChanged();
if (selectedAppointment.patientId) { if (selectedAppointment.patientId) {
void refreshPatientLabCases(selectedAppointment.patientId); void refreshPatientLabCases(selectedAppointment.patientId);
void refreshHistory(selectedAppointment.patientId);
} }
} catch (error: unknown) { } catch (error: unknown) {
showError(getUserFacingError(error, tErrors, t('errorSendCase'))); showError(getUserFacingError(error, tErrors, t('errorSendCase')));
@@ -1523,6 +1541,7 @@ export function TreatmentWorkspace({
persistDraft, persistDraft,
persistLabCases, persistLabCases,
refreshPatientLabCases, refreshPatientLabCases,
refreshHistory,
showSuccess, showSuccess,
showError, showError,
t, t,
@@ -1675,7 +1694,6 @@ export function TreatmentWorkspace({
> >
<PastTreatmentsPanel <PastTreatmentsPanel
items={historyPanelItems} items={historyPanelItems}
currentDraft={null}
currentAppointmentId={selectedAppointmentId} currentAppointmentId={selectedAppointmentId}
treatmentCatalog={treatmentCatalog} treatmentCatalog={treatmentCatalog}
labDependentCodes={labDependentCodes} labDependentCodes={labDependentCodes}