build mock views
This commit is contained in:
parent
b0ed2c68f9
commit
44d47db0ad
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@iconoir/vue": "^7.11.0",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"axios": "^1.12.2",
|
||||
"frappe-ui": "^0.1.205",
|
||||
@ -737,6 +738,12 @@
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@mdi/font": {
|
||||
"version": "7.4.47",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconoir/vue": "^7.11.0",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@primeuix/themes": "^1.2.5",
|
||||
"axios": "^1.12.2",
|
||||
"frappe-ui": "^0.1.205",
|
||||
|
||||
@ -27,6 +27,18 @@ class Api {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getServiceData() {
|
||||
const data = DataUtils.dummyServiceData;
|
||||
console.log("DEBUG: API - getServiceData result: ", data);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getRouteData() {
|
||||
const data = DataUtils.dummyRouteData;
|
||||
console.log("DEBUG: API - getRouteData result: ", data);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getDocsList(doctype, fields = []) {
|
||||
const docs = await frappe.db.get_list(doctype, { fields });
|
||||
console.log(`DEBUG: API - Fetched ${doctype} list: `, docs);
|
||||
|
||||
@ -36,6 +36,14 @@
|
||||
:severity="getBadgeColor(slotProps.data[col.fieldName])"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="col.type === 'button'" #body="slotProps">
|
||||
<Button
|
||||
:label="slotProps.data[col.fieldName]"
|
||||
size="small"
|
||||
severity="info"
|
||||
@click="$emit('rowClick', slotProps)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
@ -44,6 +52,7 @@ import { defineProps } from "vue";
|
||||
import DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
import Tag from "primevue/tag";
|
||||
import Button from "primevue/button";
|
||||
import InputText from "primevue/inputtext";
|
||||
import { ref } from "vue";
|
||||
import { FilterMatchMode } from "@primevue/core";
|
||||
@ -64,6 +73,8 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['rowClick']);
|
||||
|
||||
const filterRef = ref(props.filters);
|
||||
const selectedRows = ref();
|
||||
|
||||
|
||||
@ -137,6 +137,7 @@ const handleCategoryClick = (category) => {
|
||||
background-color: rgb(69, 112, 101);
|
||||
color: white;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,743 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Calendar</h2>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">
|
||||
<h2>Sprinkler Service Calendar</h2>
|
||||
<div class="header-controls">
|
||||
<v-btn
|
||||
@click="previousWeek"
|
||||
icon="mdi-chevron-left"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
></v-btn>
|
||||
<span class="week-display">{{ weekDisplayText }}</span>
|
||||
<v-btn
|
||||
@click="nextWeek"
|
||||
icon="mdi-chevron-right"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
></v-btn>
|
||||
<v-btn @click="goToToday" variant="outlined" size="small" class="ml-4"
|
||||
>Today</v-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-main">
|
||||
<!-- Weekly Calendar Grid -->
|
||||
<div class="calendar-section">
|
||||
<div class="weekly-calendar">
|
||||
<!-- Days Header -->
|
||||
<div class="calendar-header-row">
|
||||
<div class="time-column-header">Time</div>
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
:key="day.date"
|
||||
class="day-header"
|
||||
:class="{ today: day.isToday }"
|
||||
>
|
||||
<div class="day-name">{{ day.dayName }}</div>
|
||||
<div class="day-date">{{ day.dayDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Grid -->
|
||||
<div class="calendar-grid">
|
||||
<div v-for="timeSlot in timeSlots" :key="timeSlot.time" class="time-row">
|
||||
<!-- Time Column -->
|
||||
<div class="time-column">
|
||||
<span class="time-label">{{ timeSlot.display }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Day Columns -->
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
:key="`${day.date}-${timeSlot.time}`"
|
||||
class="day-column"
|
||||
:class="{
|
||||
'current-time': isCurrentTimeSlot(day.date, timeSlot.time),
|
||||
}"
|
||||
@click="selectTimeSlot(day.date, timeSlot.time)"
|
||||
>
|
||||
<!-- Events in this time slot -->
|
||||
<div
|
||||
v-for="event in getEventsForTimeSlot(day.date, timeSlot.time)"
|
||||
:key="event.id"
|
||||
class="calendar-event"
|
||||
:class="getPriorityClass(event.priority)"
|
||||
:style="getEventStyle(event)"
|
||||
@click.stop="showEventDetails({ event })"
|
||||
>
|
||||
<div class="event-title">{{ event.serviceType }}</div>
|
||||
<div class="event-customer">{{ event.customer }}</div>
|
||||
<div class="event-time-display">{{ event.scheduledTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unscheduled Services Column -->
|
||||
<div class="unscheduled-section">
|
||||
<div class="unscheduled-header">
|
||||
<h4>Unscheduled Services</h4>
|
||||
<v-chip
|
||||
:color="getUnscheduledCount() > 0 ? 'warning' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ getUnscheduledCount() }} pending
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="unscheduled-list">
|
||||
<v-card
|
||||
v-for="service in unscheduledServices"
|
||||
:key="service.id"
|
||||
class="unscheduled-item mb-2"
|
||||
:class="getPriorityClass(service.priority)"
|
||||
elevation="1"
|
||||
hover
|
||||
density="compact"
|
||||
>
|
||||
<v-card-text class="pa-3">
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<v-chip :color="getPriorityColor(service.priority)" size="x-small">
|
||||
{{ service.priority.toUpperCase() }}
|
||||
</v-chip>
|
||||
<span class="text-caption text-medium-emphasis"
|
||||
>${{ service.estimatedCost.toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="service-title-compact">{{ service.serviceType }}</div>
|
||||
<div class="service-customer">{{ service.customer }}</div>
|
||||
|
||||
<div class="service-compact-details mt-1">
|
||||
<div class="text-caption">
|
||||
<v-icon size="x-small" class="mr-1">mdi-clock</v-icon>
|
||||
{{ formatDuration(service.duration) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="service.notes" class="service-notes-compact mt-2">
|
||||
<span class="text-caption">{{ service.notes }}</span>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
block
|
||||
@click="scheduleService(service)"
|
||||
>
|
||||
<v-icon left size="x-small">mdi-calendar-plus</v-icon>
|
||||
Schedule
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div v-if="unscheduledServices.length === 0" class="no-unscheduled">
|
||||
<v-icon size="large" color="success">mdi-check-circle</v-icon>
|
||||
<p class="text-body-2">All services scheduled!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Details Dialog -->
|
||||
<v-dialog v-model="eventDialog" max-width="600px">
|
||||
<v-card v-if="selectedEvent">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span>{{ selectedEvent.title }}</span>
|
||||
<v-chip :color="getPriorityColor(selectedEvent.priority)" small>
|
||||
{{ selectedEvent.priority.toUpperCase() }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="event-details">
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-account</v-icon>
|
||||
<strong>Customer:</strong> {{ selectedEvent.customer }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-map-marker</v-icon>
|
||||
<strong>Address:</strong> {{ selectedEvent.address }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-wrench</v-icon>
|
||||
<strong>Service Type:</strong> {{ selectedEvent.serviceType }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-calendar</v-icon>
|
||||
<strong>Date:</strong> {{ selectedEvent.scheduledDate }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-clock</v-icon>
|
||||
<strong>Time:</strong> {{ selectedEvent.scheduledTime }} ({{
|
||||
formatDuration(selectedEvent.duration)
|
||||
}})
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-account-hard-hat</v-icon>
|
||||
<strong>Foreman:</strong> {{ selectedEvent.foreman || "Not assigned" }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-account-group</v-icon>
|
||||
<strong>Crew:</strong>
|
||||
{{ selectedEvent.crew?.join(", ") || "Not assigned" }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-currency-usd</v-icon>
|
||||
<strong>Estimated Cost:</strong> ${{
|
||||
selectedEvent.estimatedCost.toLocaleString()
|
||||
}}
|
||||
</div>
|
||||
<div v-if="selectedEvent.notes" class="detail-row">
|
||||
<v-icon class="mr-2">mdi-note-text</v-icon>
|
||||
<strong>Notes:</strong> {{ selectedEvent.notes }}
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="eventDialog = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup></script>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import Api from "../../api";
|
||||
|
||||
// Reactive data
|
||||
const services = ref([]);
|
||||
const currentWeekStart = ref(getWeekStart(new Date("2025-10-25")));
|
||||
const eventDialog = ref(false);
|
||||
const selectedEvent = ref(null);
|
||||
|
||||
// Helper function to get week start (Monday)
|
||||
function getWeekStart(date) {
|
||||
const d = new Date(date);
|
||||
const day = d.getDay();
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust when Sunday (0)
|
||||
return new Date(d.setDate(diff));
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const scheduledServices = computed(() =>
|
||||
services.value.filter((service) => service.status === "scheduled"),
|
||||
);
|
||||
|
||||
const unscheduledServices = computed(() =>
|
||||
services.value.filter((service) => service.status === "unscheduled"),
|
||||
);
|
||||
|
||||
// Weekly calendar computed properties
|
||||
const weekDays = computed(() => {
|
||||
const days = [];
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split("T")[0];
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(currentWeekStart.value);
|
||||
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(() => {
|
||||
const start = new Date(currentWeekStart.value);
|
||||
const end = new Date(currentWeekStart.value);
|
||||
end.setDate(start.getDate() + 6);
|
||||
|
||||
const startStr = start.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
const endStr = end.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return `${startStr} - ${endStr}`;
|
||||
});
|
||||
|
||||
const timeSlots = computed(() => {
|
||||
const slots = [];
|
||||
for (let hour = 7; hour < 19; hour++) {
|
||||
// 7 AM to 7 PM
|
||||
for (let minute = 0; minute < 60; minute += 30) {
|
||||
// 30-minute intervals
|
||||
const time = `${hour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}`;
|
||||
const displayHour = hour > 12 ? hour - 12 : hour === 0 ? 12 : hour;
|
||||
const ampm = hour >= 12 ? "PM" : "AM";
|
||||
const display =
|
||||
minute === 0 ? `${displayHour}:00 ${ampm}` : `${displayHour}:30 ${ampm}`;
|
||||
|
||||
slots.push({
|
||||
time,
|
||||
display: minute === 0 ? display : "", // Only show hour labels on the hour
|
||||
isHour: minute === 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const getUnscheduledCount = () => unscheduledServices.value.length;
|
||||
|
||||
const getPriorityClass = (priority) => {
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return "priority-urgent";
|
||||
case "high":
|
||||
return "priority-high";
|
||||
case "medium":
|
||||
return "priority-medium";
|
||||
case "low":
|
||||
return "priority-low";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return "red";
|
||||
case "high":
|
||||
return "orange";
|
||||
case "medium":
|
||||
return "yellow";
|
||||
case "low":
|
||||
return "green";
|
||||
default:
|
||||
return "grey";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDuration = (minutes) => {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
if (hours === 0) return `${mins}m`;
|
||||
if (mins === 0) return `${hours}h`;
|
||||
return `${hours}h ${mins}m`;
|
||||
};
|
||||
|
||||
const getEndTime = (startTime, durationMinutes) => {
|
||||
const [hours, minutes] = startTime.split(":").map(Number);
|
||||
const startDate = new Date();
|
||||
startDate.setHours(hours, minutes, 0, 0);
|
||||
const endDate = new Date(startDate.getTime() + durationMinutes * 60000);
|
||||
return endDate.toTimeString().slice(0, 5);
|
||||
};
|
||||
|
||||
// Calendar navigation methods
|
||||
const previousWeek = () => {
|
||||
const newDate = new Date(currentWeekStart.value);
|
||||
newDate.setDate(newDate.getDate() - 7);
|
||||
currentWeekStart.value = newDate;
|
||||
};
|
||||
|
||||
const nextWeek = () => {
|
||||
const newDate = new Date(currentWeekStart.value);
|
||||
newDate.setDate(newDate.getDate() + 7);
|
||||
currentWeekStart.value = newDate;
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
currentWeekStart.value = getWeekStart(new Date());
|
||||
};
|
||||
|
||||
// Event positioning and display methods
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
const getEventStyle = (event) => {
|
||||
const duration = event.duration;
|
||||
const slots = Math.ceil(duration / 30); // 30-minute slots
|
||||
const height = Math.min(slots * 40 - 4, 200); // Max height to prevent overflow
|
||||
|
||||
return {
|
||||
height: `${height}px`,
|
||||
minHeight: "36px",
|
||||
};
|
||||
};
|
||||
|
||||
const isCurrentTimeSlot = (date, time) => {
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split("T")[0];
|
||||
const currentTime = `${now.getHours().toString().padStart(2, "0")}:${Math.floor(now.getMinutes() / 30) * 30}`;
|
||||
|
||||
return date === today && time === currentTime;
|
||||
};
|
||||
|
||||
const selectTimeSlot = (date, time) => {
|
||||
console.log("Selected time slot:", date, time);
|
||||
// This will be used for drag-and-drop functionality
|
||||
};
|
||||
|
||||
const showEventDetails = (event) => {
|
||||
selectedEvent.value = event.event;
|
||||
eventDialog.value = true;
|
||||
};
|
||||
|
||||
const scheduleService = (service) => {
|
||||
// Placeholder for future drag-and-drop functionality
|
||||
console.log("Scheduling service:", service);
|
||||
// This will be implemented when drag-and-drop is added
|
||||
alert(
|
||||
`Scheduling functionality will be implemented in the future.\nService: ${service.title}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await Api.getServiceData();
|
||||
services.value = data;
|
||||
console.log("Loaded services:", data);
|
||||
} catch (error) {
|
||||
console.error("Error loading services:", error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-container {
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.calendar-main {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-section {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.weekly-calendar {
|
||||
min-width: 800px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.calendar-header-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px repeat(7, 1fr);
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.time-column-header {
|
||||
padding: 16px 8px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.day-header:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.day-header.today {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.day-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.day-date {
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px repeat(7, 1fr);
|
||||
min-height: 40px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.time-row:nth-child(odd) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.time-column {
|
||||
padding: 8px;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day-column {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.day-column:hover {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.day-column.current-time {
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
|
||||
.calendar-event {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.calendar-event:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.calendar-event .event-title {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.calendar-event .event-customer {
|
||||
font-size: 0.65em;
|
||||
opacity: 0.9;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.calendar-event .event-time-display {
|
||||
font-size: 0.6em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.calendar-event.priority-urgent {
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
}
|
||||
|
||||
.calendar-event.priority-high {
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
}
|
||||
|
||||
.calendar-event.priority-medium {
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
}
|
||||
|
||||
.calendar-event.priority-low {
|
||||
background: linear-gradient(135deg, #4caf50, #388e3c);
|
||||
}
|
||||
|
||||
.unscheduled-section {
|
||||
width: 280px;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unscheduled-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.unscheduled-header h4 {
|
||||
font-size: 1.1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unscheduled-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.unscheduled-item {
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.unscheduled-item:hover {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.priority-urgent {
|
||||
border-left-color: #f44336 !important;
|
||||
}
|
||||
|
||||
.priority-high {
|
||||
border-left-color: #ff9800 !important;
|
||||
}
|
||||
|
||||
.priority-medium {
|
||||
border-left-color: #ffeb3b !important;
|
||||
}
|
||||
|
||||
.priority-low {
|
||||
border-left-color: #4caf50 !important;
|
||||
}
|
||||
|
||||
.service-title-compact {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
color: #1976d2;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.service-customer {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.service-compact-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.service-notes-compact {
|
||||
background-color: #f8f9fa;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
border-left: 2px solid #2196f3;
|
||||
}
|
||||
|
||||
.service-notes-compact .text-caption {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.no-unscheduled {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 */
|
||||
@media (max-width: 768px) {
|
||||
.calendar-main {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.unscheduled-section {
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-left: 0;
|
||||
padding-top: 20px;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -256,6 +256,8 @@ const navigateTo = (path) => {
|
||||
.widget-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
@ -1,9 +1,547 @@
|
||||
<template lang="">
|
||||
<div>
|
||||
<h2>Routes Page</h2>
|
||||
<template>
|
||||
<div class="routes-page">
|
||||
<div class="routes-header">
|
||||
<h2>Service Routes</h2>
|
||||
<p class="routes-subtitle">Manage and track daily service routes for technicians</p>
|
||||
</div>
|
||||
|
||||
<!-- Routes Data Table -->
|
||||
<div class="routes-table-container">
|
||||
<DataTable
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
@row-click="viewRouteDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Route Details Modal -->
|
||||
<v-dialog v-model="routeDialog" max-width="1200px" persistent>
|
||||
<v-card v-if="selectedRoute">
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<div>
|
||||
<h3>{{ selectedRoute.routeName }}</h3>
|
||||
<span class="text-subtitle-1 text-medium-emphasis">{{ selectedRoute.routeId }} - {{ selectedRoute.date }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-chip
|
||||
:color="getStatusColor(selectedRoute.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ selectedRoute.status.toUpperCase() }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="routeDialog = false"
|
||||
></v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<div class="route-details-container">
|
||||
<!-- Route Info Panel -->
|
||||
<div class="route-info-panel">
|
||||
<div class="route-summary pa-4">
|
||||
<h4 class="mb-3">Route Summary</h4>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<v-icon class="mr-2" size="small">mdi-account-hard-hat</v-icon>
|
||||
<span class="label">Technician:</span>
|
||||
<span class="value">{{ selectedRoute.technician }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<v-icon class="mr-2" size="small">mdi-clock-start</v-icon>
|
||||
<span class="label">Start Time:</span>
|
||||
<span class="value">{{ selectedRoute.startTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<v-icon class="mr-2" size="small">mdi-car</v-icon>
|
||||
<span class="label">Vehicle:</span>
|
||||
<span class="value">{{ selectedRoute.vehicleId }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<v-icon class="mr-2" size="small">mdi-map-marker-distance</v-icon>
|
||||
<span class="label">Total Miles:</span>
|
||||
<span class="value">{{ selectedRoute.totalMileage }} mi</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<v-icon class="mr-2" size="small">mdi-timer</v-icon>
|
||||
<span class="label">Est. Duration:</span>
|
||||
<span class="value">{{ selectedRoute.estimatedDuration }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<v-icon class="mr-2" size="small">mdi-map-marker-multiple</v-icon>
|
||||
<span class="label">Stops:</span>
|
||||
<span class="value">{{ selectedRoute.completedStops }}/{{ selectedRoute.totalStops }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stops List -->
|
||||
<div class="stops-section pa-4">
|
||||
<h4 class="mb-3">Route Stops</h4>
|
||||
|
||||
<div class="stops-list">
|
||||
<div
|
||||
v-for="stop in selectedRoute.stops"
|
||||
:key="stop.stopId"
|
||||
class="stop-item"
|
||||
:class="getStopStatusClass(stop.status)"
|
||||
>
|
||||
<div class="stop-number">{{ stop.stopId }}</div>
|
||||
|
||||
<div class="stop-content">
|
||||
<div class="stop-header">
|
||||
<span class="customer-name">{{ stop.customer }}</span>
|
||||
<v-chip
|
||||
:color="getStopStatusColor(stop.status)"
|
||||
size="x-small"
|
||||
>
|
||||
{{ stop.status }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="stop-details">
|
||||
<div class="stop-address">
|
||||
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
||||
{{ stop.address }}
|
||||
</div>
|
||||
|
||||
<div class="stop-service">
|
||||
<v-icon size="x-small" class="mr-1">mdi-wrench</v-icon>
|
||||
{{ stop.serviceType }}
|
||||
</div>
|
||||
|
||||
<div class="stop-time">
|
||||
<v-icon size="x-small" class="mr-1">mdi-clock</v-icon>
|
||||
{{ stop.estimatedTime }} ({{ stop.duration }} min)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Panel -->
|
||||
<div class="map-panel">
|
||||
<div class="map-container">
|
||||
<div class="map-placeholder">
|
||||
<div class="map-content">
|
||||
<v-icon size="64" color="primary">mdi-map</v-icon>
|
||||
<h4 class="mt-3 mb-2">Route Map</h4>
|
||||
<p class="text-body-2 text-center mb-4">Interactive map showing route path and stops</p>
|
||||
|
||||
<!-- Mock Map Legend -->
|
||||
<div class="map-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot completed"></div>
|
||||
<span>Completed</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot in-progress"></div>
|
||||
<span>In Progress</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot not-started"></div>
|
||||
<span>Not Started</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mock Route Stats -->
|
||||
<div class="route-stats mt-4">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ selectedRoute.totalMileage }}</div>
|
||||
<div class="stat-label">Total Miles</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ selectedRoute.totalStops }}</div>
|
||||
<div class="stat-label">Stops</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ Math.round(selectedRoute.totalMileage / selectedRoute.totalStops * 10) / 10 }}</div>
|
||||
<div class="stat-label">Avg Miles/Stop</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
@click="routeDialog = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="optimizeRoute"
|
||||
>
|
||||
Optimize Route
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {};
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import DataTable from "../DataTable.vue";
|
||||
import Api from "../../api";
|
||||
|
||||
// Reactive data
|
||||
const tableData = ref([]);
|
||||
const routeDialog = ref(false);
|
||||
const selectedRoute = ref(null);
|
||||
|
||||
// Table columns configuration
|
||||
const columns = [
|
||||
{ label: "Route ID", fieldName: "routeId", type: "text", sortable: true },
|
||||
{ label: "Route Name", fieldName: "routeName", type: "text", sortable: true },
|
||||
{ label: "Technician", fieldName: "technician", type: "text", sortable: true },
|
||||
{ label: "Date", fieldName: "date", type: "text", sortable: true },
|
||||
{ label: "Status", fieldName: "status", type: "status", sortable: true },
|
||||
{ label: "Progress", fieldName: "progress", type: "text", sortable: true },
|
||||
{ label: "Total Stops", fieldName: "totalStops", type: "text", sortable: true },
|
||||
{ label: "Est. Duration", fieldName: "estimatedDuration", type: "text", sortable: true },
|
||||
{ label: "Actions", fieldName: "actions", type: "button", sortable: false }
|
||||
];
|
||||
|
||||
// Methods
|
||||
const viewRouteDetails = (event) => {
|
||||
const routeId = event.data.routeId;
|
||||
const route = tableData.value.find(r => r.routeId === routeId);
|
||||
if (route && route.fullData) {
|
||||
selectedRoute.value = route.fullData;
|
||||
routeDialog.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case "completed":
|
||||
return "success";
|
||||
case "in progress":
|
||||
return "warning";
|
||||
case "not started":
|
||||
return "info";
|
||||
default:
|
||||
return "grey";
|
||||
}
|
||||
};
|
||||
|
||||
const getStopStatusColor = (status) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case "completed":
|
||||
return "success";
|
||||
case "in progress":
|
||||
return "warning";
|
||||
case "not started":
|
||||
return "grey";
|
||||
default:
|
||||
return "grey";
|
||||
}
|
||||
};
|
||||
|
||||
const getStopStatusClass = (status) => {
|
||||
return `stop-status-${status.replace(' ', '-')}`;
|
||||
};
|
||||
|
||||
const optimizeRoute = () => {
|
||||
alert("Route optimization feature coming soon!");
|
||||
};
|
||||
|
||||
// Load data on component mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await Api.getRouteData();
|
||||
|
||||
// Transform data for table display and keep full data reference
|
||||
tableData.value = data.map(route => ({
|
||||
routeId: route.routeId,
|
||||
routeName: route.routeName,
|
||||
technician: route.technician,
|
||||
date: route.date,
|
||||
status: route.status,
|
||||
progress: `${route.completedStops}/${route.totalStops}`,
|
||||
totalStops: route.totalStops,
|
||||
estimatedDuration: route.estimatedDuration,
|
||||
actions: "View Details",
|
||||
fullData: route // Keep reference to full route data
|
||||
}));
|
||||
|
||||
console.log("Loaded routes:", tableData.value);
|
||||
} catch (error) {
|
||||
console.error("Error loading routes:", error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang=""></style>
|
||||
|
||||
<style scoped>
|
||||
.routes-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.routes-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.routes-header h2 {
|
||||
margin-bottom: 8px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.routes-subtitle {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.routes-table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.route-details-container {
|
||||
display: flex;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.route-info-panel {
|
||||
flex: 1;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.route-summary {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stops-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stops-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.stop-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.stop-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stop-item.stop-status-completed {
|
||||
background-color: #f1f8e9;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.stop-item.stop-status-in-progress {
|
||||
background-color: #fff8e1;
|
||||
border-color: #ff9800;
|
||||
}
|
||||
|
||||
.stop-item.stop-status-not-started {
|
||||
background-color: #fafafa;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.stop-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stop-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stop-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.stop-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stop-address,
|
||||
.stop-service,
|
||||
.stop-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
width: 400px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.map-placeholder {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map-content {
|
||||
background: white;
|
||||
padding: 32px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-dot.completed {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.legend-dot.in-progress {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.legend-dot.not-started {
|
||||
background-color: #9e9e9e;
|
||||
}
|
||||
|
||||
.route-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 900px) {
|
||||
.route-details-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.route-info-panel {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.map-panel {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -5,8 +5,28 @@ import router from "./router";
|
||||
import PrimeVue from "primevue/config";
|
||||
import { globalSettings } from "./globalSettings";
|
||||
|
||||
// Vuetify
|
||||
import "vuetify/styles";
|
||||
import { createVuetify } from "vuetify";
|
||||
import * as components from "vuetify/components";
|
||||
import * as directives from "vuetify/directives";
|
||||
import { mdi } from "vuetify/iconsets/mdi";
|
||||
import "@mdi/font/css/materialdesignicons.css";
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
icons: {
|
||||
defaultSet: "mdi",
|
||||
sets: {
|
||||
mdi,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(vuetify)
|
||||
.use(PrimeVue, {
|
||||
theme: {
|
||||
options: {
|
||||
|
||||
@ -503,6 +503,631 @@ class DataUtils {
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
static dummyServiceData = [
|
||||
// Scheduled services
|
||||
{
|
||||
id: "service_1",
|
||||
title: "Sprinkler System Installation - John Doe",
|
||||
customer: "John Doe",
|
||||
address: "123 Lane Dr, Cityville, MN",
|
||||
serviceType: "Sprinkler System Installation",
|
||||
foreman: "Mike Thompson",
|
||||
crew: ["Technician A", "Technician B"],
|
||||
scheduledDate: "2025-10-25",
|
||||
scheduledTime: "08:00",
|
||||
duration: 480, // minutes (8 hours)
|
||||
status: "scheduled",
|
||||
priority: "high",
|
||||
estimatedCost: 4500,
|
||||
notes: "Residential front and back yard coverage, 12 zones",
|
||||
},
|
||||
{
|
||||
id: "service_2",
|
||||
title: "Sprinkler Winterization - Jane Smith",
|
||||
customer: "Jane Smith",
|
||||
address: "456 Oak St, Townsville, CA",
|
||||
serviceType: "Sprinkler Winterization",
|
||||
foreman: "Sarah Johnson",
|
||||
crew: ["Technician C"],
|
||||
scheduledDate: "2025-10-25",
|
||||
scheduledTime: "14:00",
|
||||
duration: 90, // minutes (1.5 hours)
|
||||
status: "scheduled",
|
||||
priority: "medium",
|
||||
estimatedCost: 150,
|
||||
notes: "Blow out all lines before first freeze",
|
||||
},
|
||||
{
|
||||
id: "service_3",
|
||||
title: "Drip Line Repair - Mike Johnson",
|
||||
customer: "Mike Johnson",
|
||||
address: "789 Pine Rd, Villagetown, TX",
|
||||
serviceType: "Drip Line Repair",
|
||||
foreman: "David Martinez",
|
||||
crew: ["Technician D"],
|
||||
scheduledDate: "2025-10-26",
|
||||
scheduledTime: "09:00",
|
||||
duration: 120, // minutes (2 hours)
|
||||
status: "scheduled",
|
||||
priority: "high",
|
||||
estimatedCost: 280,
|
||||
notes: "Multiple leaks in flower bed drip system",
|
||||
},
|
||||
{
|
||||
id: "service_4",
|
||||
title: "Controller Programming - Emily Davis",
|
||||
customer: "Emily Davis",
|
||||
address: "321 Maple Ave, Hamlet, FL",
|
||||
serviceType: "Controller Programming",
|
||||
foreman: "Chris Wilson",
|
||||
crew: ["Technician E"],
|
||||
scheduledDate: "2025-10-27",
|
||||
scheduledTime: "10:00",
|
||||
duration: 60, // minutes (1 hour)
|
||||
status: "scheduled",
|
||||
priority: "medium",
|
||||
estimatedCost: 125,
|
||||
notes: "Seasonal schedule update and zone testing",
|
||||
},
|
||||
{
|
||||
id: "service_5",
|
||||
title: "Spring Startup - David Wilson",
|
||||
customer: "David Wilson",
|
||||
address: "654 Cedar Blvd, Borough, NY",
|
||||
serviceType: "Spring Startup",
|
||||
foreman: "Lisa Anderson",
|
||||
crew: ["Technician F"],
|
||||
scheduledDate: "2025-10-28",
|
||||
scheduledTime: "11:00",
|
||||
duration: 75, // minutes (1.25 hours)
|
||||
status: "scheduled",
|
||||
priority: "low",
|
||||
estimatedCost: 95,
|
||||
notes: "Annual system startup and inspection",
|
||||
},
|
||||
{
|
||||
id: "service_6",
|
||||
title: "Zone Expansion - Sarah Brown",
|
||||
customer: "Sarah Brown",
|
||||
address: "987 Birch Ln, Metropolis, IL",
|
||||
serviceType: "Zone Expansion",
|
||||
foreman: "Robert Thomas",
|
||||
crew: ["Technician G", "Technician H"],
|
||||
scheduledDate: "2025-10-29",
|
||||
scheduledTime: "08:30",
|
||||
duration: 300, // minutes (5 hours)
|
||||
status: "scheduled",
|
||||
priority: "high",
|
||||
estimatedCost: 1800,
|
||||
notes: "Add 4 new zones to existing system for landscaped areas",
|
||||
},
|
||||
{
|
||||
id: "service_13",
|
||||
title: "Backflow Testing - Robert Green",
|
||||
customer: "Robert Green",
|
||||
address: "445 Elm St, Riverside, CA",
|
||||
serviceType: "Backflow Testing",
|
||||
foreman: "Mike Thompson",
|
||||
crew: ["Technician I"],
|
||||
scheduledDate: "2025-10-27",
|
||||
scheduledTime: "14:30",
|
||||
duration: 60, // minutes (1 hour)
|
||||
status: "scheduled",
|
||||
priority: "medium",
|
||||
estimatedCost: 120,
|
||||
notes: "Annual backflow preventer inspection",
|
||||
},
|
||||
{
|
||||
id: "service_14",
|
||||
title: "Smart Controller Install - Lisa Park",
|
||||
customer: "Lisa Park",
|
||||
address: "892 Maple Dr, Springfield, TX",
|
||||
serviceType: "Smart Controller Install",
|
||||
foreman: "Sarah Johnson",
|
||||
crew: ["Technician J"],
|
||||
scheduledDate: "2025-10-28",
|
||||
scheduledTime: "09:30",
|
||||
duration: 120, // minutes (2 hours)
|
||||
status: "scheduled",
|
||||
priority: "low",
|
||||
estimatedCost: 450,
|
||||
notes: "Upgrade to WiFi-enabled smart controller",
|
||||
},
|
||||
{
|
||||
id: "service_15",
|
||||
title: "Pump Station Repair - Mark Wilson",
|
||||
customer: "Mark Wilson",
|
||||
address: "123 Oak Ave, Laketown, FL",
|
||||
serviceType: "Pump Station Repair",
|
||||
foreman: "David Martinez",
|
||||
crew: ["Technician K", "Technician L"],
|
||||
scheduledDate: "2025-10-29",
|
||||
scheduledTime: "13:00",
|
||||
duration: 180, // minutes (3 hours)
|
||||
status: "scheduled",
|
||||
priority: "urgent",
|
||||
estimatedCost: 680,
|
||||
notes: "Pump not priming, possible impeller issue",
|
||||
},
|
||||
|
||||
// Unscheduled services
|
||||
{
|
||||
id: "service_7",
|
||||
title: "Emergency Leak Repair - Amanda Martinez",
|
||||
customer: "Amanda Martinez",
|
||||
address: "753 Willow Dr, Smalltown, OH",
|
||||
serviceType: "Emergency Leak Repair",
|
||||
foreman: null,
|
||||
crew: [],
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: 90, // minutes (1.5 hours)
|
||||
status: "unscheduled",
|
||||
priority: "urgent",
|
||||
estimatedCost: 200,
|
||||
notes: "Main line leak flooding driveway",
|
||||
},
|
||||
{
|
||||
id: "service_8",
|
||||
title: "Broken Sprinkler Head - Joshua Anderson",
|
||||
customer: "Joshua Anderson",
|
||||
address: "852 Aspen Rd, Bigcity, NJ",
|
||||
serviceType: "Sprinkler Head Replacement",
|
||||
foreman: null,
|
||||
crew: [],
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: 45, // minutes
|
||||
status: "unscheduled",
|
||||
priority: "medium",
|
||||
estimatedCost: 75,
|
||||
notes: "Multiple heads damaged by mower",
|
||||
},
|
||||
{
|
||||
id: "service_9",
|
||||
title: "System Inspection - Olivia Thomas",
|
||||
customer: "Olivia Thomas",
|
||||
address: "147 Cypress St, Uptown, GA",
|
||||
serviceType: "System Inspection",
|
||||
foreman: null,
|
||||
crew: [],
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: 60, // minutes (1 hour)
|
||||
status: "unscheduled",
|
||||
priority: "low",
|
||||
estimatedCost: 85,
|
||||
notes: "Annual maintenance check requested",
|
||||
},
|
||||
{
|
||||
id: "service_10",
|
||||
title: "New Installation Quote - Daniel Jackson",
|
||||
customer: "Daniel Jackson",
|
||||
address: "369 Fir Ave, Downtown, MI",
|
||||
serviceType: "Installation Quote",
|
||||
foreman: null,
|
||||
crew: [],
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: 45, // minutes
|
||||
status: "unscheduled",
|
||||
priority: "medium",
|
||||
estimatedCost: 0,
|
||||
notes: "Estimate for new construction sprinkler system",
|
||||
},
|
||||
{
|
||||
id: "service_11",
|
||||
title: "Valve Replacement - Sophia White",
|
||||
customer: "Sophia White",
|
||||
address: "258 Palm Blvd, Riverside, AZ",
|
||||
serviceType: "Valve Replacement",
|
||||
foreman: null,
|
||||
crew: [],
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: 120, // minutes (2 hours)
|
||||
status: "unscheduled",
|
||||
priority: "medium",
|
||||
estimatedCost: 350,
|
||||
notes: "Zone 3 valve not closing properly",
|
||||
},
|
||||
{
|
||||
id: "service_12",
|
||||
title: "Controller Repair - Matthew Harris",
|
||||
customer: "Matthew Harris",
|
||||
address: "951 Olive Ln, Seaside, OR",
|
||||
serviceType: "Controller Repair",
|
||||
foreman: null,
|
||||
crew: [],
|
||||
scheduledDate: null,
|
||||
scheduledTime: null,
|
||||
duration: 90, // minutes (1.5 hours)
|
||||
status: "unscheduled",
|
||||
priority: "urgent",
|
||||
estimatedCost: 250,
|
||||
notes: "Controller display not working, possible storm damage",
|
||||
},
|
||||
];
|
||||
|
||||
static dummyRouteData = [
|
||||
{
|
||||
routeId: "TECH-001",
|
||||
routeName: "North Valley Tech Route",
|
||||
routeType: "Tech Route",
|
||||
technician: "Mike Thompson",
|
||||
date: "2025-10-25",
|
||||
status: "in progress",
|
||||
totalStops: 6,
|
||||
completedStops: 2,
|
||||
estimatedDuration: "8 hours",
|
||||
startTime: "07:00",
|
||||
vehicleId: "TECH-101",
|
||||
totalMileage: 52.3,
|
||||
stops: [
|
||||
{
|
||||
stopId: 1,
|
||||
customer: "John Doe",
|
||||
address: "123 Lane Dr, Cityville, MN",
|
||||
serviceType: "Complete Sprinkler System Install",
|
||||
estimatedTime: "07:30",
|
||||
duration: 240,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.9778, lng: -93.2650 }
|
||||
},
|
||||
{
|
||||
stopId: 2,
|
||||
customer: "Sarah Johnson",
|
||||
address: "456 Oak St, Northfield, MN",
|
||||
serviceType: "Zone Expansion Install",
|
||||
estimatedTime: "11:45",
|
||||
duration: 180,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.4583, lng: -93.1614 }
|
||||
},
|
||||
{
|
||||
stopId: 3,
|
||||
customer: "Mike Wilson",
|
||||
address: "789 Pine Rd, Faribault, MN",
|
||||
serviceType: "Drip System Installation",
|
||||
estimatedTime: "15:00",
|
||||
duration: 120,
|
||||
status: "in progress",
|
||||
coordinates: { lat: 44.2950, lng: -93.2688 }
|
||||
},
|
||||
{
|
||||
stopId: 4,
|
||||
customer: "Emily Davis",
|
||||
address: "321 Maple Ave, Owatonna, MN",
|
||||
serviceType: "Smart Controller Install",
|
||||
estimatedTime: "17:30",
|
||||
duration: 90,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.0839, lng: -93.2261 }
|
||||
},
|
||||
{
|
||||
stopId: 5,
|
||||
customer: "David Brown",
|
||||
address: "654 Cedar Blvd, Austin, MN",
|
||||
serviceType: "Backflow Preventer Install",
|
||||
estimatedTime: "19:15",
|
||||
duration: 75,
|
||||
status: "not started",
|
||||
coordinates: { lat: 43.6667, lng: -92.9746 }
|
||||
},
|
||||
{
|
||||
stopId: 6,
|
||||
customer: "Lisa Garcia",
|
||||
address: "987 Elm St, Albert Lea, MN",
|
||||
serviceType: "Pump Station Installation",
|
||||
estimatedTime: "20:45",
|
||||
duration: 150,
|
||||
status: "not started",
|
||||
coordinates: { lat: 43.6481, lng: -93.3687 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
routeId: "MAT-002",
|
||||
routeName: "South Metro Materials Route",
|
||||
routeType: "Materials Route",
|
||||
technician: "Sarah Johnson",
|
||||
date: "2025-10-25",
|
||||
status: "completed",
|
||||
totalStops: 5,
|
||||
completedStops: 5,
|
||||
estimatedDuration: "4 hours",
|
||||
startTime: "06:00",
|
||||
vehicleId: "MAT-102",
|
||||
totalMileage: 42.1,
|
||||
stops: [
|
||||
{
|
||||
stopId: 1,
|
||||
customer: "Jennifer White - Job #2245",
|
||||
address: "555 Main St, Burnsville, MN",
|
||||
serviceType: "Deliver Pipes & Fittings",
|
||||
estimatedTime: "06:30",
|
||||
duration: 30,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.7677, lng: -93.2777 }
|
||||
},
|
||||
{
|
||||
stopId: 2,
|
||||
customer: "Mark Anderson - Job #2248",
|
||||
address: "777 First Ave, Eagan, MN",
|
||||
serviceType: "Deliver Sprinkler Heads & Valves",
|
||||
estimatedTime: "07:15",
|
||||
duration: 25,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.8041, lng: -93.1668 }
|
||||
},
|
||||
{
|
||||
stopId: 3,
|
||||
customer: "Carol Thompson - Job #2250",
|
||||
address: "999 Second St, Apple Valley, MN",
|
||||
serviceType: "Deliver Controller & Wire",
|
||||
estimatedTime: "08:00",
|
||||
duration: 20,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.7319, lng: -93.2177 }
|
||||
},
|
||||
{
|
||||
stopId: 4,
|
||||
customer: "Steven Clark - Job #2253",
|
||||
address: "111 Third Ave, Rosemount, MN",
|
||||
serviceType: "Deliver Backflow Preventer",
|
||||
estimatedTime: "08:45",
|
||||
duration: 25,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.7391, lng: -93.0658 }
|
||||
},
|
||||
{
|
||||
stopId: 5,
|
||||
customer: "Nancy Rodriguez - Job #2256",
|
||||
address: "222 Fourth St, Lakeville, MN",
|
||||
serviceType: "Deliver Pump & Pressure Tank",
|
||||
estimatedTime: "09:30",
|
||||
duration: 35,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.6497, lng: -93.2427 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
routeId: "WAR-003",
|
||||
routeName: "West Metro Warranty Route",
|
||||
routeType: "Warranty Route",
|
||||
technician: "David Martinez",
|
||||
date: "2025-10-26",
|
||||
status: "not started",
|
||||
totalStops: 6,
|
||||
completedStops: 0,
|
||||
estimatedDuration: "5.5 hours",
|
||||
startTime: "08:00",
|
||||
vehicleId: "SRV-103",
|
||||
totalMileage: 48.7,
|
||||
stops: [
|
||||
{
|
||||
stopId: 1,
|
||||
customer: "Rebecca Johnson - Warranty #W-445",
|
||||
address: "444 West St, Minnetonka, MN",
|
||||
serviceType: "Warranty: Valve Not Closing",
|
||||
estimatedTime: "08:30",
|
||||
duration: 60,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.9211, lng: -93.4687 }
|
||||
},
|
||||
{
|
||||
stopId: 2,
|
||||
customer: "Kevin Brown - Warranty #W-448",
|
||||
address: "666 Lake Rd, Wayzata, MN",
|
||||
serviceType: "Warranty: Controller Malfunction",
|
||||
estimatedTime: "10:00",
|
||||
duration: 75,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.9744, lng: -93.5066 }
|
||||
},
|
||||
{
|
||||
stopId: 3,
|
||||
customer: "Michelle Davis - Warranty #W-451",
|
||||
address: "888 Shore Dr, Orono, MN",
|
||||
serviceType: "Warranty: Sprinkler Head Broken",
|
||||
estimatedTime: "11:30",
|
||||
duration: 45,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0541, lng: -93.6091 }
|
||||
},
|
||||
{
|
||||
stopId: 4,
|
||||
customer: "Daniel Wilson - Warranty #W-453",
|
||||
address: "101 Bay Ln, Mound, MN",
|
||||
serviceType: "Warranty: Pipe Leak Repair",
|
||||
estimatedTime: "12:30",
|
||||
duration: 90,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.9364, lng: -93.6719 }
|
||||
},
|
||||
{
|
||||
stopId: 5,
|
||||
customer: "Laura Smith - Warranty #W-456",
|
||||
address: "202 Hill St, Chanhassen, MN",
|
||||
serviceType: "Warranty: Low Water Pressure",
|
||||
estimatedTime: "14:15",
|
||||
duration: 60,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.8619, lng: -93.5272 }
|
||||
},
|
||||
{
|
||||
stopId: 6,
|
||||
customer: "James Lee - Warranty #W-458",
|
||||
address: "303 Valley Rd, Chaska, MN",
|
||||
serviceType: "Warranty: Zone Not Working",
|
||||
estimatedTime: "15:30",
|
||||
duration: 75,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.7886, lng: -93.6022 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
routeId: "SOD-004",
|
||||
routeName: "East Metro Sod Route",
|
||||
routeType: "Sod Route",
|
||||
technician: "Chris Wilson",
|
||||
date: "2025-10-26",
|
||||
status: "in progress",
|
||||
totalStops: 4,
|
||||
completedStops: 1,
|
||||
estimatedDuration: "6 hours",
|
||||
startTime: "07:30",
|
||||
vehicleId: "SOD-104",
|
||||
totalMileage: 35.8,
|
||||
stops: [
|
||||
{
|
||||
stopId: 1,
|
||||
customer: "Thomas Anderson - Job #2301",
|
||||
address: "505 East St, Woodbury, MN",
|
||||
serviceType: "Install 2,500 sq ft Sod",
|
||||
estimatedTime: "08:00",
|
||||
duration: 120,
|
||||
status: "completed",
|
||||
coordinates: { lat: 44.9237, lng: -92.9594 }
|
||||
},
|
||||
{
|
||||
stopId: 2,
|
||||
customer: "Patricia Miller - Job #2305",
|
||||
address: "707 North Ave, Oakdale, MN",
|
||||
serviceType: "Install 1,800 sq ft Sod",
|
||||
estimatedTime: "10:30",
|
||||
duration: 90,
|
||||
status: "in progress",
|
||||
coordinates: { lat: 44.9633, lng: -92.9652 }
|
||||
},
|
||||
{
|
||||
stopId: 3,
|
||||
customer: "Michael Taylor - Job #2308",
|
||||
address: "909 South Blvd, Maplewood, MN",
|
||||
serviceType: "Install 3,200 sq ft Sod",
|
||||
estimatedTime: "12:15",
|
||||
duration: 150,
|
||||
status: "not started",
|
||||
coordinates: { lat: 44.9530, lng: -92.9952 }
|
||||
},
|
||||
{
|
||||
stopId: 4,
|
||||
customer: "Lisa Roberts - Job #2312",
|
||||
address: "121 Center St, North St. Paul, MN",
|
||||
serviceType: "Install 1,200 sq ft Sod",
|
||||
estimatedTime: "15:00",
|
||||
duration: 75,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0119, lng: -93.0074 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
routeId: "MACH-005",
|
||||
routeName: "Northwest Machine Route",
|
||||
routeType: "Machine Route",
|
||||
technician: "Lisa Anderson",
|
||||
date: "2025-10-27",
|
||||
status: "not started",
|
||||
totalStops: 3,
|
||||
completedStops: 0,
|
||||
estimatedDuration: "5 hours",
|
||||
startTime: "07:00",
|
||||
vehicleId: "MACH-105",
|
||||
totalMileage: 45.2,
|
||||
stops: [
|
||||
{
|
||||
stopId: 1,
|
||||
customer: "Ashley White - Job #2320",
|
||||
address: "141 Northwest Dr, Plymouth, MN",
|
||||
serviceType: "Deliver Trenching Machine",
|
||||
estimatedTime: "07:30",
|
||||
duration: 90,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0105, lng: -93.4555 }
|
||||
},
|
||||
{
|
||||
stopId: 2,
|
||||
customer: "Brian Johnson - Job #2325",
|
||||
address: "151 Maple St, Maple Grove, MN",
|
||||
serviceType: "Deliver Boring Machine",
|
||||
estimatedTime: "09:15",
|
||||
duration: 75,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0724, lng: -93.4557 }
|
||||
},
|
||||
{
|
||||
stopId: 3,
|
||||
customer: "Christina Davis - Job #2328",
|
||||
address: "161 Oak Ave, Osseo, MN",
|
||||
serviceType: "Deliver Compactor & Tools",
|
||||
estimatedTime: "10:45",
|
||||
duration: 60,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.1188, lng: -93.4025 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
routeId: "TRK-006",
|
||||
routeName: "Regional Truck Route",
|
||||
routeType: "Truck Route",
|
||||
technician: "Mark Thompson",
|
||||
date: "2025-10-27",
|
||||
status: "not started",
|
||||
totalStops: 4,
|
||||
completedStops: 0,
|
||||
estimatedDuration: "6 hours",
|
||||
startTime: "06:30",
|
||||
vehicleId: "TRUCK-106",
|
||||
totalMileage: 78.5,
|
||||
stops: [
|
||||
{
|
||||
stopId: 1,
|
||||
customer: "Metro Landscaping - Pickup #445",
|
||||
address: "2250 Highway 55, Plymouth, MN",
|
||||
serviceType: "Equipment Pickup",
|
||||
estimatedTime: "07:00",
|
||||
duration: 30,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0153, lng: -93.4233 }
|
||||
},
|
||||
{
|
||||
stopId: 2,
|
||||
customer: "Irrigation Supply Co - Order #889",
|
||||
address: "1875 Industrial Blvd, Maple Grove, MN",
|
||||
serviceType: "Material Pickup",
|
||||
estimatedTime: "08:00",
|
||||
duration: 45,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0845, lng: -93.4321 }
|
||||
},
|
||||
{
|
||||
stopId: 3,
|
||||
customer: "Johnson Properties - Delivery",
|
||||
address: "3456 Elm Street, Brooklyn Park, MN",
|
||||
serviceType: "Equipment Drop-off",
|
||||
estimatedTime: "09:15",
|
||||
duration: 60,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0941, lng: -93.3563 }
|
||||
},
|
||||
{
|
||||
stopId: 4,
|
||||
customer: "Storage Facility - Return",
|
||||
address: "5200 Bass Lake Rd, Crystal, MN",
|
||||
serviceType: "Equipment Return",
|
||||
estimatedTime: "11:00",
|
||||
duration: 45,
|
||||
status: "not started",
|
||||
coordinates: { lat: 45.0341, lng: -93.3678 }
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export default DataUtils;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user