bug fixes and calendar fixes

This commit is contained in:
Casey 2026-01-27 11:21:20 -06:00
parent 0ec89a1269
commit 6cd3d138ad
9 changed files with 446 additions and 270 deletions

View file

@ -17,15 +17,9 @@
<span class="date-text">{{ weekDisplayText }}</span> <span class="date-text">{{ weekDisplayText }}</span>
<v-icon right size="small">mdi-calendar</v-icon> <v-icon right size="small">mdi-calendar</v-icon>
</v-btn> </v-btn>
<v-btn
@click="nextWeek"
icon="mdi-chevron-right"
variant="outlined"
size="small"
></v-btn>
<v-btn @click="goToThisWeek" variant="outlined" size="small" class="ml-4"> <v-btn @click="goToThisWeek" variant="outlined" size="small" class="ml-4">
This Week This Week
</v-btn> </v-btn>
<v-menu <v-menu
v-model="showTemplateMenu" v-model="showTemplateMenu"
:close-on-content-click="false" :close-on-content-click="false"
@ -203,19 +197,21 @@
<!-- Jobs in this day --> <!-- Jobs in this day -->
<div <div
v-for="job in getJobsForCell(foreman.name, day.date)" v-for="job in getJobsForCell(foreman.name, day.date)"
:key="job.id" :key="job.name"
class="calendar-job" class="calendar-job"
:style="getJobStyle(job, day.date)" :style="getJobStyle(job, day.date)"
draggable="true" :draggable="job.status === 'Scheduled'"
@click.stop="showEventDetails({ event: job })" @click.stop="showEventDetails({ event: job })"
@dragstart="handleDragStart(job, $event)" @dragstart="job.status === 'Scheduled' ? handleDragStart(job, $event) : null"
@dragend="handleDragEnd" @dragend="handleDragEnd"
@mousedown="startResize($event, job, day.date)" @mousedown="(job.status === 'Scheduled' || job.status === 'Started') ? startResize($event, job, day.date) : null"
> @drop.stop.prevent="handleScheduledDrop(job, $event, foreman.name, day.date)"
@dragover.stop.prevent="handleScheduledDragOver(job, $event, foreman.name, day.date)"
>
<v-icon v-if="jobStartsBeforeWeek(job)" size="small" class="spans-arrow-left">mdi-arrow-left</v-icon> <v-icon v-if="jobStartsBeforeWeek(job)" size="small" class="spans-arrow-left">mdi-arrow-left</v-icon>
<div class="job-content"> <div class="job-content">
<div class="job-title">{{ job.projectTemplate || job.serviceType }}</div> <div class="job-title">{{ job.projectTemplate }}</div>
<div class="job-address">{{ stripAddress(job.address || job.jobAddress) }}</div> <div class="job-address">{{ job.serviceAddress.fullAddress }}</div>
</div> </div>
<v-icon v-if="jobSpansToNextWeek(job)" size="small" class="spans-arrow-right">mdi-arrow-right</v-icon> <v-icon v-if="jobSpansToNextWeek(job)" size="small" class="spans-arrow-right">mdi-arrow-right</v-icon>
<div class="resize-handle"></div> <div class="resize-handle"></div>
@ -225,7 +221,7 @@
<template v-if="isHoliday(day.date)"> <template v-if="isHoliday(day.date)">
<div <div
v-for="job in getJobsWithConnector(foreman.name, day.date)" v-for="job in getJobsWithConnector(foreman.name, day.date)"
:key="`connector-${job.id}`" :key="`connector-${job.name}`"
class="holiday-connector" class="holiday-connector"
:class="getPriorityClass(job.priority)" :class="getPriorityClass(job.priority)"
></div> ></div>
@ -241,10 +237,10 @@
<div class="unscheduled-header"> <div class="unscheduled-header">
<h4>Unscheduled Jobs</h4> <h4>Unscheduled Jobs</h4>
<v-chip <v-chip
:color="getUnscheduledCount() > 0 ? 'warning' : 'success'" :color="unscheduledServices.length > 0 ? 'warning' : 'success'"
size="small" size="small"
> >
{{ getUnscheduledCount() }} pending {{ unscheduledServices.length }} pending
</v-chip> </v-chip>
</div> </div>
@ -255,46 +251,47 @@
@dragleave="handleUnscheduledDragLeave" @dragleave="handleUnscheduledDragLeave"
@drop="handleUnscheduledDrop" @drop="handleUnscheduledDrop"
> >
<v-card <v-card
v-for="service in unscheduledServices" v-for="service in unscheduledServices"
:key="service.id" :key="service.name"
class="unscheduled-item mb-2" class="unscheduled-item mb-2"
:class="getPriorityClass(service.priority)" :class="getPriorityClass(service.project?.priority || service.priority)"
elevation="1" elevation="1"
hover hover
density="compact" density="compact"
draggable="true" draggable="true"
@dragstart="handleDragStart(service, $event)" @click.stop="showEventDetails({ event: service })"
@dragend="handleDragEnd" @dragstart="handleDragStart(service, $event)"
> @dragend="handleDragEnd"
<v-card-text class="pa-3"> >
<div class="service-title-compact mb-2">{{ service.projectTemplate || service.serviceType }}</div> <v-card-text class="pa-3">
<div class="service-address mb-1"> <div class="service-title-compact mb-2">
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon> {{ service.project?.projectName || service.projectTemplate || service.name }}
{{ stripAddress(service.address || service.jobAddress) }} </div>
</div> <div class="service-address mb-1">
<div class="service-customer mb-2"> <v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
<v-icon size="x-small" class="mr-1">mdi-account</v-icon> {{ service.serviceAddress?.fullAddress || service.serviceAddress?.addressTitle || service.serviceAddress?.name || service.project?.jobAddress }}
{{ service.customer }} </div>
</div> <div class="service-customer mb-2">
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
<div v-if="service.notes" class="service-notes-compact mt-2"> {{ service.customer?.customerName || service.customer?.name || service.project?.customer || service.customer || 'N/A' }}
<span class="text-caption">{{ service.notes }}</span> </div>
</div> <div v-if="service.project?.notes || service.notes" class="service-notes-compact mt-2">
<span class="text-caption">{{ service.project?.notes || service.notes }}</span>
<v-btn </div>
color="primary" <v-btn
size="x-small" color="primary"
variant="outlined" size="x-small"
class="mt-2" variant="outlined"
block class="mt-2"
@click="scheduleService(service)" block
> @click="scheduleService(service)"
<v-icon left size="x-small">mdi-calendar-plus</v-icon> >
Schedule <v-icon left size="x-small">mdi-calendar-plus</v-icon>
</v-btn> Schedule
</v-card-text> </v-btn>
</v-card> </v-card-text>
</v-card>
</div> </div>
<div v-if="unscheduledServices.length === 0" class="no-unscheduled"> <div v-if="unscheduledServices.length === 0" class="no-unscheduled">
@ -309,6 +306,9 @@
v-model="eventDialog" v-model="eventDialog"
:job="selectedEvent" :job="selectedEvent"
:foremen="foremen" :foremen="foremen"
:project="selectedEvent?.project"
:customer="selectedEvent?.customer"
:service-address="selectedEvent?.serviceAddress"
@close="eventDialog = false" @close="eventDialog = false"
/> />
@ -330,7 +330,8 @@ const notifications = useNotificationStore();
const companyStore = useCompanyStore(); const companyStore = useCompanyStore();
// Reactive data // Reactive data
const services = ref([]); const scheduledServices = ref([]);
const unscheduledServices = ref([]);
const weekStartDate = ref(getWeekStart(new Date())); const weekStartDate = ref(getWeekStart(new Date()));
const eventDialog = ref(false); const eventDialog = ref(false);
const selectedEvent = ref(null); const selectedEvent = ref(null);
@ -479,7 +480,7 @@ function hasHolidayInRange(startDate, endDate) {
// Helper function to check if job spans to next week // Helper function to check if job spans to next week
function jobSpansToNextWeek(job) { function jobSpansToNextWeek(job) {
const endDate = job.scheduledEndDate || job.scheduledDate; const endDate = job.expectedEndDate || job.expectedStartDate;
const end = parseLocalDate(endDate); const end = parseLocalDate(endDate);
const weekEnd = parseLocalDate(addDays(weekStartDate.value, 6)); const weekEnd = parseLocalDate(addDays(weekStartDate.value, 6));
return end > weekEnd; return end > weekEnd;
@ -487,18 +488,12 @@ function jobSpansToNextWeek(job) {
// Helper function to check if job starts before current week // Helper function to check if job starts before current week
function jobStartsBeforeWeek(job) { function jobStartsBeforeWeek(job) {
const startDate = job.expectedStartDate || job.scheduledDate; const startDate = job.expectedStartDate;
const start = parseLocalDate(startDate); const start = parseLocalDate(startDate);
const weekStart = parseLocalDate(weekStartDate.value); const weekStart = parseLocalDate(weekStartDate.value);
return start < weekStart; return start < weekStart;
} }
// Helper function to strip address after "-#-"
function stripAddress(address) {
if (!address) return '';
const index = address.indexOf('-#-');
return index > -1 ? address.substring(0, index).trim() : address;
}
// Helper function to get crew name from foreman ID // Helper function to get crew name from foreman ID
function getCrewName(foremanId) { function getCrewName(foremanId) {
@ -524,8 +519,8 @@ function getHolidaysInRange(startDate, endDate) {
// Helper function to calculate job segments (parts between holidays/Sundays) // Helper function to calculate job segments (parts between holidays/Sundays)
function getJobSegments(job) { function getJobSegments(job) {
const startDate = job.scheduledDate; const startDate = job.expectedStartDate;
const endDate = job.scheduledEndDate || job.scheduledDate; const endDate = job.expectedEndDate || job.expectedStartDate;
const weekEndDate = addDays(weekStartDate.value, 6); // Saturday const weekEndDate = addDays(weekStartDate.value, 6); // Saturday
const segments = []; const segments = [];
@ -598,16 +593,9 @@ const visibleForemen = computed(() => {
}); });
}); });
const scheduledServices = computed(() =>
services.value.filter((service) => service.isScheduled && service.foreman),
);
const unscheduledServices = computed(() =>
services.value.filter((service) => !service.isScheduled || !service.foreman),
);
// Methods // Methods
const getUnscheduledCount = () => unscheduledServices.value.length;
const getPriorityClass = (priority) => { const getPriorityClass = (priority) => {
switch (priority) { switch (priority) {
@ -655,8 +643,8 @@ const getJobsForCell = (foremanId, date) => {
return scheduledServices.value.filter((job) => { return scheduledServices.value.filter((job) => {
if (job.foreman !== foremanId) return false; if (job.foreman !== foremanId) return false;
const jobStart = job.scheduledDate; const jobStart = job.expectedStartDate;
const jobEnd = job.scheduledEndDate || job.scheduledDate; const jobEnd = job.expectedEndDate || job.expectedStartDate;
// Check if this date falls within the job's date range // Check if this date falls within the job's date range
// AND that it's a valid segment start date // AND that it's a valid segment start date
@ -672,8 +660,8 @@ const getJobsWithConnector = (foremanId, date) => {
return scheduledServices.value.filter((job) => { return scheduledServices.value.filter((job) => {
if (job.foreman !== foremanId) return false; if (job.foreman !== foremanId) return false;
const jobStart = job.scheduledDate; const jobStart = job.expectedStartDate;
const jobEnd = job.scheduledEndDate || job.scheduledDate; const jobEnd = job.expectedEndDate || job.expectedStartDate;
// Check if this holiday date falls within the job's range // Check if this holiday date falls within the job's range
return date > jobStart && date < jobEnd; return date > jobStart && date < jobEnd;
@ -682,8 +670,8 @@ const getJobsWithConnector = (foremanId, date) => {
// Get job style for width spanning multiple days // Get job style for width spanning multiple days
const getJobStyle = (job, currentDate) => { const getJobStyle = (job, currentDate) => {
const jobStart = job.scheduledDate; const jobStart = job.expectedStartDate;
const jobEnd = job.scheduledEndDate || job.scheduledDate; const jobEnd = job.expectedEndDate || job.expectedStartDate;
const segments = getJobSegments(job); const segments = getJobSegments(job);
// Find which segment (if any) should be rendered at currentDate // Find which segment (if any) should be rendered at currentDate
@ -842,7 +830,7 @@ const scheduleService = (service) => {
const handleDragStart = (service, event) => { const handleDragStart = (service, event) => {
draggedService.value = service; draggedService.value = service;
event.dataTransfer.effectAllowed = "move"; event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", service.id); event.dataTransfer.setData("text/plain", service.name);
// Get the dimensions of the dragged element // Get the dimensions of the dragged element
const dragElement = event.target; const dragElement = event.target;
@ -860,7 +848,7 @@ const handleDragStart = (service, event) => {
// Add visual feedback // Add visual feedback
event.target.style.opacity = '0.5'; event.target.style.opacity = '0.5';
console.log("Drag started for service:", service.serviceType); console.log("Drag started for service:", service.projectTemplate);
}; };
const handleDragEnd = (event) => { const handleDragEnd = (event) => {
@ -962,43 +950,61 @@ const handleDrop = async (event, foremanId, date) => {
return; return;
} }
// Update the service with scheduling information // Check if this is scheduling an unscheduled job or moving a scheduled job
const serviceIndex = services.value.findIndex(s => s.id === draggedService.value.id); const unscheduledIndex = unscheduledServices.value.findIndex(s => s.name === draggedService.value.name);
if (serviceIndex !== -1) { const scheduledIndex = scheduledServices.value.findIndex(s => s.name === draggedService.value.name);
const wasScheduled = services.value[serviceIndex].isScheduled;
if (unscheduledIndex !== -1) {
// Scheduling an unscheduled job
const action = 'Scheduled';
console.log(`${action} ${draggedService.value.projectTemplate} for ${foreman.employeeName} on ${date} to ${endDate}`);
const action = wasScheduled ? 'Moved' : 'Scheduled'; // Call API to persist changes
console.log(`${action} ${draggedService.value.serviceType} for ${foreman.employeeName} on ${date} to ${endDate}`);
// Call API to persist changes (placeholder for now)
try { try {
await Api.updateJobScheduledDates( await Api.updateServiceAppointmentScheduledDates(
draggedService.value.id, draggedService.value.name,
date, date,
endDate, endDate,
foreman.name foreman.name
) );
services.value[serviceIndex] = { // Remove from unscheduled and add to scheduled
...services.value[serviceIndex], const scheduledService = {
isScheduled: true, ...unscheduledServices.value[unscheduledIndex],
scheduledDate: date, expectedStartDate: date,
scheduledEndDate: endDate, expectedEndDate: endDate,
foreman: foreman.name foreman: foreman.name,
status: 'Scheduled'
}; };
unscheduledServices.value.splice(unscheduledIndex, 1);
scheduledServices.value.push(scheduledService);
notifications.addSuccess("Job scheduled successfully!"); notifications.addSuccess("Job scheduled successfully!");
// Future implementation:
// await Api.updateJobSchedule({
// id: draggedService.value.id,
// expectedStartDate: date,
// expectedEndDate: endDate,
// isScheduled: true,
// customForeman: foreman.name
// });
} catch (error) { } catch (error) {
console.error("Error scheduling job:", error); console.error("Error scheduling job:", error);
notifications.addError("Failed to schedule job"); notifications.addError("Failed to schedule job");
} }
} else if (scheduledIndex !== -1 && draggedService.value.status === 'Scheduled') {
// Moving a scheduled job to a new date/foreman
console.log(`Moving ${draggedService.value.projectTemplate} to ${foreman.employeeName} on ${date}`);
// Call API to persist changes
try {
await Api.updateServiceAppointmentScheduledDates(
draggedService.value.name,
date,
draggedService.value.expectedEndDate, // Keep the same end date
foreman.name
);
// Update the scheduled job
scheduledServices.value[scheduledIndex] = {
...scheduledServices.value[scheduledIndex],
expectedStartDate: date,
foreman: foreman.name
};
notifications.addSuccess("Job moved successfully!");
} catch (error) {
console.error("Error moving job:", error);
notifications.addError("Failed to move job");
}
} }
// Reset drag state // Reset drag state
@ -1009,7 +1015,8 @@ const handleDrop = async (event, foremanId, date) => {
// Handle dropping scheduled items back to unscheduled // Handle dropping scheduled items back to unscheduled
const handleUnscheduledDragOver = (event) => { const handleUnscheduledDragOver = (event) => {
if (draggedService.value?.isScheduled) { // Only allow dropping if the dragged job is scheduled
if (draggedService.value && draggedService.value.status === 'Scheduled') {
event.preventDefault(); event.preventDefault();
event.dataTransfer.dropEffect = "move"; event.dataTransfer.dropEffect = "move";
} }
@ -1022,38 +1029,39 @@ const handleUnscheduledDragLeave = (event) => {
const handleUnscheduledDrop = async (event) => { const handleUnscheduledDrop = async (event) => {
event.preventDefault(); event.preventDefault();
if (!draggedService.value || !draggedService.value.isScheduled) return; if (!draggedService.value) return;
// Only allow unscheduling if status is 'Scheduled'
if (draggedService.value.status !== 'Scheduled') {
notifications.addError('Only jobs with status "Scheduled" can be unscheduled.');
isDragOver.value = false;
dragOverCell.value = null;
draggedService.value = null;
return;
}
// Update the service to unscheduled status // Update the service to unscheduled status
const serviceIndex = services.value.findIndex(s => s.id === draggedService.value.id); const serviceIndex = scheduledServices.value.findIndex(s => s.name === draggedService.value.name);
if (serviceIndex !== -1) { if (serviceIndex !== -1) {
services.value[serviceIndex] = { // Remove from scheduled and add to unscheduled
...services.value[serviceIndex], const unscheduledService = {
isScheduled: false, ...scheduledServices.value[serviceIndex],
scheduledDate: null, expectedStartDate: null,
scheduledEndDate: null, expectedEndDate: null,
foreman: null foreman: null
}; };
scheduledServices.value.splice(serviceIndex, 1);
unscheduledServices.value.push(unscheduledService);
console.log(`Unscheduled ${draggedService.value.serviceType}`); console.log(`Unscheduled ${draggedService.value.projectTemplate}`);
// Call API to persist changes (placeholder for now) // Call API to persist changes
try { try {
await Api.updateJobScheduledDates( await Api.updateServiceAppointmentScheduledDates(
draggedService.value.id, draggedService.value.name,
null, null,
null, null,
null null
) )
notifications.addSuccess("Job unscheduled successfully!"); notifications.addSuccess("Job unscheduled successfully!");
// Future implementation:
// await Api.updateJobSchedule({
// id: draggedService.value.id,
// expectedStartDate: null,
// expectedEndDate: null,
// isScheduled: false,
// customForeman: null
// });
} catch (error) { } catch (error) {
console.error("Error unscheduling job:", error); console.error("Error unscheduling job:", error);
notifications.addError("Failed to unschedule job"); notifications.addError("Failed to unschedule job");
@ -1066,9 +1074,56 @@ const handleUnscheduledDrop = async (event) => {
draggedService.value = null; draggedService.value = null;
}; };
// Allow moving scheduled jobs between days/crews if status is 'Scheduled'
const handleScheduledDragOver = (job, event, foremanId, date) => {
if (!draggedService.value) return;
// Only allow if dragging a scheduled job and target is a valid cell
if (draggedService.value.status === 'Scheduled' && job.status === 'Scheduled') {
event.dataTransfer.dropEffect = 'move';
}
};
const handleScheduledDrop = async (job, event, foremanId, date) => {
if (!draggedService.value) return;
// Only allow if dragging a scheduled job and target is a valid cell
if (draggedService.value.status !== 'Scheduled') return;
// Prevent dropping on same cell
if (draggedService.value.name === job.name && draggedService.value.foreman === foremanId && draggedService.value.expectedStartDate === date) return;
// Prevent dropping on Sunday or holidays
if (isSunday(date) || isHoliday(date)) return;
// Update job's foreman and date
const serviceIndex = scheduledServices.value.findIndex(s => s.name === draggedService.value.name);
if (serviceIndex !== -1) {
try {
await Api.updateServiceAppointmentScheduledDates(
draggedService.value.name,
date,
draggedService.value.expectedEndDate,
foremanId
);
scheduledServices.value[serviceIndex] = {
...scheduledServices.value[serviceIndex],
expectedStartDate: date,
foreman: foremanId
};
notifications.addSuccess('Job moved successfully!');
} catch (error) {
notifications.addError('Failed to move job');
}
}
isDragOver.value = false;
dragOverCell.value = null;
draggedService.value = null;
};
// Resize functionality // Resize functionality
const startResize = (event, job, date) => { const startResize = (event, job, date) => {
// Only allow resize from the right edge // Only allow resize from the right edge
// Only allow if status is 'Scheduled' or 'Started'
if (!(job.status === 'Scheduled' || job.status === 'Started')) return;
const target = event.target; const target = event.target;
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const clickX = event.clientX; const clickX = event.clientX;
@ -1088,7 +1143,7 @@ const startResize = (event, job, date) => {
resizingJob.value = job; resizingJob.value = job;
resizeStartX.value = event.clientX; resizeStartX.value = event.clientX;
resizeStartDate.value = date; resizeStartDate.value = date;
originalEndDate.value = job.scheduledEndDate || job.scheduledDate; originalEndDate.value = job.expectedEndDate || job.expectedStartDate;
// Add global mouse move and mouse up listeners // Add global mouse move and mouse up listeners
document.addEventListener('mousemove', handleResize); document.addEventListener('mousemove', handleResize);
@ -1163,11 +1218,11 @@ const handleResize = (event) => {
// Show popup if extending over Sunday // Show popup if extending over Sunday
showExtendToNextWeekPopup.value = extendsOverSunday; showExtendToNextWeekPopup.value = extendsOverSunday;
const serviceIndex = services.value.findIndex(s => s.id === resizingJob.value.id); const serviceIndex = scheduledServices.value.findIndex(s => s.name === resizingJob.value.name);
if (serviceIndex !== -1) { if (serviceIndex !== -1) {
services.value[serviceIndex] = { scheduledServices.value[serviceIndex] = {
...services.value[serviceIndex], ...scheduledServices.value[serviceIndex],
scheduledEndDate: newEndDate expectedEndDate: newEndDate
}; };
} }
}; };
@ -1181,12 +1236,12 @@ const stopResize = async () => {
const job = resizingJob.value; const job = resizingJob.value;
// Find the updated job in services array (because resizingJob.value is a stale reference) // Find the updated job in services array (because resizingJob.value is a stale reference)
const serviceIndex = services.value.findIndex(s => s.id === job.id); const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
if (serviceIndex === -1) return; if (serviceIndex === -1) return;
const updatedJob = services.value[serviceIndex]; const updatedJob = scheduledServices.value[serviceIndex];
const newEndDate = updatedJob.scheduledEndDate || updatedJob.scheduledDate; const newEndDate = updatedJob.expectedEndDate || updatedJob.expectedStartDate;
console.log(`Proposed new end date for job ${job.serviceType}: ${newEndDate}`); console.log(`Proposed new end date for job ${job.projectTemplate}: ${newEndDate}`);
// Only update if end date changed // Only update if end date changed
if (newEndDate !== originalEndDate.value) { if (newEndDate !== originalEndDate.value) {
@ -1197,13 +1252,13 @@ const stopResize = async () => {
notifications.addInfo(`Job extended to next Monday (${formatDate(newEndDate)})`); notifications.addInfo(`Job extended to next Monday (${formatDate(newEndDate)})`);
} }
console.log(`Extended job ${job.serviceType} from ${originalEndDate.value} to ${newEndDate}`); console.log(`Extended job ${job.projectTemplate} from ${originalEndDate.value} to ${newEndDate}`);
// Call API to persist changes // Call API to persist changes
try { try {
await Api.updateJobScheduledDates( await Api.updateServiceAppointmentScheduledDates(
job.id, job.name,
job.scheduledDate, job.expectedStartDate,
newEndDate, newEndDate,
job.foreman job.foreman
); );
@ -1212,11 +1267,11 @@ const stopResize = async () => {
console.error("Error updating job end date:", error); console.error("Error updating job end date:", error);
notifications.addError("Failed to update job end date"); notifications.addError("Failed to update job end date");
// Revert on error // Revert on error
const serviceIndex = services.value.findIndex(s => s.id === job.id); const serviceIndex = scheduledServices.value.findIndex(s => s.name === job.name);
if (serviceIndex !== -1) { if (serviceIndex !== -1) {
services.value[serviceIndex] = { scheduledServices.value[serviceIndex] = {
...services.value[serviceIndex], ...scheduledServices.value[serviceIndex],
scheduledEndDate: originalEndDate.value expectedEndDate: originalEndDate.value
}; };
} }
} }
@ -1246,69 +1301,26 @@ const fetchServiceAppointments = async (currentDate) => {
const endDate = addDays(startDate, 6); const endDate = addDays(startDate, 6);
// const data = await Api.getJobsForCalendar(startDate, endDate, companyStore.currentCompany, selectedProjectTemplates.value); // const data = await Api.getJobsForCalendar(startDate, endDate, companyStore.currentCompany, selectedProjectTemplates.value);
const data = await Api.getServiceAppointments( scheduledServices.value = await Api.getServiceAppointments(
[companyStore.currentCompany], [companyStore.currentCompany],
{ {
"expectedStartDate": ["<=", endDate], "expectedStartDate": ["<=", endDate],
"expectedEndDate": [">=", startDate] "expectedEndDate": [">=", startDate],
"status": ["not in", ["Canceled"]]
} }
); );
// Transform the API response into the format the component expects unscheduledServices.value = await Api.getServiceAppointments(
const transformedServices = []; [companyStore.currentCompany],
{
"status": "Open"
}
);
// Process scheduled projects console.log("Loaded service appointments:", scheduledServices.value);
if (data.projects && Array.isArray(data.projects)) { console.log("Loaded unscheduled service appointments:", unscheduledServices.value);
data.projects.forEach(project => {
transformedServices.push({
...project, // Include all fields from API
id: project.name,
title: project.projectName || project.jobAddress || 'Unnamed Project',
serviceType: project.projectName || project.jobAddress || 'Install Project',
customer: project.customer || 'Unknown Customer',
address: project.jobAddress || project.customInstallationAddress || '',
scheduledDate: project.expectedStartDate || startDate,
scheduledEndDate: project.expectedEndDate || project.expectedStartDate || startDate,
foreman: project.customForeman,
priority: (project.priority || 'Medium').toLowerCase(),
estimatedCost: project.totalSalesAmount || project.estimatedCosting || 0,
notes: project.notes || '',
isScheduled: true,
projectTemplate: project.projectTemplate,
calendarColor: project.calendarColor,
jobAddress: project.jobAddress
});
});
}
// Process unscheduled projects
if (data.unscheduledProjects && Array.isArray(data.unscheduledProjects)) {
data.unscheduledProjects.forEach(project => {
transformedServices.push({
...project, // Include all fields from API
id: project.name,
title: project.projectName || project.jobAddress || 'Unnamed Project',
serviceType: project.projectName || project.jobAddress || 'Install Project',
customer: project.customer || 'Unknown Customer',
address: project.jobAddress || project.customInstallationAddress || '',
scheduledDate: null,
scheduledEndDate: null,
foreman: null,
priority: (project.priority || 'Medium').toLowerCase(),
estimatedCost: project.totalSalesAmount || project.estimatedCosting || 0,
notes: project.notes || '',
isScheduled: false,
projectTemplate: project.projectTemplate,
calendarColor: project.calendarColor,
jobAddress: project.jobAddress
});
});
}
services.value = transformedServices;
console.log("Loaded install projects:", transformedServices);
} catch (error) { } catch (error) {
console.error("Error loading install projects:", error); console.error("Error loading service appointments:", error);
notifications.addError("Failed to load install projects"); notifications.addError("Failed to load service appointments");
} }
}; };

View file

@ -59,6 +59,15 @@
style="margin-top: 0" style="margin-top: 0"
/> />
<label :for="`isBilling-${index}`"><i class="pi pi-dollar" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Is Billing Address</label> <label :for="`isBilling-${index}`"><i class="pi pi-dollar" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Is Billing Address</label>
<input
type="checkbox"
:id="`isService-${index}`"
v-model="address.isServiceAddress"
:disabled="isSubmitting"
@change="handleServiceChange(index)"
style="margin-top: 0; margin-left: 1.5rem;"
/>
<label :for="`isService-${index}`"><i class="pi pi-truck" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Is Service Address</label>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-field"> <div class="form-field">
@ -120,7 +129,7 @@
<Select <Select
:id="`primaryContact-${index}`" :id="`primaryContact-${index}`"
v-model="address.primaryContact" v-model="address.primaryContact"
:options="contactOptions" :options="address.contacts.map(c => contactOptions.find(opt => opt.value === c))"
optionLabel="label" optionLabel="label"
optionValue="value" optionValue="value"
:disabled="isSubmitting || contactOptions.length === 0" :disabled="isSubmitting || contactOptions.length === 0"
@ -174,6 +183,7 @@ const localFormData = computed({
addressLine1: "", addressLine1: "",
addressLine2: "", addressLine2: "",
isBillingAddress: true, isBillingAddress: true,
isServiceAddress: true,
pincode: "", pincode: "",
city: "", city: "",
state: "", state: "",
@ -205,6 +215,7 @@ onMounted(() => {
addressLine1: "", addressLine1: "",
addressLine2: "", addressLine2: "",
isBillingAddress: true, isBillingAddress: true,
isServiceAddress: true,
pincode: "", pincode: "",
city: "", city: "",
state: "", state: "",
@ -221,6 +232,7 @@ const addAddress = () => {
addressLine1: "", addressLine1: "",
addressLine2: "", addressLine2: "",
isBillingAddress: false, isBillingAddress: false,
isServiceAddress: true,
pincode: "", pincode: "",
city: "", city: "",
state: "", state: "",
@ -259,6 +271,7 @@ const handleBillingChange = (selectedIndex) => {
localFormData.value.addresses.forEach((addr, idx) => { localFormData.value.addresses.forEach((addr, idx) => {
if (idx !== selectedIndex) { if (idx !== selectedIndex) {
addr.isBillingAddress = false; addr.isBillingAddress = false;
addr.isServiceAddress = true; // Ensure service address is true for others
} }
}); });
@ -279,6 +292,21 @@ const handleBillingChange = (selectedIndex) => {
localFormData.value.addresses[selectedIndex].primaryContact = 0; localFormData.value.addresses[selectedIndex].primaryContact = 0;
} }
} }
} else {
localFormData.value.addresses[selectedIndex].isBillingAddress = true;
notificationStore.addInfo("At least one of Billing Address must be selected.");
}
};
const handleServiceChange = (selectedIndex) => {
// If the address does not have billing address selected, ensure that service address is always true
if (!localFormData.value.addresses[selectedIndex].isBillingAddress) {
localFormData.value.addresses[selectedIndex].isServiceAddress = true;
notificationStore.addInfo("Service Address must be selected if not a Billing Address.");
}
if (!localFormData.value.addresses.some(addr => addr.isServiceAddress)) {
localFormData.value.addresses[selectedIndex].isServiceAddress = true;
notificationStore.addInfo("At least one Service Address must be selected.");
} }
}; };

View file

@ -11,18 +11,18 @@
/> />
</div> </div>
<Dropdown <Select
v-model="selectedAddressIndex" v-model="selectedAddressIndex"
:options="addressOptions" :options="addressOptions"
option-label="label" optionLabel="label"
option-value="value" optionValue="value"
placeholder="Select an address" placeholder="Select an address"
class="w-full address-dropdown" class="w-full address-dropdown"
@change="handleAddressChange" @change="handleAddressChange"
> >
<template #value="slotProps"> <template #value="slotProps">
<div v-if="slotProps.value !== null && slotProps.value !== undefined" class="dropdown-value"> <div v-if="slotProps.value !== null && slotProps.value !== undefined" class="dropdown-value">
<span class="address-title">{{ addresses[slotProps.value]?.addressTitle || 'Unnamed Address' }}</span> <span class="address-title">{{ addresses[slotProps.value]?.fullAddress || 'Unnamed Address' }}</span>
<div class="address-badges"> <div class="address-badges">
<Badge <Badge
v-if="addresses[slotProps.value]?.isPrimaryAddress && !addresses[slotProps.value]?.isServiceAddress" v-if="addresses[slotProps.value]?.isPrimaryAddress && !addresses[slotProps.value]?.isServiceAddress"
@ -78,7 +78,7 @@
</div> </div>
</div> </div>
</template> </template>
</Dropdown> </Select>
<!-- Selected Address Info --> <!-- Selected Address Info -->
<div v-if="selectedAddress" class="selected-address-info"> <div v-if="selectedAddress" class="selected-address-info">
@ -146,13 +146,17 @@
</template> </template>
<script setup> <script setup>
import { computed, ref, watch } from "vue"; import { computed, ref, watch, nextTick } from "vue";
import { useRoute } from "vue-router";
import Badge from "primevue/badge"; import Badge from "primevue/badge";
import Button from "primevue/button"; import Button from "primevue/button";
import Dialog from "primevue/dialog"; import Dialog from "primevue/dialog";
import Dropdown from "primevue/dropdown"; import Select from "primevue/select";
import DataUtils from "../../utils"; import DataUtils from "../../utils";
const route = useRoute();
const addressParam = route.query.address || null;
const props = defineProps({ const props = defineProps({
addresses: { addresses: {
type: Array, type: Array,
@ -168,10 +172,27 @@ const props = defineProps({
}, },
}); });
const findAddressIndexByParam = (addressStr) => {
const trimmedParam = addressStr.trim();
for (let i = 0; i < props.addresses.length; i++) {
const addr = props.addresses[i];
const fullAddr = (addr.fullAddress || DataUtils.calculateFullAddress(addr)).trim();
if (fullAddr === trimmedParam) {
return i;
}
}
return null;
};
const emit = defineEmits(["update:selectedAddressIdx"]); const emit = defineEmits(["update:selectedAddressIdx"]);
const showAddAddressModal = ref(false); const showAddAddressModal = ref(false);
const selectedAddressIndex = ref(props.selectedAddressIdx); const selectedAddressIndex = ref(addressParam ? findAddressIndexByParam(addressParam) : props.selectedAddressIdx);
// Emit update if the initial index is different from props
if (addressParam && selectedAddressIndex.value !== null && selectedAddressIndex.value !== props.selectedAddressIdx) {
nextTick(() => emit("update:selectedAddressIdx", selectedAddressIndex.value));
}
// Watch for external changes to selectedAddressIdx // Watch for external changes to selectedAddressIdx
watch(() => props.selectedAddressIdx, (newVal) => { watch(() => props.selectedAddressIdx, (newVal) => {
@ -189,9 +210,9 @@ const selectedAddress = computed(() => {
// Address options for dropdown // Address options for dropdown
const addressOptions = computed(() => { const addressOptions = computed(() => {
return props.addresses.map((addr, idx) => ({ return props.addresses.map((addr, idx) => ({
label: addr.addressTitle || DataUtils.calculateFullAddress(addr), label: addr.fullAddress || DataUtils.calculateFullAddress(addr),
value: idx, value: idx,
addressTitle: addr.addressTitle || 'Unnamed Address', addressTitle: addr.fullAddress || 'Unnamed Address',
isPrimaryAddress: addr.isPrimaryAddress, isPrimaryAddress: addr.isPrimaryAddress,
isServiceAddress: addr.isServiceAddress, isServiceAddress: addr.isServiceAddress,
projectCount: addr.projects?.length || 0, projectCount: addr.projects?.length || 0,
@ -277,11 +298,12 @@ const handleAddressChange = () => {
.selected-address-info { .selected-address-info {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 0.75rem;
background: var(--surface-ground); background: var(--surface-ground);
border-radius: 8px; border-radius: 8px;
align-items: center;
} }
.address-status { .address-status {
@ -290,9 +312,10 @@ const handleAddressChange = () => {
} }
.service-details { .service-details {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); flex-direction: row;
gap: 1rem; gap: 1rem;
flex-wrap: wrap;
} }
.detail-item { .detail-item {
@ -316,7 +339,7 @@ const handleAddressChange = () => {
} }
.detail-item.primary-contact { .detail-item.primary-contact {
grid-column: 1 / -1; flex: 1;
} }
.contact-info { .contact-info {

View file

@ -7,7 +7,7 @@
:class="getStatusClass(onsiteMeetingStatus)" :class="getStatusClass(onsiteMeetingStatus)"
@click="handleBidMeetingClick" @click="handleBidMeetingClick"
> >
<span class="status-label">Meeting</span> <span class="status-label">Bid Meeting</span>
<span class="status-badge">{{ onsiteMeetingStatus }}</span> <span class="status-badge">{{ onsiteMeetingStatus }}</span>
</div> </div>
@ -16,7 +16,7 @@
:class="getStatusClass(estimateSentStatus)" :class="getStatusClass(estimateSentStatus)"
@click="handleEstimateClick" @click="handleEstimateClick"
> >
<span class="status-label">Estimate</span> <span class="status-label">Estimate Sent</span>
<span class="status-badge">{{ estimateSentStatus }}</span> <span class="status-badge">{{ estimateSentStatus }}</span>
</div> </div>
@ -109,8 +109,10 @@ const handleBidMeetingClick = () => {
}; };
const handleEstimateClick = () => { const handleEstimateClick = () => {
if (props.estimateSentStatus === "Not Started") { if ((props.estimateSentStatus === "Not Started") && props.onsiteMeetingStatus != "Completed") {
router.push(`/estimate?new=true&address=${encodeURIComponent(props.fullAddress)}&template=SNW%20Install`); notificationStore.addWarning("Bid Meeting must be scheduled and completed before an Estimate can be made for a SNW Install")
} else if (props.estimateSentStatus === "Not Started") {
router.push(`/estimate?new=true&address=${encodeURIComponent(props.fullAddress)}&project-template=SNW%20Install&from-meeting=${encodeURIComponent(props.bidMeeting)}`);
} else { } else {
router.push(`/estimate?name=${encodeURIComponent(props.estimate)}`); router.push(`/estimate?name=${encodeURIComponent(props.estimate)}`);
} }

View file

@ -327,14 +327,14 @@ const emitChanges = () => {
.property-details { .property-details {
background: var(--surface-card); background: var(--surface-card);
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 1rem;
border: 1px solid var(--surface-border); border: 1px solid var(--surface-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.property-details > h3 { .property-details > h3 {
margin: 0 0 1.5rem 0; margin: 0 0 1rem 0;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
color: var(--text-color); color: var(--text-color);
@ -343,13 +343,13 @@ const emitChanges = () => {
.details-grid { .details-grid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1rem;
} }
.detail-section { .detail-section {
background: var(--surface-ground); background: var(--surface-ground);
border-radius: 8px; border-radius: 8px;
padding: 1.25rem; padding: 1rem;
} }
.detail-section.full-width { .detail-section.full-width {
@ -360,7 +360,7 @@ const emitChanges = () => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
margin-bottom: 1rem; margin-bottom: 0.75rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
border-bottom: 2px solid var(--surface-border); border-bottom: 2px solid var(--surface-border);
} }

View file

@ -3,11 +3,11 @@
<v-card v-if="job"> <v-card v-if="job">
<v-card-title class="d-flex justify-space-between align-center bg-primary"> <v-card-title class="d-flex justify-space-between align-center bg-primary">
<div> <div>
<div class="text-h6">{{ job.projectTemplate || job.serviceType }}</div> <div class="text-h6">{{ job.project?.projectName || job.projectTemplate || job.serviceType || job.name }}</div>
<div class="text-caption">{{ job.name }}</div> <div class="text-caption">{{ job.name }}</div>
</div> </div>
<v-chip :color="getPriorityColor(job.priority)" size="small"> <v-chip :color="getPriorityColor(job.project?.priority || job.priority)" size="small">
{{ job.priority }} {{ job.project?.priority || job.priority || 'Normal' }}
</v-chip> </v-chip>
</v-card-title> </v-card-title>
@ -19,11 +19,15 @@
<h4 class="text-subtitle-1 mb-2">Basic Information</h4> <h4 class="text-subtitle-1 mb-2">Basic Information</h4>
<div class="detail-row"> <div class="detail-row">
<v-icon size="small" class="mr-2">mdi-account</v-icon> <v-icon size="small" class="mr-2">mdi-account</v-icon>
<strong>Customer:</strong> {{ job.customer }} <strong>Customer:</strong> {{ job.customer?.customerName || job.customer?.name || 'N/A' }}
</div> </div>
<div class="detail-row"> <div class="detail-row">
<v-icon size="small" class="mr-2">mdi-map-marker</v-icon> <v-icon size="small" class="mr-2">mdi-map-marker</v-icon>
<strong>Address:</strong> {{ stripAddress(job.address || job.jobAddress) }} <strong>Service Address:</strong> {{ job.serviceAddress?.fullAddress || job.serviceAddress?.addressTitle || 'N/A' }}
</div>
<div class="detail-row">
<v-icon size="small" class="mr-2">mdi-office-building</v-icon>
<strong>Project:</strong> {{ job.project?.projectName || job.projectTemplate || 'N/A' }}
</div> </div>
<div class="detail-row"> <div class="detail-row">
<v-icon size="small" class="mr-2">mdi-clipboard-text</v-icon> <v-icon size="small" class="mr-2">mdi-clipboard-text</v-icon>
@ -33,22 +37,34 @@
<v-icon size="small" class="mr-2">mdi-file-document</v-icon> <v-icon size="small" class="mr-2">mdi-file-document</v-icon>
<strong>Sales Order:</strong> {{ job.salesOrder || 'N/A' }} <strong>Sales Order:</strong> {{ job.salesOrder || 'N/A' }}
</div> </div>
<div class="detail-row">
<v-icon size="small" class="mr-2">mdi-wrench</v-icon>
<strong>Service Type:</strong> {{ job.serviceType || 'N/A' }}
</div>
</div> </div>
<div class="detail-section mb-4"> <div class="detail-section mb-4">
<h4 class="text-subtitle-1 mb-2">Schedule</h4> <h4 class="text-subtitle-1 mb-2">Schedule</h4>
<div class="detail-row"> <div class="detail-row">
<v-icon size="small" class="mr-2">mdi-calendar-start</v-icon> <v-icon size="small" class="mr-2">mdi-calendar-start</v-icon>
<strong>Start Date:</strong> {{ job.expectedStartDate || job.scheduledDate }} <strong>Start Date:</strong> {{ formatDate(job.expectedStartDate || job.scheduledDate) }}
</div> </div>
<div class="detail-row"> <div class="detail-row">
<v-icon size="small" class="mr-2">mdi-calendar-end</v-icon> <v-icon size="small" class="mr-2">mdi-calendar-end</v-icon>
<strong>End Date:</strong> {{ job.expectedEndDate || job.scheduledEndDate }} <strong>End Date:</strong> {{ formatDate(job.expectedEndDate || job.scheduledEndDate) }}
</div> </div>
<div class="detail-row"> <div class="detail-row">
<v-icon size="small" class="mr-2">mdi-account-hard-hat</v-icon> <v-icon size="small" class="mr-2">mdi-account-hard-hat</v-icon>
<strong>Assigned Crew:</strong> {{ getCrewName(job.foreman || job.customForeman) }} <strong>Assigned Crew:</strong> {{ getCrewName(job.foreman || job.customForeman) }}
</div> </div>
<div class="detail-row" v-if="job.expectedStartTime">
<v-icon size="small" class="mr-2">mdi-clock-start</v-icon>
<strong>Start Time:</strong> {{ job.expectedStartTime }}
</div>
<div class="detail-row" v-if="job.expectedEndTime">
<v-icon size="small" class="mr-2">mdi-clock-end</v-icon>
<strong>End Time:</strong> {{ job.expectedEndTime }}
</div>
</div> </div>
<div class="detail-section mb-4"> <div class="detail-section mb-4">
@ -71,6 +87,10 @@
<v-icon size="small" class="mr-2">mdi-shield-check</v-icon> <v-icon size="small" class="mr-2">mdi-shield-check</v-icon>
<strong>Warranty:</strong> {{ job.customWarrantyDurationDays }} days <strong>Warranty:</strong> {{ job.customWarrantyDurationDays }} days
</div> </div>
<div class="detail-row" v-if="job.customJobType">
<v-icon size="small" class="mr-2">mdi-tools</v-icon>
<strong>Job Type:</strong> {{ job.customJobType }}
</div>
</div> </div>
</v-col> </v-col>
@ -128,7 +148,7 @@
<!-- Map Display --> <!-- Map Display -->
<div v-if="hasCoordinates" class="detail-section mb-4"> <div v-if="hasCoordinates" class="detail-section mb-4">
<h4 class="text-subtitle-1 mb-2">Location</h4> <h4 class="text-subtitle-1 mb-2">Service Location</h4>
<div class="map-container"> <div class="map-container">
<iframe <iframe
:src="mapUrl" :src="mapUrl"
@ -136,13 +156,21 @@
height="200" height="200"
frameborder="0" frameborder="0"
style="border: 1px solid var(--surface-border); border-radius: 6px;" style="border: 1px solid var(--surface-border); border-radius: 6px;"
allowfullscreen
></iframe> ></iframe>
</div> </div>
</div> </div>
<div v-if="job.notes" class="detail-section"> <div v-if="job.notes || job.project?.notes" class="detail-section">
<h4 class="text-subtitle-1 mb-2">Notes</h4> <h4 class="text-subtitle-1 mb-2">Notes</h4>
<p class="text-body-2">{{ job.notes }}</p> <div v-if="job.project?.notes" class="mb-3">
<strong>Project Notes:</strong>
<p class="text-body-2 mt-1">{{ job.project.notes }}</p>
</div>
<div v-if="job.notes">
<strong>Job Notes:</strong>
<p class="text-body-2 mt-1">{{ job.notes }}</p>
</div>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>
@ -199,17 +227,17 @@ const showModal = computed({
}); });
const hasCoordinates = computed(() => { const hasCoordinates = computed(() => {
if (!props.job?.jobAddress) return false; if (!props.job?.serviceAddress) return false;
// Check if address has coordinates - you may need to adjust based on your data structure // Check if service address has coordinates
const lat = props.job.latitude || props.job.customLatitude; const lat = props.job.serviceAddress.latitude || props.job.serviceAddress.customLatitude;
const lon = props.job.longitude || props.job.customLongitude; const lon = props.job.serviceAddress.longitude || props.job.serviceAddress.customLongitude;
return lat && lon && parseFloat(lat) !== 0 && parseFloat(lon) !== 0; return lat && lon && parseFloat(lat) !== 0 && parseFloat(lon) !== 0;
}); });
const mapUrl = computed(() => { const mapUrl = computed(() => {
if (!hasCoordinates.value) return ""; if (!hasCoordinates.value) return "";
const lat = parseFloat(props.job.latitude || props.job.customLatitude); const lat = parseFloat(props.job.serviceAddress.latitude || props.job.serviceAddress.customLatitude);
const lon = parseFloat(props.job.longitude || props.job.customLongitude); const lon = parseFloat(props.job.serviceAddress.longitude || props.job.serviceAddress.customLongitude);
// Using OpenStreetMap embed with marker // Using OpenStreetMap embed with marker
return `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.01},${lat - 0.01},${lon + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lon}`; return `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.01},${lat - 0.01},${lon + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lon}`;
}); });
@ -221,6 +249,20 @@ const stripAddress = (address) => {
return index > -1 ? address.substring(0, index).trim() : address; return index > -1 ? address.substring(0, index).trim() : address;
}; };
const formatDate = (dateStr) => {
if (!dateStr) return 'Not scheduled';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch (e) {
return dateStr;
}
};
const getCrewName = (foremanId) => { const getCrewName = (foremanId) => {
if (!foremanId) return 'Not assigned'; if (!foremanId) return 'Not assigned';
const foreman = props.foremen.find(f => f.name === foremanId); const foreman = props.foremen.find(f => f.name === foremanId);

View file

@ -407,7 +407,7 @@ const handleCreateEstimate = () => {
new: "true", new: "true",
address: addressText, address: addressText,
"from-meeting": fromMeeting, "from-meeting": fromMeeting,
template: template, "project-template": template,
contact: contactName, contact: contactName,
}, },
}); });

View file

@ -101,7 +101,7 @@ const loadingStore = useLoadingStore();
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const companyStore = useCompanyStore(); const companyStore = useCompanyStore();
const address = route.query.address || null; const address = (route.query.address || '').trim();
const clientName = route.query.client || null; const clientName = route.query.client || null;
const isNew = computed(() => route.query.new === "true" || false); const isNew = computed(() => route.query.new === "true" || false);
@ -118,7 +118,7 @@ const selectedAddressObject = computed(() =>
); );
const addresses = computed(() => { const addresses = computed(() => {
if (client.value && client.value.addresses) { if (client.value && client.value.addresses) {
return client.value.addresses.map((addr) => DataUtils.calculateFullAddress(addr)); return client.value.addresses.map((addr) => DataUtils.calculateFullAddress(addr).trim());
} }
return []; return [];
}); });
@ -144,7 +144,7 @@ const selectedAddressData = computed(() => {
return null; return null;
} }
return client.value.addresses.find( return client.value.addresses.find(
(addr) => DataUtils.calculateFullAddress(addr) === selectedAddress.value (addr) => DataUtils.calculateFullAddress(addr).trim() === selectedAddress.value.trim()
); );
}); });
@ -174,10 +174,11 @@ const getClient = async (name) => {
// Set initial selected address if provided in route or use first address // Set initial selected address if provided in route or use first address
if (address && client.value.addresses) { if (address && client.value.addresses) {
const fullAddresses = client.value.addresses.map((addr) => const fullAddresses = client.value.addresses.map((addr) =>
DataUtils.calculateFullAddress(addr), DataUtils.calculateFullAddress(addr).trim(),
); );
if (fullAddresses.includes(address)) { const trimmedAddress = address.trim();
selectedAddress.value = address; if (fullAddresses.includes(trimmedAddress)) {
selectedAddress.value = trimmedAddress;
} else if (fullAddresses.length > 0) { } else if (fullAddresses.length > 0) {
selectedAddress.value = fullAddresses[0]; selectedAddress.value = fullAddresses[0];
} }
@ -297,7 +298,7 @@ const handleSubmit = async () => {
console.log("Created client:", createdClient); console.log("Created client:", createdClient);
notificationStore.addSuccess("Client created successfully!"); notificationStore.addSuccess("Client created successfully!");
// Navigate to the created client // Navigate to the created client
window.location.hash = '#/client?client=' + encodeURIComponent(createdClient.name || createdClient.customerName); router.push('/client?client=' + encodeURIComponent(createdClient.name));
} else { } else {
// TODO: Implement save logic // TODO: Implement save logic
notificationStore.addSuccess("Changes saved successfully!"); notificationStore.addSuccess("Changes saved successfully!");

View file

@ -444,6 +444,7 @@ const nameQuery = computed(() => route.query.name || "");
const templateQuery = computed(() => route.query.template || ""); const templateQuery = computed(() => route.query.template || "");
const fromMeetingQuery = computed(() => route.query["from-meeting"] || ""); const fromMeetingQuery = computed(() => route.query["from-meeting"] || "");
const contactQuery = computed(() => route.query.contact || ""); const contactQuery = computed(() => route.query.contact || "");
const projectTemplateQuery = computed(() => route.query["project-template"] || "");
const isNew = computed(() => route.query.new === "true"); const isNew = computed(() => route.query.new === "true");
const isSubmitting = ref(false); const isSubmitting = ref(false);
@ -457,7 +458,7 @@ const formData = reactive({
estimateName: null, estimateName: null,
requiresHalfPayment: false, requiresHalfPayment: false,
projectTemplate: null, projectTemplate: null,
fromMeeting: null, fromOnsiteMeeting: null,
}); });
const selectedAddress = ref(null); const selectedAddress = ref(null);
@ -757,7 +758,7 @@ const saveDraft = async () => {
estimateName: formData.estimateName, estimateName: formData.estimateName,
requiresHalfPayment: formData.requiresHalfPayment, requiresHalfPayment: formData.requiresHalfPayment,
projectTemplate: formData.projectTemplate, projectTemplate: formData.projectTemplate,
fromMeeting: formData.fromMeeting, fromOnsiteMeeting: formData.fromOnsiteMeeting,
company: company.currentCompany company: company.currentCompany
}; };
estimate.value = await Api.createEstimate(data); estimate.value = await Api.createEstimate(data);
@ -923,6 +924,8 @@ watch(
const newIsNew = newQuery.new === "true"; const newIsNew = newQuery.new === "true";
const newAddressQuery = newQuery.address; const newAddressQuery = newQuery.address;
const newNameQuery = newQuery.name; const newNameQuery = newQuery.name;
const newFromMeetingQuery = newQuery["from-meeting"];
const newProjectTemplateQuery = newQuery["project-template"];
if (newAddressQuery && newIsNew) { if (newAddressQuery && newIsNew) {
// Creating new estimate - pre-fill address // Creating new estimate - pre-fill address
@ -966,7 +969,15 @@ watch(
}; };
}); });
} }
formData.requiresHalfPayment = estimate.value.custom_requires_half_payment || false; formData.requiresHalfPayment = Boolean(estimate.value.requiresHalfPayment || estimate.value.custom_requires_half_payment);
// If estimate has fromOnsiteMeeting, fetch bid meeting
if (estimate.value.fromOnsiteMeeting) {
try {
bidMeeting.value = await Api.getBidMeeting(estimate.value.fromOnsiteMeeting);
} catch (error) {
console.error("Error fetching bid meeting for existing estimate:", error);
}
}
} }
} catch (error) { } catch (error) {
console.error("Error loading estimate:", error); console.error("Error loading estimate:", error);
@ -976,6 +987,35 @@ watch(
); );
} }
} }
// Handle from-meeting for new estimates
if (newFromMeetingQuery && newIsNew) {
formData.fromOnsiteMeeting = newFromMeetingQuery;
try {
bidMeeting.value = await Api.getBidMeeting(newFromMeetingQuery);
if (bidMeeting.value?.bidNotes?.quantities) {
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
const item = quotationItems.value.find(i => i.itemCode === q.item);
return {
itemCode: q.item,
itemName: item?.itemName || q.item,
qty: q.quantity,
standardRate: item?.standardRate || 0,
discountAmount: null,
discountPercentage: null,
discountType: 'currency'
};
});
}
} catch (error) {
console.error("Error fetching bid meeting:", error);
}
}
// Handle project-template
if (newProjectTemplateQuery) {
formData.projectTemplate = newProjectTemplateQuery;
}
} }
}, },
{ deep: true } { deep: true }
@ -995,16 +1035,36 @@ onMounted(async () => {
// Handle from-meeting query parameter // Handle from-meeting query parameter
if (fromMeetingQuery.value) { if (fromMeetingQuery.value) {
formData.fromMeeting = fromMeetingQuery.value; formData.fromOnsiteMeeting = fromMeetingQuery.value;
// Fetch the bid meeting to check for bidNotes // Fetch the bid meeting to check for bidNotes
try { try {
bidMeeting.value = await Api.getBidMeeting(fromMeetingQuery.value); bidMeeting.value = await Api.getBidMeeting(fromMeetingQuery.value);
// If new estimate and bid notes have quantities, set default items
if (isNew.value && bidMeeting.value?.bidNotes?.quantities) {
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
const item = quotationItems.value.find(i => i.itemCode === q.item);
return {
itemCode: q.item,
itemName: item?.itemName || q.item,
qty: q.quantity,
standardRate: item?.standardRate || 0,
discountAmount: null,
discountPercentage: null,
discountType: 'currency'
};
});
}
} catch (error) { } catch (error) {
console.error("Error fetching bid meeting:", error); console.error("Error fetching bid meeting:", error);
} }
} }
} }
// Handle project-template query parameter
if (projectTemplateQuery.value) {
formData.projectTemplate = projectTemplateQuery.value;
}
if (addressQuery.value && isNew.value) { if (addressQuery.value && isNew.value) {
// Creating new estimate - pre-fill address // Creating new estimate - pre-fill address
await selectAddress(addressQuery.value); await selectAddress(addressQuery.value);
@ -1053,7 +1113,15 @@ onMounted(async () => {
}; };
}); });
} }
formData.requiresHalfPayment = estimate.value.requiresHalfPayment || false; formData.requiresHalfPayment = Boolean(estimate.value.requiresHalfPayment || estimate.value.custom_requires_half_payment);
// If estimate has fromOnsiteMeeting, fetch bid meeting
if (estimate.value.fromOnsiteMeeting) {
try {
bidMeeting.value = await Api.getBidMeeting(estimate.value.fromOnsiteMeeting);
} catch (error) {
console.error("Error fetching bid meeting for existing estimate:", error);
}
}
estimateResponse.value = estimate.value.customResponse; estimateResponse.value = estimate.value.customResponse;
estimateResponseSelection.value = estimate.value.customResponse; estimateResponseSelection.value = estimate.value.customResponse;
} }