create estimate
This commit is contained in:
parent
2ea20a86e3
commit
afa161a0cf
15 changed files with 1234 additions and 435 deletions
|
|
@ -2,10 +2,10 @@
|
|||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3>Client Information</h3>
|
||||
<div class="toggle-container" v-if="!isEditMode">
|
||||
<label for="new-client-toggle" class="toggle-label">New Client</label>
|
||||
<ToggleSwitch v-model="isNewClient" inputId="new-client-toggle" />
|
||||
</div>
|
||||
<label class="toggle-container" v-if="!isEditMode">
|
||||
<v-switch v-model="isNewClient" color="success" />
|
||||
<span class="toggle-label">New Client</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
|
|
@ -23,10 +23,9 @@
|
|||
@click="searchCustomers"
|
||||
:disabled="isSubmitting || !localFormData.customerName.trim()"
|
||||
size="small"
|
||||
class="iconoir-btn"
|
||||
>
|
||||
<IconoirMagnifyingGlass width="20" height="20" />
|
||||
</Button>
|
||||
icon="pi pi-search"
|
||||
class="search-btn"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
|
|
@ -77,11 +76,9 @@
|
|||
import { ref, watch, computed } from "vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Select from "primevue/select";
|
||||
import ToggleSwitch from "primevue/toggleswitch";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
import { DocMagnifyingGlass as IconoirMagnifyingGlass } from "@iconoir/vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
|
|
@ -189,11 +186,12 @@ defineExpose({
|
|||
.toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
|
|
@ -308,6 +306,28 @@ defineExpose({
|
|||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.search-btn:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
|
|||
|
|
@ -2,111 +2,102 @@
|
|||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3>Contact Information</h3>
|
||||
<div class="toggle-container" v-if="!isEditMode">
|
||||
<label for="new-contact-toggle" class="toggle-label">New Contact</label>
|
||||
<ToggleSwitch
|
||||
v-model="isNewContact"
|
||||
inputId="new-contact-toggle"
|
||||
:disabled="isNewClientLocked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<!-- Select existing contact mode -->
|
||||
<template v-if="!isNewContact">
|
||||
<div class="form-field full-width">
|
||||
<label for="contact-select"> Contact <span class="required">*</span> </label>
|
||||
<Select
|
||||
id="contact-select"
|
||||
v-model="selectedContact"
|
||||
:options="contactOptions"
|
||||
optionLabel="label"
|
||||
:disabled="isSubmitting || contactOptions.length === 0"
|
||||
placeholder="Select a contact"
|
||||
class="w-full"
|
||||
@change="handleContactSelect"
|
||||
/>
|
||||
<small v-if="contactOptions.length === 0" class="helper-text">
|
||||
No contacts available. Toggle "New Contact" to add one.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="contact-phone">Phone</label>
|
||||
<InputText
|
||||
id="contact-phone"
|
||||
v-model="localFormData.phoneNumber"
|
||||
disabled
|
||||
class="w-full"
|
||||
<div
|
||||
v-for="(contact, index) in localFormData.contacts"
|
||||
:key="index"
|
||||
class="contact-item"
|
||||
>
|
||||
<div class="contact-header">
|
||||
<h4>Contact {{ index + 1 }}</h4>
|
||||
<Button
|
||||
v-if="localFormData.contacts.length > 1"
|
||||
@click="removeContact(index)"
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Delete"
|
||||
class="remove-btn"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="contact-email">Email</label>
|
||||
<InputText
|
||||
id="contact-email"
|
||||
v-model="localFormData.email"
|
||||
disabled
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- New contact mode -->
|
||||
<template v-else>
|
||||
<div class="form-field full-width">
|
||||
<div class="checkbox-container">
|
||||
<Checkbox
|
||||
v-model="sameAsClientName"
|
||||
inputId="same-as-client"
|
||||
:binary="true"
|
||||
:disabled="isSubmitting || isEditMode || !isNewClientLocked"
|
||||
/>
|
||||
<label for="same-as-client" class="checkbox-label">
|
||||
Same as Client Name
|
||||
</label>
|
||||
<div class="form-rows">
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label :for="`first-name-${index}`">
|
||||
First Name <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`first-name-${index}`"
|
||||
v-model="contact.firstName"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Enter first name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`last-name-${index}`">
|
||||
Last Name <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`last-name-${index}`"
|
||||
v-model="contact.lastName"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Enter last name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`contact-role-${index}`">Role</label>
|
||||
<Select
|
||||
:id="`contact-role-${index}`"
|
||||
v-model="contact.contactRole"
|
||||
:options="roleOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Select a role"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label :for="`email-${index}`">Email</label>
|
||||
<InputText
|
||||
:id="`email-${index}`"
|
||||
v-model="contact.email"
|
||||
:disabled="isSubmitting"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`phone-number-${index}`">Phone</label>
|
||||
<InputText
|
||||
:id="`phone-number-${index}`"
|
||||
v-model="contact.phoneNumber"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="(555) 123-4567"
|
||||
class="w-full"
|
||||
@input="formatPhone(index, $event)"
|
||||
@keydown="handlePhoneKeydown($event, index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<v-checkbox
|
||||
v-model="contact.isPrimary"
|
||||
label="Primary Contact"
|
||||
:disabled="isSubmitting"
|
||||
@change="setPrimary(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="first-name"> First Name <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="first-name"
|
||||
v-model="localFormData.firstName"
|
||||
:disabled="isSubmitting || sameAsClientName"
|
||||
placeholder="Enter first name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="last-name"> Last Name <span class="required">*</span> </label>
|
||||
<InputText
|
||||
id="last-name"
|
||||
v-model="localFormData.lastName"
|
||||
:disabled="isSubmitting || sameAsClientName"
|
||||
placeholder="Enter last name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="phone-number">Phone</label>
|
||||
<InputText
|
||||
id="phone-number"
|
||||
v-model="localFormData.phoneNumber"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="(555) 123-4567"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="email">Email</label>
|
||||
<InputText
|
||||
id="email"
|
||||
v-model="localFormData.email"
|
||||
:disabled="isSubmitting"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="form-field full-width">
|
||||
<Button label="Add another contact" @click="addContact" :disabled="isSubmitting" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -115,8 +106,7 @@
|
|||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Select from "primevue/select";
|
||||
import ToggleSwitch from "primevue/toggleswitch";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import Button from "primevue/button";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
|
|
@ -135,131 +125,137 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
availableContacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData", "newContactToggle"]);
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => props.formData,
|
||||
get: () => {
|
||||
if (!props.formData.contacts || props.formData.contacts.length === 0) {
|
||||
props.formData.contacts = [
|
||||
{
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return props.formData;
|
||||
},
|
||||
set: (value) => emit("update:formData", value),
|
||||
});
|
||||
|
||||
// Default to true for new-client flows; if editing keep it off
|
||||
const isNewContact = ref(!props.isEditMode);
|
||||
const selectedContact = ref(null);
|
||||
const sameAsClientName = ref(false);
|
||||
const roleOptions = ref([
|
||||
{ label: "Owner", value: "Owner" },
|
||||
{ label: "Property Manager", value: "Property Manager" },
|
||||
{ label: "Tenant", value: "Tenant" },
|
||||
{ label: "Builder", value: "Builder" },
|
||||
{ label: "Neighbor", value: "Neighbor" },
|
||||
{ label: "Family Member", value: "Family Member" },
|
||||
{ label: "Realtor", value: "Realtor" },
|
||||
{ label: "Other", value: "Other" },
|
||||
]);
|
||||
|
||||
// Compute contact options from available contacts
|
||||
const contactOptions = computed(() => {
|
||||
if (!props.availableContacts || props.availableContacts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return props.availableContacts.map((contact) => ({
|
||||
label: `${contact.firstName} ${contact.lastName}`,
|
||||
value: contact,
|
||||
}));
|
||||
});
|
||||
|
||||
// Ensure New Contact is ON and locked when New Client is ON
|
||||
watch(
|
||||
() => props.isNewClientLocked,
|
||||
(locked) => {
|
||||
if (locked) {
|
||||
isNewContact.value = true;
|
||||
} else {
|
||||
isNewContact.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// On mount, set isNewContact to true if isNewClientLocked is true
|
||||
// Ensure at least one contact
|
||||
onMounted(() => {
|
||||
if (props.isNewClientLocked) {
|
||||
isNewContact.value = true;
|
||||
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
||||
localFormData.value.contacts = [
|
||||
{
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-check "Same as Client Name" when customer type is Individual
|
||||
watch(
|
||||
() => props.formData.customerType,
|
||||
(customerType) => {
|
||||
if (customerType === "Individual" && props.isNewClientLocked && !props.isEditMode) {
|
||||
sameAsClientName.value = true;
|
||||
const addContact = () => {
|
||||
localFormData.value.contacts.push({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: false,
|
||||
});
|
||||
};
|
||||
|
||||
const removeContact = (index) => {
|
||||
if (localFormData.value.contacts.length > 1) {
|
||||
const wasPrimary = localFormData.value.contacts[index].isPrimary;
|
||||
localFormData.value.contacts.splice(index, 1);
|
||||
if (wasPrimary && localFormData.value.contacts.length > 0) {
|
||||
localFormData.value.contacts[0].isPrimary = true;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Reset "Same as Client Name" when editing or using existing customer
|
||||
watch([() => props.isEditMode, () => props.isNewClientLocked], ([editMode, newClientLocked]) => {
|
||||
if (editMode || !newClientLocked) {
|
||||
sameAsClientName.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-fill name fields when "Same as Client Name" is checked
|
||||
watch(sameAsClientName, (checked) => {
|
||||
if (checked && props.formData.customerName) {
|
||||
const nameParts = props.formData.customerName.trim().split(" ");
|
||||
if (nameParts.length === 1) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = "";
|
||||
} else if (nameParts.length >= 2) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = nameParts.slice(1).join(" ");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for customer name changes when "Same as Client Name" is checked
|
||||
watch(
|
||||
() => props.formData.customerName,
|
||||
(newName) => {
|
||||
if (sameAsClientName.value && newName) {
|
||||
const nameParts = newName.trim().split(" ");
|
||||
if (nameParts.length === 1) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = "";
|
||||
} else if (nameParts.length >= 2) {
|
||||
localFormData.value.firstName = nameParts[0];
|
||||
localFormData.value.lastName = nameParts.slice(1).join(" ");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Watch for toggle changes
|
||||
watch(isNewContact, (newValue) => {
|
||||
if (newValue) {
|
||||
// Clear contact selection when switching to new contact mode
|
||||
selectedContact.value = null;
|
||||
localFormData.value.firstName = "";
|
||||
localFormData.value.lastName = "";
|
||||
localFormData.value.phoneNumber = "";
|
||||
localFormData.value.email = "";
|
||||
}
|
||||
emit("newContactToggle", newValue);
|
||||
});
|
||||
|
||||
const handleContactSelect = () => {
|
||||
if (selectedContact.value && selectedContact.value.value) {
|
||||
const contact = selectedContact.value.value;
|
||||
localFormData.value.firstName = contact.firstName;
|
||||
localFormData.value.lastName = contact.lastName;
|
||||
localFormData.value.phoneNumber = contact.phone || "";
|
||||
localFormData.value.email = contact.email || "";
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
isNewContact,
|
||||
});
|
||||
const setPrimary = (index) => {
|
||||
localFormData.value.contacts.forEach((contact, i) => {
|
||||
contact.isPrimary = i === index;
|
||||
});
|
||||
};
|
||||
|
||||
const formatPhoneNumber = (value) => {
|
||||
const digits = value.replace(/\D/g, "").slice(0, 10);
|
||||
if (digits.length <= 3) return digits;
|
||||
if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
|
||||
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
||||
};
|
||||
|
||||
const formatPhone = (index, event) => {
|
||||
const value = event.target.value;
|
||||
const formatted = formatPhoneNumber(value);
|
||||
localFormData.value.contacts[index].phoneNumber = formatted;
|
||||
};
|
||||
|
||||
const handlePhoneKeydown = (event, index) => {
|
||||
const allowedKeys = [
|
||||
"Backspace",
|
||||
"Delete",
|
||||
"Tab",
|
||||
"Escape",
|
||||
"Enter",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"Home",
|
||||
"End",
|
||||
];
|
||||
|
||||
if (allowedKeys.includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow Ctrl+A, Ctrl+C, Ctrl+V, etc.
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a digit
|
||||
if (!/\d/.test(event.key)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current digit count
|
||||
const currentDigits = localFormData.value.contacts[index].phoneNumber.replace(
|
||||
/\D/g,
|
||||
"",
|
||||
).length;
|
||||
if (currentDigits >= 10) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -285,23 +281,47 @@ defineExpose({
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
.contact-item {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--surface-section);
|
||||
}
|
||||
|
||||
.contact-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
.contact-header h4 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
.remove-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.contact-item .form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
|
@ -325,25 +345,6 @@ defineExpose({
|
|||
color: var(--red-500);
|
||||
}
|
||||
|
||||
.helper-text {
|
||||
color: var(--text-color-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue