update calendar
This commit is contained in:
parent
7395d3e048
commit
e67805c01f
10 changed files with 848 additions and 325 deletions
299
frontend/src/components/modals/JobDetailsModal.vue
Normal file
299
frontend/src/components/modals/JobDetailsModal.vue
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
<template>
|
||||
<v-dialog v-model="showModal" max-width="900px" scrollable>
|
||||
<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-caption">{{ job.name }}</div>
|
||||
</div>
|
||||
<v-chip :color="getPriorityColor(job.priority)" size="small">
|
||||
{{ job.priority }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-row>
|
||||
<!-- Left Column -->
|
||||
<v-col cols="12" md="6">
|
||||
<div class="detail-section mb-4">
|
||||
<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 }}
|
||||
</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) }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-clipboard-text</v-icon>
|
||||
<strong>Status:</strong> {{ job.status || 'Open' }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-file-document</v-icon>
|
||||
<strong>Sales Order:</strong> {{ job.salesOrder || '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 }}
|
||||
</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 }}
|
||||
</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>
|
||||
|
||||
<div class="detail-section mb-4">
|
||||
<h4 class="text-subtitle-1 mb-2">Compliance</h4>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-file-certificate</v-icon>
|
||||
<strong>Permit Status:</strong>
|
||||
<v-chip size="x-small" :color="getPermitStatusColor(job.customPermitStatus)" class="ml-2">
|
||||
{{ job.customPermitStatus || 'N/A' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-map-search</v-icon>
|
||||
<strong>Utility Locate:</strong>
|
||||
<v-chip size="x-small" :color="getLocateStatusColor(job.customUtlityLocateStatus)" class="ml-2">
|
||||
{{ job.customUtlityLocateStatus || 'N/A' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-if="job.customWarrantyDurationDays" class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-shield-check</v-icon>
|
||||
<strong>Warranty:</strong> {{ job.customWarrantyDurationDays }} days
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Right Column -->
|
||||
<v-col cols="12" md="6">
|
||||
<div class="detail-section mb-4">
|
||||
<h4 class="text-subtitle-1 mb-3">Progress</h4>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-space-between mb-1">
|
||||
<span class="text-caption">Completion</span>
|
||||
<span class="text-caption font-weight-bold">{{ job.percentComplete || 0 }}%</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="job.percentComplete || 0"
|
||||
color="success"
|
||||
height="8"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section mb-4">
|
||||
<h4 class="text-subtitle-1 mb-2">Financial Summary</h4>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-currency-usd</v-icon>
|
||||
<strong>Total Sales:</strong> ${{ (job.totalSalesAmount || 0).toLocaleString() }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-receipt</v-icon>
|
||||
<strong>Billed Amount:</strong> ${{ (job.totalBilledAmount || 0).toLocaleString() }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-calculator</v-icon>
|
||||
<strong>Total Cost:</strong> ${{ (job.totalCostingAmount || 0).toLocaleString() }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon size="small" class="mr-2">mdi-chart-line</v-icon>
|
||||
<strong>Gross Margin:</strong> {{ (job.perGrossMargin || 0).toFixed(1) }}%
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-space-between mb-1">
|
||||
<span class="text-caption">Billing Progress</span>
|
||||
<span class="text-caption font-weight-bold">
|
||||
${{ (job.totalBilledAmount || 0).toLocaleString() }} / ${{ (job.totalSalesAmount || 0).toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="getBillingProgress(job)"
|
||||
color="primary"
|
||||
height="8"
|
||||
rounded
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Display -->
|
||||
<div v-if="hasCoordinates" class="detail-section mb-4">
|
||||
<h4 class="text-subtitle-1 mb-2">Location</h4>
|
||||
<div class="map-container">
|
||||
<iframe
|
||||
:src="mapUrl"
|
||||
width="100%"
|
||||
height="200"
|
||||
frameborder="0"
|
||||
style="border: 1px solid var(--surface-border); border-radius: 6px;"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="job.notes" class="detail-section">
|
||||
<h4 class="text-subtitle-1 mb-2">Notes</h4>
|
||||
<p class="text-body-2">{{ job.notes }}</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@click="viewJob"
|
||||
>
|
||||
<v-icon left>mdi-open-in-new</v-icon>
|
||||
View Job
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="handleClose">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
job: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
foremen: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["update:visible", "close"]);
|
||||
|
||||
// Computed
|
||||
const showModal = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(value) {
|
||||
emit("update:visible", value);
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
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);
|
||||
// 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}`;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const stripAddress = (address) => {
|
||||
if (!address) return '';
|
||||
const index = address.indexOf('-#-');
|
||||
return index > -1 ? address.substring(0, index).trim() : address;
|
||||
};
|
||||
|
||||
const getCrewName = (foremanId) => {
|
||||
if (!foremanId) return 'Not assigned';
|
||||
const foreman = props.foremen.find(f => f.name === foremanId);
|
||||
if (!foreman) return foremanId;
|
||||
return foreman.customCrew ? `${foreman.employeeName} (Crew ${foreman.customCrew})` : foreman.employeeName;
|
||||
};
|
||||
|
||||
const getBillingProgress = (job) => {
|
||||
if (!job.totalSalesAmount || job.totalSalesAmount === 0) return 0;
|
||||
return Math.min(100, (job.totalBilledAmount / job.totalSalesAmount) * 100);
|
||||
};
|
||||
|
||||
const getPermitStatusColor = (status) => {
|
||||
if (!status) return 'grey';
|
||||
if (status.toLowerCase().includes('approved')) return 'success';
|
||||
if (status.toLowerCase().includes('pending')) return 'warning';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const getLocateStatusColor = (status) => {
|
||||
if (!status) return 'grey';
|
||||
if (status.toLowerCase().includes('complete')) return 'success';
|
||||
if (status.toLowerCase().includes('incomplete')) return 'error';
|
||||
return 'warning';
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority) => {
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return "red";
|
||||
case "high":
|
||||
return "orange";
|
||||
case "medium":
|
||||
return "yellow";
|
||||
case "low":
|
||||
return "green";
|
||||
default:
|
||||
return "grey";
|
||||
}
|
||||
};
|
||||
|
||||
const viewJob = () => {
|
||||
if (props.job?.name) {
|
||||
window.location.href = `/job?name=${encodeURIComponent(props.job.name)}`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit("close");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-section {
|
||||
background-color: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue