diff --git a/frontend/src/pages/OwnerScheduler.tsx b/frontend/src/pages/OwnerScheduler.tsx index 3c1a34b..77160df 100644 --- a/frontend/src/pages/OwnerScheduler.tsx +++ b/frontend/src/pages/OwnerScheduler.tsx @@ -15,6 +15,7 @@ import Portal from '../components/Portal'; const START_HOUR = 0; // Midnight const END_HOUR = 24; // Midnight next day const PIXELS_PER_MINUTE = 2.5; +const OVERLAY_HOUR_HEIGHT = 60; const HEADER_HEIGHT = 48; const SIDEBAR_WIDTH = 250; @@ -971,8 +972,8 @@ const OwnerScheduler: React.FC = ({ user, business }) => { style={{ top: Math.min(window.innerHeight - 320, Math.max(10, monthDropTarget.rect.top - 20)), // Try to center vertically or keep on screen left: monthDropTarget.rect.left - 10, - width: monthDropTarget.rect.width + 20, - height: 300, + width: Math.max(300, monthDropTarget.rect.width + 150), // Wider to show lanes + height: 400, animation: 'fadeIn 0.2s ease-out' }} onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }} // Consume drag over @@ -985,52 +986,109 @@ const OwnerScheduler: React.FC = ({ user, business }) => { Drop in slot -
- {Array.from({ length: END_HOUR - START_HOUR }, (_, i) => START_HOUR + i).map(hour => { - // Check if we have conflicts (simple check) - const dayStart = new Date(monthDropTarget.date); - dayStart.setHours(hour, 0, 0, 0); - const dayEnd = new Date(dayStart); - dayEnd.setMinutes(59); - - const hasAppt = filteredAppointments.some(a => { - if (a.id === draggedAppointmentId) return false; // Don't count self - const t = new Date(a.startTime); - return t >= dayStart && t <= dayEnd; - }); - - return ( -
- {/* Hour Slot */} +
+ {/* Background Slots */} +
+ {Array.from({ length: END_HOUR - START_HOUR }, (_, i) => START_HOUR + i).map(hour => ( +
+ {/* Hour Slot (Top Half) */}
handleMonthTimeDrop(e, hour, 0)} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }} > -
- {hour > 12 ? `${hour - 12} PM` : `${hour} ${hour === 12 ? 'PM' : 'AM'}`} +
+ {hour === 0 ? '12 AM' : hour > 12 ? `${hour - 12} PM` : `${hour} ${hour === 12 ? 'PM' : 'AM'}`}
-
- {hasAppt &&
} -
+
+
- {/* Half Hour Slot */} + {/* Half Hour Slot (Bottom Half) */}
handleMonthTimeDrop(e, hour, 30)} onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }} > -
- :30 -
-
-
+
+
+
- ); - })} + ))} + + {/* Existing Appointments Layer */} +
+ {(() => { + // Calculate layout for this day + const dayStart = new Date(monthDropTarget.date); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(dayStart); + dayEnd.setHours(23, 59, 59, 999); + + const dayApps = appointments.filter(apt => { + if (!apt.resourceId || apt.id === draggedAppointmentId) return false; + const t = new Date(apt.startTime); + return t >= dayStart && t <= dayEnd; + }).sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime() || b.durationMinutes - a.durationMinutes); + + // Calculate lanes + const lanes: number[] = []; + const laidOutApps = dayApps.map(apt => { + const start = new Date(apt.startTime); + const startMinutes = (start.getHours() * 60) + start.getMinutes(); + const endMinutes = startMinutes + apt.durationMinutes; + + let laneIndex = -1; + for (let i = 0; i < lanes.length; i++) { + if (lanes[i] <= startMinutes) { + laneIndex = i; + lanes[i] = endMinutes; + break; + } + } + if (laneIndex === -1) { + lanes.push(endMinutes); + laneIndex = lanes.length - 1; + } + + return { ...apt, laneIndex, startMinutes, endMinutes }; + }); + + const totalLanes = Math.max(1, lanes.length); + + return laidOutApps.map(apt => { + const top = (apt.startMinutes / 60) * OVERLAY_HOUR_HEIGHT; + const height = (apt.durationMinutes / 60) * OVERLAY_HOUR_HEIGHT; + const widthPercent = 100 / totalLanes; + const leftPercent = apt.laneIndex * widthPercent; + const service = services.find(s => s.id === apt.serviceId); + + return ( +
+
+ {apt.customerName} +
+
+ {service?.name} +
+
+ ); + }); + })()} +
+