551 lines
14 KiB
Vue
551 lines
14 KiB
Vue
<template>
|
|
<div>
|
|
<!-- New Meeting Creation Modal -->
|
|
<Modal
|
|
:visible="showModal"
|
|
@update:visible="showModal = $event"
|
|
:options="modalOptions"
|
|
@confirm="handleConfirm"
|
|
@cancel="handleCancel"
|
|
>
|
|
<template #title>Schedule New Bid Meeting</template>
|
|
<div class="new-meeting-form">
|
|
<div class="form-group">
|
|
<label for="meeting-address">Address: <span class="required">*</span></label>
|
|
<div class="address-input-group">
|
|
<InputText
|
|
id="meeting-address"
|
|
v-model="formData.address"
|
|
class="address-input"
|
|
placeholder="Enter meeting address"
|
|
@input="validateForm"
|
|
/>
|
|
<Button
|
|
label="Search"
|
|
icon="pi pi-search"
|
|
size="small"
|
|
:disabled="!formData.address.trim()"
|
|
@click="searchAddress"
|
|
class="search-btn"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="meeting-contact">Contact: <span class="required">*</span></label>
|
|
<Select
|
|
id="meeting-contact"
|
|
v-model="formData.contact"
|
|
:options="availableContacts"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
:disabled="!formData.addressName || availableContacts.length === 0"
|
|
placeholder="Select a contact"
|
|
class="w-full"
|
|
@change="validateForm"
|
|
>
|
|
<template #option="slotProps">
|
|
<div class="contact-option">
|
|
<div class="contact-name">{{ slotProps.option.displayName }}</div>
|
|
<div class="contact-details">
|
|
<span v-if="slotProps.option.role" class="contact-role">{{ slotProps.option.role }}</span>
|
|
<span v-if="slotProps.option.email" class="contact-email">{{ slotProps.option.email }}</span>
|
|
<span v-if="slotProps.option.phone" class="contact-phone">{{ slotProps.option.phone }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="meeting-project-template">Project Template (Optional):</label>
|
|
<Select
|
|
id="meeting-project-template"
|
|
v-model="formData.projectTemplate"
|
|
:options="availableProjectTemplates"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
placeholder="Select a project template"
|
|
class="w-full"
|
|
showClear
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="meeting-notes">Notes (Optional):</label>
|
|
<Textarea
|
|
id="meeting-notes"
|
|
v-model="formData.notes"
|
|
class="w-full"
|
|
placeholder="Additional notes..."
|
|
rows="3"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
|
|
<!-- Address Search Results Modal -->
|
|
<Modal
|
|
:visible="showAddressSearchModal"
|
|
@update:visible="showAddressSearchModal = $event"
|
|
:options="searchModalOptions"
|
|
@confirm="closeAddressSearch"
|
|
>
|
|
<template #title>Address Search Results</template>
|
|
<div class="address-search-results">
|
|
<div v-if="addressSearchResults.length === 0" class="no-results">
|
|
<i class="pi pi-info-circle"></i>
|
|
<p>No addresses found matching your search.</p>
|
|
</div>
|
|
<div v-else class="results-list">
|
|
<div
|
|
v-for="(address, index) in addressSearchResults"
|
|
:key="index"
|
|
class="address-result-item"
|
|
@click="selectAddress(address)"
|
|
>
|
|
<i class="pi pi-map-marker"></i>
|
|
<span>{{ typeof address === 'string' ? address : (address.fullAddress || address.name) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import Modal from "../common/Modal.vue";
|
|
import InputText from "primevue/inputtext";
|
|
import Textarea from "primevue/textarea";
|
|
import Button from "primevue/button";
|
|
import Select from "primevue/select";
|
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
|
import { useCompanyStore } from "../../stores/company";
|
|
import Api from "../../api";
|
|
|
|
const notificationStore = useNotificationStore();
|
|
const companyStore = useCompanyStore();
|
|
const route = useRoute();
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
initialAddress: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
});
|
|
|
|
// Emits
|
|
const emit = defineEmits(["update:visible", "confirm", "cancel"]);
|
|
|
|
// Local state
|
|
const showModal = computed({
|
|
get() {
|
|
return props.visible;
|
|
},
|
|
set(value) {
|
|
emit("update:visible", value);
|
|
},
|
|
});
|
|
|
|
const showAddressSearchModal = ref(false);
|
|
const addressSearchResults = ref([]);
|
|
const availableContacts = ref([]);
|
|
const availableProjectTemplates = ref([]);
|
|
const selectedAddressDetails = ref(null);
|
|
const isFormValid = ref(false);
|
|
|
|
// Form data
|
|
const formData = ref({
|
|
address: "",
|
|
addressName: "",
|
|
contact: "",
|
|
projectTemplate: "",
|
|
notes: "",
|
|
});
|
|
|
|
// Form validation state
|
|
|
|
// Modal options
|
|
const modalOptions = computed(() => ({
|
|
maxWidth: "500px",
|
|
persistent: true,
|
|
confirmButtonText: "Create",
|
|
cancelButtonText: "Cancel",
|
|
confirmButtonColor: "primary",
|
|
showConfirmButton: true,
|
|
showCancelButton: true,
|
|
confirmButtonProps: {
|
|
disabled: !isFormValid.value,
|
|
},
|
|
}));
|
|
|
|
const searchModalOptions = computed(() => ({
|
|
maxWidth: "600px",
|
|
showCancelButton: false,
|
|
confirmButtonText: "Close",
|
|
confirmButtonColor: "primary",
|
|
}));
|
|
|
|
// Methods
|
|
const validateForm = () => {
|
|
const hasValidAddress = formData.value.address && formData.value.address.trim().length > 0;
|
|
const hasValidAddressName = formData.value.addressName && formData.value.addressName.trim().length > 0;
|
|
const hasValidContact = formData.value.contact && formData.value.contact.trim().length > 0;
|
|
isFormValid.value = hasValidAddress && hasValidAddressName && hasValidContact;
|
|
};
|
|
|
|
const searchAddress = async () => {
|
|
const searchTerm = formData.value.address.trim();
|
|
if (!searchTerm) return;
|
|
|
|
try {
|
|
const results = await Api.searchAddresses(searchTerm);
|
|
console.info("Address search results:", results);
|
|
|
|
// Store full address objects instead of just strings
|
|
addressSearchResults.value = results;
|
|
|
|
if (results.length === 0) {
|
|
notificationStore.addWarning("No addresses found matching your search criteria.");
|
|
} else {
|
|
showAddressSearchModal.value = true;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error searching addresses:", error);
|
|
addressSearchResults.value = [];
|
|
notificationStore.addError("Failed to search addresses. Please try again.");
|
|
}
|
|
};
|
|
|
|
const selectAddress = async (addressData) => {
|
|
// Get the address string for the API call
|
|
const addressString = typeof addressData === 'string' ? addressData : (addressData.fullAddress || addressData.name);
|
|
|
|
// Set the display address immediately
|
|
formData.value.address = addressString;
|
|
showAddressSearchModal.value = false;
|
|
|
|
try {
|
|
// Fetch the full address details with contacts
|
|
const fullAddressDetails = await Api.getAddressByFullAddress(addressString);
|
|
console.info("Fetched address details:", fullAddressDetails);
|
|
|
|
// Store the fetched address details
|
|
selectedAddressDetails.value = fullAddressDetails;
|
|
|
|
// Set the address name for the API request
|
|
formData.value.addressName = fullAddressDetails.name;
|
|
|
|
// Populate contacts from the fetched address
|
|
if (fullAddressDetails.contacts && Array.isArray(fullAddressDetails.contacts)) {
|
|
availableContacts.value = fullAddressDetails.contacts.map(contact => ({
|
|
label: contact.fullName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
value: contact.name,
|
|
displayName: contact.fullName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(),
|
|
role: contact.role || contact.designation || '',
|
|
email: contact.email || contact.emailId || '',
|
|
phone: contact.phone || contact.mobileNo || ''
|
|
}));
|
|
|
|
// Auto-select primary contact if available, otherwise first contact if only one
|
|
if (fullAddressDetails.primaryContact) {
|
|
formData.value.contact = fullAddressDetails.primaryContact;
|
|
} else if (availableContacts.value.length === 1) {
|
|
formData.value.contact = availableContacts.value[0].value;
|
|
} else {
|
|
formData.value.contact = "";
|
|
}
|
|
} else {
|
|
availableContacts.value = [];
|
|
formData.value.contact = "";
|
|
notificationStore.addWarning("No contacts found for this address.");
|
|
}
|
|
|
|
validateForm();
|
|
} catch (error) {
|
|
console.error("Error fetching address details:", error);
|
|
notificationStore.addError("Failed to fetch address details. Please try again.");
|
|
|
|
// Reset on error
|
|
formData.value.addressName = "";
|
|
availableContacts.value = [];
|
|
formData.value.contact = "";
|
|
selectedAddressDetails.value = null;
|
|
validateForm();
|
|
}
|
|
};
|
|
|
|
const closeAddressSearch = () => {
|
|
showAddressSearchModal.value = false;
|
|
};
|
|
|
|
const fetchProjectTemplates = async () => {
|
|
try {
|
|
const company = companyStore.currentCompany;
|
|
if (!company) {
|
|
console.warn("No company selected, cannot fetch project templates");
|
|
return;
|
|
}
|
|
|
|
const templates = await Api.getJobTemplates(company);
|
|
console.info("Fetched project templates:", templates);
|
|
|
|
if (templates && Array.isArray(templates)) {
|
|
availableProjectTemplates.value = templates.map(template => ({
|
|
label: template.name,
|
|
value: template.name
|
|
}));
|
|
} else {
|
|
availableProjectTemplates.value = [];
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching project templates:", error);
|
|
availableProjectTemplates.value = [];
|
|
notificationStore.addWarning("Failed to load project templates.");
|
|
}
|
|
};
|
|
|
|
const handleConfirm = () => {
|
|
if (!isFormValid.value) return;
|
|
|
|
// Send only the necessary data (addressName and contact, not full address)
|
|
const confirmData = {
|
|
address: formData.value.addressName,
|
|
contact: formData.value.contact,
|
|
projectTemplate: formData.value.projectTemplate || null,
|
|
notes: formData.value.notes,
|
|
};
|
|
|
|
console.log("BidMeetingModal - Emitting confirm with data:", confirmData);
|
|
|
|
emit("confirm", confirmData);
|
|
resetForm();
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
showModal.value = false;
|
|
resetForm();
|
|
};
|
|
|
|
const resetForm = () => {
|
|
formData.value = {
|
|
address: props.initialAddress || "",
|
|
addressName: "",
|
|
contact: "",
|
|
projectTemplate: "",
|
|
notes: "",
|
|
};
|
|
availableContacts.value = [];
|
|
validateForm();
|
|
};
|
|
|
|
// Watch for prop changes
|
|
watch(
|
|
() => props.initialAddress,
|
|
(newAddress) => {
|
|
formData.value.address = newAddress || "";
|
|
validateForm();
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(
|
|
() => companyStore.currentCompany,
|
|
async (newCompany) => {
|
|
if (newCompany && props.visible) {
|
|
await fetchProjectTemplates();
|
|
}
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.visible,
|
|
async (isVisible) => {
|
|
if (isVisible) {
|
|
resetForm();
|
|
|
|
// Fetch project templates
|
|
await fetchProjectTemplates();
|
|
|
|
// Auto-select template from query parameter if provided
|
|
if (route.query.template) {
|
|
const templateName = decodeURIComponent(route.query.template);
|
|
const templateExists = availableProjectTemplates.value.some(
|
|
t => t.value === templateName
|
|
);
|
|
if (templateExists) {
|
|
formData.value.projectTemplate = templateName;
|
|
}
|
|
}
|
|
|
|
// If there's an initial address, automatically search and fetch it
|
|
if (formData.value.address && formData.value.address.trim()) {
|
|
try {
|
|
const results = await Api.searchAddresses(formData.value.address.trim());
|
|
console.info("Auto-search results for initial address:", results);
|
|
|
|
if (results.length === 1) {
|
|
// Auto-select if only one result
|
|
await selectAddress(results[0]);
|
|
} else if (results.length > 1) {
|
|
// Try to find exact match
|
|
const exactMatch = results.find(addr => {
|
|
const addrString = typeof addr === 'string' ? addr : (addr.fullAddress || addr.name);
|
|
return addrString === formData.value.address;
|
|
});
|
|
|
|
if (exactMatch) {
|
|
await selectAddress(exactMatch);
|
|
} else {
|
|
// Show search results if multiple matches
|
|
addressSearchResults.value = results;
|
|
showAddressSearchModal.value = true;
|
|
}
|
|
} else {
|
|
notificationStore.addWarning("No addresses found for the provided address.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error auto-searching address:", error);
|
|
notificationStore.addError("Failed to load address details.");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
// Initial validation
|
|
validateForm();
|
|
</script>
|
|
|
|
<style scoped>
|
|
.new-meeting-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: 500;
|
|
color: #333;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.required {
|
|
color: #e74c3c;
|
|
}
|
|
|
|
.address-input-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.address-input {
|
|
flex: 1;
|
|
}
|
|
|
|
.search-btn {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.address-search-results {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.no-results {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: #666;
|
|
}
|
|
|
|
.no-results i {
|
|
font-size: 2em;
|
|
color: #f39c12;
|
|
margin-bottom: 10px;
|
|
display: block;
|
|
}
|
|
|
|
.results-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.address-result-item {
|
|
padding: 12px 16px;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.address-result-item:hover {
|
|
background-color: #f8f9fa;
|
|
border-color: #2196f3;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.address-result-item i {
|
|
color: #2196f3;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.address-result-item span {
|
|
flex: 1;
|
|
font-size: 0.9em;
|
|
color: #333;
|
|
}
|
|
|
|
.contact-option {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.contact-name {
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.contact-details {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
font-size: 0.85em;
|
|
color: #666;
|
|
}
|
|
|
|
.contact-role {
|
|
color: #2196f3;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.contact-email,
|
|
.contact-phone {
|
|
color: #666;
|
|
}
|
|
|
|
.contact-email::before {
|
|
content: "📧 ";
|
|
}
|
|
|
|
.contact-phone::before {
|
|
content: "📞 ";
|
|
}
|
|
</style>
|