build mock views

This commit is contained in:
Casey Wittrock 2025-10-24 14:05:10 -05:00
parent b0ed2c68f9
commit 44d47db0ad
10 changed files with 1963 additions and 9 deletions

View file

@ -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();

View file

@ -137,6 +137,7 @@ const handleCategoryClick = (category) => {
background-color: rgb(69, 112, 101);
color: white;
display: flex;
width: 100%;
align-items: center;
}

View file

@ -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>

View file

@ -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;

View file

@ -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>