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

View File

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

View File

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

View File

@@ -1,12 +1,8 @@
'use client';
import { Trash2 } from 'lucide-react';
import type { AppointmentColumnProvider, AppointmentRecord } from '@/types/appointment';
import { formatHourLabel } from '@/lib/appointmentTime';
import {
purposeDeleteIconClass,
purposeStyle,
} from '@/components/ui/appointments/appointmentPurposeStyles';
import { purposeStyle } from '@/components/ui/appointments/appointmentPurposeStyles';
const HOUR_PX = 40;
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}%` };
}
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 {
day: Date;
providers: AppointmentColumnProvider[];
appointments: AppointmentRecord[];
canBook: boolean;
canDelete?: boolean;
onDeleteAppointment?: (id: string) => void;
onSlotClick: (hour: number, providerUserId: string, providerName: string) => void;
onAppointmentClick?: (appointment: AppointmentRecord) => void;
}
@@ -43,8 +43,6 @@ export function AppointmentScheduleGrid({
providers,
appointments,
canBook,
canDelete = false,
onDeleteAppointment,
onSlotClick,
onAppointmentClick,
}: AppointmentScheduleGridProps) {
@@ -121,35 +119,22 @@ export function AppointmentScheduleGrid({
if (!pos) {
return null;
}
const durationMin = appointmentDurationMinutes(apt);
return (
<button
type="button"
key={apt.id}
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`}
style={{ top: pos.top, height: pos.height, minHeight: 36 }}
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 }}
>
<div className="pointer-events-none flex-1 min-w-0 overflow-hidden text-left">
<p className="text-[11px] font-medium leading-tight truncate">
{apt.patient.firstName} {apt.patient.lastName}
<p className="text-[11px] font-medium leading-tight truncate pointer-events-none">
{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>
{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>
);

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';
}
/** 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). */
export const APPOINTMENT_PURPOSE_LEGEND_SWATCH: Record<AppointmentPurpose, string> = {
consultation: 'bg-violet-500/85 border-violet-400/75',