bugfix/demo-bugs-fixed #19

Merged
admin merged 10 commits from bugfix/demo-bugs-fixed into master 2026-05-18 19:11:30 +03:30
4 changed files with 50 additions and 90 deletions
Showing only changes of commit 3b12c52fd3 - Show all commits

View File

@@ -285,7 +285,6 @@ export default function AppointmentsPage() {
<div className="flex flex-col sm:flex-row sm:items-end gap-4 sm:justify-between"> <div className="flex flex-col sm:flex-row sm:items-end gap-4 sm:justify-between">
<ScheduleDayPicker <ScheduleDayPicker
value={scheduleDate} value={scheduleDate}
minDate={todayStart}
onChange={(d) => setScheduleDate(startOfLocalDay(d))} onChange={(d) => setScheduleDate(startOfLocalDay(d))}
/> />
{loadingSchedule && ( {loadingSchedule && (

View File

@@ -2,17 +2,11 @@
import { useEffect, useId, useRef, useState } from 'react'; import { useEffect, useId, useRef, useState } from 'react';
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
import { import { addCalendarDays, startOfLocalDay } from '@/lib/appointmentTime';
addCalendarDays,
compareLocalDayStart,
startOfLocalDay,
} from '@/lib/appointmentTime';
interface ScheduleDayPickerProps { interface ScheduleDayPickerProps {
value: Date; value: Date;
onChange: (day: Date) => void; onChange: (day: Date) => void;
/** Optional lower bound for day selection and previous-day navigation. */
minDate?: Date;
label?: string; label?: string;
} }
@@ -39,27 +33,10 @@ function buildLocalDay(year: number, month: number, day: number): Date {
return new Date(year, month, day, 0, 0, 0, 0); return new Date(year, month, day, 0, 0, 0, 0);
} }
function clampToValidDay( function yearRange(anchor: Date): number[] {
year: number, const anchorYear = anchor.getFullYear();
month: number, const startYear = anchorYear - 10;
day: number, const endYear = anchorYear + 2;
min?: Date,
): Date {
const maxDay = daysInMonth(year, month);
let next = buildLocalDay(year, month, Math.min(Math.max(1, day), maxDay));
if (min) {
const floor = startOfLocalDay(min);
if (compareLocalDayStart(next, floor) < 0) {
next = floor;
}
}
return next;
}
function yearRange(min?: Date, anchor?: Date): number[] {
const now = new Date();
const startYear = min ? min.getFullYear() : now.getFullYear() - 5;
const endYear = Math.max(now.getFullYear() + 2, anchor?.getFullYear() ?? now.getFullYear());
const years: number[] = []; const years: number[] = [];
for (let y = startYear; y <= endYear; y += 1) { for (let y = startYear; y <= endYear; y += 1) {
years.push(y); years.push(y);
@@ -72,21 +49,18 @@ const selectClassName = `
bg-background-card/90 text-text-primary text-sm bg-background-card/90 text-text-primary text-sm
pl-2 pr-7 py-1.5 pl-2 pr-7 py-1.5
focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong focus:outline-none focus:ring-2 focus:ring-primary/35 focus:border-border-strong
disabled:opacity-50 disabled:cursor-not-allowed
`; `;
export function ScheduleDayPicker({ /**
value, * Calendar day navigator (arrows + year/month/day panel).
onChange, * Does not restrict past dates — parent pages enforce read-only vs editable for schedule grids/forms.
minDate, */
label = 'Schedule date', export function ScheduleDayPicker({ value, onChange, label = 'Schedule date' }: ScheduleDayPickerProps) {
}: ScheduleDayPickerProps) {
const panelId = useId(); const panelId = useId();
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const [panelOpen, setPanelOpen] = useState(false); const [panelOpen, setPanelOpen] = useState(false);
const normalizedValue = startOfLocalDay(value); const normalizedValue = startOfLocalDay(value);
const normalizedMin = minDate ? startOfLocalDay(minDate) : undefined;
const labelText = normalizedValue.toLocaleDateString(undefined, { const labelText = normalizedValue.toLocaleDateString(undefined, {
weekday: 'short', weekday: 'short',
@@ -95,28 +69,20 @@ export function ScheduleDayPicker({
year: 'numeric', year: 'numeric',
}); });
const previousDay = addCalendarDays(normalizedValue, -1); const years = yearRange(normalizedValue);
const canGoPrevious =
!normalizedMin || compareLocalDayStart(previousDay, normalizedMin) >= 0;
const years = yearRange(normalizedMin, normalizedValue);
const selectedYear = normalizedValue.getFullYear(); const selectedYear = normalizedValue.getFullYear();
const selectedMonth = normalizedValue.getMonth(); const selectedMonth = normalizedValue.getMonth();
const selectedDay = normalizedValue.getDate(); const selectedDay = normalizedValue.getDate();
const dayCount = daysInMonth(selectedYear, selectedMonth); const dayCount = daysInMonth(selectedYear, selectedMonth);
function applyParts(year: number, month: number, day: number, closePanel = false) { function applyParts(year: number, month: number, day: number, closePanel = false) {
onChange(clampToValidDay(year, month, day, normalizedMin)); const maxDay = daysInMonth(year, month);
onChange(buildLocalDay(year, month, Math.min(Math.max(1, day), maxDay)));
if (closePanel) { if (closePanel) {
setPanelOpen(false); setPanelOpen(false);
} }
} }
function handlePreviousDay() {
if (!canGoPrevious) return;
onChange(previousDay);
}
useEffect(() => { useEffect(() => {
if (!panelOpen) return; if (!panelOpen) return;
@@ -146,9 +112,8 @@ export function ScheduleDayPicker({
<div className="flex items-center gap-1 rounded-[var(--radius-md)] border border-border bg-background-secondary/90 px-1 py-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]"> <div className="flex items-center gap-1 rounded-[var(--radius-md)] border border-border bg-background-secondary/90 px-1 py-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]">
<button <button
type="button" type="button"
onClick={handlePreviousDay} onClick={() => onChange(addCalendarDays(normalizedValue, -1))}
disabled={!canGoPrevious} className="shrink-0 rounded-[var(--radius-sm)] p-2 text-text-muted hover:text-text-primary hover:bg-background-card/80 focus:outline-none focus:ring-2 focus:ring-primary/35"
className="shrink-0 rounded-[var(--radius-sm)] p-2 text-text-muted hover:text-text-primary hover:bg-background-card/80 focus:outline-none focus:ring-2 focus:ring-primary/35 disabled:opacity-40 disabled:pointer-events-none"
aria-label="Previous day" aria-label="Previous day"
> >
<ChevronLeft className="h-4 w-4 icon-flat" /> <ChevronLeft className="h-4 w-4 icon-flat" />
@@ -232,17 +197,11 @@ export function ScheduleDayPicker({
} }
className={selectClassName} className={selectClassName}
> >
{MONTH_LABELS.map((name, index) => { {MONTH_LABELS.map((name, index) => (
const disabled = <option key={name} value={index}>
normalizedMin && {name}
selectedYear === normalizedMin.getFullYear() && </option>
index < normalizedMin.getMonth(); ))}
return (
<option key={name} value={index} disabled={disabled}>
{name}
</option>
);
})}
</select> </select>
<ChevronDown <ChevronDown
className="pointer-events-none absolute right-1.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-text-muted icon-flat" className="pointer-events-none absolute right-1.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-text-muted icon-flat"
@@ -272,18 +231,11 @@ export function ScheduleDayPicker({
} }
className={selectClassName} className={selectClassName}
> >
{Array.from({ length: dayCount }, (_, i) => i + 1).map((day) => { {Array.from({ length: dayCount }, (_, i) => i + 1).map((day) => (
const disabled = <option key={day} value={day}>
normalizedMin && {day}
selectedYear === normalizedMin.getFullYear() && </option>
selectedMonth === normalizedMin.getMonth() && ))}
day < normalizedMin.getDate();
return (
<option key={day} value={day} disabled={disabled}>
{day}
</option>
);
})}
</select> </select>
<ChevronDown <ChevronDown
className="pointer-events-none absolute right-1.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-text-muted icon-flat" className="pointer-events-none absolute right-1.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-text-muted icon-flat"

View File

@@ -12,8 +12,6 @@ interface AppointmentsStripProps {
onToggleStripHidden: () => void; onToggleStripHidden: () => void;
selectedDay: Date; selectedDay: Date;
onSelectDay: (day: Date) => void; onSelectDay: (day: Date) => void;
/** Same lower bound as Appointments schedule (cannot pick days before this). */
minScheduleDate: Date;
appointments: TreatmentAppointment[]; appointments: TreatmentAppointment[];
selectedAppointmentId: string | null; selectedAppointmentId: string | null;
onSelectAppointment: (id: string) => void; onSelectAppointment: (id: string) => void;
@@ -25,7 +23,6 @@ export function AppointmentsStrip({
onToggleStripHidden, onToggleStripHidden,
selectedDay, selectedDay,
onSelectDay, onSelectDay,
minScheduleDate,
appointments, appointments,
selectedAppointmentId, selectedAppointmentId,
onSelectAppointment, onSelectAppointment,
@@ -65,7 +62,6 @@ export function AppointmentsStrip({
<div className="flex flex-col sm:flex-row sm:items-end gap-4 sm:justify-between"> <div className="flex flex-col sm:flex-row sm:items-end gap-4 sm:justify-between">
<ScheduleDayPicker <ScheduleDayPicker
value={selectedDay} value={selectedDay}
minDate={minScheduleDate}
onChange={(d) => onSelectDay(startOfLocalDay(d))} onChange={(d) => onSelectDay(startOfLocalDay(d))}
/> />
{loading && ( {loading && (

View File

@@ -9,7 +9,7 @@ import { Button } from '@/components/ui/shared/Button';
import { Dropdown } from '@/components/ui/shared/Dropdown'; import { Dropdown } from '@/components/ui/shared/Dropdown';
import { SearchBar } from '@/components/ui/shared/SearchBar'; import { SearchBar } from '@/components/ui/shared/SearchBar';
import { Toast } from '@/components/ui/shared/Toast'; import { Toast } from '@/components/ui/shared/Toast';
import { isSameLocalCalendarDay, startOfLocalDay } from '@/lib/appointmentTime'; import { compareLocalDayStart, isSameLocalCalendarDay, startOfLocalDay } from '@/lib/appointmentTime';
import { import {
fetchLinkedOrganizations, fetchLinkedOrganizations,
fetchMyAppointmentsForDay, fetchMyAppointmentsForDay,
@@ -54,7 +54,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
const [stripHidden, setStripHidden] = useState(false); const [stripHidden, setStripHidden] = useState(false);
const scheduleMinDate = useMemo(() => startOfLocalDay(new Date()), []); const todayStart = useMemo(() => startOfLocalDay(new Date()), []);
const [selectedDay, setSelectedDay] = useState(() => startOfLocalDay(new Date())); const [selectedDay, setSelectedDay] = useState(() => startOfLocalDay(new Date()));
const [appointments, setAppointments] = useState<TreatmentAppointment[]>([]); const [appointments, setAppointments] = useState<TreatmentAppointment[]>([]);
@@ -87,6 +87,13 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
[appointments, selectedAppointmentId], [appointments, selectedAppointmentId],
); );
const isViewingPastDay = useMemo(
() => compareLocalDayStart(selectedDay, todayStart) < 0,
[selectedDay, todayStart],
);
const canEditTreatmentForDay = Boolean(selectedAppointment) && !isViewingPastDay;
const activeRecord = useMemo( const activeRecord = useMemo(
() => records.find((r) => r.clientId === activeRecordId) ?? records[0], () => records.find((r) => r.clientId === activeRecordId) ?? records[0],
[records, activeRecordId], [records, activeRecordId],
@@ -339,13 +346,19 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
onToggleStripHidden={() => setStripHidden((s) => !s)} onToggleStripHidden={() => setStripHidden((s) => !s)}
selectedDay={selectedDay} selectedDay={selectedDay}
onSelectDay={setSelectedDay} onSelectDay={setSelectedDay}
minScheduleDate={scheduleMinDate}
appointments={appointments} appointments={appointments}
selectedAppointmentId={selectedAppointmentId} selectedAppointmentId={selectedAppointmentId}
onSelectAppointment={onPickAppointment} onSelectAppointment={onPickAppointment}
loading={apptsLoading} loading={apptsLoading}
/> />
{isViewingPastDay && (
<p className="text-sm text-text-secondary rounded-[var(--radius-md)] border border-border/60 bg-background-secondary/50 px-3 py-2">
Past days are view-only. You can review appointments and history, but treatment records
cannot be added or changed.
</p>
)}
<div className="grid grid-cols-1 xl:grid-cols-[minmax(280px,380px)_minmax(0,1fr)] gap-6 items-start"> <div className="grid grid-cols-1 xl:grid-cols-[minmax(280px,380px)_minmax(0,1fr)] gap-6 items-start">
<div className="space-y-4"> <div className="space-y-4">
{selectedAppointment ? ( {selectedAppointment ? (
@@ -423,7 +436,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<FdiToothChart <FdiToothChart
selected={selectedTeethSet} selected={selectedTeethSet}
onToggle={toggleTooth} onToggle={toggleTooth}
disabled={!selectedAppointment} disabled={!canEditTreatmentForDay}
/> />
<div className="surface-card p-4 space-y-4"> <div className="surface-card p-4 space-y-4">
@@ -437,7 +450,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<Button <Button
type="button" type="button"
variant="primary" variant="primary"
disabled={!selectedAppointment} disabled={!canEditTreatmentForDay}
onClick={() => { onClick={() => {
const nr = newRecord(); const nr = newRecord();
fixActiveAfterRecordsChange([...records, nr]); fixActiveAfterRecordsChange([...records, nr]);
@@ -486,7 +499,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
}} }}
placeholder="Write clinical notes for this record…" placeholder="Write clinical notes for this record…"
rows={5} rows={5}
disabled={!selectedAppointment} disabled={!canEditTreatmentForDay}
className="mt-1.5 w-full rounded-[var(--radius-md)] border border-border bg-background-secondary/90 text-text-primary text-sm px-3 py-2 placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/35 resize-y min-h-[120px]" className="mt-1.5 w-full rounded-[var(--radius-md)] border border-border bg-background-secondary/90 text-text-primary text-sm px-3 py-2 placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/35 resize-y min-h-[120px]"
/> />
</label> </label>
@@ -503,7 +516,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
), ),
); );
}} }}
disabled={!selectedAppointment} disabled={!canEditTreatmentForDay}
className="capitalize" className="capitalize"
style={{ color: treatmentTypeTextColor }} style={{ color: treatmentTypeTextColor }}
> >
@@ -522,7 +535,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
id="treatment-record-attachments" id="treatment-record-attachments"
type="file" type="file"
multiple multiple
disabled={!selectedAppointment} disabled={!canEditTreatmentForDay}
onChange={(e) => { onChange={(e) => {
addAttachments(e.target.files); addAttachments(e.target.files);
e.target.value = ''; e.target.value = '';
@@ -533,7 +546,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<Button <Button
type="button" type="button"
variant="primary" variant="primary"
disabled={!selectedAppointment} disabled={!canEditTreatmentForDay}
onClick={() => attachmentInputRef.current?.click()} onClick={() => attachmentInputRef.current?.click()}
aria-controls="treatment-record-attachments" aria-controls="treatment-record-attachments"
> >
@@ -589,7 +602,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<Checkbox <Checkbox
key={o.id} key={o.id}
checked={activeRecord.sendToOrganizationIds.includes(o.id)} checked={activeRecord.sendToOrganizationIds.includes(o.id)}
disabled={!selectedAppointment || Boolean(activeRecord.sentAt)} disabled={!canEditTreatmentForDay || Boolean(activeRecord.sentAt)}
onChange={(checked) => { onChange={(checked) => {
setRecords((prev) => setRecords((prev) =>
prev.map((r) => { prev.map((r) => {
@@ -614,7 +627,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<Button <Button
type="button" type="button"
variant="primary" variant="primary"
disabled={!selectedAppointment || Boolean(activeRecord.sentAt) || sendBusyId === activeRecord.clientId} disabled={!canEditTreatmentForDay || Boolean(activeRecord.sentAt) || sendBusyId === activeRecord.clientId}
isLoading={sendBusyId === activeRecord.clientId} isLoading={sendBusyId === activeRecord.clientId}
onClick={() => void handleSendRecord(activeRecord)} onClick={() => void handleSendRecord(activeRecord)}
> >
@@ -633,7 +646,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<Button <Button
type="button" type="button"
variant="primary" variant="primary"
disabled={!selectedAppointment || saveBusy} disabled={!canEditTreatmentForDay || saveBusy}
isLoading={saveBusy} isLoading={saveBusy}
onClick={() => void handleSaveAll()} onClick={() => void handleSaveAll()}
> >