improvement: some improvement done for appointment and treatment shared components.

This commit is contained in:
2026-07-17 11:21:08 +03:30
parent d2ad1fd7b6
commit 1b63bfff00
12 changed files with 204 additions and 38 deletions

View File

@@ -203,13 +203,16 @@ export function AppointmentBookingModal({
}
const now = new Date();
if (isSameLocalCalendarDay(scheduleDate, now) && startAt.getTime() < now.getTime()) {
if (
!editingAppointment &&
isSameLocalCalendarDay(scheduleDate, now) &&
startAt.getTime() < now.getTime()
) {
setError(t('errorPastSchedule'));
return;
}
const today = new Date();
if (compareLocalDayStart(scheduleDate, today) < 0) {
if (!editingAppointment && compareLocalDayStart(scheduleDate, now) < 0) {
setError(t('errorPastViewOnly'));
return;
}

View File

@@ -81,6 +81,10 @@ interface AppointmentScheduleGridProps {
appointments: AppointmentRecord[];
treatmentCatalog: TreatmentCatalogEntry[];
canBook: boolean;
/** Same message used for title + click toast when booking is blocked. */
slotBlockReason?: string | null;
/** Same message used for title + click toast when editing an appointment is blocked. */
appointmentEditBlockReason?: (appointment: AppointmentRecord) => string | null;
onSlotClick: (startMinute: number, providerUserId: string, providerName: string) => void;
onAppointmentClick?: (appointment: AppointmentRecord) => void;
onAppointmentOutsideHours?: (appointment: AppointmentRecord) => void;
@@ -92,6 +96,8 @@ export function AppointmentScheduleGrid({
appointments,
treatmentCatalog,
canBook,
slotBlockReason = null,
appointmentEditBlockReason,
onSlotClick,
onAppointmentClick,
onAppointmentOutsideHours,
@@ -161,9 +167,6 @@ export function AppointmentScheduleGrid({
});
return;
}
if (!canBook) {
return;
}
onAppointmentClick?.(apt);
}
@@ -216,7 +219,7 @@ export function AppointmentScheduleGrid({
return (
<div
key={hour}
className="absolute left-0 right-0 text-[11px] text-text-muted flex items-start justify-end pr-1.5 pt-0.5 border-b border-border/50"
className="absolute inset-x-0 text-[11px] text-text-muted flex items-center justify-center px-0.5 border-b border-border/50"
style={{ top, height }}
>
{formatMinuteLabel(hour * 60, locale)}
@@ -247,13 +250,15 @@ export function AppointmentScheduleGrid({
SCHEDULE_SLOT_MINUTES,
p.dayBlocks,
);
const slotDisabled = !canBook || columnFullyDisabled || !slotActive;
const structuralDisabled = columnFullyDisabled || !slotActive;
const bookingBlocked = Boolean(slotBlockReason) || !canBook;
const slotLooksDisabled = structuralDisabled || bookingBlocked;
return (
<button
key={`${p.userId}-${slotStartMinute}`}
type="button"
disabled={slotDisabled}
disabled={structuralDisabled}
title={
columnFullyDisabled
? p.hasWorkingHours
@@ -261,14 +266,16 @@ export function AppointmentScheduleGrid({
: t('slotHoursNotConfigured')
: !slotActive
? t('slotOutsideHours')
: slotDisabled
? t('slotCannotCreate')
: t('slotBookAt', {
time: formatMinuteLabel(slotStartMinute, locale),
})
: slotBlockReason
? slotBlockReason
: bookingBlocked
? t('slotCannotCreate')
: t('slotBookAt', {
time: formatMinuteLabel(slotStartMinute, locale),
})
}
className={`absolute left-0 right-0 border-b border-border/50 transition-colors ${
slotDisabled
slotLooksDisabled
? 'cursor-not-allowed bg-background-secondary/35 opacity-60'
: 'hover:bg-primary/8 cursor-pointer'
}`}
@@ -302,8 +309,10 @@ export function AppointmentScheduleGrid({
p.dayBlocks,
);
const patientName = `${apt.patient.firstName} ${apt.patient.lastName}`;
const editBlockReason = appointmentEditBlockReason?.(apt) ?? null;
const bannerTitle = [
patientName,
editBlockReason,
outsideHours ? t('outsideHoursBlocked') : null,
clusterSize > 1
? t('overlappingChoose', { count: clusterSize })
@@ -328,7 +337,11 @@ export function AppointmentScheduleGrid({
)
}
className={`absolute min-h-0 overflow-hidden rounded-[var(--radius-sm)] border pointer-events-auto z-10 flex text-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 ${
outsideHours ? 'opacity-70 ring-1 ring-amber-500/60' : ''
outsideHours || editBlockReason
? 'opacity-70'
: ''
} ${
outsideHours ? 'ring-1 ring-amber-500/60' : ''
} ${
isUnderOneHour
? 'items-center justify-center px-0.5 py-0'
@@ -377,10 +390,6 @@ export function AppointmentScheduleGrid({
treatmentCatalog={treatmentCatalog}
anchorRect={overlapPopover.anchorRect}
onSelect={(apt) => {
if (!canBook) {
setOverlapPopover(null);
return;
}
const provider = providers.find((p) => p.userId === apt.providerUserId);
if (
provider &&

View File

@@ -25,6 +25,7 @@ import { compareLocalDayStart, getLocalDayIsoRange, startOfLocalDay } from '@/co
export function AppointmentsPage() {
const t = useTranslations('appointments');
const tErrors = useTranslations('errors');
const tCommon = useTranslations('common');
const router = useRouter();
const { currentOrganization } = useAuth();
const [scheduleDate, setScheduleDate] = useState(() => startOfLocalDay(new Date()));
@@ -117,8 +118,19 @@ export function AppointmentsPage() {
.catch(() => {});
}, []);
function appointmentEditBlockReason(appointment: AppointmentRecord): string | null {
if (!canManageAppointments) {
return tCommon('readOnlyAccess');
}
if (isViewingPastDay && appointment.hasTreatment) {
return t('infoEditBlockedHasTreatment');
}
return null;
}
function handleSlotClick(startMinute: number, providerUserId: string, providerName: string) {
if (!canManageAppointments) {
toast.showInfo(tCommon('readOnlyAccess'));
return;
}
if (isViewingPastDay) {
@@ -133,11 +145,9 @@ export function AppointmentsPage() {
}
function handleAppointmentClick(appointment: AppointmentRecord) {
if (!canManageAppointments) {
return;
}
if (isViewingPastDay) {
toast.showInfo(t('infoPastViewOnly'));
const blockReason = appointmentEditBlockReason(appointment);
if (blockReason) {
toast.showInfo(blockReason);
return;
}
const provider = providers.find((p) => p.userId === appointment.providerUserId);
@@ -254,6 +264,14 @@ export function AppointmentsPage() {
appointments={appointments}
treatmentCatalog={treatmentCatalog}
canBook={canManageAppointments && !isViewingPastDay}
appointmentEditBlockReason={appointmentEditBlockReason}
slotBlockReason={
!canManageAppointments
? tCommon('readOnlyAccess')
: isViewingPastDay
? t('infoPastViewOnly')
: null
}
onSlotClick={(startMinute, uid, name) => handleSlotClick(startMinute, uid, name)}
onAppointmentClick={(apt) => handleAppointmentClick(apt)}
onAppointmentOutsideHours={(apt) => handleAppointmentOutsideHours(apt)}
@@ -281,7 +299,6 @@ export function AppointmentsPage() {
loading={savingAppointment}
canDelete={
canManageAppointments &&
!isViewingPastDay &&
!!activeEditingAppointment &&
!activeEditingAppointment.hasTreatment
}