501 lines
12 KiB
Vue
501 lines
12 KiB
Vue
<template>
|
|
<Modal
|
|
:visible="isVisible"
|
|
:options="modalOptions"
|
|
@update:visible="handleVisibilityChange"
|
|
@close="handleClose"
|
|
>
|
|
<template #title> Create New Client </template>
|
|
|
|
<!-- Status Message -->
|
|
<div v-if="statusMessage" class="status-message" :class="`status-${statusType}`">
|
|
<i :class="getStatusIcon(statusType)" class="status-icon"></i>
|
|
{{ statusMessage }}
|
|
</div>
|
|
|
|
<Form
|
|
ref="formRef"
|
|
:fields="formFields"
|
|
:form-data="formData"
|
|
:show-cancel-button="true"
|
|
:validate-on-change="false"
|
|
:validate-on-blur="true"
|
|
:validate-on-submit="true"
|
|
:loading="isSubmitting"
|
|
:disable-on-loading="true"
|
|
submit-button-text="Create Client"
|
|
cancel-button-text="Cancel"
|
|
@submit="handleSubmit"
|
|
@cancel="handleCancel"
|
|
/>
|
|
</Modal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, computed, watch } from "vue";
|
|
import { useModalStore } from "@/stores/modal";
|
|
import Modal from "@/components/common/Modal.vue";
|
|
import Form from "@/components/common/Form.vue";
|
|
import Api from "@/api";
|
|
import DataUtils from "../../utils";
|
|
|
|
const modalStore = useModalStore();
|
|
|
|
// Modal visibility computed property
|
|
const isVisible = computed(() => modalStore.isModalOpen("createClient"));
|
|
const customerNames = ref([]);
|
|
// Form reference for controlling its state
|
|
const formRef = ref(null);
|
|
// Form data
|
|
const formData = reactive({
|
|
customertype: "",
|
|
customerName: "",
|
|
addressLine1: "",
|
|
phone: "",
|
|
email: "",
|
|
pincode: "",
|
|
city: "",
|
|
state: "",
|
|
});
|
|
|
|
// Available cities for the selected zipcode
|
|
const availableCities = ref([]);
|
|
|
|
// Loading state for zipcode lookup
|
|
const isLoadingZipcode = ref(false);
|
|
|
|
// Status message for user feedback
|
|
const statusMessage = ref("");
|
|
const statusType = ref("info"); // 'info', 'warning', 'error', 'success'
|
|
|
|
// Modal configuration
|
|
const modalOptions = {
|
|
maxWidth: "600px",
|
|
persistent: false,
|
|
showActions: false,
|
|
title: "Create New Client",
|
|
overlayColor: "rgb(59, 130, 246)", // Blue background
|
|
overlayOpacity: 0.8,
|
|
cardClass: "create-client-modal",
|
|
closeOnOutsideClick: true,
|
|
closeOnEscape: true,
|
|
};
|
|
|
|
// Form field definitions
|
|
const formFields = computed(() => [
|
|
{
|
|
name: "addressTitle",
|
|
label: "Address Title",
|
|
type: "text",
|
|
required: true,
|
|
placeholder: "Enter address title",
|
|
helpText: "A short title to identify this address (e.g., Johnson Home, Johnson Office)",
|
|
cols: 12,
|
|
md: 6,
|
|
},
|
|
{
|
|
name: "customertype",
|
|
label: "Client Type",
|
|
type: "select",
|
|
required: true,
|
|
placeholder: "Select client type",
|
|
cols: 12,
|
|
md: 6,
|
|
options: [
|
|
{ label: "Individual", value: "Individual" },
|
|
{ label: "Company", value: "Company" },
|
|
],
|
|
helpText: "Select whether this is an individual or company client",
|
|
},
|
|
{
|
|
name: "customerName",
|
|
label: "Client Name",
|
|
type: "autocomplete",
|
|
required: true,
|
|
placeholder: "Type or select client name",
|
|
cols: 12,
|
|
md: 6,
|
|
options: customerNames.value,
|
|
forceSelection: false, // Allow custom entries not in the list
|
|
dropdown: true,
|
|
helpText: "Select an existing client or enter a new client name",
|
|
},
|
|
{
|
|
name: "addressLine1",
|
|
label: "Address",
|
|
type: "text",
|
|
required: true,
|
|
placeholder: "Enter street address",
|
|
cols: 12,
|
|
md: 12,
|
|
},
|
|
{
|
|
name: "phone",
|
|
label: "Phone Number",
|
|
type: "text",
|
|
required: true,
|
|
placeholder: "Enter phone number",
|
|
format: "tel",
|
|
cols: 12,
|
|
md: 6,
|
|
validate: (value) => {
|
|
if (value && !/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(value)) {
|
|
return "Please enter a valid phone number";
|
|
}
|
|
return null;
|
|
},
|
|
},
|
|
{
|
|
name: "email",
|
|
label: "Email Address",
|
|
type: "text",
|
|
required: true,
|
|
placeholder: "Enter email address",
|
|
format: "email",
|
|
cols: 12,
|
|
md: 6,
|
|
},
|
|
{
|
|
name: "pincode",
|
|
label: "Zip Code",
|
|
type: "text",
|
|
required: true,
|
|
placeholder: "Enter 5-digit zip code",
|
|
cols: 12,
|
|
md: 4,
|
|
maxLength: 5,
|
|
inputMode: "numeric",
|
|
pattern: "[0-9]*",
|
|
onChangeOverride: handleZipcodeChange,
|
|
onInput: (value) => {
|
|
// Only allow numbers and limit to 5 digits
|
|
return value.replace(/\D/g, "").substring(0, 5);
|
|
},
|
|
validate: (value) => {
|
|
if (value && !/^\d{5}$/.test(value)) {
|
|
return "Please enter a valid 5-digit zip code";
|
|
}
|
|
return null;
|
|
},
|
|
},
|
|
{
|
|
name: "city",
|
|
label: "City",
|
|
type: availableCities.value.length > 0 ? "select" : "text",
|
|
required: true,
|
|
disabled: false,
|
|
showClear: availableCities.value.length > 1,
|
|
placeholder: availableCities.value.length > 0 ? "Select city" : "Enter city name",
|
|
options: availableCities.value.map((place) => ({
|
|
label: place["place name"],
|
|
value: place["place name"],
|
|
})),
|
|
cols: 12,
|
|
md: 4,
|
|
helpText: isLoadingZipcode.value
|
|
? "Loading cities..."
|
|
: availableCities.value.length > 0
|
|
? "Select from available cities"
|
|
: "Enter city manually (auto-lookup unavailable)",
|
|
},
|
|
{
|
|
name: "state",
|
|
label: "State",
|
|
type: "select",
|
|
options: DataUtils.US_STATES.map((stateAbbr) => ({
|
|
label: stateAbbr,
|
|
value: stateAbbr,
|
|
})),
|
|
required: true,
|
|
disabled: availableCities.value.length > 0,
|
|
placeholder:
|
|
availableCities.value.length > 0 ? "Auto-populated" : "Enter state (e.g., CA, TX, NY)",
|
|
cols: 12,
|
|
md: 4,
|
|
helpText:
|
|
availableCities.value.length > 0
|
|
? "Auto-populated from zip code"
|
|
: "Enter state abbreviation manually",
|
|
validate: (value) => {
|
|
// Only validate manually entered states (when API lookup failed)
|
|
if (availableCities.value.length === 0 && value) {
|
|
const upperValue = value.toUpperCase();
|
|
if (!DataUtils.US_STATES.includes(upperValue)) {
|
|
return "Please enter a valid US state abbreviation (e.g., CA, TX, NY)";
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
},
|
|
]);
|
|
|
|
// Handle zipcode change and API lookup
|
|
async function handleZipcodeChange(value, fieldName, currentFormData) {
|
|
if (value.length < 5) {
|
|
return;
|
|
}
|
|
if (fieldName === "pincode" && value && value.length >= 5) {
|
|
// Only process if it's a valid zipcode format
|
|
const zipcode = value.replace(/\D/g, "").substring(0, 5);
|
|
|
|
if (zipcode.length === 5) {
|
|
isLoadingZipcode.value = true;
|
|
|
|
try {
|
|
const places = await Api.getCityStateByZip(zipcode);
|
|
console.log("API response for zipcode", zipcode, ":", places);
|
|
|
|
if (places && places.length > 0) {
|
|
availableCities.value = places;
|
|
|
|
// Update the reactive formData directly to ensure reactivity
|
|
// Use "state abbreviation" instead of "state" for proper abbreviation format
|
|
const stateValue = places[0]["state abbreviation"] || places[0].state;
|
|
console.log("Setting state to:", stateValue, "from place object:", places[0]);
|
|
formData.state = stateValue;
|
|
|
|
// If only one city, auto-select it
|
|
if (places.length === 1) {
|
|
formData.city = places[0]["place name"];
|
|
showStatusMessage(
|
|
`Location found: ${places[0]["place name"]}, ${places[0]["state abbreviation"] || places[0].state}`,
|
|
"success",
|
|
);
|
|
} else {
|
|
// Clear city selection if multiple cities
|
|
formData.city = "";
|
|
showStatusMessage(
|
|
`Found ${places.length} cities for this zip code. Please select one.`,
|
|
"info",
|
|
);
|
|
}
|
|
} else {
|
|
// No results found - enable manual entry
|
|
handleApiFailure("No location data found for this zip code");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching city/state data:", error);
|
|
|
|
// Check if it's a network/CORS error
|
|
if (error.code === "ERR_NETWORK" || error.message.includes("Network Error")) {
|
|
handleApiFailure(
|
|
"Unable to fetch location data. Please enter city and state manually.",
|
|
);
|
|
} else {
|
|
handleApiFailure(
|
|
"Location lookup failed. Please enter city and state manually.",
|
|
);
|
|
}
|
|
} finally {
|
|
isLoadingZipcode.value = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle API failure by enabling manual entry
|
|
function handleApiFailure(message) {
|
|
console.warn("Zipcode API failed:", message);
|
|
|
|
// Clear existing data
|
|
availableCities.value = [];
|
|
formData.city = "";
|
|
formData.state = "";
|
|
|
|
// Show user-friendly message
|
|
showStatusMessage(message, "warning");
|
|
}
|
|
|
|
// Show status message to user
|
|
function showStatusMessage(message, type = "info") {
|
|
statusMessage.value = message;
|
|
statusType.value = type;
|
|
|
|
// Auto-clear message after 5 seconds
|
|
setTimeout(() => {
|
|
statusMessage.value = "";
|
|
}, 5000);
|
|
}
|
|
|
|
// Get icon class for status messages
|
|
function getStatusIcon(type) {
|
|
switch (type) {
|
|
case "warning":
|
|
return "pi pi-exclamation-triangle";
|
|
case "error":
|
|
return "pi pi-times-circle";
|
|
case "success":
|
|
return "pi pi-check-circle";
|
|
default:
|
|
return "pi pi-info-circle";
|
|
}
|
|
}
|
|
|
|
// Submission state to prevent double submission
|
|
const isSubmitting = ref(false);
|
|
|
|
// Handle form submission
|
|
async function handleSubmit(formDataFromEvent) {
|
|
// Prevent double submission with detailed logging
|
|
if (isSubmitting.value) {
|
|
console.warn(
|
|
"CreateClientModal: Form submission already in progress, ignoring duplicate submission",
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
"CreateClientModal: Form submission started with data:",
|
|
formDataFromEvent || formData,
|
|
);
|
|
|
|
isSubmitting.value = true;
|
|
|
|
try {
|
|
showStatusMessage("Creating client...", "info");
|
|
|
|
// Use the form data from the event if provided, otherwise use reactive formData
|
|
const dataToSubmit = formDataFromEvent || formData;
|
|
|
|
console.log("CreateClientModal: Calling API with data:", dataToSubmit);
|
|
|
|
// Call API to create client
|
|
const response = await Api.createClient(dataToSubmit);
|
|
|
|
console.log("CreateClientModal: API response received:", response);
|
|
|
|
if (response && response.success) {
|
|
showStatusMessage("Client created successfully!", "success");
|
|
|
|
// Close modal after a brief delay
|
|
setTimeout(() => {
|
|
handleClose();
|
|
}, 1500);
|
|
} else {
|
|
throw new Error(response?.message || "Failed to create client");
|
|
}
|
|
} catch (error) {
|
|
console.error("CreateClientModal: Error creating client:", error);
|
|
showStatusMessage(error.message || "Failed to create client. Please try again.", "error");
|
|
} finally {
|
|
isSubmitting.value = false;
|
|
// Also reset the Form component's internal submission state
|
|
if (formRef.value && formRef.value.stopLoading) {
|
|
formRef.value.stopLoading();
|
|
}
|
|
console.log("CreateClientModal: Form submission completed, isSubmitting reset to false");
|
|
}
|
|
}
|
|
|
|
// Handle cancel action
|
|
function handleCancel() {
|
|
handleClose();
|
|
}
|
|
|
|
// Handle modal close
|
|
function handleClose() {
|
|
modalStore.closeModal("createClient");
|
|
resetForm();
|
|
}
|
|
|
|
// Handle visibility changes
|
|
function handleVisibilityChange(visible) {
|
|
if (!visible) {
|
|
handleClose();
|
|
}
|
|
}
|
|
|
|
// Reset form data
|
|
function resetForm() {
|
|
Object.keys(formData).forEach((key) => {
|
|
formData[key] = "";
|
|
});
|
|
availableCities.value = [];
|
|
isLoadingZipcode.value = false;
|
|
statusMessage.value = "";
|
|
statusType.value = "info";
|
|
}
|
|
|
|
// Initialize modal in store when component mounts
|
|
modalStore.initializeModal("createClient", {
|
|
closeOnEscape: true,
|
|
closeOnOutsideClick: true,
|
|
});
|
|
|
|
watch(isVisible, async () => {
|
|
if (isVisible.value) {
|
|
try {
|
|
const names = await Api.getCustomerNames();
|
|
customerNames.value = names;
|
|
} catch (error) {
|
|
console.error("Error loading customer names:", error);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.create-client-modal {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
/* Custom styling for the modal content */
|
|
:deep(.modal-header) {
|
|
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
|
color: white;
|
|
}
|
|
|
|
:deep(.modal-title) {
|
|
font-weight: 600;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
:deep(.modal-close-btn) {
|
|
color: white !important;
|
|
}
|
|
|
|
:deep(.modal-content) {
|
|
padding: 24px;
|
|
}
|
|
|
|
/* Status message styling */
|
|
.status-message {
|
|
padding: 12px 16px;
|
|
margin-bottom: 16px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 0.9rem;
|
|
border-left: 4px solid;
|
|
}
|
|
|
|
.status-icon {
|
|
margin-right: 8px;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.status-info {
|
|
background-color: #e3f2fd;
|
|
color: #1565c0;
|
|
border-left-color: #2196f3;
|
|
}
|
|
|
|
.status-warning {
|
|
background-color: #fff3e0;
|
|
color: #ef6c00;
|
|
border-left-color: #ff9800;
|
|
}
|
|
|
|
.status-error {
|
|
background-color: #ffebee;
|
|
color: #c62828;
|
|
border-left-color: #f44336;
|
|
}
|
|
|
|
.status-success {
|
|
background-color: #e8f5e8;
|
|
color: #2e7d32;
|
|
border-left-color: #4caf50;
|
|
}
|
|
</style>
|