bug fixes and calendar fixes
This commit is contained in:
parent
0ec89a1269
commit
6cd3d138ad
9 changed files with 446 additions and 270 deletions
|
|
@ -17,12 +17,6 @@
|
||||||
<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>
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -257,31 +253,32 @@
|
||||||
>
|
>
|
||||||
<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"
|
||||||
|
@click.stop="showEventDetails({ event: service })"
|
||||||
@dragstart="handleDragStart(service, $event)"
|
@dragstart="handleDragStart(service, $event)"
|
||||||
@dragend="handleDragEnd"
|
@dragend="handleDragEnd"
|
||||||
>
|
>
|
||||||
<v-card-text class="pa-3">
|
<v-card-text class="pa-3">
|
||||||
<div class="service-title-compact mb-2">{{ service.projectTemplate || service.serviceType }}</div>
|
<div class="service-title-compact mb-2">
|
||||||
|
{{ service.project?.projectName || service.projectTemplate || service.name }}
|
||||||
|
</div>
|
||||||
<div class="service-address mb-1">
|
<div class="service-address mb-1">
|
||||||
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
||||||
{{ stripAddress(service.address || service.jobAddress) }}
|
{{ service.serviceAddress?.fullAddress || service.serviceAddress?.addressTitle || service.serviceAddress?.name || service.project?.jobAddress }}
|
||||||
</div>
|
</div>
|
||||||
<div class="service-customer mb-2">
|
<div class="service-customer mb-2">
|
||||||
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
|
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
|
||||||
{{ service.customer }}
|
{{ service.customer?.customerName || service.customer?.name || service.project?.customer || service.customer || 'N/A' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="service.project?.notes || service.notes" class="service-notes-compact mt-2">
|
||||||
<div v-if="service.notes" class="service-notes-compact mt-2">
|
<span class="text-caption">{{ service.project?.notes || service.notes }}</span>
|
||||||
<span class="text-caption">{{ service.notes }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
|
|
@ -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],
|
||||||
|
{
|
||||||
// Process scheduled projects
|
"status": "Open"
|
||||||
if (data.projects && Array.isArray(data.projects)) {
|
|
||||||
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
|
console.log("Loaded service appointments:", scheduledServices.value);
|
||||||
if (data.unscheduledProjects && Array.isArray(data.unscheduledProjects)) {
|
console.log("Loaded unscheduled service appointments:", unscheduledServices.value);
|
||||||
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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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!");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue