big updates
This commit is contained in:
parent
34f2c110d6
commit
03a230b8f7
14 changed files with 2417 additions and 242 deletions
117
frontend/src/components/modals/MeetingDetailsModal.vue
Normal file
117
frontend/src/components/modals/MeetingDetailsModal.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<Modal v-model:visible="showModal" :options="modalOptions" @confirm="handleClose">
|
||||
<template #title>Meeting Details</template>
|
||||
<div v-if="meeting" class="meeting-details">
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-map-marker</v-icon>
|
||||
<strong>Address:</strong> {{ meeting.address }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="meeting.client">
|
||||
<v-icon class="mr-2">mdi-account</v-icon>
|
||||
<strong>Client:</strong> {{ meeting.client }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-calendar</v-icon>
|
||||
<strong>Date:</strong> {{ formatDate(meeting.date) }}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-clock</v-icon>
|
||||
<strong>Time:</strong> {{ formatTimeDisplay(meeting.scheduledTime) }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="meeting.duration">
|
||||
<v-icon class="mr-2">mdi-timer</v-icon>
|
||||
<strong>Duration:</strong> {{ meeting.duration }} minutes
|
||||
</div>
|
||||
<div class="detail-row" v-if="meeting.notes">
|
||||
<v-icon class="mr-2">mdi-note-text</v-icon>
|
||||
<strong>Notes:</strong> {{ meeting.notes }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="meeting.status">
|
||||
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
||||
<strong>Status:</strong> {{ meeting.status }}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import Modal from "../common/Modal.vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
meeting: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["update:visible", "close"]);
|
||||
|
||||
// Local state
|
||||
const showModal = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(value) {
|
||||
emit("update:visible", value);
|
||||
},
|
||||
});
|
||||
|
||||
// Modal options
|
||||
const modalOptions = computed(() => ({
|
||||
maxWidth: "600px",
|
||||
showCancelButton: false,
|
||||
confirmButtonText: "Close",
|
||||
confirmButtonColor: "primary",
|
||||
}));
|
||||
|
||||
// Methods
|
||||
const handleClose = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
const formatTimeDisplay = (time) => {
|
||||
if (!time) return "";
|
||||
const [hours, minutes] = time.split(":").map(Number);
|
||||
const displayHour = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;
|
||||
const ampm = hours >= 12 ? "PM" : "AM";
|
||||
return `${displayHour}:${minutes.toString().padStart(2, "0")} ${ampm}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.meeting-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.detail-row strong {
|
||||
margin-right: 8px;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
311
frontend/src/components/modals/OnSiteMeetingModal.vue
Normal file
311
frontend/src/components/modals/OnSiteMeetingModal.vue
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue