custom_ui/frontend/src/components/modals/MeetingDetailsModal.vue
2026-01-15 17:32:06 -06:00

381 lines
10 KiB
Vue

<template>
<Modal :visible="showModal" @update:visible="showModal = $event" :options="modalOptions" @confirm="handleClose">
<template #title>
<div class="modal-header">
<i class="pi pi-calendar" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
Meeting Details
</div>
</template>
<div v-if="meeting" class="meeting-details">
<!-- Status Badge -->
<div class="status-section">
<div class="status-badge" :class="`status-${meeting.status?.toLowerCase()}`">
<i class="pi pi-circle-fill"></i>
{{ meeting.status }}
</div>
</div>
<!-- Key Information Grid -->
<div class="info-grid">
<!-- Customer Name -->
<div class="info-card" v-if="customerName">
<div class="info-label">
<i class="pi pi-user"></i>
Customer
</div>
<div class="info-value">{{ customerName }}</div>
<div class="info-meta" v-if="meeting.partyType">{{ meeting.partyType }}</div>
</div>
<!-- Project Template -->
<div class="info-card" v-if="meeting.projectTemplate">
<div class="info-label">
<i class="pi pi-folder"></i>
Project Type
</div>
<div class="info-value">{{ meeting.projectTemplate }}</div>
</div>
<!-- Scheduled Time -->
<div class="info-card" v-if="meeting.startTime">
<div class="info-label">
<i class="pi pi-clock"></i>
Scheduled
</div>
<div class="info-value">{{ formatDateTime(meeting.startTime) }}</div>
<div class="info-meta" v-if="meeting.endTime">Duration: {{ calculateDuration(meeting.startTime, meeting.endTime) }} min</div>
</div>
<!-- Assigned Employee -->
<div class="info-card" v-if="meeting.assignedEmployee">
<div class="info-label">
<i class="pi pi-user-edit"></i>
Assigned To
</div>
<div class="info-value">{{ meeting.assignedEmployee }}</div>
</div>
</div>
<!-- Address Section -->
<div class="section-divider">
<i class="pi pi-map-marker"></i>
<span>Location</span>
</div>
<div class="address-section">
<div class="address-text">
<strong>{{ addressText }}</strong>
<div class="meeting-id">ID: {{ meeting.name }}</div>
</div>
<div v-if="hasCoordinates" 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>
<!-- Contact Information -->
<div class="section-divider" v-if="contactInfo">
<i class="pi pi-phone"></i>
<span>Contact Information</span>
</div>
<div class="contact-section" v-if="contactInfo">
<div class="contact-item">
<i class="pi pi-user"></i>
<span class="contact-label">Name:</span>
<span class="contact-value">{{ contactInfo.fullName }}</span>
</div>
<div class="contact-item" v-if="contactInfo.role">
<i class="pi pi-briefcase"></i>
<span class="contact-label">Role:</span>
<span class="contact-value">{{ contactInfo.role }}</span>
</div>
<div class="contact-item" v-if="contactInfo.phone">
<i class="pi pi-phone"></i>
<span class="contact-label">Phone:</span>
<a :href="`tel:${contactInfo.phone}`" class="contact-value contact-link">{{ contactInfo.phone }}</a>
</div>
<div class="contact-item" v-if="contactInfo.email">
<i class="pi pi-envelope"></i>
<span class="contact-label">Email:</span>
<a :href="`mailto:${contactInfo.email}`" class="contact-value contact-link">{{ contactInfo.email }}</a>
</div>
</div>
<!-- Notes -->
<div v-if="meeting.notes" class="notes-section">
<div class="section-divider">
<i class="pi pi-file-edit"></i>
<span>Notes</span>
</div>
<div class="notes-content">{{ meeting.notes }}</div>
</div>
<!-- Additional Info -->
<div class="additional-info" v-if="meeting.company || meeting.completedBy">
<div class="info-item" v-if="meeting.company">
<i class="pi pi-building"></i>
<span>{{ meeting.company }}</span>
</div>
<div class="info-item" v-if="meeting.completedBy">
<i class="pi pi-check-circle"></i>
<span>Completed by: {{ meeting.completedBy }}</span>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<v-btn
v-if="meeting.status !== 'Completed' && meeting.status !== 'Unscheduled'"
@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 { 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({
visible: {
type: Boolean,
default: false,
},
meeting: {
type: Object,
default: null,
},
});
// Emits
const emit = defineEmits(["update:visible", "close", "meetingUpdated"]);
// Local state
const isUpdating = ref(false);
const showModal = computed({
get() {
return props.visible;
},
set(value) {
emit("update:visible", value);
},
});
// Modal options
const modalOptions = computed(() => ({
maxWidth: "800px",
showCancelButton: false,
confirmButtonText: "Close",
confirmButtonColor: "primary",
}));
// Computed properties for data extraction
const customerName = computed(() => {
if (props.meeting?.address?.customerName) {
return props.meeting.address.customerName;
}
if (props.meeting?.partyName) {
return props.meeting.partyName;
}
return null;
});
const addressText = computed(() => {
return props.meeting?.address?.fullAddress || props.meeting?.address || "";
});
const hasCoordinates = computed(() => {
const lat = props.meeting?.address?.customLatitude || props.meeting?.address?.latitude;
const lon = props.meeting?.address?.customLongitude || props.meeting?.address?.longitude;
return lat && lon && parseFloat(lat) !== 0 && parseFloat(lon) !== 0;
});
const mapUrl = computed(() => {
if (!hasCoordinates.value) return "";
const lat = parseFloat(props.meeting?.address?.customLatitude || props.meeting?.address?.latitude);
const lon = parseFloat(props.meeting?.address?.customLongitude || props.meeting?.address?.longitude);
const zoom = 15;
// 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}`;
});
const contactInfo = computed(() => {
console.log('=== CONTACT DEBUG ===');
console.log('Full meeting object:', props.meeting);
console.log('Meeting contact value:', props.meeting?.contact);
console.log('Contact type:', typeof props.meeting?.contact);
const contact = props.meeting?.contact;
if (!contact) {
console.log('No contact found - returning null');
return null;
}
// Handle both string and object contact
if (typeof contact === 'string') {
console.log('Contact is a string:', contact);
return { fullName: contact };
}
// Log the contact object to see what properties are available
console.log('Contact object keys:', Object.keys(contact));
console.log('Contact object:', contact);
const contactData = {
fullName: contact.name || contact.fullName || contact.contactName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || '',
phone: contact.phone || contact.mobileNo || contact.mobile || contact.phoneNos?.[0]?.phone || '',
email: contact.emailId || contact.email || contact.emailAddress || contact.emailIds?.[0]?.emailId || '',
role: contact.role || contact.designation || '',
};
console.log('Extracted contact data:', contactData);
return contactData;
});
// Methods
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 || "";
const contactName = props.meeting.contact?.name || "";
router.push({
path: "/estimate",
query: {
new: "true",
address: addressText,
"from-meeting": fromMeeting,
template: template,
contact: contactName,
},
});
};
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 formatDateTime = (dateString) => {
if (!dateString) return "";
return new Date(dateString).toLocaleString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
};
</script>
<style scoped>
.meeting-details {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 0;
}
.detail-row {
display: flex;
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 {
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>