Compare commits
2 Commits
a7ba33fb68
...
953e609fb8
| Author | SHA1 | Date | |
|---|---|---|---|
| 953e609fb8 | |||
| b7682a2d1c |
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
? history.filter((item) => item.appointmentId !== currentDraft.appointmentId)
|
|
||||||
: history;
|
|
||||||
return [currentDraft, ...withoutCurrentAppointment];
|
|
||||||
})();
|
|
||||||
|
|
||||||
return source.filter((treatment) => {
|
return source.filter((treatment) => {
|
||||||
if (!treatmentMatchesHistoryDate(treatment.treatmentAt, filters.date)) {
|
if (!treatmentMatchesHistoryDate(treatment.treatmentAt, filters.date)) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user