bug fixes and calendar fixes
This commit is contained in:
parent
0ec89a1269
commit
6cd3d138ad
9 changed files with 446 additions and 270 deletions
|
|
@ -17,15 +17,9 @@
|
|||
<span class="date-text">{{ weekDisplayText }}</span>
|
||||
<v-icon right size="small">mdi-calendar</v-icon>
|
||||
</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">
|
||||
This Week
|
||||
</v-btn>
|
||||
</v-btn>
|
||||
<v-menu
|
||||
v-model="showTemplateMenu"
|
||||
:close-on-content-click="false"
|
||||
|
|
@ -203,19 +197,21 @@
|
|||
<!-- Jobs in this day -->
|
||||
<div
|
||||
v-for="job in getJobsForCell(foreman.name, day.date)"
|
||||
:key="job.id"
|
||||
:key="job.name"
|
||||
class="calendar-job"
|
||||
:style="getJobStyle(job, day.date)"
|
||||
draggable="true"
|
||||
@click.stop="showEventDetails({ event: job })"
|
||||
@dragstart="handleDragStart(job, $event)"
|
||||
@dragend="handleDragEnd"
|
||||
@mousedown="startResize($event, job, day.date)"
|
||||
>
|
||||
:style="getJobStyle(job, day.date)"
|
||||
:draggable="job.status === 'Scheduled'"
|
||||
@click.stop="showEventDetails({ event: job })"
|
||||
@dragstart="job.status === 'Scheduled' ? handleDragStart(job, $event) : null"
|
||||
@dragend="handleDragEnd"
|
||||
@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>
|
||||
<div class="job-content">
|
||||
<div class="job-title">{{ job.projectTemplate || job.serviceType }}</div>
|
||||
<div class="job-address">{{ stripAddress(job.address || job.jobAddress) }}</div>
|
||||
<div class="job-title">{{ job.projectTemplate }}</div>
|
||||
<div class="job-address">{{ job.serviceAddress.fullAddress }}</div>
|
||||
</div>
|
||||
<v-icon v-if="jobSpansToNextWeek(job)" size="small" class="spans-arrow-right">mdi-arrow-right</v-icon>
|
||||
<div class="resize-handle"></div>
|
||||
|
|
@ -225,7 +221,7 @@
|
|||
<template v-if="isHoliday(day.date)">
|
||||
<div
|
||||
v-for="job in getJobsWithConnector(foreman.name, day.date)"
|
||||
:key="`connector-${job.id}`"
|
||||
:key="`connector-${job.name}`"
|
||||
class="holiday-connector"
|
||||
:class="getPriorityClass(job.priority)"
|
||||
></div>
|
||||
|
|
@ -241,10 +237,10 @@
|
|||
<div class="unscheduled-header">
|
||||
<h4>Unscheduled Jobs</h4>
|
||||
<v-chip
|
||||
:color="getUnscheduledCount() > 0 ? 'warning' : 'success'"
|
||||
:color="unscheduledServices.length > 0 ? 'warning' : 'success'"
|
||||
size="small"
|
||||
>
|
||||
{{ getUnscheduledCount() }} pending
|
||||
{{ unscheduledServices.length }} pending
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
|
|
@ -255,46 +251,47 @@
|
|||
@dragleave="handleUnscheduledDragLeave"
|
||||
@drop="handleUnscheduledDrop"
|
||||
>
|
||||
<v-card
|
||||
v-for="service in unscheduledServices"
|
||||
:key="service.id"
|
||||
class="unscheduled-item mb-2"
|
||||
:class="getPriorityClass(service.priority)"
|
||||
elevation="1"
|
||||
hover
|
||||
density="compact"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart(service, $event)"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<v-card-text class="pa-3">
|
||||
<div class="service-title-compact mb-2">{{ service.projectTemplate || service.serviceType }}</div>
|
||||
<div class="service-address mb-1">
|
||||
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
||||
{{ stripAddress(service.address || service.jobAddress) }}
|
||||
</div>
|
||||
<div class="service-customer mb-2">
|
||||
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
|
||||
{{ service.customer }}
|
||||
</div>
|
||||
|
||||
<div v-if="service.notes" class="service-notes-compact mt-2">
|
||||
<span class="text-caption">{{ service.notes }}</span>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
block
|
||||
@click="scheduleService(service)"
|
||||
>
|
||||
<v-icon left size="x-small">mdi-calendar-plus</v-icon>
|
||||
Schedule
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card
|
||||
v-for="service in unscheduledServices"
|
||||
:key="service.name"
|
||||
class="unscheduled-item mb-2"
|
||||
:class="getPriorityClass(service.project?.priority || service.priority)"
|
||||
elevation="1"
|
||||
hover
|
||||
density="compact"
|
||||
draggable="true"
|
||||
@click.stop="showEventDetails({ event: service })"
|
||||
@dragstart="handleDragStart(service, $event)"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<v-card-text class="pa-3">
|
||||
<div class="service-title-compact mb-2">
|
||||
{{ service.project?.projectName || service.projectTemplate || service.name }}
|
||||
</div>
|
||||
<div class="service-address mb-1">
|
||||
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
||||
{{ service.serviceAddress?.fullAddress || service.serviceAddress?.addressTitle || service.serviceAddress?.name || service.project?.jobAddress }}
|
||||
</div>
|
||||
<div class="service-customer mb-2">
|
||||
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
|
||||
{{ service.customer?.customerName || service.customer?.name || service.project?.customer || service.customer || 'N/A' }}
|
||||
</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>
|
||||
</div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
block
|
||||
@click="scheduleService(service)"
|
||||
>
|
||||
<v-icon left size="x-small">mdi-calendar-plus</v-icon>
|
||||
Schedule
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div v-if="unscheduledServices.length === 0" class="no-unscheduled">
|
||||
|
|
@ -309,6 +306,9 @@
|
|||
v-model="eventDialog"
|
||||
:job="selectedEvent"
|
||||
:foremen="foremen"
|
||||
:project="selectedEvent?.project"
|
||||
:customer="selectedEvent?.customer"
|
||||
:service-address="selectedEvent?.serviceAddress"
|
||||
@close="eventDialog = false"
|
||||
/>
|
||||
|
||||
|
|
@ -330,7 +330,8 @@ const notifications = useNotificationStore();
|
|||
const companyStore = useCompanyStore();
|
||||
|
||||
// Reactive data
|
||||
const services = ref([]);
|
||||
const scheduledServices = ref([]);
|
||||
const unscheduledServices = ref([]);
|
||||
const weekStartDate = ref(getWeekStart(new Date()));
|
||||
const eventDialog = ref(false);
|
||||
const selectedEvent = ref(null);
|
||||
|
|
@ -479,7 +480,7 @@ function hasHolidayInRange(startDate, endDate) {
|
|||
|
||||
// Helper function to check if job spans to next week
|
||||
function jobSpansToNextWeek(job) {
|
||||
const endDate = job.scheduledEndDate || job.scheduledDate;
|
||||
const endDate = job.expectedEndDate || job.expectedStartDate;
|
||||
const end = parseLocalDate(endDate);
|
||||
const weekEnd = parseLocalDate(addDays(weekStartDate.value, 6));
|
||||
return end > weekEnd;
|
||||
|
|
@ -487,18 +488,12 @@ function jobSpansToNextWeek(job) {
|
|||
|
||||
// Helper function to check if job starts before current week
|
||||
function jobStartsBeforeWeek(job) {
|
||||
const startDate = job.expectedStartDate || job.scheduledDate;
|
||||
const startDate = job.expectedStartDate;
|
||||
const start = parseLocalDate(startDate);
|
||||
const weekStart = parseLocalDate(weekStartDate.value);
|
||||
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
|
||||
function getCrewName(foremanId) {
|
||||
|
|
@ -524,8 +519,8 @@ function getHolidaysInRange(startDate, endDate) {
|
|||
|
||||
// Helper function to calculate job segments (parts between holidays/Sundays)
|
||||
function getJobSegments(job) {
|
||||
const startDate = job.scheduledDate;
|
||||
const endDate = job.scheduledEndDate || job.scheduledDate;
|
||||
const startDate = job.expectedStartDate;
|
||||
const endDate = job.expectedEndDate || job.expectedStartDate;
|
||||
const weekEndDate = addDays(weekStartDate.value, 6); // Saturday
|
||||
|
||||
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
|
||||
const getUnscheduledCount = () => unscheduledServices.value.length;
|
||||
|
||||
const getPriorityClass = (priority) => {
|
||||
switch (priority) {
|
||||
|
|
@ -655,8 +643,8 @@ const getJobsForCell = (foremanId, date) => {
|
|||
return scheduledServices.value.filter((job) => {
|
||||
if (job.foreman !== foremanId) return false;
|
||||
|
||||
const jobStart = job.scheduledDate;
|
||||
const jobEnd = job.scheduledEndDate || job.scheduledDate;
|
||||
const jobStart = job.expectedStartDate;
|
||||
const jobEnd = job.expectedEndDate || job.expectedStartDate;
|
||||
|
||||
// Check if this date falls within the job's date range
|
||||
// AND that it's a valid segment start date
|
||||
|
|
@ -672,8 +660,8 @@ const getJobsWithConnector = (foremanId, date) => {
|
|||
return scheduledServices.value.filter((job) => {
|
||||
if (job.foreman !== foremanId) return false;
|
||||
|
||||
const jobStart = job.scheduledDate;
|
||||
const jobEnd = job.scheduledEndDate || job.scheduledDate;
|
||||
const jobStart = job.expectedStartDate;
|
||||
const jobEnd = job.expectedEndDate || job.expectedStartDate;
|
||||
|
||||
// Check if this holiday date falls within the job's range
|
||||
return date > jobStart && date < jobEnd;
|
||||
|
|
@ -682,8 +670,8 @@ const getJobsWithConnector = (foremanId, date) => {
|
|||
|
||||
// Get job style for width spanning multiple days
|
||||
const getJobStyle = (job, currentDate) => {
|
||||
const jobStart = job.scheduledDate;
|
||||
const jobEnd = job.scheduledEndDate || job.scheduledDate;
|
||||
const jobStart = job.expectedStartDate;
|
||||
const jobEnd = job.expectedEndDate || job.expectedStartDate;
|
||||
const segments = getJobSegments(job);
|
||||
|
||||
// Find which segment (if any) should be rendered at currentDate
|
||||
|
|
@ -842,7 +830,7 @@ const scheduleService = (service) => {
|
|||
const handleDragStart = (service, event) => {
|
||||
draggedService.value = service;
|
||||
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
|
||||
const dragElement = event.target;
|
||||
|
|
@ -860,7 +848,7 @@ const handleDragStart = (service, event) => {
|
|||
|
||||
// Add visual feedback
|
||||
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) => {
|
||||
|
|
@ -962,43 +950,61 @@ const handleDrop = async (event, foremanId, date) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update the service with scheduling information
|
||||
const serviceIndex = services.value.findIndex(s => s.id === draggedService.value.id);
|
||||
if (serviceIndex !== -1) {
|
||||
const wasScheduled = services.value[serviceIndex].isScheduled;
|
||||
// Check if this is scheduling an unscheduled job or moving a scheduled job
|
||||
const unscheduledIndex = unscheduledServices.value.findIndex(s => s.name === draggedService.value.name);
|
||||
const scheduledIndex = scheduledServices.value.findIndex(s => s.name === draggedService.value.name);
|
||||
|
||||
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';
|
||||
console.log(`${action} ${draggedService.value.serviceType} for ${foreman.employeeName} on ${date} to ${endDate}`);
|
||||
|
||||
// Call API to persist changes (placeholder for now)
|
||||
// Call API to persist changes
|
||||
try {
|
||||
await Api.updateJobScheduledDates(
|
||||
draggedService.value.id,
|
||||
await Api.updateServiceAppointmentScheduledDates(
|
||||
draggedService.value.name,
|
||||
date,
|
||||
endDate,
|
||||
foreman.name
|
||||
)
|
||||
services.value[serviceIndex] = {
|
||||
...services.value[serviceIndex],
|
||||
isScheduled: true,
|
||||
scheduledDate: date,
|
||||
scheduledEndDate: endDate,
|
||||
foreman: foreman.name
|
||||
);
|
||||
// Remove from unscheduled and add to scheduled
|
||||
const scheduledService = {
|
||||
...unscheduledServices.value[unscheduledIndex],
|
||||
expectedStartDate: date,
|
||||
expectedEndDate: endDate,
|
||||
foreman: foreman.name,
|
||||
status: 'Scheduled'
|
||||
};
|
||||
unscheduledServices.value.splice(unscheduledIndex, 1);
|
||||
scheduledServices.value.push(scheduledService);
|
||||
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) {
|
||||
console.error("Error scheduling job:", error);
|
||||
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
|
||||
|
|
@ -1009,7 +1015,8 @@ const handleDrop = async (event, foremanId, date) => {
|
|||
|
||||
// Handle dropping scheduled items back to unscheduled
|
||||
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.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
|
|
@ -1021,79 +1028,127 @@ const handleUnscheduledDragLeave = (event) => {
|
|||
|
||||
const handleUnscheduledDrop = async (event) => {
|
||||
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
|
||||
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) {
|
||||
services.value[serviceIndex] = {
|
||||
...services.value[serviceIndex],
|
||||
isScheduled: false,
|
||||
scheduledDate: null,
|
||||
scheduledEndDate: null,
|
||||
// Remove from scheduled and add to unscheduled
|
||||
const unscheduledService = {
|
||||
...scheduledServices.value[serviceIndex],
|
||||
expectedStartDate: null,
|
||||
expectedEndDate: null,
|
||||
foreman: null
|
||||
};
|
||||
|
||||
console.log(`Unscheduled ${draggedService.value.serviceType}`);
|
||||
scheduledServices.value.splice(serviceIndex, 1);
|
||||
unscheduledServices.value.push(unscheduledService);
|
||||
|
||||
console.log(`Unscheduled ${draggedService.value.projectTemplate}`);
|
||||
|
||||
// Call API to persist changes (placeholder for now)
|
||||
// Call API to persist changes
|
||||
try {
|
||||
await Api.updateJobScheduledDates(
|
||||
draggedService.value.id,
|
||||
await Api.updateServiceAppointmentScheduledDates(
|
||||
draggedService.value.name,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
notifications.addSuccess("Job unscheduled successfully!");
|
||||
// Future implementation:
|
||||
// await Api.updateJobSchedule({
|
||||
// id: draggedService.value.id,
|
||||
// expectedStartDate: null,
|
||||
// expectedEndDate: null,
|
||||
// isScheduled: false,
|
||||
// customForeman: null
|
||||
// });
|
||||
} catch (error) {
|
||||
console.error("Error unscheduling job:", error);
|
||||
notifications.addError("Failed to unschedule job");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reset drag state
|
||||
isDragOver.value = false;
|
||||
dragOverCell.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
|
||||
const startResize = (event, job, date) => {
|
||||
// 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 rect = target.getBoundingClientRect();
|
||||
const clickX = event.clientX;
|
||||
|
||||
|
||||
// Check if click is on the resize handle or near right edge
|
||||
const isNearRightEdge = clickX > rect.right - 10;
|
||||
const isResizeHandle = target.classList.contains('resize-handle');
|
||||
|
||||
|
||||
if (!isNearRightEdge && !isResizeHandle) return;
|
||||
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
// Set flag immediately to prevent modal from opening
|
||||
justFinishedResize.value = true;
|
||||
|
||||
|
||||
resizingJob.value = job;
|
||||
resizeStartX.value = event.clientX;
|
||||
resizeStartDate.value = date;
|
||||
originalEndDate.value = job.scheduledEndDate || job.scheduledDate;
|
||||
|
||||
originalEndDate.value = job.expectedEndDate || job.expectedStartDate;
|
||||
|
||||
// Add global mouse move and mouse up listeners
|
||||
document.addEventListener('mousemove', handleResize);
|
||||
document.addEventListener('mouseup', stopResize);
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
};
|
||||
|
|
@ -1163,11 +1218,11 @@ const handleResize = (event) => {
|
|||
// Show popup if extending over Sunday
|
||||
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) {
|
||||
services.value[serviceIndex] = {
|
||||
...services.value[serviceIndex],
|
||||
scheduledEndDate: newEndDate
|
||||
scheduledServices.value[serviceIndex] = {
|
||||
...scheduledServices.value[serviceIndex],
|
||||
expectedEndDate: newEndDate
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -1181,12 +1236,12 @@ const stopResize = async () => {
|
|||
const job = resizingJob.value;
|
||||
|
||||
// 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;
|
||||
|
||||
const updatedJob = services.value[serviceIndex];
|
||||
const newEndDate = updatedJob.scheduledEndDate || updatedJob.scheduledDate;
|
||||
console.log(`Proposed new end date for job ${job.serviceType}: ${newEndDate}`);
|
||||
const updatedJob = scheduledServices.value[serviceIndex];
|
||||
const newEndDate = updatedJob.expectedEndDate || updatedJob.expectedStartDate;
|
||||
console.log(`Proposed new end date for job ${job.projectTemplate}: ${newEndDate}`);
|
||||
|
||||
// Only update if end date changed
|
||||
if (newEndDate !== originalEndDate.value) {
|
||||
|
|
@ -1197,13 +1252,13 @@ const stopResize = async () => {
|
|||
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
|
||||
try {
|
||||
await Api.updateJobScheduledDates(
|
||||
job.id,
|
||||
job.scheduledDate,
|
||||
await Api.updateServiceAppointmentScheduledDates(
|
||||
job.name,
|
||||
job.expectedStartDate,
|
||||
newEndDate,
|
||||
job.foreman
|
||||
);
|
||||
|
|
@ -1212,11 +1267,11 @@ const stopResize = async () => {
|
|||
console.error("Error updating job end date:", error);
|
||||
notifications.addError("Failed to update job end date");
|
||||
// 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) {
|
||||
services.value[serviceIndex] = {
|
||||
...services.value[serviceIndex],
|
||||
scheduledEndDate: originalEndDate.value
|
||||
scheduledServices.value[serviceIndex] = {
|
||||
...scheduledServices.value[serviceIndex],
|
||||
expectedEndDate: originalEndDate.value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1246,69 +1301,26 @@ const fetchServiceAppointments = async (currentDate) => {
|
|||
const endDate = addDays(startDate, 6);
|
||||
|
||||
// const data = await Api.getJobsForCalendar(startDate, endDate, companyStore.currentCompany, selectedProjectTemplates.value);
|
||||
const data = await Api.getServiceAppointments(
|
||||
scheduledServices.value = await Api.getServiceAppointments(
|
||||
[companyStore.currentCompany],
|
||||
{
|
||||
"expectedStartDate": ["<=", endDate],
|
||||
"expectedEndDate": [">=", startDate]
|
||||
"expectedEndDate": [">=", startDate],
|
||||
"status": ["not in", ["Canceled"]]
|
||||
}
|
||||
);
|
||||
// Transform the API response into the format the component expects
|
||||
const transformedServices = [];
|
||||
unscheduledServices.value = await Api.getServiceAppointments(
|
||||
[companyStore.currentCompany],
|
||||
{
|
||||
"status": "Open"
|
||||
}
|
||||
);
|
||||
|
||||
// Process scheduled projects
|
||||
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
|
||||
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);
|
||||
console.log("Loaded service appointments:", scheduledServices.value);
|
||||
console.log("Loaded unscheduled service appointments:", unscheduledServices.value);
|
||||
} catch (error) {
|
||||
console.error("Error loading install projects:", error);
|
||||
notifications.addError("Failed to load install projects");
|
||||
console.error("Error loading service appointments:", error);
|
||||
notifications.addError("Failed to load service appointments");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,15 @@
|
|||
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>
|
||||
<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 class="form-row">
|
||||
<div class="form-field">
|
||||
|
|
@ -120,7 +129,7 @@
|
|||
<Select
|
||||
:id="`primaryContact-${index}`"
|
||||
v-model="address.primaryContact"
|
||||
:options="contactOptions"
|
||||
:options="address.contacts.map(c => contactOptions.find(opt => opt.value === c))"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="isSubmitting || contactOptions.length === 0"
|
||||
|
|
@ -174,6 +183,7 @@ const localFormData = computed({
|
|||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
isBillingAddress: true,
|
||||
isServiceAddress: true,
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
|
|
@ -205,6 +215,7 @@ onMounted(() => {
|
|||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
isBillingAddress: true,
|
||||
isServiceAddress: true,
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
|
|
@ -221,6 +232,7 @@ const addAddress = () => {
|
|||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
isBillingAddress: false,
|
||||
isServiceAddress: true,
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
|
|
@ -259,6 +271,7 @@ const handleBillingChange = (selectedIndex) => {
|
|||
localFormData.value.addresses.forEach((addr, idx) => {
|
||||
if (idx !== selectedIndex) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} 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>
|
||||
|
||||
<Dropdown
|
||||
<Select
|
||||
v-model="selectedAddressIndex"
|
||||
:options="addressOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select an address"
|
||||
class="w-full address-dropdown"
|
||||
@change="handleAddressChange"
|
||||
>
|
||||
<template #value="slotProps">
|
||||
<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">
|
||||
<Badge
|
||||
v-if="addresses[slotProps.value]?.isPrimaryAddress && !addresses[slotProps.value]?.isServiceAddress"
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</Select>
|
||||
|
||||
<!-- Selected Address Info -->
|
||||
<div v-if="selectedAddress" class="selected-address-info">
|
||||
|
|
@ -146,13 +146,17 @@
|
|||
</template>
|
||||
|
||||
<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 Button from "primevue/button";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Dropdown from "primevue/dropdown";
|
||||
import Select from "primevue/select";
|
||||
import DataUtils from "../../utils";
|
||||
|
||||
const route = useRoute();
|
||||
const addressParam = route.query.address || null;
|
||||
|
||||
const props = defineProps({
|
||||
addresses: {
|
||||
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 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(() => props.selectedAddressIdx, (newVal) => {
|
||||
|
|
@ -189,9 +210,9 @@ const selectedAddress = computed(() => {
|
|||
// Address options for dropdown
|
||||
const addressOptions = computed(() => {
|
||||
return props.addresses.map((addr, idx) => ({
|
||||
label: addr.addressTitle || DataUtils.calculateFullAddress(addr),
|
||||
label: addr.fullAddress || DataUtils.calculateFullAddress(addr),
|
||||
value: idx,
|
||||
addressTitle: addr.addressTitle || 'Unnamed Address',
|
||||
addressTitle: addr.fullAddress || 'Unnamed Address',
|
||||
isPrimaryAddress: addr.isPrimaryAddress,
|
||||
isServiceAddress: addr.isServiceAddress,
|
||||
projectCount: addr.projects?.length || 0,
|
||||
|
|
@ -277,11 +298,12 @@ const handleAddressChange = () => {
|
|||
|
||||
.selected-address-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--surface-ground);
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.address-status {
|
||||
|
|
@ -290,9 +312,10 @@ const handleAddressChange = () => {
|
|||
}
|
||||
|
||||
.service-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
|
|
@ -316,7 +339,7 @@ const handleAddressChange = () => {
|
|||
}
|
||||
|
||||
.detail-item.primary-contact {
|
||||
grid-column: 1 / -1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
:class="getStatusClass(onsiteMeetingStatus)"
|
||||
@click="handleBidMeetingClick"
|
||||
>
|
||||
<span class="status-label">Meeting</span>
|
||||
<span class="status-label">Bid Meeting</span>
|
||||
<span class="status-badge">{{ onsiteMeetingStatus }}</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
:class="getStatusClass(estimateSentStatus)"
|
||||
@click="handleEstimateClick"
|
||||
>
|
||||
<span class="status-label">Estimate</span>
|
||||
<span class="status-label">Estimate Sent</span>
|
||||
<span class="status-badge">{{ estimateSentStatus }}</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -109,8 +109,10 @@ const handleBidMeetingClick = () => {
|
|||
};
|
||||
|
||||
const handleEstimateClick = () => {
|
||||
if (props.estimateSentStatus === "Not Started") {
|
||||
router.push(`/estimate?new=true&address=${encodeURIComponent(props.fullAddress)}&template=SNW%20Install`);
|
||||
if ((props.estimateSentStatus === "Not Started") && props.onsiteMeetingStatus != "Completed") {
|
||||
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 {
|
||||
router.push(`/estimate?name=${encodeURIComponent(props.estimate)}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,14 +327,14 @@ const emitChanges = () => {
|
|||
.property-details {
|
||||
background: var(--surface-card);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.property-details > h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
|
|
@ -343,13 +343,13 @@ const emitChanges = () => {
|
|||
.details-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background: var(--surface-ground);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detail-section.full-width {
|
||||
|
|
@ -360,7 +360,7 @@ const emitChanges = () => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
<v-card v-if="job">
|
||||
<v-card-title class="d-flex justify-space-between align-center bg-primary">
|
||||
<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>
|
||||
<v-chip :color="getPriorityColor(job.priority)" size="small">
|
||||
{{ job.priority }}
|
||||
<v-chip :color="getPriorityColor(job.project?.priority || job.priority)" size="small">
|
||||
{{ job.project?.priority || job.priority || 'Normal' }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
|
|
@ -19,11 +19,15 @@
|
|||
<h4 class="text-subtitle-1 mb-2">Basic Information</h4>
|
||||
<div class="detail-row">
|
||||
<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 class="detail-row">
|
||||
<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 class="detail-row">
|
||||
<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>
|
||||
<strong>Sales Order:</strong> {{ job.salesOrder || 'N/A' }}
|
||||
</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 class="detail-section mb-4">
|
||||
<h4 class="text-subtitle-1 mb-2">Schedule</h4>
|
||||
<div class="detail-row">
|
||||
<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 class="detail-row">
|
||||
<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 class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-account-hard-hat</v-icon>
|
||||
<strong>Assigned Crew:</strong> {{ getCrewName(job.foreman || job.customForeman) }}
|
||||
</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 class="detail-section mb-4">
|
||||
|
|
@ -71,6 +87,10 @@
|
|||
<v-icon size="small" class="mr-2">mdi-shield-check</v-icon>
|
||||
<strong>Warranty:</strong> {{ job.customWarrantyDurationDays }} days
|
||||
</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>
|
||||
</v-col>
|
||||
|
||||
|
|
@ -128,7 +148,7 @@
|
|||
|
||||
<!-- Map Display -->
|
||||
<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">
|
||||
<iframe
|
||||
:src="mapUrl"
|
||||
|
|
@ -136,13 +156,21 @@
|
|||
height="200"
|
||||
frameborder="0"
|
||||
style="border: 1px solid var(--surface-border); border-radius: 6px;"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</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>
|
||||
<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>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
@ -199,17 +227,17 @@ const showModal = computed({
|
|||
});
|
||||
|
||||
const hasCoordinates = computed(() => {
|
||||
if (!props.job?.jobAddress) return false;
|
||||
// Check if address has coordinates - you may need to adjust based on your data structure
|
||||
const lat = props.job.latitude || props.job.customLatitude;
|
||||
const lon = props.job.longitude || props.job.customLongitude;
|
||||
if (!props.job?.serviceAddress) return false;
|
||||
// Check if service address has coordinates
|
||||
const lat = props.job.serviceAddress.latitude || props.job.serviceAddress.customLatitude;
|
||||
const lon = props.job.serviceAddress.longitude || props.job.serviceAddress.customLongitude;
|
||||
return lat && lon && parseFloat(lat) !== 0 && parseFloat(lon) !== 0;
|
||||
});
|
||||
|
||||
const mapUrl = computed(() => {
|
||||
if (!hasCoordinates.value) return "";
|
||||
const lat = parseFloat(props.job.latitude || props.job.customLatitude);
|
||||
const lon = parseFloat(props.job.longitude || props.job.customLongitude);
|
||||
const lat = parseFloat(props.job.serviceAddress.latitude || props.job.serviceAddress.customLatitude);
|
||||
const lon = parseFloat(props.job.serviceAddress.longitude || props.job.serviceAddress.customLongitude);
|
||||
// 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}`;
|
||||
});
|
||||
|
|
@ -221,6 +249,20 @@ const stripAddress = (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) => {
|
||||
if (!foremanId) return 'Not assigned';
|
||||
const foreman = props.foremen.find(f => f.name === foremanId);
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ const handleCreateEstimate = () => {
|
|||
new: "true",
|
||||
address: addressText,
|
||||
"from-meeting": fromMeeting,
|
||||
template: template,
|
||||
"project-template": template,
|
||||
contact: contactName,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ const loadingStore = useLoadingStore();
|
|||
const notificationStore = useNotificationStore();
|
||||
const companyStore = useCompanyStore();
|
||||
|
||||
const address = route.query.address || null;
|
||||
const address = (route.query.address || '').trim();
|
||||
const clientName = route.query.client || null;
|
||||
const isNew = computed(() => route.query.new === "true" || false);
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ const selectedAddressObject = computed(() =>
|
|||
);
|
||||
const addresses = computed(() => {
|
||||
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 [];
|
||||
});
|
||||
|
|
@ -144,7 +144,7 @@ const selectedAddressData = computed(() => {
|
|||
return null;
|
||||
}
|
||||
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
|
||||
if (address && client.value.addresses) {
|
||||
const fullAddresses = client.value.addresses.map((addr) =>
|
||||
DataUtils.calculateFullAddress(addr),
|
||||
DataUtils.calculateFullAddress(addr).trim(),
|
||||
);
|
||||
if (fullAddresses.includes(address)) {
|
||||
selectedAddress.value = address;
|
||||
const trimmedAddress = address.trim();
|
||||
if (fullAddresses.includes(trimmedAddress)) {
|
||||
selectedAddress.value = trimmedAddress;
|
||||
} else if (fullAddresses.length > 0) {
|
||||
selectedAddress.value = fullAddresses[0];
|
||||
}
|
||||
|
|
@ -297,7 +298,7 @@ const handleSubmit = async () => {
|
|||
console.log("Created client:", createdClient);
|
||||
notificationStore.addSuccess("Client created successfully!");
|
||||
// Navigate to the created client
|
||||
window.location.hash = '#/client?client=' + encodeURIComponent(createdClient.name || createdClient.customerName);
|
||||
router.push('/client?client=' + encodeURIComponent(createdClient.name));
|
||||
} else {
|
||||
// TODO: Implement save logic
|
||||
notificationStore.addSuccess("Changes saved successfully!");
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ const nameQuery = computed(() => route.query.name || "");
|
|||
const templateQuery = computed(() => route.query.template || "");
|
||||
const fromMeetingQuery = computed(() => route.query["from-meeting"] || "");
|
||||
const contactQuery = computed(() => route.query.contact || "");
|
||||
const projectTemplateQuery = computed(() => route.query["project-template"] || "");
|
||||
const isNew = computed(() => route.query.new === "true");
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
|
|
@ -457,7 +458,7 @@ const formData = reactive({
|
|||
estimateName: null,
|
||||
requiresHalfPayment: false,
|
||||
projectTemplate: null,
|
||||
fromMeeting: null,
|
||||
fromOnsiteMeeting: null,
|
||||
});
|
||||
|
||||
const selectedAddress = ref(null);
|
||||
|
|
@ -757,7 +758,7 @@ const saveDraft = async () => {
|
|||
estimateName: formData.estimateName,
|
||||
requiresHalfPayment: formData.requiresHalfPayment,
|
||||
projectTemplate: formData.projectTemplate,
|
||||
fromMeeting: formData.fromMeeting,
|
||||
fromOnsiteMeeting: formData.fromOnsiteMeeting,
|
||||
company: company.currentCompany
|
||||
};
|
||||
estimate.value = await Api.createEstimate(data);
|
||||
|
|
@ -923,6 +924,8 @@ watch(
|
|||
const newIsNew = newQuery.new === "true";
|
||||
const newAddressQuery = newQuery.address;
|
||||
const newNameQuery = newQuery.name;
|
||||
const newFromMeetingQuery = newQuery["from-meeting"];
|
||||
const newProjectTemplateQuery = newQuery["project-template"];
|
||||
|
||||
if (newAddressQuery && newIsNew) {
|
||||
// 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) {
|
||||
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 }
|
||||
|
|
@ -995,16 +1035,36 @@ onMounted(async () => {
|
|||
|
||||
// Handle from-meeting query parameter
|
||||
if (fromMeetingQuery.value) {
|
||||
formData.fromMeeting = fromMeetingQuery.value;
|
||||
formData.fromOnsiteMeeting = fromMeetingQuery.value;
|
||||
// Fetch the bid meeting to check for bidNotes
|
||||
try {
|
||||
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) {
|
||||
console.error("Error fetching bid meeting:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle project-template query parameter
|
||||
if (projectTemplateQuery.value) {
|
||||
formData.projectTemplate = projectTemplateQuery.value;
|
||||
}
|
||||
|
||||
if (addressQuery.value && isNew.value) {
|
||||
// Creating new estimate - pre-fill address
|
||||
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;
|
||||
estimateResponseSelection.value = estimate.value.customResponse;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue