2025-11-26 16:47:53 -06:00

820 lines
20 KiB
Vue

<template>
<div class="overview-container">
<!-- Form Mode (new=true or edit mode) -->
<template v-if="isNew || editMode">
<ClientInformationForm
ref="clientInfoRef"
v-model:form-data="formData"
:is-submitting="isSubmitting"
:is-edit-mode="editMode"
@new-client-toggle="handleNewClientToggle"
@customer-selected="handleCustomerSelected"
/>
<ContactInformationForm
ref="contactInfoRef"
v-model:form-data="formData"
:is-submitting="isSubmitting"
:is-edit-mode="editMode"
:is-new-client-locked="isNewClientMode"
:available-contacts="availableContacts"
@new-contact-toggle="handleNewContactToggle"
/>
<AddressInformationForm
v-model:form-data="formData"
:is-submitting="isSubmitting"
:is-edit-mode="editMode"
/>
</template>
<!-- Display Mode (existing client view) -->
<template v-else>
<!-- Client Basic Info Card -->
<div class="info-card">
<div class="card-header">
<h3>Client Information</h3>
<Button
@click="toggleEditMode"
icon="pi pi-pencil"
label="Edit"
size="small"
severity="secondary"
/>
</div>
<div class="info-grid">
<div class="info-item">
<label>Customer Name:</label>
<span>{{ clientData?.customerName || "N/A" }}</span>
</div>
<div class="info-item">
<label>Customer Type:</label>
<span>{{ clientData?.customerType || "N/A" }}</span>
</div>
<div class="info-item">
<label>Customer Group:</label>
<span>{{ clientData?.customerGroup || "N/A" }}</span>
</div>
<div class="info-item">
<label>Territory:</label>
<span>{{ clientData?.territory || "N/A" }}</span>
</div>
</div>
</div>
<!-- Address Info Card -->
<div class="info-card" v-if="selectedAddressData">
<h3>Address Information</h3>
<div class="info-grid">
<div class="info-item full-width">
<label>Address Title:</label>
<span>{{ selectedAddressData.addressTitle || "N/A" }}</span>
</div>
<div class="info-item full-width">
<label>Full Address:</label>
<span>{{ fullAddress }}</span>
</div>
<div class="info-item">
<label>City:</label>
<span>{{ selectedAddressData.city || "N/A" }}</span>
</div>
<div class="info-item">
<label>State:</label>
<span>{{ selectedAddressData.state || "N/A" }}</span>
</div>
<div class="info-item">
<label>Zip Code:</label>
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
</div>
</div>
</div>
<!-- Contact Info Card -->
<div class="info-card" v-if="selectedAddressData">
<h3>Contact Information</h3>
<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-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>
</template>
<template v-else>
<p>No contacts available for this address.</p>
</template>
</div>
</template>
<!-- Status Cards (only for existing clients) -->
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData">
<div class="status-card">
<h4>On-Site Meeting</h4>
<Button
:label="selectedAddressData.customOnsiteMeetingScheduled || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customOnsiteMeetingScheduled)"
@click="handleStatusClick('onsite')"
/>
</div>
<div class="status-card">
<h4>Estimate Sent</h4>
<Button
:label="selectedAddressData.customEstimateSentStatus || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customEstimateSentStatus)"
@click="handleStatusClick('estimate')"
/>
</div>
<div class="status-card">
<h4>Job Status</h4>
<Button
:label="selectedAddressData.customJobStatus || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customJobStatus)"
@click="handleStatusClick('job')"
/>
</div>
<div class="status-card">
<h4>Payment Received</h4>
<Button
:label="selectedAddressData.customPaymentReceivedStatus || 'Not Started'"
:severity="getStatusSeverity(selectedAddressData.customPaymentReceivedStatus)"
@click="handleStatusClick('payment')"
/>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions" v-if="isNew || editMode">
<Button
@click="handleCancel"
:label="editMode ? 'Cancel' : 'Clear'"
severity="secondary"
:disabled="isSubmitting"
/>
<Button
@click="handleSave"
:label="isNew ? 'Create' : 'Update'"
:loading="isSubmitting"
:disabled="!isFormValid"
/>
</div>
<!-- Location Map (only for existing clients) -->
<div class="map-card" v-if="!isNew && !editMode">
<h3>Location</h3>
<LeafletMap
:latitude="latitude"
:longitude="longitude"
:address-title="selectedAddressData?.addressTitle || 'Client Location'"
map-height="350px"
:zoom-level="16"
/>
<div v-if="latitude && longitude" class="coordinates-info">
<small>
<strong>Coordinates:</strong>
{{ parseFloat(latitude).toFixed(6) }}, {{ parseFloat(longitude).toFixed(6) }}
</small>
</div>
</div>
<!-- Edit Confirmation Dialog -->
<Dialog
v-model:visible="showEditConfirmDialog"
header="Confirm Edit"
:modal="true"
:closable="false"
class="confirm-dialog"
>
<p>
Are you sure you want to edit this client information? This will enable editing
mode.
</p>
<template #footer>
<Button
label="Cancel"
severity="secondary"
@click="showEditConfirmDialog = false"
/>
<Button label="Yes, Edit" @click="confirmEdit" />
</template>
</Dialog>
</div>
</template>
<script setup>
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";
import AddressInformationForm from "./AddressInformationForm.vue";
import DataUtils from "../../utils";
import Api from "../../api";
import { useRouter } from "vue-router";
import { useNotificationStore } from "../../stores/notifications-primevue";
const props = defineProps({
clientData: {
type: Object,
default: () => ({}),
},
selectedAddress: {
type: String,
default: "",
},
isNew: {
type: Boolean,
default: false,
},
});
const router = useRouter();
const notificationStore = useNotificationStore();
// Refs for child components
const clientInfoRef = ref(null);
const contactInfoRef = ref(null);
// Form state
const editMode = ref(false);
const showEditConfirmDialog = ref(false);
const isSubmitting = ref(false);
const isNewClientMode = ref(false);
const availableContacts = ref([]);
// Form data
const formData = ref({
customerName: "",
customerType: "",
addressTitle: "",
addressLine1: "",
addressLine2: "",
pincode: "",
city: "",
state: "",
contacts: [],
});
// Initialize form data when component mounts
onMounted(() => {
if (props.isNew) {
resetForm();
isNewClientMode.value = true; // Set to true for new client mode
console.log("Mounted in new client mode - initialized empty form");
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
populateFormFromClientData();
console.log("Mounted with existing client data - populated form");
} else {
resetForm();
console.log("Mounted with no client data - initialized empty form");
}
});
// Watch for clientData changes
watch(
() => props.clientData,
(newData) => {
if (props.isNew) {
resetForm();
} else if (newData && Object.keys(newData).length > 0) {
populateFormFromClientData();
} else {
resetForm();
}
},
{ deep: true },
);
// Watch for isNew prop changes
watch(
() => props.isNew,
(isNewValue) => {
if (isNewValue) {
resetForm();
editMode.value = false;
console.log("Switched to new client mode - reset form data");
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
populateFormFromClientData();
} else {
resetForm();
}
},
{ immediate: false },
);
// Find the address data object that matches the selected address string
const selectedAddressData = computed(() => {
if (!props.clientData?.addresses || !props.selectedAddress) {
return null;
}
return props.clientData.addresses.find(
(addr) => DataUtils.calculateFullAddress(addr) === props.selectedAddress,
);
});
// Get coordinates from the selected address
const latitude = computed(() => {
if (!selectedAddressData.value) return null;
return selectedAddressData.value.customLatitude || selectedAddressData.value.latitude || null;
});
const longitude = computed(() => {
if (!selectedAddressData.value) return null;
return (
selectedAddressData.value.customLongitude || selectedAddressData.value.longitude || null
);
});
// Calculate full address for display
const fullAddress = computed(() => {
if (!selectedAddressData.value) return "N/A";
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 (!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
const isFormValid = computed(() => {
const hasCustomerName = formData.value.customerName?.trim();
const hasCustomerType = formData.value.customerType?.trim();
const hasAddressTitle = formData.value.addressTitle?.trim();
const hasAddressLine1 = formData.value.addressLine1?.trim();
const hasPincode = formData.value.pincode?.trim();
const hasCity = formData.value.city?.trim();
const hasState = formData.value.state?.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 &&
hasCustomerType &&
hasAddressTitle &&
hasAddressLine1 &&
hasPincode &&
hasCity &&
hasState &&
hasContacts &&
hasFirstName &&
hasLastName
);
});
// Helper function to get badge severity based on status
const getStatusSeverity = (status) => {
switch (status) {
case "Not Started":
return "danger";
case "In Progress":
return "warn";
case "Completed":
return "success";
default:
return "secondary";
}
};
// 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 = {
customerName: "",
customerType: "",
addressTitle: "",
addressLine1: "",
addressLine2: "",
pincode: "",
city: "",
state: "",
contacts: [],
};
availableContacts.value = [];
isNewClientMode.value = false;
editMode.value = false;
console.log("Form reset - all fields cleared");
};
const populateFormFromClientData = () => {
if (!selectedAddressData.value) return;
formData.value = {
customerName: props.clientData.customerName || "",
customerType: props.clientData.customerType || "",
addressTitle: selectedAddressData.value.addressTitle || "",
addressLine1: selectedAddressData.value.addressLine1 || "",
addressLine2: selectedAddressData.value.addressLine2 || "",
pincode: selectedAddressData.value.pincode || "",
city: selectedAddressData.value.city || "",
state: selectedAddressData.value.state || "",
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 (contactsForAddress.value.length > 0) {
availableContacts.value = contactsForAddress.value;
}
};
// Event handlers
const handleNewClientToggle = (isNewClient) => {
isNewClientMode.value = isNewClient;
if (isNewClient) {
// Reset form when toggling to new client
resetForm();
}
};
const handleCustomerSelected = (clientData) => {
// When a customer is selected, populate available contacts from the first address
if (clientData.addresses && clientData.addresses.length > 0) {
availableContacts.value = clientData.addresses[0].contacts || [];
} else {
availableContacts.value = [];
}
};
const handleNewContactToggle = (isNewContact) => {
if (!isNewContact && availableContacts.value.length === 0) {
notificationStore.addWarning("No contacts available for this customer.");
}
};
// Edit mode methods
const toggleEditMode = () => {
showEditConfirmDialog.value = true;
};
const confirmEdit = () => {
showEditConfirmDialog.value = false;
editMode.value = true;
populateFormFromClientData();
};
// Save/Cancel actions
const handleSave = async () => {
if (!isFormValid.value) {
notificationStore.addError("Please fill in all required fields");
return;
}
isSubmitting.value = true;
try {
// Prepare client data for upsert
const clientData = {
customerName: formData.value.customerName,
customerType: formData.value.customerType,
addressTitle: formData.value.addressTitle,
addressLine1: formData.value.addressLine1,
addressLine2: formData.value.addressLine2,
pincode: formData.value.pincode,
city: formData.value.city,
state: formData.value.state,
contacts: formData.value.contacts,
};
console.log("Upserting client with data:", clientData);
// Call the upsert API
const result = await Api.createClient(clientData);
// Calculate full address for redirect
const fullAddressParts = [formData.value.addressLine1];
if (formData.value.addressLine2?.trim()) {
fullAddressParts.push(formData.value.addressLine2);
}
fullAddressParts.push(`${formData.value.city}, ${formData.value.state}`);
fullAddressParts.push(formData.value.pincode);
const fullAddress = fullAddressParts.join(" ");
if (props.isNew) {
notificationStore.addSuccess(
`Client ${formData.value.customerName} created successfully!`,
);
// Redirect to the new client page
await router.push({
path: "/client",
query: {
client: formData.value.customerName,
address: fullAddress,
},
});
} else {
notificationStore.addSuccess("Client updated successfully!");
editMode.value = false;
// Reload the client data
// Note: Parent component should handle reloading
}
} catch (error) {
console.error("Error saving client:", error);
notificationStore.addError("Failed to save client information");
} finally {
isSubmitting.value = false;
}
};
const handleCancel = () => {
if (props.isNew) {
// Clear form for new client
resetForm();
} else {
// Exit edit mode and restore original data
editMode.value = false;
populateFormFromClientData();
}
};
</script>
<style scoped>
.overview-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1rem;
}
.info-card,
.map-card {
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);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.card-header h3 {
margin: 0;
}
.info-card h3,
.map-card h3 {
margin: 0 0 1rem 0;
color: var(--text-color);
font-size: 1.25rem;
font-weight: 600;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.info-item.full-width {
grid-column: 1 / -1;
}
.info-item label {
font-weight: 500;
color: var(--text-color-secondary);
font-size: 0.9rem;
}
.info-item span {
color: var(--text-color);
font-size: 0.95rem;
}
.contact-selector {
margin-bottom: 1rem;
}
/* Form input styling */
.info-item :deep(.p-inputtext),
.info-item :deep(.p-autocomplete),
.info-item :deep(.p-select) {
width: 100%;
}
.info-item :deep(.p-autocomplete .p-inputtext) {
width: 100%;
}
/* Required field indicator */
.info-item label:has(+ .p-inputtext[required])::after,
.info-item label:has(+ .p-autocomplete)::after {
content: " *";
color: var(--red-500);
}
.status-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.status-card {
background: var(--surface-card);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--surface-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.75rem;
}
.status-card h4 {
margin: 0;
font-size: 1rem;
font-weight: 500;
color: var(--text-color);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding: 1.5rem;
background: var(--surface-card);
border-radius: 8px;
border: 1px solid var(--surface-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.coordinates-info {
margin-top: 0.75rem;
text-align: center;
color: var(--text-color-secondary);
padding-top: 0.75rem;
border-top: 1px solid var(--surface-border);
}
.confirm-dialog {
max-width: 400px;
}
.confirm-dialog :deep(.p-dialog-footer) {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
/* Utilities */
.w-full {
width: 100% !important;
}
@media (max-width: 768px) {
.overview-container {
padding: 0.5rem;
gap: 1rem;
}
.info-card,
.map-card {
padding: 1rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.status-cards {
grid-template-columns: repeat(2, 1fr);
}
.form-actions {
padding: 1rem;
flex-direction: column;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
@media (max-width: 480px) {
.status-cards {
grid-template-columns: 1fr;
}
}
</style>