custom_ui/frontend/src/components/modals/OnSiteMeetingModal.vue
2025-11-21 12:29:31 -06:00

311 lines
6 KiB
Vue

<template>
<!-- New Meeting Creation Modal -->
<Modal
v-model:visible="showModal"
:options="modalOptions"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<template #title>Schedule New On-Site 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-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
v-model:visible="showAddressSearchModal"
: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>{{ address }}</span>
</div>
</div>
</div>
</Modal>
</template>
<script setup>
import { ref, computed, watch } from "vue";
import Modal from "../common/Modal.vue";
import InputText from "primevue/inputtext";
import Textarea from "primevue/textarea";
import Button from "primevue/button";
import { useNotificationStore } from "../../stores/notifications-primevue";
import Api from "../../api";
const notificationStore = useNotificationStore();
// 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 isFormValid = ref(false);
// Form data
const formData = ref({
address: "",
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;
isFormValid.value = hasValidAddress;
};
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);
// Ensure results is always an array
// const safeResults = Array.isArray(results) ? results : [];
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 = (address) => {
formData.value.address = address;
showAddressSearchModal.value = false;
validateForm();
};
const closeAddressSearch = () => {
showAddressSearchModal.value = false;
};
const handleConfirm = () => {
if (!isFormValid.value) return;
emit("confirm", { ...formData.value });
resetForm();
};
const handleCancel = () => {
emit("cancel");
resetForm();
};
const resetForm = () => {
formData.value = {
address: props.initialAddress || "",
notes: "",
};
validateForm();
};
// Watch for prop changes
watch(
() => props.initialAddress,
(newAddress) => {
formData.value.address = newAddress || "";
validateForm();
},
{ immediate: true },
);
watch(
() => props.visible,
(isVisible) => {
if (isVisible) {
resetForm();
}
},
);
// 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;
}
</style>