bugfix: delete action removed from appointment banners to avoid banner sizing issues. the appointment can be deleted via edit modal.

This commit is contained in:
2026-05-17 17:14:00 +03:30
parent 375fdc60b4
commit e119d02759
5 changed files with 65 additions and 56 deletions

View File

@@ -51,6 +51,7 @@ export default function AppointmentsPage() {
const [bookingProviderName, setBookingProviderName] = useState(''); const [bookingProviderName, setBookingProviderName] = useState('');
const [editingAppointmentId, setEditingAppointmentId] = useState<string | null>(null); const [editingAppointmentId, setEditingAppointmentId] = useState<string | null>(null);
const [savingAppointment, setSavingAppointment] = useState(false); const [savingAppointment, setSavingAppointment] = useState(false);
const [deletingAppointment, setDeletingAppointment] = useState(false);
const canManageAppointments = canEditAppointments(currentOrganization); const canManageAppointments = canEditAppointments(currentOrganization);
@@ -221,13 +222,19 @@ export default function AppointmentsPage() {
} }
} }
async function handleDeleteAppointment(id: string) { async function handleDeleteEditingAppointment() {
if (!activeEditingAppointment) {
return;
}
if (!window.confirm('Remove this appointment?')) { if (!window.confirm('Remove this appointment?')) {
return; return;
} }
setDeletingAppointment(true);
toast.setError(''); toast.setError('');
try { try {
await appointmentsApi.remove(id); await appointmentsApi.remove(activeEditingAppointment.id);
setBookingOpen(false);
setEditingAppointmentId(null);
toast.showSuccess('Appointment removed.'); toast.showSuccess('Appointment removed.');
await loadSchedule(); await loadSchedule();
} catch (err: unknown) { } catch (err: unknown) {
@@ -236,6 +243,8 @@ export default function AppointmentsPage() {
? String((err as { message: unknown }).message) ? String((err as { message: unknown }).message)
: 'Could not delete appointment.'; : 'Could not delete appointment.';
toast.showError(message); toast.showError(message);
} finally {
setDeletingAppointment(false);
} }
} }
@@ -289,8 +298,6 @@ export default function AppointmentsPage() {
providers={providers} providers={providers}
appointments={appointments} appointments={appointments}
canBook={canManageAppointments && !isViewingPastDay} canBook={canManageAppointments && !isViewingPastDay}
canDelete={canManageAppointments}
onDeleteAppointment={(id) => void handleDeleteAppointment(id)}
onSlotClick={(hour, uid, name) => handleSlotClick(hour, uid, name)} onSlotClick={(hour, uid, name) => handleSlotClick(hour, uid, name)}
onAppointmentClick={(apt) => handleAppointmentClick(apt)} onAppointmentClick={(apt) => handleAppointmentClick(apt)}
/> />
@@ -311,6 +318,9 @@ export default function AppointmentsPage() {
}} }}
onSubmit={handleSaveAppointment} onSubmit={handleSaveAppointment}
loading={savingAppointment} loading={savingAppointment}
canDelete={canManageAppointments && !isViewingPastDay && !!activeEditingAppointment}
onDelete={() => void handleDeleteEditingAppointment()}
deleting={deletingAppointment}
/> />
{isCreateOpen && ( {isCreateOpen && (

View File

@@ -12,7 +12,7 @@ export default function TodayPage() {
return ( return (
<div> <div>
<h1 className="text-2xl font-semibold mb-6"> <h1 className="text-2xl font-semibold mb-6">
Welcome back Babak !! Welcome back!!
</h1> </h1>
{showNoSubscriptionNotice && ( {showNoSubscriptionNotice && (

View File

@@ -31,6 +31,9 @@ interface AppointmentBookingModalProps {
}) => Promise<void>; }) => Promise<void>;
editingAppointment?: AppointmentRecord | null; editingAppointment?: AppointmentRecord | null;
loading?: boolean; loading?: boolean;
canDelete?: boolean;
onDelete?: () => void | Promise<void>;
deleting?: boolean;
} }
export function AppointmentBookingModal({ export function AppointmentBookingModal({
@@ -44,6 +47,9 @@ export function AppointmentBookingModal({
onSubmit, onSubmit,
editingAppointment = null, editingAppointment = null,
loading = false, loading = false,
canDelete = false,
onDelete,
deleting = false,
}: AppointmentBookingModalProps) { }: AppointmentBookingModalProps) {
const [startTime, setStartTime] = useState('09:00'); const [startTime, setStartTime] = useState('09:00');
const [endTime, setEndTime] = useState('10:00'); const [endTime, setEndTime] = useState('10:00');
@@ -227,13 +233,34 @@ export function AppointmentBookingModal({
{error && <p className="text-sm text-red-400">{error}</p>} {error && <p className="text-sm text-red-400">{error}</p>}
<div className="flex gap-2 justify-end"> <div className="flex flex-wrap items-center gap-2 justify-between">
<Button type="button" variant="ghost" onClick={onClose} disabled={loading}> {editingAppointment && canDelete && onDelete ? (
Cancel <Button
</Button> type="button"
<Button type="button" variant="primary" onClick={() => void handleSubmit()} isLoading={loading}> variant="danger"
Save onClick={() => void onDelete()}
</Button> disabled={loading || deleting}
isLoading={deleting}
>
Delete
</Button>
) : (
<span />
)}
<div className="flex gap-2 ml-auto">
<Button type="button" variant="ghost" onClick={onClose} disabled={loading || deleting}>
Cancel
</Button>
<Button
type="button"
variant="primary"
onClick={() => void handleSubmit()}
isLoading={loading}
disabled={deleting}
>
Save
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,8 @@
'use client'; 'use client';
import { Trash2 } from 'lucide-react';
import type { AppointmentColumnProvider, AppointmentRecord } from '@/types/appointment'; import type { AppointmentColumnProvider, AppointmentRecord } from '@/types/appointment';
import { formatHourLabel } from '@/lib/appointmentTime'; import { formatHourLabel } from '@/lib/appointmentTime';
import { import { purposeStyle } from '@/components/ui/appointments/appointmentPurposeStyles';
purposeDeleteIconClass,
purposeStyle,
} from '@/components/ui/appointments/appointmentPurposeStyles';
const HOUR_PX = 40; const HOUR_PX = 40;
const HOURS = Array.from({ length: 24 }, (_, i) => i); const HOURS = Array.from({ length: 24 }, (_, i) => i);
@@ -27,13 +23,17 @@ function layoutBlock(apt: AppointmentRecord, day: Date): { top: string; height:
return { top: `${top}%`, height: `${height}%` }; return { top: `${top}%`, height: `${height}%` };
} }
function appointmentDurationMinutes(apt: AppointmentRecord): number {
const start = new Date(apt.startAt).getTime();
const end = new Date(apt.endAt).getTime();
return Math.max(0, Math.round((end - start) / 60_000));
}
interface AppointmentScheduleGridProps { interface AppointmentScheduleGridProps {
day: Date; day: Date;
providers: AppointmentColumnProvider[]; providers: AppointmentColumnProvider[];
appointments: AppointmentRecord[]; appointments: AppointmentRecord[];
canBook: boolean; canBook: boolean;
canDelete?: boolean;
onDeleteAppointment?: (id: string) => void;
onSlotClick: (hour: number, providerUserId: string, providerName: string) => void; onSlotClick: (hour: number, providerUserId: string, providerName: string) => void;
onAppointmentClick?: (appointment: AppointmentRecord) => void; onAppointmentClick?: (appointment: AppointmentRecord) => void;
} }
@@ -43,8 +43,6 @@ export function AppointmentScheduleGrid({
providers, providers,
appointments, appointments,
canBook, canBook,
canDelete = false,
onDeleteAppointment,
onSlotClick, onSlotClick,
onAppointmentClick, onAppointmentClick,
}: AppointmentScheduleGridProps) { }: AppointmentScheduleGridProps) {
@@ -121,35 +119,22 @@ export function AppointmentScheduleGrid({
if (!pos) { if (!pos) {
return null; return null;
} }
const durationMin = appointmentDurationMinutes(apt);
return ( return (
<button <button
type="button" type="button"
key={apt.id} key={apt.id}
onClick={() => onAppointmentClick?.(apt)} onClick={() => onAppointmentClick?.(apt)}
className={`absolute left-0.5 right-0.5 rounded-[var(--radius-sm)] border pointer-events-auto z-10 flex flex-row items-center gap-1.5 px-1.5 py-1 min-h-[36px] text-left ${purposeStyle(apt.purpose)} focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35`} className={`absolute left-0.5 right-0.5 min-h-0 overflow-hidden rounded-[var(--radius-sm)] border pointer-events-auto z-10 flex flex-col justify-start px-1.5 py-0.5 text-left ${purposeStyle(apt.purpose)} focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35`}
style={{ top: pos.top, height: pos.height, minHeight: 36 }} style={{ top: pos.top, height: pos.height }}
> >
<div className="pointer-events-none flex-1 min-w-0 overflow-hidden text-left"> <p className="text-[11px] font-medium leading-tight truncate pointer-events-none">
<p className="text-[11px] font-medium leading-tight truncate"> {apt.patient.firstName} {apt.patient.lastName}
{apt.patient.firstName} {apt.patient.lastName} </p>
{durationMin >= 30 && apt.patient.phone && (
<p className="text-[10px] opacity-90 truncate pointer-events-none">
{apt.patient.phone}
</p> </p>
{apt.patient.phone && (
<p className="text-[10px] opacity-90 truncate">{apt.patient.phone}</p>
)}
</div>
{canDelete && onDeleteAppointment && (
<button
type="button"
className="group pointer-events-auto shrink-0 self-center z-20 mr-0.5 ml-0.5 inline-flex cursor-pointer items-center justify-center rounded-[var(--radius-sm)] bg-transparent p-1 outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
aria-label="Delete appointment"
title="Delete appointment"
onClick={(e) => {
e.stopPropagation();
onDeleteAppointment(apt.id);
}}
>
<Trash2 className={`w-4 h-4 ${purposeDeleteIconClass(apt.purpose)}`} />
</button>
)} )}
</button> </button>
); );

View File

@@ -23,19 +23,6 @@ export function purposeStyle(purpose: string): string {
return APPOINTMENT_PURPOSE_STYLES[p] ?? 'bg-surface-elevated border-border text-text-secondary'; return APPOINTMENT_PURPOSE_STYLES[p] ?? 'bg-surface-elevated border-border text-text-secondary';
} }
/** Trash icon — legend hues; `!` overrides global `.lucide { color: var(--color-icon) }`. */
export function purposeDeleteIconClass(purpose: string): string {
const p = purpose as AppointmentPurpose;
const map: Record<AppointmentPurpose, string> = {
consultation: '!text-purpose-consultation-fg',
filling: '!text-purpose-filling-fg',
endo: '!text-purpose-endo-fg',
visit: '!text-purpose-visit-fg',
hygiene: '!text-purpose-hygiene-fg',
};
return map[p] ?? '!text-text-muted';
}
/** Small swatch for legend (background + border only). */ /** Small swatch for legend (background + border only). */
export const APPOINTMENT_PURPOSE_LEGEND_SWATCH: Record<AppointmentPurpose, string> = { export const APPOINTMENT_PURPOSE_LEGEND_SWATCH: Record<AppointmentPurpose, string> = {
consultation: 'bg-violet-500/85 border-violet-400/75', consultation: 'bg-violet-500/85 border-violet-400/75',