update with migration data

This commit is contained in:
Casey 2026-02-19 15:06:14 -06:00
parent d84c9fd20c
commit 30031c4c56
13 changed files with 489314 additions and 11952 deletions

View file

@ -118,7 +118,7 @@
v-model="address.contacts"
:options="contactOptions"
optionLabel="label"
optionValue="value"
dataKey="value"
:disabled="isSubmitting || contactOptions.length === 0"
placeholder="Select contacts"
class="w-full"
@ -130,10 +130,10 @@
<Select
:id="`primaryContact-${index}`"
v-model="address.primaryContact"
:options="address.contacts.map(c => contactOptions.find(opt => opt.value === c))"
:options="address.contacts"
optionLabel="label"
optionValue="value"
:disabled="isSubmitting || contactOptions.length === 0"
dataKey="value"
:disabled="isSubmitting || !address.contacts || address.contacts.length === 0"
placeholder="Select primary contact"
class="w-full"
/>
@ -208,8 +208,13 @@ const localFormData = computed({
});
const contactOptions = computed(() => {
// When contactOptions prop is provided (e.g. from modal with merged contacts), use it
if (props.contactOptions && props.contactOptions.length > 0) {
return props.contactOptions;
}
// Fallback: derive from localFormData.contacts (e.g. client creation wizard)
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
return props.contactOptions;
return [];
}
return localFormData.value.contacts.map((contact, index) => ({
label: `${contact.firstName || ""} ${contact.lastName || ""}`.trim() || `Contact ${index + 1}`,
@ -262,21 +267,25 @@ const handleBillingChange = (selectedIndex) => {
}
});
// Auto-select all contacts
// Auto-select all contacts (store full objects)
if (contactOptions.value.length > 0) {
localFormData.value.addresses[selectedIndex].contacts = contactOptions.value.map(
(opt) => opt.value,
);
localFormData.value.addresses[selectedIndex].contacts = [...contactOptions.value];
}
// Auto-select primary contact
if (localFormData.value.contacts && localFormData.value.contacts.length > 0) {
const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary);
if (primaryIndex !== -1) {
localFormData.value.addresses[selectedIndex].primaryContact = primaryIndex;
// Auto-select primary contact (store full object)
const allOpts = contactOptions.value;
if (allOpts.length > 0) {
// Try to find the primary from localFormData contacts
if (localFormData.value.contacts && localFormData.value.contacts.length > 0) {
const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary);
if (primaryIndex !== -1) {
const primaryOpt = allOpts.find((o) => o.value === primaryIndex);
localFormData.value.addresses[selectedIndex].primaryContact = primaryOpt || allOpts[0];
} else {
localFormData.value.addresses[selectedIndex].primaryContact = allOpts[0];
}
} else {
// Fallback to first contact if no primary found
localFormData.value.addresses[selectedIndex].primaryContact = 0;
localFormData.value.addresses[selectedIndex].primaryContact = allOpts[0];
}
}
} else {

View file

@ -1,18 +1,47 @@
<template>
<Dialog :visible="visible" @update:visible="val => emit('update:visible', val)" modal :closable="false" :style="{ width: '700px', maxWidth: '95vw' }">
<template #header>
<span class="modal-title">Add Contact/Address</span>
<span class="modal-title">Add Contact / Address</span>
</template>
<div class="modal-body">
<ContactInformationForm
:formData="contactFormData.value"
@update:formData="val => contactFormData.value = val"
<!-- Toggle buttons -->
<div class="toggle-buttons">
<Button
:label="showContacts ? 'Hide Contacts' : 'Add Contacts'"
:icon="showContacts ? 'pi pi-minus' : 'pi pi-user-plus'"
:severity="showContacts ? 'secondary' : 'primary'"
:outlined="!showContacts"
@click="showContacts = !showContacts"
size="small"
/>
<Button
:label="showAddresses ? 'Hide Addresses' : 'Add Addresses'"
:icon="showAddresses ? 'pi pi-minus' : 'pi pi-map-marker'"
:severity="showAddresses ? 'secondary' : 'primary'"
:outlined="!showAddresses"
@click="showAddresses = !showAddresses"
size="small"
/>
</div>
<!-- Hint when nothing is selected -->
<div v-if="!showContacts && !showAddresses" class="empty-hint">
<i class="pi pi-info-circle"></i>
<span>Click a button above to add contacts or addresses to this client.</span>
</div>
<!-- Contact form -->
<ModalContactForm
v-if="showContacts"
:contacts="newContacts"
:isSubmitting="isSubmitting"
:existingContacts="existingContacts"
/>
<AddressInformationForm
:formData="addressFormData.value"
@update:formData="val => addressFormData.value = val"
<!-- Address form -->
<ModalAddressForm
v-if="showAddresses"
:addresses="newAddresses"
:isSubmitting="isSubmitting"
:contactOptions="allContactOptions"
:existingAddresses="existingAddresses"
@ -20,7 +49,13 @@
</div>
<template #footer>
<Button label="Cancel" @click="close" severity="secondary" />
<Button label="Create" @click="create" severity="primary" :loading="isSubmitting" />
<Button
label="Create"
@click="create"
severity="primary"
:loading="isSubmitting"
:disabled="!showContacts && !showAddresses"
/>
</template>
</Dialog>
</template>
@ -29,8 +64,8 @@
import { ref, computed, watch } from 'vue';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import ContactInformationForm from '../clientSubPages/ContactInformationForm.vue';
import AddressInformationForm from '../clientSubPages/AddressInformationForm.vue';
import ModalContactForm from './ModalContactForm.vue';
import ModalAddressForm from './ModalAddressForm.vue';
const props = defineProps({
visible: Boolean,
@ -41,46 +76,75 @@ const props = defineProps({
});
const emit = defineEmits(['update:visible', 'created']);
const contactFormData = ref({ contacts: [] });
const addressFormData = ref({ addresses: [], contacts: [] });
const showContacts = ref(false);
const showAddresses = ref(false);
// Keep addressFormData.contacts in sync with new contacts
watch(
() => contactFormData.value.contacts,
(newContacts) => {
addressFormData.value.contacts = newContacts || [];
},
{ deep: true }
);
// Direct arrays instead of wrapping in formData objects
const newContacts = ref([
{ firstName: '', lastName: '', phoneNumber: '', email: '', contactRole: '', isPrimary: true },
]);
const newAddresses = ref([
{ addressLine1: '', addressLine2: '', pincode: '', city: '', state: '', contacts: [], primaryContact: null, zipcodeLookupDisabled: true },
]);
// All contact options = clientContacts + new contacts
// Reset forms when modal opens
watch(() => props.visible, (isVisible) => {
if (isVisible) {
showContacts.value = false;
showAddresses.value = false;
newContacts.value = [
{ firstName: '', lastName: '', phoneNumber: '', email: '', contactRole: '', isPrimary: true },
];
newAddresses.value = [
{ addressLine1: '', addressLine2: '', pincode: '', city: '', state: '', contacts: [], primaryContact: null, zipcodeLookupDisabled: true },
];
}
});
// All contact options = existing client contacts + new contacts being created in this modal
const allContactOptions = computed(() => {
const clientOpts = (props.clientContacts || []).map((c, idx) => ({
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
value: `client-${idx}`,
...c,
}));
const newOpts = (contactFormData.value.contacts || []).map((c, idx) => ({
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
value: `new-${idx}`,
...c,
}));
const clientOpts = (props.clientContacts || []).map((c, idx) => {
const firstName = c.firstName || c.first_name || '';
const lastName = c.lastName || c.last_name || '';
const email = c.email || c.emailId || c.email_id || c.customEmail || '';
return {
label: `${firstName} ${lastName}`.trim() || c.fullName || c.name || `Contact ${idx + 1}`,
value: `client-${idx}`,
firstName,
lastName,
email,
};
});
const newOpts = showContacts.value
? (newContacts.value || []).map((c, idx) => {
const firstName = c.firstName || '';
const lastName = c.lastName || '';
const email = c.email || '';
return {
label: `${firstName} ${lastName}`.trim() || `New Contact ${idx + 1}`,
value: `new-${idx}`,
firstName,
lastName,
email,
};
})
: [];
return [...clientOpts, ...newOpts];
});
function close() {
emit('update:visible', false);
}
function create() {
// Dummy create handler
console.log('Create clicked', {
contacts: contactFormData.value.contacts,
addresses: addressFormData.value.addresses,
});
emit('created', {
contacts: contactFormData.value.contacts,
addresses: addressFormData.value.addresses,
});
const payload = {};
if (showContacts.value) {
payload.contacts = newContacts.value;
}
if (showAddresses.value) {
payload.addresses = newAddresses.value;
}
emit('created', payload);
close();
}
</script>
@ -93,7 +157,25 @@ function create() {
.modal-body {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 1rem;
padding: 0.5rem 0;
}
.toggle-buttons {
display: flex;
gap: 0.75rem;
}
.empty-hint {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: var(--surface-ground);
border-radius: 6px;
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.empty-hint i {
font-size: 1rem;
color: var(--primary-color);
}
</style>

View file

@ -0,0 +1,362 @@
<template>
<div class="form-section">
<div class="section-header">
<i class="pi pi-map-marker" style="color: var(--primary-color); font-size: 1.2rem;"></i>
<h3>Add Addresses</h3>
</div>
<div class="form-grid">
<div
v-for="(address, index) in addresses"
:key="index"
class="address-item"
:class="{ 'existing-highlight': isExistingAddress(address) }"
>
<div class="address-header">
<div class="address-title">
<i class="pi pi-home" style="font-size: 0.9rem; color: var(--primary-color);"></i>
<h4>Address {{ index + 1 }}</h4>
</div>
<Button
v-if="addresses.length > 1"
@click="removeAddress(index)"
size="small"
severity="danger"
icon="pi pi-trash"
class="remove-btn"
/>
</div>
<div class="address-fields">
<div class="form-field full-width">
<label :for="`modal-addr-line1-${index}`">
<i class="pi pi-map" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Address Line 1 <span class="required">*</span>
</label>
<InputText
:id="`modal-addr-line1-${index}`"
v-model="address.addressLine1"
:disabled="isSubmitting"
placeholder="Street address"
class="w-full"
@input="formatAddressLine(index, 'addressLine1', $event)"
/>
</div>
<div class="form-field full-width">
<label :for="`modal-addr-line2-${index}`"><i class="pi pi-map" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Address Line 2</label>
<InputText
:id="`modal-addr-line2-${index}`"
v-model="address.addressLine2"
:disabled="isSubmitting"
placeholder="Apt, suite, unit, etc."
class="w-full"
@input="formatAddressLine(index, 'addressLine2', $event)"
/>
</div>
<div class="form-row">
<div class="form-field">
<label :for="`modal-zip-${index}`">
<i class="pi pi-hashtag" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Zip Code <span class="required">*</span>
</label>
<InputText
:id="`modal-zip-${index}`"
v-model="address.pincode"
:disabled="isSubmitting"
@input="handleZipcodeInput(index, $event)"
maxlength="5"
placeholder="12345"
class="w-full"
/>
</div>
<div class="form-field">
<label :for="`modal-city-${index}`">
<i class="pi pi-building" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>City <span class="required">*</span>
</label>
<InputText
:id="`modal-city-${index}`"
v-model="address.city"
:disabled="isSubmitting || address.zipcodeLookupDisabled"
placeholder="City"
class="w-full"
/>
</div>
<div class="form-field">
<label :for="`modal-state-${index}`">
<i class="pi pi-flag" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>State <span class="required">*</span>
</label>
<InputText
:id="`modal-state-${index}`"
v-model="address.state"
:disabled="isSubmitting || address.zipcodeLookupDisabled"
placeholder="State"
class="w-full"
/>
</div>
</div>
<div class="form-row">
<div class="form-field">
<label :for="`modal-contacts-${index}`"><i class="pi pi-users" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Assigned Contacts</label>
<MultiSelect
:id="`modal-contacts-${index}`"
v-model="address.contacts"
:options="contactOptions"
optionLabel="label"
dataKey="value"
:disabled="isSubmitting || contactOptions.length === 0"
placeholder="Select contacts"
class="w-full"
display="chip"
/>
</div>
<div class="form-field">
<label :for="`modal-primary-${index}`"><i class="pi pi-star-fill" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Primary Contact</label>
<Select
:id="`modal-primary-${index}`"
v-model="address.primaryContact"
:options="address.contacts || []"
optionLabel="label"
dataKey="value"
:disabled="isSubmitting || !address.contacts || address.contacts.length === 0"
placeholder="Select primary contact"
class="w-full"
/>
</div>
</div>
</div>
</div>
<div class="form-field full-width">
<Button label="Add another address" @click="addAddress" :disabled="isSubmitting" size="small" />
</div>
</div>
</div>
</template>
<script setup>
import InputText from "primevue/inputtext";
import Select from "primevue/select";
import MultiSelect from "primevue/multiselect";
import Button from "primevue/button";
import Api from "../../api";
import { useNotificationStore } from "../../stores/notifications-primevue";
const props = defineProps({
addresses: {
type: Array,
required: true,
},
isSubmitting: {
type: Boolean,
default: false,
},
contactOptions: {
type: Array,
default: () => [],
},
existingAddresses: {
type: Array,
default: () => [],
},
});
const notificationStore = useNotificationStore();
const createEmptyAddress = () => ({
addressLine1: "",
addressLine2: "",
pincode: "",
city: "",
state: "",
contacts: [],
primaryContact: null,
zipcodeLookupDisabled: true,
});
const addAddress = () => {
props.addresses.push(createEmptyAddress());
};
const removeAddress = (index) => {
if (props.addresses.length > 1) {
props.addresses.splice(index, 1);
}
};
const formatAddressLine = (index, field, event) => {
const value = event.target.value;
if (!value) return;
const formatted = value
.split(" ")
.map((word) => {
if (!word) return word;
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(" ");
props.addresses[index][field] = formatted;
};
const handleZipcodeInput = async (index, event) => {
const value = event.target.value;
props.addresses[index].pincode = value;
if (value.length === 5) {
try {
const zipInfo = await Api.getCityStateByZip(value);
if (zipInfo && zipInfo.length > 0) {
props.addresses[index].city = zipInfo[0].city;
props.addresses[index].state = zipInfo[0].state;
props.addresses[index].zipcodeLookupDisabled = false;
} else {
throw new Error("No data returned");
}
} catch (error) {
console.error("Zipcode lookup failed:", error);
props.addresses[index].zipcodeLookupDisabled = true;
props.addresses[index].city = "";
props.addresses[index].state = "";
notificationStore.addError("Invalid zipcode or lookup failed");
}
} else {
props.addresses[index].zipcodeLookupDisabled = true;
props.addresses[index].city = "";
props.addresses[index].state = "";
}
};
const getFullAddress = (address) => {
return `${address.addressLine1 || ""} ${address.addressLine2 || ""} ${address.city || ""} ${address.state || ""} ${address.pincode || ""}`
.trim()
.replace(/\s+/g, " ");
};
const normalizeAddressString = (s = "") => {
return (s || "")
.toString()
.replace(/,/g, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
};
const isExistingAddress = (address) => {
const fullAddr = getFullAddress(address);
const normFull = normalizeAddressString(fullAddr);
if (!props.existingAddresses || props.existingAddresses.length === 0) return false;
return props.existingAddresses.some((ea) => normalizeAddressString(ea) === normFull);
};
</script>
<style scoped>
.form-section {
background: var(--surface-card);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--surface-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease;
}
.form-section:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--surface-border);
}
.section-header h3 {
margin: 0;
color: var(--text-color);
font-size: 1.1rem;
font-weight: 600;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.address-item {
border: 1px solid var(--surface-border);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: var(--surface-section);
transition: all 0.2s ease;
}
.address-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.address-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface-border);
}
.address-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.address-header h4 {
margin: 0;
color: var(--text-color);
font-size: 0.95rem;
font-weight: 600;
}
.remove-btn {
margin-left: auto;
}
.address-fields {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.form-row {
display: flex;
gap: 0.625rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
flex: 1;
}
.form-field.full-width {
width: 100%;
}
.form-field label {
font-weight: 500;
color: var(--text-color);
font-size: 0.85rem;
display: flex;
align-items: center;
}
.required {
color: var(--red-500);
}
.w-full {
width: 100% !important;
}
.address-item.existing-highlight {
border-color: var(--red-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>

View file

@ -0,0 +1,357 @@
<template>
<div class="form-section">
<div class="section-header">
<i class="pi pi-users" style="color: var(--primary-color); font-size: 1.2rem;"></i>
<h3>Add Contacts</h3>
</div>
<div class="form-grid">
<div
v-for="(contact, index) in contacts"
:key="index"
class="contact-item"
:class="{ 'existing-highlight': isExistingContact(contact) }"
>
<div class="contact-header">
<div class="contact-title">
<i class="pi pi-user" style="font-size: 0.9rem; color: var(--primary-color);"></i>
<h4>Contact {{ index + 1 }}</h4>
</div>
<div class="interactables">
<div class="form-field header-row">
<input
type="checkbox"
class="contact-checkbox"
:id="`modal-check-${index}`"
v-model="contact.isPrimary"
:disabled="isSubmitting"
@change="setPrimary(index)"
/>
<label :for="`modal-check-${index}`">
<i class="pi pi-star-fill" style="font-size: 0.7rem; margin-right: 0.25rem;"></i>Primary
</label>
</div>
<Button
v-if="contacts.length > 1"
@click="removeContact(index)"
size="small"
severity="danger"
icon="pi pi-trash"
class="remove-btn"
/>
</div>
</div>
<div class="form-rows">
<div class="form-row">
<div class="form-field">
<label :for="`modal-first-name-${index}`">
<i class="pi pi-user" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>First Name <span class="required">*</span>
</label>
<InputText
:id="`modal-first-name-${index}`"
v-model="contact.firstName"
:disabled="isSubmitting"
placeholder="First name"
class="w-full"
@input="formatName(index, 'firstName', $event)"
/>
</div>
<div class="form-field">
<label :for="`modal-last-name-${index}`">
<i class="pi pi-user" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Last Name <span class="required">*</span>
</label>
<InputText
:id="`modal-last-name-${index}`"
v-model="contact.lastName"
:disabled="isSubmitting"
placeholder="Last name"
class="w-full"
@input="formatName(index, 'lastName', $event)"
/>
</div>
<div class="form-field">
<label :for="`modal-contact-role-${index}`"><i class="pi pi-briefcase" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Role</label>
<Select
:id="`modal-contact-role-${index}`"
v-model="contact.contactRole"
:options="roleOptions"
optionLabel="label"
optionValue="value"
:disabled="isSubmitting"
placeholder="Select role"
class="w-full"
/>
</div>
</div>
<div class="form-row">
<div class="form-field">
<label :for="`modal-email-${index}`"><i class="pi pi-envelope" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Email</label>
<InputText
:id="`modal-email-${index}`"
v-model="contact.email"
:disabled="isSubmitting"
type="email"
placeholder="email@example.com"
class="w-full"
/>
</div>
<div class="form-field">
<label :for="`modal-phone-${index}`"><i class="pi pi-phone" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Phone</label>
<InputText
:id="`modal-phone-${index}`"
v-model="contact.phoneNumber"
:disabled="isSubmitting"
placeholder="(555) 123-4567"
class="w-full"
@input="formatPhone(index, $event)"
@keydown="handlePhoneKeydown($event, index)"
/>
</div>
</div>
</div>
</div>
<div class="form-field full-width">
<Button label="Add another contact" @click="addContact" :disabled="isSubmitting" size="small" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import InputText from "primevue/inputtext";
import Select from "primevue/select";
import Button from "primevue/button";
const props = defineProps({
contacts: {
type: Array,
required: true,
},
isSubmitting: {
type: Boolean,
default: false,
},
existingContacts: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:contacts"]);
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" },
]);
const addContact = () => {
props.contacts.push({
firstName: "",
lastName: "",
phoneNumber: "",
email: "",
contactRole: "",
isPrimary: false,
});
};
const removeContact = (index) => {
if (props.contacts.length > 1) {
const wasPrimary = props.contacts[index].isPrimary;
props.contacts.splice(index, 1);
if (wasPrimary && props.contacts.length > 0) {
props.contacts[0].isPrimary = true;
}
}
};
const setPrimary = (index) => {
props.contacts.forEach((contact, i) => {
contact.isPrimary = i === index;
});
};
const formatName = (index, field, event) => {
const value = event.target.value;
if (!value) return;
const formatted = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
props.contacts[index][field] = formatted;
};
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;
props.contacts[index].phoneNumber = formatPhoneNumber(value);
};
const handlePhoneKeydown = (event, index) => {
const allowedKeys = [
"Backspace", "Delete", "Tab", "Escape", "Enter",
"ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End",
];
if (allowedKeys.includes(event.key)) return;
if (event.ctrlKey || event.metaKey) return;
if (!/\d/.test(event.key)) {
event.preventDefault();
return;
}
const currentDigits = props.contacts[index].phoneNumber.replace(/\D/g, "").length;
if (currentDigits >= 10) event.preventDefault();
};
const getFullName = (contact) => {
return `${contact.firstName || ""} ${contact.lastName || ""}`.trim();
};
const isExistingContact = (contact) => {
const fullName = getFullName(contact);
return props.existingContacts.includes(fullName);
};
</script>
<style scoped>
.form-section {
background: var(--surface-card);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--surface-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease;
}
.form-section:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--surface-border);
}
.section-header h3 {
margin: 0;
color: var(--text-color);
font-size: 1.1rem;
font-weight: 600;
}
.form-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.contact-item {
border: 1px solid var(--surface-border);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: var(--surface-section);
min-width: 33%;
transition: all 0.2s ease;
}
.contact-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
.contact-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--surface-border);
}
.contact-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.contact-header h4 {
margin: 0;
color: var(--text-color);
font-size: 0.95rem;
font-weight: 600;
}
.interactables {
display: flex;
align-items: center;
flex-direction: row;
gap: 1rem;
}
.remove-btn {
margin-left: auto;
}
.form-rows {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.form-row {
display: flex;
gap: 0.625rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
flex: 1;
}
.form-field.full-width {
width: 100%;
}
.form-field label {
font-weight: 500;
color: var(--text-color-secondary);
font-size: 0.85rem;
}
.form-field.header-row {
flex-direction: row;
align-items: baseline;
}
.contact-checkbox {
margin-top: 0px;
}
.required {
color: var(--red-500);
}
.w-full {
width: 100% !important;
}
.contact-item.existing-highlight {
border-color: var(--red-500);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>