improvement: some improvement done for appointment and treatment shared components.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user