switch calendar to be per foreman, added date select, added foreman select
This commit is contained in:
parent
5bf9b45861
commit
697add510f
2 changed files with 360 additions and 131 deletions
90
frontend/CALENDAR_UPDATE_SUMMARY.md
Normal file
90
frontend/CALENDAR_UPDATE_SUMMARY.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Calendar View Update Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Updated the Calendar.vue component from a weekly view to a daily view with foremen as columns and 30-minute time slots as rows.
|
||||||
|
|
||||||
|
## Key Changes Made
|
||||||
|
|
||||||
|
### 1. Layout Structure
|
||||||
|
- **Before**: Weekly calendar with 7 day columns
|
||||||
|
- **After**: Daily calendar with 10 foreman columns
|
||||||
|
|
||||||
|
### 2. Header Changes
|
||||||
|
- Changed from "Sprinkler Service Calendar" to "Daily Schedule - Sprinkler Service"
|
||||||
|
- Navigation changed from week-based (previousWeek/nextWeek) to day-based (previousDay/nextDay)
|
||||||
|
- Display shows full day name instead of week range
|
||||||
|
|
||||||
|
### 3. Grid Structure
|
||||||
|
- **Columns**: Now shows foremen names instead of days of the week
|
||||||
|
- **Rows**: Still uses 30-minute time slots from 7 AM to 7 PM
|
||||||
|
- Grid template updated from `repeat(7, 1fr)` to `repeat(10, 1fr)` for 10 foremen
|
||||||
|
|
||||||
|
### 4. Foremen Data
|
||||||
|
Added 10 foremen to the system:
|
||||||
|
- Mike Thompson
|
||||||
|
- Sarah Johnson
|
||||||
|
- David Martinez
|
||||||
|
- Chris Wilson
|
||||||
|
- Lisa Anderson
|
||||||
|
- Robert Thomas
|
||||||
|
- Maria White
|
||||||
|
- James Clark
|
||||||
|
- Patricia Lewis
|
||||||
|
- Kevin Walker
|
||||||
|
|
||||||
|
### 5. Event Scheduling Logic
|
||||||
|
- Events now filter by foreman name instead of day
|
||||||
|
- Drag and drop updated to assign services to specific foremen
|
||||||
|
- Time slot conflict detection now checks per foreman instead of per day
|
||||||
|
- Preview slots updated to show foreman-specific scheduling
|
||||||
|
|
||||||
|
### 6. Visual Updates
|
||||||
|
- Foreman headers show name and job count for the day
|
||||||
|
- CSS classes renamed from `day-column` to `foreman-column`
|
||||||
|
- Updated styling to accommodate wider layout for 10 columns
|
||||||
|
- Maintained all existing drag-and-drop visual feedback
|
||||||
|
|
||||||
|
### 7. Functionality Preserved
|
||||||
|
- All existing drag-and-drop functionality
|
||||||
|
- Service priority handling
|
||||||
|
- Unscheduled services panel
|
||||||
|
- Event details modal
|
||||||
|
- Time slot highlighting for current time
|
||||||
|
|
||||||
|
## Technical Implementation Details
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
1. `currentDate` (string) - tracks the currently viewed date
|
||||||
|
2. `foremen` (array) - contains foreman ID and name pairs
|
||||||
|
3. Services filter by `foreman` name and `scheduledDate` matching `currentDate`
|
||||||
|
4. Grid renders 10 columns × ~24 time slots (30-min intervals)
|
||||||
|
|
||||||
|
### Key Methods Updated
|
||||||
|
- `getEventsForTimeSlot(foremanName, time, date)` - filters by foreman and date
|
||||||
|
- `isTimeSlotOccupied(foremanName, startTime, duration)` - checks conflicts per foreman
|
||||||
|
- `getOccupiedSlots(foremanId, startTime, duration)` - preview slots per foreman
|
||||||
|
- `handleDragOver/handleDrop` - updated to work with foreman IDs
|
||||||
|
- Navigation: `previousDay()`, `nextDay()`, `goToToday()`
|
||||||
|
|
||||||
|
### CSS Grid Layout
|
||||||
|
```css
|
||||||
|
.calendar-header-row, .time-row {
|
||||||
|
grid-template-columns: 80px repeat(10, 1fr);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This provides a time column (80px) plus 10 equal-width foreman columns.
|
||||||
|
|
||||||
|
## Benefits of New Design
|
||||||
|
1. **Better resource allocation** - See all foremen's schedules at once
|
||||||
|
2. **Easier scheduling** - Drag services directly to specific foremen
|
||||||
|
3. **Conflict prevention** - Visual feedback for time conflicts per foreman
|
||||||
|
4. **Daily focus** - Concentrate on optimizing a single day's schedule
|
||||||
|
5. **Scalable** - Easy to add/remove foremen by updating the foremen array
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
- Use left/right arrows to navigate between days
|
||||||
|
- Drag unscheduled services from the right panel to specific foreman time slots
|
||||||
|
- Services automatically get assigned to the foreman and time slot where dropped
|
||||||
|
- Current time slot is highlighted across all foremen columns
|
||||||
|
- Each foreman header shows their job count for the selected day
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="calendar-container">
|
<div class="calendar-container">
|
||||||
<div class="calendar-header">
|
<div class="calendar-header">
|
||||||
<h2>Sprinkler Service Calendar</h2>
|
<h2>Daily Schedule - Sprinkler Service</h2>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="previousWeek"
|
@click="previousDay"
|
||||||
icon="mdi-chevron-left"
|
icon="mdi-chevron-left"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
<span class="week-display">{{ weekDisplayText }}</span>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="nextWeek"
|
@click="showDatePicker = true"
|
||||||
|
variant="text"
|
||||||
|
class="day-display-btn"
|
||||||
|
>
|
||||||
|
<span class="date-text">{{ dayDisplayText }}</span>
|
||||||
|
<v-icon right size="small">mdi-calendar</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
@click="nextDay"
|
||||||
icon="mdi-chevron-right"
|
icon="mdi-chevron-right"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
|
|
@ -19,54 +26,121 @@
|
||||||
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
|
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
|
||||||
>Today</v-btn
|
>Today</v-btn
|
||||||
>
|
>
|
||||||
|
<v-menu
|
||||||
|
v-model="showForemenMenu"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
location="bottom"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
class="ml-4"
|
||||||
|
>
|
||||||
|
<v-icon left size="small">mdi-account-group</v-icon>
|
||||||
|
Foremen ({{ selectedForemen.length }})
|
||||||
|
<v-icon right size="small">mdi-chevron-down</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card min-width="250" max-width="300">
|
||||||
|
<v-card-title class="text-subtitle-1 py-2">
|
||||||
|
Select Foremen
|
||||||
|
</v-card-title>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-card-text class="pa-2">
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item @click="toggleAllForemen">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-checkbox-btn
|
||||||
|
:model-value="selectedForemen.length === foremen.length"
|
||||||
|
:indeterminate="selectedForemen.length > 0 && selectedForemen.length < foremen.length"
|
||||||
|
></v-checkbox-btn>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Select All</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-list-item
|
||||||
|
v-for="foreman in foremen"
|
||||||
|
:key="foreman.id"
|
||||||
|
@click="toggleForeman(foreman.id)"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-checkbox-btn
|
||||||
|
:model-value="selectedForemen.includes(foreman.id)"
|
||||||
|
></v-checkbox-btn>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ foreman.name }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Picker Dialog -->
|
||||||
|
<v-dialog v-model="showDatePicker" max-width="400px">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Select Date</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-date-picker
|
||||||
|
v-model="selectedDate"
|
||||||
|
@update:model-value="onDateSelected"
|
||||||
|
full-width
|
||||||
|
></v-date-picker>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="showDatePicker = false">Cancel</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<div class="calendar-main">
|
<div class="calendar-main">
|
||||||
<!-- Weekly Calendar Grid -->
|
<!-- Daily Calendar Grid -->
|
||||||
<div class="calendar-section">
|
<div class="calendar-section">
|
||||||
<div class="weekly-calendar">
|
<div class="daily-calendar">
|
||||||
<!-- Days Header -->
|
<!-- Foremen Header -->
|
||||||
<div class="calendar-header-row">
|
<div class="calendar-header-row" :style="{ gridTemplateColumns: `80px repeat(${visibleForemen.length}, 1fr)` }">
|
||||||
<div class="time-column-header">Time</div>
|
<div class="time-column-header">Time</div>
|
||||||
<div
|
<div
|
||||||
v-for="day in weekDays"
|
v-for="foreman in visibleForemen"
|
||||||
:key="day.date"
|
:key="foreman.id"
|
||||||
class="day-header"
|
class="foreman-header"
|
||||||
:class="{ today: day.isToday }"
|
|
||||||
>
|
>
|
||||||
<div class="day-name">{{ day.dayName }}</div>
|
<div class="foreman-name">{{ foreman.name }}</div>
|
||||||
<div class="day-date">{{ day.dayDate }}</div>
|
<div class="foreman-jobs">{{ getJobsCountForForeman(foreman.name) }} jobs</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Time Grid -->
|
<!-- Time Grid -->
|
||||||
<div class="calendar-grid">
|
<div class="calendar-grid">
|
||||||
<div v-for="timeSlot in timeSlots" :key="timeSlot.time" class="time-row">
|
<div v-for="timeSlot in timeSlots" :key="timeSlot.time" class="time-row" :style="{ gridTemplateColumns: `80px repeat(${visibleForemen.length}, 1fr)` }">
|
||||||
<!-- Time Column -->
|
<!-- Time Column -->
|
||||||
<div class="time-column">
|
<div class="time-column">
|
||||||
<span class="time-label">{{ timeSlot.display }}</span>
|
<span class="time-label">{{ timeSlot.display }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Day Columns -->
|
<!-- Foreman Columns -->
|
||||||
<div
|
<div
|
||||||
v-for="day in weekDays"
|
v-for="foreman in visibleForemen"
|
||||||
:key="`${day.date}-${timeSlot.time}`"
|
:key="`${foreman.id}-${timeSlot.time}`"
|
||||||
class="day-column"
|
class="foreman-column"
|
||||||
:class="{
|
:class="{
|
||||||
'current-time': isCurrentTimeSlot(day.date, timeSlot.time),
|
'current-time': isCurrentTimeSlot(currentDate, timeSlot.time),
|
||||||
'drag-over': isDragOver && dragOverSlot?.date === day.date && dragOverSlot?.time === timeSlot.time && !dragOverSlot?.isOccupied,
|
'drag-over': isDragOver && dragOverSlot?.foremanId === foreman.id && dragOverSlot?.time === timeSlot.time && !dragOverSlot?.isOccupied,
|
||||||
'drag-over-occupied': isDragOver && dragOverSlot?.date === day.date && dragOverSlot?.time === timeSlot.time && dragOverSlot?.isOccupied,
|
'drag-over-occupied': isDragOver && dragOverSlot?.foremanId === foreman.id && dragOverSlot?.time === timeSlot.time && dragOverSlot?.isOccupied,
|
||||||
'drag-preview': isDragOver && isInPreviewSlots(day.date, timeSlot.time),
|
'drag-preview': isDragOver && isInPreviewSlots(foreman.id, timeSlot.time),
|
||||||
}"
|
}"
|
||||||
@click="selectTimeSlot(day.date, timeSlot.time)"
|
@click="selectTimeSlot(foreman.id, timeSlot.time)"
|
||||||
@dragover="handleDragOver($event, day.date, timeSlot.time)"
|
@dragover="handleDragOver($event, foreman.id, timeSlot.time)"
|
||||||
@dragleave="handleDragLeave"
|
@dragleave="handleDragLeave"
|
||||||
@drop="handleDrop($event, day.date, timeSlot.time)"
|
@drop="handleDrop($event, foreman.id, timeSlot.time)"
|
||||||
>
|
>
|
||||||
<!-- Events in this time slot -->
|
<!-- Events in this time slot -->
|
||||||
<div
|
<div
|
||||||
v-for="event in getEventsForTimeSlot(day.date, timeSlot.time)"
|
v-for="event in getEventsForTimeSlot(foreman.name, timeSlot.time, currentDate)"
|
||||||
:key="event.id"
|
:key="event.id"
|
||||||
class="calendar-event"
|
class="calendar-event"
|
||||||
:class="getPriorityClass(event.priority)"
|
:class="getPriorityClass(event.priority)"
|
||||||
|
|
@ -80,7 +154,7 @@
|
||||||
>
|
>
|
||||||
<div class="event-title">{{ event.serviceType }}</div>
|
<div class="event-title">{{ event.serviceType }}</div>
|
||||||
<div class="event-customer">{{ event.customer }}</div>
|
<div class="event-customer">{{ event.customer }}</div>
|
||||||
<div class="event-time-display">{{ event.scheduledTime }}</div>
|
<div class="event-time-display">{{ formatTimeDisplay(event.scheduledTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -236,7 +310,7 @@ import Api from "../../api";
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const services = ref([]);
|
const services = ref([]);
|
||||||
const currentWeekStart = ref(getWeekStart(new Date("2025-10-25")));
|
const currentDate = ref("2025-10-25");
|
||||||
const eventDialog = ref(false);
|
const eventDialog = ref(false);
|
||||||
const selectedEvent = ref(null);
|
const selectedEvent = ref(null);
|
||||||
|
|
||||||
|
|
@ -246,13 +320,27 @@ const isDragOver = ref(false);
|
||||||
const dragOverSlot = ref(null);
|
const dragOverSlot = ref(null);
|
||||||
const previewSlots = ref([]);
|
const previewSlots = ref([]);
|
||||||
|
|
||||||
// Helper function to get week start (Monday)
|
// Foremen data
|
||||||
function getWeekStart(date) {
|
const foremen = ref([
|
||||||
const d = new Date(date);
|
{ id: 1, name: "Mike Thompson" },
|
||||||
const day = d.getDay();
|
{ id: 2, name: "Sarah Johnson" },
|
||||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust when Sunday (0)
|
{ id: 3, name: "David Martinez" },
|
||||||
return new Date(d.setDate(diff));
|
{ id: 4, name: "Chris Wilson" },
|
||||||
}
|
{ id: 5, name: "Lisa Anderson" },
|
||||||
|
{ id: 6, name: "Robert Thomas" },
|
||||||
|
{ id: 7, name: "Maria White" },
|
||||||
|
{ id: 8, name: "James Clark" },
|
||||||
|
{ id: 9, name: "Patricia Lewis" },
|
||||||
|
{ id: 10, name: "Kevin Walker" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Foremen selection
|
||||||
|
const selectedForemen = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // Default to all selected
|
||||||
|
const showForemenMenu = ref(false);
|
||||||
|
|
||||||
|
// Date picker
|
||||||
|
const showDatePicker = ref(false);
|
||||||
|
const selectedDate = ref(null);
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const scheduledServices = computed(() =>
|
const scheduledServices = computed(() =>
|
||||||
|
|
@ -263,41 +351,27 @@ const unscheduledServices = computed(() =>
|
||||||
services.value.filter((service) => service.status === "unscheduled"),
|
services.value.filter((service) => service.status === "unscheduled"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Weekly calendar computed properties
|
// Daily calendar computed properties
|
||||||
const weekDays = computed(() => {
|
const dayDisplayText = computed(() => {
|
||||||
const days = [];
|
const date = new Date(currentDate.value);
|
||||||
const today = new Date();
|
return date.toLocaleDateString("en-US", {
|
||||||
const todayStr = today.toISOString().split("T")[0];
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
for (let i = 0; i < 7; i++) {
|
month: "long",
|
||||||
const date = new Date(currentWeekStart.value);
|
day: "numeric"
|
||||||
date.setDate(currentWeekStart.value.getDate() + i);
|
|
||||||
const dateStr = date.toISOString().split("T")[0];
|
|
||||||
|
|
||||||
days.push({
|
|
||||||
date: dateStr,
|
|
||||||
dayName: date.toLocaleDateString("en-US", { weekday: "short" }),
|
|
||||||
dayDate: date.getDate(),
|
|
||||||
fullDate: date,
|
|
||||||
isToday: dateStr === todayStr,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return days;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const weekDisplayText = computed(() => {
|
// Foremen computed properties
|
||||||
const start = new Date(currentWeekStart.value);
|
const foremenOptions = computed(() => {
|
||||||
const end = new Date(currentWeekStart.value);
|
return foremen.value.map(foreman => ({
|
||||||
end.setDate(start.getDate() + 6);
|
id: foreman.id,
|
||||||
|
name: foreman.name
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
const startStr = start.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
const visibleForemen = computed(() => {
|
||||||
const endStr = end.toLocaleDateString("en-US", {
|
return foremen.value.filter(foreman => selectedForemen.value.includes(foreman.id));
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
return `${startStr} - ${endStr}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeSlots = computed(() => {
|
const timeSlots = computed(() => {
|
||||||
|
|
@ -371,15 +445,16 @@ const getEndTime = (startTime, durationMinutes) => {
|
||||||
return endDate.toTimeString().slice(0, 5);
|
return endDate.toTimeString().slice(0, 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if a time slot has any conflicts with existing events
|
// Check if a time slot has any conflicts with existing events for a specific foreman
|
||||||
const isTimeSlotOccupied = (date, startTime, durationMinutes, excludeServiceId = null) => {
|
const isTimeSlotOccupied = (foremanName, startTime, durationMinutes, excludeServiceId = null) => {
|
||||||
const endTime = getEndTime(startTime, durationMinutes);
|
const endTime = getEndTime(startTime, durationMinutes);
|
||||||
|
|
||||||
return scheduledServices.value.some((service) => {
|
return scheduledServices.value.some((service) => {
|
||||||
// Exclude the service being moved
|
// Exclude the service being moved
|
||||||
if (excludeServiceId && service.id === excludeServiceId) return false;
|
if (excludeServiceId && service.id === excludeServiceId) return false;
|
||||||
|
|
||||||
if (service.scheduledDate !== date) return false;
|
// Check if this service is for the same foreman and date
|
||||||
|
if (service.foreman !== foremanName || service.scheduledDate !== currentDate.value) return false;
|
||||||
|
|
||||||
const serviceEndTime = getEndTime(service.scheduledTime, service.duration);
|
const serviceEndTime = getEndTime(service.scheduledTime, service.duration);
|
||||||
|
|
||||||
|
|
@ -392,15 +467,15 @@ const isTimeSlotOccupied = (date, startTime, durationMinutes, excludeServiceId =
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get all time slots that would be occupied by an event
|
// Get all time slots that would be occupied by an event for a specific foreman
|
||||||
const getOccupiedSlots = (date, startTime, durationMinutes) => {
|
const getOccupiedSlots = (foremanId, startTime, durationMinutes) => {
|
||||||
const slots = [];
|
const slots = [];
|
||||||
const endTime = getEndTime(startTime, durationMinutes);
|
const endTime = getEndTime(startTime, durationMinutes);
|
||||||
|
|
||||||
// Generate all 30-minute slots between start and end time
|
// Generate all 30-minute slots between start and end time
|
||||||
let currentTime = startTime;
|
let currentTime = startTime;
|
||||||
while (currentTime < endTime) {
|
while (currentTime < endTime) {
|
||||||
slots.push({ date, time: currentTime });
|
slots.push({ foremanId, time: currentTime });
|
||||||
|
|
||||||
// Add 30 minutes
|
// Add 30 minutes
|
||||||
const [hours, minutes] = currentTime.split(':').map(Number);
|
const [hours, minutes] = currentTime.split(':').map(Number);
|
||||||
|
|
@ -418,32 +493,75 @@ const getOccupiedSlots = (date, startTime, durationMinutes) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calendar navigation methods
|
// Calendar navigation methods
|
||||||
const previousWeek = () => {
|
const previousDay = () => {
|
||||||
const newDate = new Date(currentWeekStart.value);
|
const date = new Date(currentDate.value);
|
||||||
newDate.setDate(newDate.getDate() - 7);
|
date.setDate(date.getDate() - 1);
|
||||||
currentWeekStart.value = newDate;
|
currentDate.value = date.toISOString().split("T")[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextWeek = () => {
|
const nextDay = () => {
|
||||||
const newDate = new Date(currentWeekStart.value);
|
const date = new Date(currentDate.value);
|
||||||
newDate.setDate(newDate.getDate() + 7);
|
date.setDate(date.getDate() + 1);
|
||||||
currentWeekStart.value = newDate;
|
currentDate.value = date.toISOString().split("T")[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToToday = () => {
|
const goToToday = () => {
|
||||||
currentWeekStart.value = getWeekStart(new Date());
|
const today = new Date();
|
||||||
|
currentDate.value = today.toISOString().split("T")[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Event positioning and display methods
|
// Event positioning and display methods
|
||||||
const getEventsForTimeSlot = (date, time) => {
|
const getEventsForTimeSlot = (foremanName, time, date) => {
|
||||||
return scheduledServices.value.filter((service) => {
|
return scheduledServices.value.filter((service) => {
|
||||||
if (service.scheduledDate !== date) return false;
|
if (service.scheduledDate !== date || service.foreman !== foremanName) return false;
|
||||||
|
|
||||||
// Only show the event in its starting time slot to prevent duplication
|
// Only show the event in its starting time slot to prevent duplication
|
||||||
return service.scheduledTime === time;
|
return service.scheduledTime === time;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getJobsCountForForeman = (foremanName) => {
|
||||||
|
return scheduledServices.value.filter((service) =>
|
||||||
|
service.foreman === foremanName && service.scheduledDate === currentDate.value
|
||||||
|
).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeDisplay = (time) => {
|
||||||
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
|
const displayHour = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;
|
||||||
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||||
|
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${ampm}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Foremen selection methods
|
||||||
|
const toggleAllForemen = () => {
|
||||||
|
if (selectedForemen.value.length === foremen.value.length) {
|
||||||
|
// Deselect all
|
||||||
|
selectedForemen.value = [];
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
selectedForemen.value = foremen.value.map(f => f.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleForeman = (foremanId) => {
|
||||||
|
const index = selectedForemen.value.indexOf(foremanId);
|
||||||
|
if (index > -1) {
|
||||||
|
selectedForemen.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
selectedForemen.value.push(foremanId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Date picker methods
|
||||||
|
const onDateSelected = (date) => {
|
||||||
|
if (date) {
|
||||||
|
const dateObj = Array.isArray(date) ? date[0] : date;
|
||||||
|
currentDate.value = dateObj.toISOString().split('T')[0];
|
||||||
|
showDatePicker.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getEventStyle = (event) => {
|
const getEventStyle = (event) => {
|
||||||
const duration = event.duration;
|
const duration = event.duration;
|
||||||
const slots = Math.ceil(duration / 30); // 30-minute slots
|
const slots = Math.ceil(duration / 30); // 30-minute slots
|
||||||
|
|
@ -463,13 +581,13 @@ const isCurrentTimeSlot = (date, time) => {
|
||||||
return date === today && time === currentTime;
|
return date === today && time === currentTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectTimeSlot = (date, time) => {
|
const selectTimeSlot = (foremanId, time) => {
|
||||||
console.log("Selected time slot:", date, time);
|
console.log("Selected time slot:", foremanId, time);
|
||||||
// This will be used for drag-and-drop functionality
|
// This will be used for drag-and-drop functionality
|
||||||
};
|
};
|
||||||
|
|
||||||
const isInPreviewSlots = (date, time) => {
|
const isInPreviewSlots = (foremanId, time) => {
|
||||||
return previewSlots.value.some(slot => slot.date === date && slot.time === time);
|
return previewSlots.value.some(slot => slot.foremanId === foremanId && slot.time === time);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEventDetails = (event) => {
|
const showEventDetails = (event) => {
|
||||||
|
|
@ -521,27 +639,31 @@ const handleDragEnd = (event) => {
|
||||||
previewSlots.value = [];
|
previewSlots.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (event, date, time) => {
|
const handleDragOver = (event, foremanId, time) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation(); // Prevent event bubbling to avoid conflicts with nested elements
|
event.stopPropagation(); // Prevent event bubbling to avoid conflicts with nested elements
|
||||||
|
|
||||||
if (!draggedService.value) return;
|
if (!draggedService.value) return;
|
||||||
|
|
||||||
|
// Get foreman name from ID - check both all foremen and visible foremen
|
||||||
|
const foreman = foremen.value.find(f => f.id === foremanId);
|
||||||
|
if (!foreman) return;
|
||||||
|
|
||||||
// Check if the slot would be occupied (excluding the current service)
|
// Check if the slot would be occupied (excluding the current service)
|
||||||
const isOccupied = isTimeSlotOccupied(date, time, draggedService.value.duration, draggedService.value.id);
|
const isOccupied = isTimeSlotOccupied(foreman.name, time, draggedService.value.duration, draggedService.value.id);
|
||||||
|
|
||||||
// Calculate which slots would be occupied by this event
|
// Calculate which slots would be occupied by this event
|
||||||
const wouldOccupySlots = getOccupiedSlots(date, time, draggedService.value.duration);
|
const wouldOccupySlots = getOccupiedSlots(foremanId, time, draggedService.value.duration);
|
||||||
|
|
||||||
event.dataTransfer.dropEffect = isOccupied ? "none" : "move";
|
event.dataTransfer.dropEffect = isOccupied ? "none" : "move";
|
||||||
|
|
||||||
// Only update state if it's actually different to prevent flickering
|
// Only update state if it's actually different to prevent flickering
|
||||||
const currentSlotKey = `${date}-${time}`;
|
const currentSlotKey = `${foremanId}-${time}`;
|
||||||
const previousSlotKey = dragOverSlot.value ? `${dragOverSlot.value.date}-${dragOverSlot.value.time}` : null;
|
const previousSlotKey = dragOverSlot.value ? `${dragOverSlot.value.foremanId}-${dragOverSlot.value.time}` : null;
|
||||||
|
|
||||||
if (currentSlotKey !== previousSlotKey || dragOverSlot.value?.isOccupied !== isOccupied) {
|
if (currentSlotKey !== previousSlotKey || dragOverSlot.value?.isOccupied !== isOccupied) {
|
||||||
isDragOver.value = true;
|
isDragOver.value = true;
|
||||||
dragOverSlot.value = { date, time, isOccupied };
|
dragOverSlot.value = { foremanId, time, isOccupied };
|
||||||
previewSlots.value = wouldOccupySlots;
|
previewSlots.value = wouldOccupySlots;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -573,14 +695,18 @@ const handleDragLeave = (event) => {
|
||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (event, date, time) => {
|
const handleDrop = (event, foremanId, time) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!draggedService.value) return;
|
if (!draggedService.value) return;
|
||||||
|
|
||||||
|
// Get foreman name from ID
|
||||||
|
const foreman = foremen.value.find(f => f.id === foremanId);
|
||||||
|
if (!foreman) return;
|
||||||
|
|
||||||
// Check if the slot is occupied (excluding the service being moved)
|
// Check if the slot is occupied (excluding the service being moved)
|
||||||
const isOccupied = isTimeSlotOccupied(date, time, draggedService.value.duration, draggedService.value.id);
|
const isOccupied = isTimeSlotOccupied(foreman.name, time, draggedService.value.duration, draggedService.value.id);
|
||||||
|
|
||||||
if (isOccupied) {
|
if (isOccupied) {
|
||||||
console.log("Cannot drop here - time slot is occupied");
|
console.log("Cannot drop here - time slot is occupied");
|
||||||
|
|
@ -605,13 +731,14 @@ const handleDrop = (event, date, time) => {
|
||||||
services.value[serviceIndex] = {
|
services.value[serviceIndex] = {
|
||||||
...services.value[serviceIndex],
|
...services.value[serviceIndex],
|
||||||
status: 'scheduled',
|
status: 'scheduled',
|
||||||
scheduledDate: date,
|
scheduledDate: currentDate.value,
|
||||||
scheduledTime: time,
|
scheduledTime: time,
|
||||||
scheduledTimeDisplay: formattedTime
|
scheduledTimeDisplay: formattedTime,
|
||||||
|
foreman: foreman.name
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = wasScheduled ? 'Moved' : 'Scheduled';
|
const action = wasScheduled ? 'Moved' : 'Scheduled';
|
||||||
console.log(`${action} ${draggedService.value.serviceType} for ${date} at ${formattedTime}`);
|
console.log(`${action} ${draggedService.value.serviceType} for ${foreman.name} on ${currentDate.value} at ${formattedTime}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset drag state
|
// Reset drag state
|
||||||
|
|
@ -688,6 +815,32 @@ onMounted(async () => {
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-display-btn {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #1976d2 !important;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-display-btn:hover {
|
||||||
|
background-color: rgba(25, 118, 210, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-text {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-main {
|
.calendar-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
@ -700,16 +853,16 @@ onMounted(async () => {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-calendar {
|
.daily-calendar {
|
||||||
min-width: 800px;
|
min-width: 800px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-header-row {
|
.calendar-header-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 80px repeat(7, 1fr);
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
border-bottom: 2px solid #e0e0e0;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
@ -722,7 +875,7 @@ onMounted(async () => {
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-header {
|
.foreman-header {
|
||||||
padding: 12px 8px;
|
padding: 12px 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-right: 1px solid #e0e0e0;
|
border-right: 1px solid #e0e0e0;
|
||||||
|
|
@ -730,23 +883,20 @@ onMounted(async () => {
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-header:hover {
|
.foreman-header:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-header.today {
|
.foreman-name {
|
||||||
background-color: #e3f2fd;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-name {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
color: #1976d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-date {
|
.foreman-jobs {
|
||||||
font-size: 1.2em;
|
font-size: 0.75em;
|
||||||
|
color: #666;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -757,7 +907,6 @@ onMounted(async () => {
|
||||||
|
|
||||||
.time-row {
|
.time-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 80px repeat(7, 1fr);
|
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
@ -781,7 +930,7 @@ onMounted(async () => {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-column {
|
.foreman-column {
|
||||||
border-right: 1px solid #e0e0e0;
|
border-right: 1px solid #e0e0e0;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -789,27 +938,27 @@ onMounted(async () => {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-column:hover {
|
.foreman-column:hover {
|
||||||
background-color: #f0f8ff;
|
background-color: #f0f8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-column.current-time {
|
.foreman-column.current-time {
|
||||||
background-color: #fff3e0;
|
background-color: #fff3e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-column.drag-over {
|
.foreman-column.drag-over {
|
||||||
background-color: #e8f5e8;
|
background-color: #e8f5e8;
|
||||||
border: 2px dashed #4caf50;
|
border: 2px dashed #4caf50;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-column.drag-over-occupied {
|
.foreman-column.drag-over-occupied {
|
||||||
background-color: #ffebee;
|
background-color: #ffebee;
|
||||||
border: 2px dashed #f44336;
|
border: 2px dashed #f44336;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-column.drag-preview {
|
.foreman-column.drag-preview {
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
border: 1px solid #2196f3;
|
border: 1px solid #2196f3;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -1000,19 +1149,9 @@ onMounted(async () => {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.week-display {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1em;
|
|
||||||
color: #1976d2;
|
|
||||||
min-width: 200px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue