@@ -220,6 +240,12 @@ const currentWeekStart = ref(getWeekStart(new Date("2025-10-25")));
const eventDialog = ref(false);
const selectedEvent = ref(null);
+// Drag and drop state
+const draggedService = ref(null);
+const isDragOver = ref(false);
+const dragOverSlot = ref(null);
+const previewSlots = ref([]);
+
// Helper function to get week start (Monday)
function getWeekStart(date) {
const d = new Date(date);
@@ -345,6 +371,52 @@ const getEndTime = (startTime, durationMinutes) => {
return endDate.toTimeString().slice(0, 5);
};
+// Check if a time slot has any conflicts with existing events
+const isTimeSlotOccupied = (date, startTime, durationMinutes, excludeServiceId = null) => {
+ const endTime = getEndTime(startTime, durationMinutes);
+
+ return scheduledServices.value.some((service) => {
+ // Exclude the service being moved
+ if (excludeServiceId && service.id === excludeServiceId) return false;
+
+ if (service.scheduledDate !== date) return false;
+
+ const serviceEndTime = getEndTime(service.scheduledTime, service.duration);
+
+ // Check for time overlap
+ return (
+ (startTime >= service.scheduledTime && startTime < serviceEndTime) ||
+ (endTime > service.scheduledTime && endTime <= serviceEndTime) ||
+ (startTime <= service.scheduledTime && endTime >= serviceEndTime)
+ );
+ });
+};
+
+// Get all time slots that would be occupied by an event
+const getOccupiedSlots = (date, startTime, durationMinutes) => {
+ const slots = [];
+ const endTime = getEndTime(startTime, durationMinutes);
+
+ // Generate all 30-minute slots between start and end time
+ let currentTime = startTime;
+ while (currentTime < endTime) {
+ slots.push({ date, time: currentTime });
+
+ // Add 30 minutes
+ const [hours, minutes] = currentTime.split(':').map(Number);
+ const nextMinutes = minutes + 30;
+ const nextHours = hours + Math.floor(nextMinutes / 60);
+ const finalMinutes = nextMinutes % 60;
+
+ currentTime = `${nextHours.toString().padStart(2, '0')}:${finalMinutes.toString().padStart(2, '0')}`;
+
+ // Safety check to prevent infinite loop
+ if (nextHours >= 24) break;
+ }
+
+ return slots;
+};
+
// Calendar navigation methods
const previousWeek = () => {
const newDate = new Date(currentWeekStart.value);
@@ -367,11 +439,8 @@ const getEventsForTimeSlot = (date, time) => {
return scheduledServices.value.filter((service) => {
if (service.scheduledDate !== date) return false;
- const serviceTime = service.scheduledTime;
- const serviceEndTime = getEndTime(service.scheduledTime, service.duration);
-
- // Check if this time slot falls within the service duration
- return time >= serviceTime && time < serviceEndTime;
+ // Only show the event in its starting time slot to prevent duplication
+ return service.scheduledTime === time;
});
};
@@ -399,6 +468,10 @@ const selectTimeSlot = (date, time) => {
// This will be used for drag-and-drop functionality
};
+const isInPreviewSlots = (date, time) => {
+ return previewSlots.value.some(slot => slot.date === date && slot.time === time);
+};
+
const showEventDetails = (event) => {
selectedEvent.value = event.event;
eventDialog.value = true;
@@ -413,6 +486,179 @@ const scheduleService = (service) => {
);
};
+// Drag and Drop methods
+const handleDragStart = (service, event) => {
+ draggedService.value = service;
+ event.dataTransfer.effectAllowed = "move";
+ event.dataTransfer.setData("text/plain", service.id);
+
+ // Get the dimensions of the dragged element
+ const dragElement = event.target;
+ const rect = dragElement.getBoundingClientRect();
+
+ // Set the drag image offset to center the element horizontally and position cursor at top
+ const offsetX = rect.width / 2;
+ const offsetY = 10; // Position cursor near the top of the element
+
+ try {
+ event.dataTransfer.setDragImage(dragElement, offsetX, offsetY);
+ } catch (e) {
+ // Fallback if setDragImage fails
+ console.log("Could not set custom drag image");
+ }
+
+ // Add visual feedback
+ event.target.style.opacity = '0.5';
+ console.log("Drag started for service:", service.serviceType);
+};
+
+const handleDragEnd = (event) => {
+ // Reset visual feedback
+ event.target.style.opacity = '1';
+ draggedService.value = null;
+ isDragOver.value = false;
+ dragOverSlot.value = null;
+ previewSlots.value = [];
+};
+
+const handleDragOver = (event, date, time) => {
+ event.preventDefault();
+ event.stopPropagation(); // Prevent event bubbling to avoid conflicts with nested elements
+
+ if (!draggedService.value) return;
+
+ // Check if the slot would be occupied (excluding the current service)
+ const isOccupied = isTimeSlotOccupied(date, time, draggedService.value.duration, draggedService.value.id);
+
+ // Calculate which slots would be occupied by this event
+ const wouldOccupySlots = getOccupiedSlots(date, time, draggedService.value.duration);
+
+ event.dataTransfer.dropEffect = isOccupied ? "none" : "move";
+
+ // Only update state if it's actually different to prevent flickering
+ const currentSlotKey = `${date}-${time}`;
+ const previousSlotKey = dragOverSlot.value ? `${dragOverSlot.value.date}-${dragOverSlot.value.time}` : null;
+
+ if (currentSlotKey !== previousSlotKey || dragOverSlot.value?.isOccupied !== isOccupied) {
+ isDragOver.value = true;
+ dragOverSlot.value = { date, time, isOccupied };
+ previewSlots.value = wouldOccupySlots;
+ }
+};
+
+const handleDragLeave = (event) => {
+ // Use a small delay to prevent flickering when moving between child elements
+ setTimeout(() => {
+ // Check if we're still dragging and if the mouse has actually left the calendar area
+ if (!draggedService.value) return;
+
+ const calendarGrid = document.querySelector('.calendar-grid');
+ if (!calendarGrid) return;
+
+ const rect = calendarGrid.getBoundingClientRect();
+ const x = event.clientX;
+ const y = event.clientY;
+
+ // Only clear if mouse is outside the entire calendar grid
+ if (
+ x < rect.left ||
+ x > rect.right ||
+ y < rect.top ||
+ y > rect.bottom
+ ) {
+ isDragOver.value = false;
+ dragOverSlot.value = null;
+ previewSlots.value = [];
+ }
+ }, 10);
+};
+
+const handleDrop = (event, date, time) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (!draggedService.value) return;
+
+ // Check if the slot is occupied (excluding the service being moved)
+ const isOccupied = isTimeSlotOccupied(date, time, draggedService.value.duration, draggedService.value.id);
+
+ if (isOccupied) {
+ console.log("Cannot drop here - time slot is occupied");
+ // Reset drag state
+ isDragOver.value = false;
+ dragOverSlot.value = null;
+ previewSlots.value = [];
+ draggedService.value = null;
+ return;
+ }
+
+ // Convert time to 12-hour format for display
+ const [hours, minutes] = time.split(':').map(Number);
+ const displayHour = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;
+ const ampm = hours >= 12 ? 'PM' : 'AM';
+ const formattedTime = `${displayHour}:${minutes.toString().padStart(2, '0')} ${ampm}`;
+
+ // Update the service with scheduling information
+ const serviceIndex = services.value.findIndex(s => s.id === draggedService.value.id);
+ if (serviceIndex !== -1) {
+ const wasScheduled = services.value[serviceIndex].status === 'scheduled';
+ services.value[serviceIndex] = {
+ ...services.value[serviceIndex],
+ status: 'scheduled',
+ scheduledDate: date,
+ scheduledTime: time,
+ scheduledTimeDisplay: formattedTime
+ };
+
+ const action = wasScheduled ? 'Moved' : 'Scheduled';
+ console.log(`${action} ${draggedService.value.serviceType} for ${date} at ${formattedTime}`);
+ }
+
+ // Reset drag state
+ isDragOver.value = false;
+ dragOverSlot.value = null;
+ previewSlots.value = [];
+ draggedService.value = null;
+};
+
+// Handle dropping scheduled items back to unscheduled
+const handleUnscheduledDragOver = (event) => {
+ if (draggedService.value?.status === 'scheduled') {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }
+};
+
+const handleUnscheduledDragLeave = (event) => {
+ // Visual feedback will be handled by CSS
+};
+
+const handleUnscheduledDrop = (event) => {
+ event.preventDefault();
+
+ if (!draggedService.value || draggedService.value.status !== 'scheduled') return;
+
+ // Update the service to unscheduled status
+ const serviceIndex = services.value.findIndex(s => s.id === draggedService.value.id);
+ if (serviceIndex !== -1) {
+ services.value[serviceIndex] = {
+ ...services.value[serviceIndex],
+ status: 'unscheduled',
+ scheduledDate: null,
+ scheduledTime: null,
+ scheduledTimeDisplay: null
+ };
+
+ console.log(`Unscheduled ${draggedService.value.serviceType}`);
+ }
+
+ // Reset drag state
+ isDragOver.value = false;
+ dragOverSlot.value = null;
+ previewSlots.value = [];
+ draggedService.value = null;
+};
+
// Lifecycle
onMounted(async () => {
try {
@@ -551,6 +797,25 @@ onMounted(async () => {
background-color: #fff3e0;
}
+.day-column.drag-over {
+ background-color: #e8f5e8;
+ border: 2px dashed #4caf50;
+ box-sizing: border-box;
+}
+
+.day-column.drag-over-occupied {
+ background-color: #ffebee;
+ border: 2px dashed #f44336;
+ box-sizing: border-box;
+}
+
+.day-column.drag-preview {
+ background-color: #e3f2fd;
+ border: 1px solid #2196f3;
+ box-sizing: border-box;
+ opacity: 0.7;
+}
+
.calendar-event {
position: absolute;
left: 2px;
@@ -573,6 +838,14 @@ onMounted(async () => {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
+.calendar-event[draggable="true"] {
+ cursor: grab;
+}
+
+.calendar-event[draggable="true"]:active {
+ cursor: grabbing;
+}
+
.calendar-event .event-title {
font-weight: 600;
white-space: nowrap;
@@ -634,6 +907,14 @@ onMounted(async () => {
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 200px);
+ transition: all 0.3s ease;
+ border-radius: 8px;
+}
+
+.unscheduled-list.unscheduled-drop-zone {
+ background-color: #e8f5e8;
+ border: 2px dashed #4caf50;
+ box-sizing: border-box;
}
.unscheduled-item {
@@ -645,6 +926,14 @@ onMounted(async () => {
transform: translateX(3px);
}
+.unscheduled-item[draggable="true"] {
+ cursor: grab;
+}
+
+.unscheduled-item[draggable="true"]:active {
+ cursor: grabbing;
+}
+
.priority-urgent {
border-left-color: #f44336 !important;
}