364 lines
7.5 KiB
Vue
364 lines
7.5 KiB
Vue
<template>
|
|
<div class="form-section">
|
|
<div class="section-header">
|
|
<h3>Contact Information</h3>
|
|
</div>
|
|
<div class="form-grid">
|
|
<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-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>
|
|
<div class="form-field full-width">
|
|
<Button label="Add another contact" @click="addContact" :disabled="isSubmitting" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, computed, onMounted } from "vue";
|
|
import InputText from "primevue/inputtext";
|
|
import Select from "primevue/select";
|
|
import Button from "primevue/button";
|
|
|
|
const props = defineProps({
|
|
formData: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
isSubmitting: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isEditMode: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isNewClientLocked: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["update:formData"]);
|
|
|
|
const localFormData = computed({
|
|
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),
|
|
});
|
|
|
|
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" },
|
|
]);
|
|
|
|
// Ensure at least one contact
|
|
onMounted(() => {
|
|
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
|
localFormData.value.contacts = [
|
|
{
|
|
firstName: "",
|
|
lastName: "",
|
|
phoneNumber: "",
|
|
email: "",
|
|
contactRole: "",
|
|
isPrimary: 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;
|
|
}
|
|
}
|
|
};
|
|
|
|
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>
|
|
.form-section {
|
|
background: var(--surface-card);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
border: 1px solid var(--surface-border);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.section-header h3 {
|
|
margin: 0;
|
|
color: var(--text-color);
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.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;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.contact-header h4 {
|
|
margin: 0;
|
|
color: var(--text-color);
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.remove-btn {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.contact-item .form-grid {
|
|
display: grid;
|
|
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;
|
|
}
|
|
|
|
.form-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.form-field.full-width {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.form-field label {
|
|
font-weight: 500;
|
|
color: var(--text-color-secondary);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.required {
|
|
color: var(--red-500);
|
|
}
|
|
|
|
.w-full {
|
|
width: 100% !important;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.section-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
</style>
|