big update

This commit is contained in:
Casey 2026-01-15 08:36:08 -06:00
parent d53ebf9ecd
commit 5c7e93fcc7
26 changed files with 1890 additions and 423 deletions

View file

@ -2,41 +2,128 @@
<Modal v-model:visible="showModal" :options="modalOptions" @confirm="handleClose">
<template #title>Meeting Details</template>
<div v-if="meeting" class="meeting-details">
<!-- Meeting ID -->
<div class="detail-row">
<v-icon class="mr-2">mdi-identifier</v-icon>
<strong>Meeting ID:</strong>
<span class="detail-value">{{ meeting.name }}</span>
</div>
<!-- Address -->
<div class="detail-row">
<v-icon class="mr-2">mdi-map-marker</v-icon>
<strong>Addresss:</strong> {{ meeting.address.fullAddress }}
<strong>Address:</strong>
<span class="detail-value">{{ meeting.address?.fullAddress || meeting.address }}</span>
</div>
<div class="detail-row" v-if="meeting.client">
<!-- Contact -->
<div class="detail-row" v-if="meeting.contact">
<v-icon class="mr-2">mdi-account</v-icon>
<strong>Client:</strong> {{ meeting.client }}
<strong>Contact:</strong>
<span class="detail-value">{{ meeting.contact }}</span>
</div>
<div class="detail-row">
<v-icon class="mr-2">mdi-calendar</v-icon>
<strong>Date:</strong> {{ formatDate(meeting.date) }}
<!-- Party Name (Customer) -->
<div class="detail-row" v-if="meeting.partyName">
<v-icon class="mr-2">mdi-account-group</v-icon>
<strong>Customer:</strong>
<span class="detail-value">{{ meeting.partyName }} ({{ meeting.partyType }})</span>
</div>
<div class="detail-row">
<v-icon class="mr-2">mdi-clock</v-icon>
<strong>Time:</strong> {{ formatTimeDisplay(meeting.scheduledTime) }}
<!-- Project Template -->
<div class="detail-row" v-if="meeting.projectTemplate">
<v-icon class="mr-2">mdi-folder-outline</v-icon>
<strong>Template:</strong>
<span class="detail-value">{{ meeting.projectTemplate }}</span>
</div>
<div class="detail-row" v-if="meeting.duration">
<!-- Scheduled Time -->
<div class="detail-row" v-if="meeting.startTime">
<v-icon class="mr-2">mdi-calendar-clock</v-icon>
<strong>Scheduled:</strong>
<span class="detail-value">{{ formatDateTime(meeting.startTime) }}</span>
</div>
<!-- Duration -->
<div class="detail-row" v-if="meeting.startTime && meeting.endTime">
<v-icon class="mr-2">mdi-timer</v-icon>
<strong>Duration:</strong> {{ meeting.duration }} minutes
<strong>Duration:</strong>
<span class="detail-value">{{ calculateDuration(meeting.startTime, meeting.endTime) }} minutes</span>
</div>
<!-- Status -->
<div class="detail-row">
<v-icon class="mr-2">mdi-check-circle</v-icon>
<strong>Status:</strong>
<v-chip size="small" :color="getStatusColor(meeting.status)">
{{ meeting.status }}
</v-chip>
</div>
<!-- Assigned Employee -->
<div class="detail-row" v-if="meeting.assignedEmployee">
<v-icon class="mr-2">mdi-account-tie</v-icon>
<strong>Assigned To:</strong>
<span class="detail-value">{{ meeting.assignedEmployee }}</span>
</div>
<!-- Completed By -->
<div class="detail-row" v-if="meeting.completedBy">
<v-icon class="mr-2">mdi-account-check</v-icon>
<strong>Completed By:</strong>
<span class="detail-value">{{ meeting.completedBy }}</span>
</div>
<!-- Company -->
<div class="detail-row" v-if="meeting.company">
<v-icon class="mr-2">mdi-domain</v-icon>
<strong>Company:</strong>
<span class="detail-value">{{ meeting.company }}</span>
</div>
<!-- Notes -->
<div class="detail-row" v-if="meeting.notes">
<v-icon class="mr-2">mdi-note-text</v-icon>
<strong>Notes:</strong> {{ meeting.notes }}
<strong>Notes:</strong>
<span class="detail-value">{{ meeting.notes }}</span>
</div>
<div class="detail-row" v-if="meeting.status">
<v-icon class="mr-2">mdi-check-circle</v-icon>
<strong>Status:</strong> {{ meeting.status }}
<!-- Action Buttons -->
<div class="action-buttons">
<v-btn
v-if="meeting.status !== 'Completed'"
@click="handleMarkComplete"
color="success"
variant="elevated"
:loading="isUpdating"
>
<v-icon left>mdi-check</v-icon>
Mark as Completed
</v-btn>
<v-btn
v-if="meeting.status === 'Completed'"
@click="handleCreateEstimate"
color="primary"
variant="elevated"
>
<v-icon left>mdi-file-document-outline</v-icon>
Create Estimate
</v-btn>
</div>
</div>
</Modal>
</template>
<script setup>
import { computed } from "vue";
import { ref, computed } from "vue";
import { useRouter } from "vue-router";
import Modal from "../common/Modal.vue";
import Api from "../../api";
import { useNotificationStore } from "../../stores/notifications-primevue";
const router = useRouter();
const notificationStore = useNotificationStore();
// Props
const props = defineProps({
@ -51,9 +138,11 @@ const props = defineProps({
});
// Emits
const emit = defineEmits(["update:visible", "close"]);
const emit = defineEmits(["update:visible", "close", "meetingUpdated"]);
// Local state
const isUpdating = ref(false);
const showModal = computed({
get() {
return props.visible;
@ -65,7 +154,7 @@ const showModal = computed({
// Modal options
const modalOptions = computed(() => ({
maxWidth: "600px",
maxWidth: "700px",
showCancelButton: false,
confirmButtonText: "Close",
confirmButtonColor: "primary",
@ -76,6 +165,89 @@ const handleClose = () => {
emit("close");
};
const handleMarkComplete = async () => {
if (!props.meeting?.name) return;
try {
isUpdating.value = true;
await Api.updateBidMeeting(props.meeting.name, {
status: "Completed",
});
notificationStore.addNotification({
type: "success",
title: "Meeting Completed",
message: "The meeting has been marked as completed.",
duration: 4000,
});
// Emit event to refresh the calendar
emit("meetingUpdated");
handleClose();
} catch (error) {
console.error("Error marking meeting as complete:", error);
notificationStore.addNotification({
type: "error",
title: "Error",
message: "Failed to update meeting status.",
duration: 5000,
});
} finally {
isUpdating.value = false;
}
};
const handleCreateEstimate = () => {
if (!props.meeting) return;
const addressText = props.meeting.address?.fullAddress || props.meeting.address || "";
const template = props.meeting.projectTemplate || "";
const fromMeeting = props.meeting.name || "";
router.push({
path: "/estimate",
query: {
new: "true",
address: addressText,
template: template,
from: fromMeeting,
},
});
};
const formatDateTime = (dateTimeStr) => {
if (!dateTimeStr) return "";
const date = new Date(dateTimeStr);
return date.toLocaleString("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
const calculateDuration = (startTime, endTime) => {
if (!startTime || !endTime) return 0;
const start = new Date(startTime);
const end = new Date(endTime);
const diffMs = end - start;
return Math.round(diffMs / (1000 * 60)); // Convert to minutes
};
const getStatusColor = (status) => {
const statusColors = {
Unscheduled: "warning",
Scheduled: "info",
Completed: "success",
Cancelled: "error",
};
return statusColors[status] || "default";
};
const formatTimeDisplay = (time) => {
if (!time) return "";
const [hours, minutes] = time.split(":").map(Number);
@ -101,17 +273,39 @@ const formatDate = (dateStr) => {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 0;
}
.detail-row {
display: flex;
align-items: center;
align-items: flex-start;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-row:last-of-type {
border-bottom: none;
}
.detail-row strong {
margin-right: 8px;
min-width: 120px;
color: #666;
font-size: 0.9em;
}
.detail-value {
flex: 1;
color: #333;
word-break: break-word;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 2px solid #e0e0e0;
justify-content: center;
}
</style>