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:
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user