create estimate

This commit is contained in:
Casey 2025-11-26 16:47:53 -06:00
parent 2ea20a86e3
commit afa161a0cf
15 changed files with 1234 additions and 435 deletions

View file

@ -92,20 +92,35 @@
<!-- Contact Info Card -->
<div class="info-card" v-if="selectedAddressData">
<h3>Contact Information</h3>
<div class="info-grid">
<div class="info-item">
<label>Contact Name:</label>
<span>{{ contactFullName }}</span>
<template v-if="contactsForAddress.length > 0">
<div v-if="contactsForAddress.length > 1" class="contact-selector">
<Dropdown
v-model="selectedContactIndex"
:options="contactOptions"
option-label="label"
option-value="value"
placeholder="Select Contact"
class="w-full"
/>
</div>
<div class="info-item">
<label>Phone:</label>
<span>{{ selectedAddressData.phone || "N/A" }}</span>
<div class="info-grid">
<div class="info-item">
<label>Contact Name:</label>
<span>{{ contactFullName }}</span>
</div>
<div class="info-item">
<label>Phone:</label>
<span>{{ primaryContactPhone }}</span>
</div>
<div class="info-item">
<label>Email:</label>
<span>{{ primaryContactEmail }}</span>
</div>
</div>
<div class="info-item">
<label>Email:</label>
<span>{{ selectedAddressData.emailId || "N/A" }}</span>
</div>
</div>
</template>
<template v-else>
<p>No contacts available for this address.</p>
</template>
</div>
</template>
@ -113,30 +128,34 @@
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData">
<div class="status-card">
<h4>On-Site Meeting</h4>
<Badge
:value="selectedAddressData.customOnsiteMeetingScheduled || 'Not Started'"
<Button
:label="selectedAddressData.customOnsiteMeetingScheduled || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customOnsiteMeetingScheduled)"
@click="handleStatusClick('onsite')"
/>
</div>
<div class="status-card">
<h4>Estimate Sent</h4>
<Badge
:value="selectedAddressData.customEstimateSentStatus || 'Not Started'"
<Button
:label="selectedAddressData.customEstimateSentStatus || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customEstimateSentStatus)"
@click="handleStatusClick('estimate')"
/>
</div>
<div class="status-card">
<h4>Job Status</h4>
<Badge
:value="selectedAddressData.customJobStatus || 'Not Started'"
<Button
:label="selectedAddressData.customJobStatus || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customJobStatus)"
@click="handleStatusClick('job')"
/>
</div>
<div class="status-card">
<h4>Payment Received</h4>
<Badge
:value="selectedAddressData.customPaymentReceivedStatus || 'Not Started'"
<Button
:label="selectedAddressData.customPaymentReceivedStatus || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customPaymentReceivedStatus)"
@click="handleStatusClick('payment')"
/>
</div>
</div>
@ -204,6 +223,7 @@ import { computed, ref, watch, onMounted } from "vue";
import Badge from "primevue/badge";
import Button from "primevue/button";
import Dialog from "primevue/dialog";
import Dropdown from "primevue/dropdown";
import LeafletMap from "../common/LeafletMap.vue";
import ClientInformationForm from "./ClientInformationForm.vue";
import ContactInformationForm from "./ContactInformationForm.vue";
@ -252,10 +272,7 @@ const formData = ref({
pincode: "",
city: "",
state: "",
firstName: "",
lastName: "",
phoneNumber: "",
email: "",
contacts: [],
});
// Initialize form data when component mounts
@ -335,12 +352,51 @@ const fullAddress = computed(() => {
return DataUtils.calculateFullAddress(selectedAddressData.value);
});
// Get contacts linked to the selected address
const contactsForAddress = computed(() => {
if (!selectedAddressData.value?.customLinkedContacts || !props.clientData?.contacts) return [];
return selectedAddressData.value.customLinkedContacts
.map((link) => props.clientData.contacts.find((c) => c.name === link.contact))
.filter(Boolean);
});
// Selected contact index for display
const selectedContactIndex = ref(0);
// Options for contact dropdown
const contactOptions = computed(() =>
contactsForAddress.value.map((c, i) => ({
label:
c.fullName || `${c.firstName || ""} ${c.lastName || ""}`.trim() || "Unnamed Contact",
value: i,
})),
);
// Selected contact for display
const selectedContact = computed(
() => contactsForAddress.value[selectedContactIndex.value] || null,
);
// Calculate contact full name
const contactFullName = computed(() => {
if (!selectedAddressData.value) return "N/A";
const firstName = selectedAddressData.value.customContactFirstName || "";
const lastName = selectedAddressData.value.customContactLastName || "";
return `${firstName} ${lastName}`.trim() || "N/A";
if (!selectedContact.value) return "N/A";
return (
selectedContact.value.fullName ||
`${selectedContact.value.firstName || ""} ${selectedContact.value.lastName || ""}`.trim() ||
"N/A"
);
});
// Calculate primary contact phone
const primaryContactPhone = computed(() => {
if (!selectedContact.value) return "N/A";
return selectedContact.value.phone || selectedContact.value.mobileNo || "N/A";
});
// Calculate primary contact email
const primaryContactEmail = computed(() => {
if (!selectedContact.value) return "N/A";
return selectedContact.value.emailId || selectedContact.value.customEmail || "N/A";
});
// Form validation
@ -352,8 +408,10 @@ const isFormValid = computed(() => {
const hasPincode = formData.value.pincode?.trim();
const hasCity = formData.value.city?.trim();
const hasState = formData.value.state?.trim();
const hasFirstName = formData.value.firstName?.trim();
const hasLastName = formData.value.lastName?.trim();
const hasContacts = formData.value.contacts && formData.value.contacts.length > 0;
const primaryContact = formData.value.contacts?.find((c) => c.isPrimary);
const hasFirstName = primaryContact?.firstName?.trim();
const hasLastName = primaryContact?.lastName?.trim();
return (
hasCustomerName &&
@ -363,6 +421,7 @@ const isFormValid = computed(() => {
hasPincode &&
hasCity &&
hasState &&
hasContacts &&
hasFirstName &&
hasLastName
);
@ -372,7 +431,7 @@ const isFormValid = computed(() => {
const getStatusSeverity = (status) => {
switch (status) {
case "Not Started":
return "secondary";
return "danger";
case "In Progress":
return "warn";
case "Completed":
@ -382,6 +441,40 @@ const getStatusSeverity = (status) => {
}
};
// Handle status button clicks
const handleStatusClick = (type) => {
let status;
let path;
switch (type) {
case "onsite":
status = selectedAddressData.value.customOnsiteMeetingScheduled || "Not Started";
path = "/schedule-onsite";
break;
case "estimate":
status = selectedAddressData.value.customEstimateSentStatus || "Not Started";
path = "/estimate";
break;
case "job":
status = selectedAddressData.value.customJobStatus || "Not Started";
path = "/job";
break;
case "payment":
status = selectedAddressData.value.customPaymentReceivedStatus || "Not Started";
path = "/invoices";
break;
default:
return;
}
const query = { address: fullAddress.value };
if (status === "Not Started") {
query.new = true;
}
router.push({ path, query });
};
// Form methods
const resetForm = () => {
formData.value = {
@ -393,10 +486,7 @@ const resetForm = () => {
pincode: "",
city: "",
state: "",
firstName: "",
lastName: "",
phoneNumber: "",
email: "",
contacts: [],
};
availableContacts.value = [];
isNewClientMode.value = false;
@ -416,15 +506,20 @@ const populateFormFromClientData = () => {
pincode: selectedAddressData.value.pincode || "",
city: selectedAddressData.value.city || "",
state: selectedAddressData.value.state || "",
firstName: selectedAddressData.value.customContactFirstName || "",
lastName: selectedAddressData.value.customContactLastName || "",
phoneNumber: selectedAddressData.value.phone || "",
email: selectedAddressData.value.emailId || "",
contacts:
contactsForAddress.value.map((c) => ({
firstName: c.firstName || "",
lastName: c.lastName || "",
phoneNumber: c.phone || c.mobileNo || "",
email: c.emailId || c.customEmail || "",
contactRole: c.role || "",
isPrimary: c.isPrimaryContact || false,
})) || [],
};
// Populate available contacts if any
if (selectedAddressData.value.contacts && selectedAddressData.value.contacts.length > 0) {
availableContacts.value = selectedAddressData.value.contacts;
if (contactsForAddress.value.length > 0) {
availableContacts.value = contactsForAddress.value;
}
};
@ -483,10 +578,7 @@ const handleSave = async () => {
pincode: formData.value.pincode,
city: formData.value.city,
state: formData.value.state,
firstName: formData.value.firstName,
lastName: formData.value.lastName,
phoneNumber: formData.value.phoneNumber,
email: formData.value.email,
contacts: formData.value.contacts,
};
console.log("Upserting client with data:", clientData);
@ -606,6 +698,10 @@ const handleCancel = () => {
font-size: 0.95rem;
}
.contact-selector {
margin-bottom: 1rem;
}
/* Form input styling */
.info-item :deep(.p-inputtext),
.info-item :deep(.p-autocomplete),