get create client working
This commit is contained in:
parent
4a3576168a
commit
cb33d0c3b3
@ -96,6 +96,8 @@ def get_client(client_name):
|
|||||||
customer = frappe.get_doc("Customer", client_name)
|
customer = frappe.get_doc("Customer", client_name)
|
||||||
clientData = {**clientData, **customer.as_dict()}
|
clientData = {**clientData, **customer.as_dict()}
|
||||||
addresses = frappe.db.get_all("Address", fields=["*"], filters={"custom_customer_to_bill": client_name})
|
addresses = frappe.db.get_all("Address", fields=["*"], filters={"custom_customer_to_bill": client_name})
|
||||||
|
contacts = frappe.db.get_all("Contact", fields=["*"], filters={"custom_customer": client_name})
|
||||||
|
clientData["contacts"] = contacts
|
||||||
for address in addresses if addresses else []:
|
for address in addresses if addresses else []:
|
||||||
addressData = {"jobs": []}
|
addressData = {"jobs": []}
|
||||||
addressData = {**addressData, **address}
|
addressData = {**addressData, **address}
|
||||||
@ -263,7 +265,7 @@ def upsert_client(data):
|
|||||||
"customer_type": data.get("customer_type")
|
"customer_type": data.get("customer_type")
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
else:
|
else:
|
||||||
customer_doc = frappe.get_doc("Customer", customer)
|
customer_doc = frappe.get_doc("Customer", data.get("customer_name"))
|
||||||
|
|
||||||
print("Customer:", customer_doc.as_dict())
|
print("Customer:", customer_doc.as_dict())
|
||||||
|
|
||||||
@ -281,6 +283,7 @@ def upsert_client(data):
|
|||||||
# Create address
|
# Create address
|
||||||
address_doc = frappe.get_doc({
|
address_doc = frappe.get_doc({
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
|
"address_title": data.get("address_title"),
|
||||||
"address_line1": data.get("address_line1"),
|
"address_line1": data.get("address_line1"),
|
||||||
"address_line2": data.get("address_line2"),
|
"address_line2": data.get("address_line2"),
|
||||||
"city": data.get("city"),
|
"city": data.get("city"),
|
||||||
@ -297,12 +300,43 @@ def upsert_client(data):
|
|||||||
}
|
}
|
||||||
address_doc.append("links", link)
|
address_doc.append("links", link)
|
||||||
address_doc.save(ignore_permissions=True)
|
address_doc.save(ignore_permissions=True)
|
||||||
|
contact_exists = frappe.db.exists("Contact", {"email_id": data.get("contact_email")})
|
||||||
|
if not contact_exists:
|
||||||
|
contact_doc = frappe.get_doc({
|
||||||
|
"doctype": "Contact",
|
||||||
|
"first_name": data.get("first_name"),
|
||||||
|
"last_name": data.get("last_name"),
|
||||||
|
"email_id": data.get("email"),
|
||||||
|
"phone": data.get("phone_number"),
|
||||||
|
"custom_customer": customer_doc.name,
|
||||||
|
"links": [{
|
||||||
|
"link_doctype": "Customer",
|
||||||
|
"link_name": customer_doc.name
|
||||||
|
}]
|
||||||
|
}).insert(ignore_permissions=True)
|
||||||
|
print("Created new contact:", contact_doc.as_dict())
|
||||||
|
else:
|
||||||
|
contact_doc = frappe.get_doc("Contact", {"email_id": data.get("contact_email")})
|
||||||
|
print("Contact already exists:", contact_doc.as_dict())
|
||||||
|
|
||||||
return build_success_response({
|
return build_success_response({
|
||||||
"customer": customer_doc.as_dict(),
|
"customer": customer_doc.as_dict(),
|
||||||
"address": address_doc.as_dict()
|
"address": address_doc.as_dict(),
|
||||||
|
"contact": contact_doc.as_dict()
|
||||||
})
|
})
|
||||||
except frappe.ValidationError as ve:
|
except frappe.ValidationError as ve:
|
||||||
return build_error_response(str(ve), 400)
|
return build_error_response(str(ve), 400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
return build_error_response(str(e), 500)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_client_names(search_term):
|
||||||
|
"""Search for client names matching the search term."""
|
||||||
|
try:
|
||||||
|
search_pattern = f"%{search_term}%"
|
||||||
|
client_names = frappe.db.get_all(
|
||||||
|
"Customer",
|
||||||
|
pluck="name")
|
||||||
|
return build_success_response(client_names)
|
||||||
|
except Exception as e:
|
||||||
|
return build_error_response(str(e), 500)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from custom_ui.db_utils import build_success_response, build_error_response
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
allowed_hosts = ["api.zippopotam.us", "nominatim.openstreetmap.org"] # Update this list with trusted domains as needed
|
allowed_hosts = ["api.zippopotam.us", "nominatim.openstreetmap.org"] # Update this list with trusted domains as needed
|
||||||
@ -24,9 +25,9 @@ def request(url, method="GET", data=None, headers=None):
|
|||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
try:
|
try:
|
||||||
return resp.json()
|
return build_success_response(resp.json())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return {"text": resp.text}
|
return build_success_response({"text": resp.text})
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
frappe.log_error(message=str(e), title="Proxy Request Failed")
|
frappe.log_error(message=str(e), title="Proxy Request Failed")
|
||||||
frappe.throw("Failed to fetch data from external API.")
|
frappe.throw("Failed to fetch data from external API.")
|
||||||
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"frappe-ui": "^0.1.205",
|
"frappe-ui": "^0.1.205",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.4.1",
|
"primevue": "^4.4.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
@ -3634,6 +3635,12 @@
|
|||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/primeicons": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/primevue": {
|
"node_modules/primevue": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"frappe-ui": "^0.1.205",
|
"frappe-ui": "^0.1.205",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.4.1",
|
"primevue": "^4.4.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
|
|||||||
@ -328,8 +328,12 @@ class Api {
|
|||||||
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type });
|
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getClientNames(type) {
|
static async getClientNames(clientName) {
|
||||||
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type });
|
return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { searchTerm: clientName });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async searchClientNames(searchTerm) {
|
||||||
|
return await this.request("custom_ui.api.db.clients.search_client_names", { searchTerm });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getCompanyNames() {
|
static async getCompanyNames() {
|
||||||
@ -342,8 +346,7 @@ class Api {
|
|||||||
// Create methods
|
// Create methods
|
||||||
|
|
||||||
static async createClient(clientData) {
|
static async createClient(clientData) {
|
||||||
const payload = DataUtils.toSnakeCaseObject(clientData);
|
const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: clientData });
|
||||||
const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: payload });
|
|
||||||
console.log("DEBUG: API - Created/Updated Client: ", result);
|
console.log("DEBUG: API - Created/Updated Client: ", result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -391,7 +394,10 @@ class Api {
|
|||||||
if (!places || places.length === 0) {
|
if (!places || places.length === 0) {
|
||||||
throw new Error(`No location data found for zip code ${zipcode}`);
|
throw new Error(`No location data found for zip code ${zipcode}`);
|
||||||
}
|
}
|
||||||
return places;
|
return places.map((place) => ({
|
||||||
|
city: place["place name"],
|
||||||
|
state: place["state abbreviation"],
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getGeocode(address) {
|
static async getGeocode(address) {
|
||||||
|
|||||||
@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Address Information</h3>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field full-width">
|
||||||
|
<label for="address-title"> Address Title <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="address-title"
|
||||||
|
v-model="localFormData.addressTitle"
|
||||||
|
:disabled="isSubmitting || isEditMode"
|
||||||
|
placeholder="e.g., Home, Office, Site A"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field full-width">
|
||||||
|
<label for="address-line1"> Address Line 1 <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="address-line1"
|
||||||
|
v-model="localFormData.addressLine1"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
placeholder="Street address"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field full-width">
|
||||||
|
<label for="address-line2">Address Line 2</label>
|
||||||
|
<InputText
|
||||||
|
id="address-line2"
|
||||||
|
v-model="localFormData.addressLine2"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
placeholder="Apt, suite, unit, etc."
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="zipcode"> Zip Code <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="zipcode"
|
||||||
|
v-model="localFormData.pincode"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@input="handleZipcodeInput"
|
||||||
|
maxlength="5"
|
||||||
|
placeholder="12345"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="city"> City <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="city"
|
||||||
|
v-model="localFormData.city"
|
||||||
|
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||||
|
placeholder="City"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="state"> State <span class="required">*</span> </label>
|
||||||
|
<InputText
|
||||||
|
id="state"
|
||||||
|
v-model="localFormData.state"
|
||||||
|
:disabled="isSubmitting || zipcodeLookupDisabled"
|
||||||
|
placeholder="State"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import InputText from "primevue/inputtext";
|
||||||
|
import Api from "../../api";
|
||||||
|
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSubmitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isEditMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:formData"]);
|
||||||
|
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
const localFormData = computed({
|
||||||
|
get: () => props.formData,
|
||||||
|
set: (value) => emit("update:formData", value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipcodeLookupDisabled = ref(true);
|
||||||
|
|
||||||
|
const handleZipcodeInput = async (event) => {
|
||||||
|
const input = event.target.value;
|
||||||
|
|
||||||
|
// Only allow digits
|
||||||
|
const digitsOnly = input.replace(/\D/g, "");
|
||||||
|
|
||||||
|
// Limit to 5 digits
|
||||||
|
if (digitsOnly.length > 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localFormData.value.pincode = digitsOnly;
|
||||||
|
|
||||||
|
// Reset city/state if zipcode is not complete
|
||||||
|
if (digitsOnly.length < 5 && zipcodeLookupDisabled.value) {
|
||||||
|
localFormData.value.city = "";
|
||||||
|
localFormData.value.state = "";
|
||||||
|
zipcodeLookupDisabled.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch city/state when 5 digits entered
|
||||||
|
if (digitsOnly.length === 5) {
|
||||||
|
try {
|
||||||
|
console.log("DEBUG: Looking up city/state for zip code:", digitsOnly);
|
||||||
|
const places = await Api.getCityStateByZip(digitsOnly);
|
||||||
|
console.log("DEBUG: Retrieved places:", places);
|
||||||
|
if (places && places.length > 0) {
|
||||||
|
// Auto-populate city and state
|
||||||
|
localFormData.value.city = places[0]["city"];
|
||||||
|
localFormData.value.state = places[0]["state"];
|
||||||
|
zipcodeLookupDisabled.value = true;
|
||||||
|
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Enable manual entry if lookup fails
|
||||||
|
zipcodeLookupDisabled.value = false;
|
||||||
|
notificationStore.addWarning(
|
||||||
|
"Could not find city/state for this zip code. Please enter manually.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
322
frontend/src/components/clientSubPages/ClientInformationForm.vue
Normal file
322
frontend/src/components/clientSubPages/ClientInformationForm.vue
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="customer-name"> Customer Name <span class="required">*</span> </label>
|
||||||
|
<div class="input-with-button">
|
||||||
|
<InputText
|
||||||
|
id="customer-name"
|
||||||
|
v-model="localFormData.customerName"
|
||||||
|
:disabled="isSubmitting || isEditMode"
|
||||||
|
placeholder="Enter customer name"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="!isNewClient && !isEditMode"
|
||||||
|
@click="searchCustomers"
|
||||||
|
:disabled="isSubmitting || !localFormData.customerName.trim()"
|
||||||
|
size="small"
|
||||||
|
class="iconoir-btn"
|
||||||
|
>
|
||||||
|
<IconoirMagnifyingGlass width="20" height="20" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="customer-type"> Customer Type <span class="required">*</span> </label>
|
||||||
|
<Select
|
||||||
|
id="customer-type"
|
||||||
|
v-model="localFormData.customerType"
|
||||||
|
:options="customerTypeOptions"
|
||||||
|
:disabled="isSubmitting || (!isNewClient && !isEditMode)"
|
||||||
|
placeholder="Select customer type"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Customer Search Results Modal -->
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="showCustomerSearchModal"
|
||||||
|
header="Select Customer"
|
||||||
|
:modal="true"
|
||||||
|
class="search-dialog"
|
||||||
|
>
|
||||||
|
<div class="search-results">
|
||||||
|
<div v-if="customerSearchResults.length === 0" class="no-results">
|
||||||
|
<i class="pi pi-info-circle"></i>
|
||||||
|
<p>No customers found matching your search.</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="results-list">
|
||||||
|
<div
|
||||||
|
v-for="(customerName, index) in customerSearchResults"
|
||||||
|
:key="index"
|
||||||
|
class="result-item"
|
||||||
|
@click="selectCustomer(customerName)"
|
||||||
|
>
|
||||||
|
<strong>{{ customerName }}</strong>
|
||||||
|
<i class="pi pi-chevron-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSubmitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isEditMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:formData", "newClientToggle", "customerSelected"]);
|
||||||
|
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
const localFormData = computed({
|
||||||
|
get: () => props.formData,
|
||||||
|
set: (value) => emit("update:formData", value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isNewClient = ref(true);
|
||||||
|
const showCustomerSearchModal = ref(false);
|
||||||
|
const customerSearchResults = ref([]);
|
||||||
|
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
||||||
|
|
||||||
|
// Watch for toggle changes
|
||||||
|
watch(isNewClient, (newValue) => {
|
||||||
|
emit("newClientToggle", newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchCustomers = async () => {
|
||||||
|
const searchTerm = localFormData.value.customerName.trim();
|
||||||
|
if (!searchTerm) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all customers and filter by search term
|
||||||
|
const allCustomers = await Api.getClientNames(searchTerm);
|
||||||
|
const matchingNames = allCustomers.filter((name) =>
|
||||||
|
name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingNames.length === 0) {
|
||||||
|
notificationStore.addWarning("No customers found matching your search criteria.");
|
||||||
|
} else {
|
||||||
|
// Store just the names for display
|
||||||
|
customerSearchResults.value = matchingNames;
|
||||||
|
showCustomerSearchModal.value = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching customers:", error);
|
||||||
|
notificationStore.addError("Failed to search customers. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCustomer = async (customerName) => {
|
||||||
|
try {
|
||||||
|
// Fetch full customer data
|
||||||
|
const clientData = await Api.getClient(customerName);
|
||||||
|
|
||||||
|
localFormData.value.customerName = clientData.customerName;
|
||||||
|
localFormData.value.customerType = clientData.customerType;
|
||||||
|
showCustomerSearchModal.value = false;
|
||||||
|
|
||||||
|
// Pass the full client data including contacts
|
||||||
|
emit("customerSelected", clientData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching client ${customerName}:`, error);
|
||||||
|
notificationStore.addError("Failed to load customer details. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
isNewClient,
|
||||||
|
});
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--red-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-button {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-dialog {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results i {
|
||||||
|
font-size: 2em;
|
||||||
|
color: var(--orange-500);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-type {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconoir-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconoir-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconoir-btn:hover:not(:disabled) {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,361 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isSubmitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isEditMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isNewClientLocked: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
availableContacts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:formData", "newContactToggle"]);
|
||||||
|
|
||||||
|
const localFormData = computed({
|
||||||
|
get: () => props.formData,
|
||||||
|
set: (value) => emit("update:formData", value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isNewContact = ref(false);
|
||||||
|
const selectedContact = ref(null);
|
||||||
|
const sameAsClientName = ref(false);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.isNewClientLocked) {
|
||||||
|
isNewContact.value = 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 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,
|
||||||
|
});
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,172 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overview-container">
|
<div class="overview-container">
|
||||||
<!-- Client Basic Info Card -->
|
<!-- Form Mode (new=true or edit mode) -->
|
||||||
<div class="info-card">
|
<template v-if="isNew || editMode">
|
||||||
<div class="card-header">
|
<ClientInformationForm
|
||||||
<h3>Client Information</h3>
|
ref="clientInfoRef"
|
||||||
<Button
|
v-model:form-data="formData"
|
||||||
v-if="!isNew && !editMode"
|
:is-submitting="isSubmitting"
|
||||||
@click="toggleEditMode"
|
:is-edit-mode="editMode"
|
||||||
icon="pi pi-pencil"
|
@new-client-toggle="handleNewClientToggle"
|
||||||
label="Edit"
|
@customer-selected="handleCustomerSelected"
|
||||||
size="small"
|
/>
|
||||||
severity="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<label>Customer Name:</label>
|
|
||||||
<AutoComplete
|
|
||||||
v-if="isNew || editMode"
|
|
||||||
v-model="formData.customerName"
|
|
||||||
:suggestions="customerSuggestions"
|
|
||||||
@complete="searchCustomers"
|
|
||||||
@item-select="onCustomerSelect"
|
|
||||||
placeholder="Type or select customer name"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
<span v-else>{{ clientData?.customerName || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>Customer Type:</label>
|
|
||||||
<Select
|
|
||||||
v-if="isNew || editMode"
|
|
||||||
v-model="formData.customerType"
|
|
||||||
:options="customerTypeOptions"
|
|
||||||
placeholder="Select customer type"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
<span v-else>{{ clientData?.customerType || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="!isNew && !editMode">
|
|
||||||
<label>Customer Group:</label>
|
|
||||||
<span>{{ clientData?.customerGroup || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="!isNew && !editMode">
|
|
||||||
<label>Territory:</label>
|
|
||||||
<span>{{ clientData?.territory || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Address Info Card -->
|
<ContactInformationForm
|
||||||
<div class="info-card">
|
ref="contactInfoRef"
|
||||||
<h3>Address Information</h3>
|
v-model:form-data="formData"
|
||||||
<div class="info-grid">
|
:is-submitting="isSubmitting"
|
||||||
<div class="info-item" v-if="isNew || editMode">
|
:is-edit-mode="editMode"
|
||||||
<label>Address Line 1 *:</label>
|
:is-new-client-locked="isNewClientMode"
|
||||||
<InputText
|
:available-contacts="availableContacts"
|
||||||
v-model="formData.addressLine1"
|
@new-contact-toggle="handleNewContactToggle"
|
||||||
placeholder="Street address"
|
/>
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="isNew || editMode">
|
|
||||||
<label>Address Line 2:</label>
|
|
||||||
<InputText
|
|
||||||
v-model="formData.addressLine2"
|
|
||||||
placeholder="Apt, suite, unit, etc."
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="isNew || editMode">
|
|
||||||
<label>Zip Code *:</label>
|
|
||||||
<InputText
|
|
||||||
v-model="formData.zipcode"
|
|
||||||
placeholder="12345"
|
|
||||||
@input="handleZipcodeInput"
|
|
||||||
maxlength="5"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="isNew || editMode">
|
|
||||||
<label>City *:</label>
|
|
||||||
<InputText
|
|
||||||
v-model="formData.city"
|
|
||||||
placeholder="City"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting || zipcodeLookupDisabled"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="isNew || editMode">
|
|
||||||
<label>State *:</label>
|
|
||||||
<InputText
|
|
||||||
v-model="formData.state"
|
|
||||||
placeholder="State"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isSubmitting || zipcodeLookupDisabled"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- Read-only mode for existing clients -->
|
|
||||||
<div
|
|
||||||
class="info-item full-width"
|
|
||||||
v-if="!isNew && !editMode && selectedAddressData"
|
|
||||||
>
|
|
||||||
<label>Full Address:</label>
|
|
||||||
<span>{{ fullAddress }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData">
|
|
||||||
<label>City:</label>
|
|
||||||
<span>{{ selectedAddressData.city || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData">
|
|
||||||
<label>State:</label>
|
|
||||||
<span>{{ selectedAddressData.state || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item" v-if="!isNew && !editMode && selectedAddressData">
|
|
||||||
<label>Zip Code:</label>
|
|
||||||
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Info Card (only for new/edit mode) -->
|
<AddressInformationForm
|
||||||
<div class="info-card" v-if="isNew || editMode">
|
v-model:form-data="formData"
|
||||||
<h3>Contact Information</h3>
|
:is-submitting="isSubmitting"
|
||||||
<div class="info-grid">
|
:is-edit-mode="editMode"
|
||||||
<div class="info-item">
|
/>
|
||||||
<label>Contact Name *:</label>
|
</template>
|
||||||
<AutoComplete
|
|
||||||
v-model="formData.contactName"
|
<!-- Display Mode (existing client view) -->
|
||||||
:suggestions="contactSuggestions"
|
<template v-else>
|
||||||
@complete="searchContacts"
|
<!-- Client Basic Info Card -->
|
||||||
@item-select="onContactSelect"
|
<div class="info-card">
|
||||||
placeholder="Type or select contact name"
|
<div class="card-header">
|
||||||
class="w-full"
|
<h3>Client Information</h3>
|
||||||
:disabled="isSubmitting || !formData.customerName"
|
<Button
|
||||||
required
|
@click="toggleEditMode"
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
label="Edit"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-grid">
|
||||||
<label>Phone Number:</label>
|
<div class="info-item">
|
||||||
<InputText
|
<label>Customer Name:</label>
|
||||||
v-model="formData.phoneNumber"
|
<span>{{ clientData?.customerName || "N/A" }}</span>
|
||||||
placeholder="(555) 123-4567"
|
</div>
|
||||||
class="w-full"
|
<div class="info-item">
|
||||||
:disabled="isSubmitting"
|
<label>Customer Type:</label>
|
||||||
/>
|
<span>{{ clientData?.customerType || "N/A" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<label>Email:</label>
|
<label>Customer Group:</label>
|
||||||
<InputText
|
<span>{{ clientData?.customerGroup || "N/A" }}</span>
|
||||||
v-model="formData.email"
|
</div>
|
||||||
placeholder="email@example.com"
|
<div class="info-item">
|
||||||
type="email"
|
<label>Territory:</label>
|
||||||
class="w-full"
|
<span>{{ clientData?.territory || "N/A" }}</span>
|
||||||
:disabled="isSubmitting"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Contact Name:</label>
|
||||||
|
<span>{{ contactFullName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Phone:</label>
|
||||||
|
<span>{{ selectedAddressData.phone || "N/A" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Email:</label>
|
||||||
|
<span>{{ selectedAddressData.emailId || "N/A" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Status Cards (only for existing clients) -->
|
<!-- Status Cards (only for existing clients) -->
|
||||||
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData">
|
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData">
|
||||||
@ -204,13 +145,13 @@
|
|||||||
<div class="form-actions" v-if="isNew || editMode">
|
<div class="form-actions" v-if="isNew || editMode">
|
||||||
<Button
|
<Button
|
||||||
@click="handleCancel"
|
@click="handleCancel"
|
||||||
label="Cancel"
|
:label="editMode ? 'Cancel' : 'Clear'"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@click="handleSave"
|
@click="handleSave"
|
||||||
:label="isNew ? 'Create Client' : 'Save Changes'"
|
:label="isNew ? 'Create' : 'Update'"
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
/>
|
/>
|
||||||
@ -262,11 +203,11 @@
|
|||||||
import { computed, ref, watch, onMounted } from "vue";
|
import { computed, ref, watch, onMounted } from "vue";
|
||||||
import Badge from "primevue/badge";
|
import Badge from "primevue/badge";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import InputText from "primevue/inputtext";
|
|
||||||
import AutoComplete from "primevue/autocomplete";
|
|
||||||
import Select from "primevue/select";
|
|
||||||
import Dialog from "primevue/dialog";
|
import Dialog from "primevue/dialog";
|
||||||
import LeafletMap from "../common/LeafletMap.vue";
|
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 DataUtils from "../../utils";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
@ -290,46 +231,42 @@ const props = defineProps({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
// Refs for child components
|
||||||
|
const clientInfoRef = ref(null);
|
||||||
|
const contactInfoRef = ref(null);
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const editMode = ref(false);
|
const editMode = ref(false);
|
||||||
const showEditConfirmDialog = ref(false);
|
const showEditConfirmDialog = ref(false);
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
const zipcodeLookupDisabled = ref(true);
|
const isNewClientMode = ref(false);
|
||||||
|
const availableContacts = ref([]);
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
customerName: "",
|
customerName: "",
|
||||||
customerType: "",
|
customerType: "",
|
||||||
|
addressTitle: "",
|
||||||
addressLine1: "",
|
addressLine1: "",
|
||||||
addressLine2: "",
|
addressLine2: "",
|
||||||
zipcode: "",
|
pincode: "",
|
||||||
city: "",
|
city: "",
|
||||||
state: "",
|
state: "",
|
||||||
contactName: "",
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
email: "",
|
email: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Autocomplete data
|
|
||||||
const customerSuggestions = ref([]);
|
|
||||||
const contactSuggestions = ref([]);
|
|
||||||
const selectedCustomerData = ref(null);
|
|
||||||
|
|
||||||
// Options
|
|
||||||
const customerTypeOptions = ref(["Company", "Individual"]);
|
|
||||||
|
|
||||||
// Initialize form data when component mounts
|
// Initialize form data when component mounts
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.isNew) {
|
if (props.isNew) {
|
||||||
// Initialize empty form for new client
|
|
||||||
resetForm();
|
resetForm();
|
||||||
console.log("Mounted in new client mode - initialized empty form");
|
console.log("Mounted in new client mode - initialized empty form");
|
||||||
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
||||||
// Populate form with existing client data
|
|
||||||
populateFormFromClientData();
|
populateFormFromClientData();
|
||||||
console.log("Mounted with existing client data - populated form");
|
console.log("Mounted with existing client data - populated form");
|
||||||
} else {
|
} else {
|
||||||
// Default to empty form if no client data
|
|
||||||
resetForm();
|
resetForm();
|
||||||
console.log("Mounted with no client data - initialized empty form");
|
console.log("Mounted with no client data - initialized empty form");
|
||||||
}
|
}
|
||||||
@ -340,32 +277,27 @@ watch(
|
|||||||
() => props.clientData,
|
() => props.clientData,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
if (props.isNew) {
|
if (props.isNew) {
|
||||||
// Always keep form empty for new clients, regardless of clientData
|
|
||||||
resetForm();
|
resetForm();
|
||||||
} else if (newData && Object.keys(newData).length > 0) {
|
} else if (newData && Object.keys(newData).length > 0) {
|
||||||
populateFormFromClientData();
|
populateFormFromClientData();
|
||||||
} else {
|
} else {
|
||||||
// No client data, reset form
|
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Watch for isNew prop changes to reset form when switching to new client mode
|
// Watch for isNew prop changes
|
||||||
watch(
|
watch(
|
||||||
() => props.isNew,
|
() => props.isNew,
|
||||||
(isNewValue) => {
|
(isNewValue) => {
|
||||||
if (isNewValue) {
|
if (isNewValue) {
|
||||||
// Reset form when switching to new client mode
|
|
||||||
resetForm();
|
resetForm();
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
console.log("Switched to new client mode - reset form data");
|
console.log("Switched to new client mode - reset form data");
|
||||||
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
} else if (props.clientData && Object.keys(props.clientData).length > 0) {
|
||||||
// Populate form when switching back to existing client
|
|
||||||
populateFormFromClientData();
|
populateFormFromClientData();
|
||||||
} else {
|
} else {
|
||||||
// No client data, reset form
|
|
||||||
resetForm();
|
resetForm();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -386,15 +318,11 @@ const selectedAddressData = computed(() => {
|
|||||||
// Get coordinates from the selected address
|
// Get coordinates from the selected address
|
||||||
const latitude = computed(() => {
|
const latitude = computed(() => {
|
||||||
if (!selectedAddressData.value) return null;
|
if (!selectedAddressData.value) return null;
|
||||||
|
|
||||||
// Check custom fields first, then fallback to regular fields
|
|
||||||
return selectedAddressData.value.customLatitude || selectedAddressData.value.latitude || null;
|
return selectedAddressData.value.customLatitude || selectedAddressData.value.latitude || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const longitude = computed(() => {
|
const longitude = computed(() => {
|
||||||
if (!selectedAddressData.value) return null;
|
if (!selectedAddressData.value) return null;
|
||||||
|
|
||||||
// Check custom fields first, then fallback to regular fields
|
|
||||||
return (
|
return (
|
||||||
selectedAddressData.value.customLongitude || selectedAddressData.value.longitude || null
|
selectedAddressData.value.customLongitude || selectedAddressData.value.longitude || null
|
||||||
);
|
);
|
||||||
@ -406,16 +334,36 @@ const fullAddress = computed(() => {
|
|||||||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
});
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
const isFormValid = computed(() => {
|
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 hasFirstName = formData.value.firstName?.trim();
|
||||||
|
const hasLastName = formData.value.lastName?.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
formData.value.customerName &&
|
hasCustomerName &&
|
||||||
formData.value.customerType &&
|
hasCustomerType &&
|
||||||
formData.value.addressLine1 &&
|
hasAddressTitle &&
|
||||||
formData.value.zipcode &&
|
hasAddressLine1 &&
|
||||||
formData.value.city &&
|
hasPincode &&
|
||||||
formData.value.state &&
|
hasCity &&
|
||||||
formData.value.contactName
|
hasState &&
|
||||||
|
hasFirstName &&
|
||||||
|
hasLastName
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -425,7 +373,7 @@ const getStatusSeverity = (status) => {
|
|||||||
case "Not Started":
|
case "Not Started":
|
||||||
return "secondary";
|
return "secondary";
|
||||||
case "In Progress":
|
case "In Progress":
|
||||||
return "warn"; // Use 'warn' instead of 'warning' for PrimeVue Badge
|
return "warn";
|
||||||
case "Completed":
|
case "Completed":
|
||||||
return "success";
|
return "success";
|
||||||
default:
|
default:
|
||||||
@ -438,18 +386,20 @@ const resetForm = () => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
customerName: "",
|
customerName: "",
|
||||||
customerType: "",
|
customerType: "",
|
||||||
|
addressTitle: "",
|
||||||
addressLine1: "",
|
addressLine1: "",
|
||||||
addressLine2: "",
|
addressLine2: "",
|
||||||
zipcode: "",
|
pincode: "",
|
||||||
city: "",
|
city: "",
|
||||||
state: "",
|
state: "",
|
||||||
contactName: "",
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
email: "",
|
email: "",
|
||||||
};
|
};
|
||||||
selectedCustomerData.value = null;
|
availableContacts.value = [];
|
||||||
zipcodeLookupDisabled.value = false; // Allow manual entry for new clients
|
isNewClientMode.value = false;
|
||||||
editMode.value = false; // Ensure edit mode is off
|
editMode.value = false;
|
||||||
console.log("Form reset - all fields cleared");
|
console.log("Form reset - all fields cleared");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -459,15 +409,46 @@ const populateFormFromClientData = () => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
customerName: props.clientData.customerName || "",
|
customerName: props.clientData.customerName || "",
|
||||||
customerType: props.clientData.customerType || "",
|
customerType: props.clientData.customerType || "",
|
||||||
|
addressTitle: selectedAddressData.value.addressTitle || "",
|
||||||
addressLine1: selectedAddressData.value.addressLine1 || "",
|
addressLine1: selectedAddressData.value.addressLine1 || "",
|
||||||
addressLine2: selectedAddressData.value.addressLine2 || "",
|
addressLine2: selectedAddressData.value.addressLine2 || "",
|
||||||
zipcode: selectedAddressData.value.pincode || "",
|
pincode: selectedAddressData.value.pincode || "",
|
||||||
city: selectedAddressData.value.city || "",
|
city: selectedAddressData.value.city || "",
|
||||||
state: selectedAddressData.value.state || "",
|
state: selectedAddressData.value.state || "",
|
||||||
contactName: selectedAddressData.value.customContactName || "",
|
firstName: selectedAddressData.value.customContactFirstName || "",
|
||||||
|
lastName: selectedAddressData.value.customContactLastName || "",
|
||||||
phoneNumber: selectedAddressData.value.phone || "",
|
phoneNumber: selectedAddressData.value.phone || "",
|
||||||
email: selectedAddressData.value.emailId || "",
|
email: selectedAddressData.value.emailId || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Populate available contacts if any
|
||||||
|
if (selectedAddressData.value.contacts && selectedAddressData.value.contacts.length > 0) {
|
||||||
|
availableContacts.value = selectedAddressData.value.contacts;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
// Edit mode methods
|
||||||
@ -478,95 +459,7 @@ const toggleEditMode = () => {
|
|||||||
const confirmEdit = () => {
|
const confirmEdit = () => {
|
||||||
showEditConfirmDialog.value = false;
|
showEditConfirmDialog.value = false;
|
||||||
editMode.value = true;
|
editMode.value = true;
|
||||||
populateFormFromClientData(); // Refresh form with current data
|
populateFormFromClientData();
|
||||||
};
|
|
||||||
|
|
||||||
// Zipcode handling
|
|
||||||
const handleZipcodeInput = async (event) => {
|
|
||||||
const input = event.target.value;
|
|
||||||
|
|
||||||
// Only allow digits
|
|
||||||
const digitsOnly = input.replace(/\D/g, "");
|
|
||||||
|
|
||||||
// Limit to 5 digits
|
|
||||||
if (digitsOnly.length > 5) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
formData.value.zipcode = digitsOnly;
|
|
||||||
|
|
||||||
// Fetch city/state when 5 digits entered
|
|
||||||
if (digitsOnly.length === 5) {
|
|
||||||
try {
|
|
||||||
const places = await Api.getCityStateByZip(digitsOnly);
|
|
||||||
if (places && places.length > 0) {
|
|
||||||
// Auto-populate city and state
|
|
||||||
formData.value.city = places[0]["place name"];
|
|
||||||
formData.value.state = places[0]["state abbreviation"];
|
|
||||||
zipcodeLookupDisabled.value = true;
|
|
||||||
notificationStore.addSuccess(
|
|
||||||
`Found: ${places[0]["place name"]}, ${places[0]["state abbreviation"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Enable manual entry if lookup fails
|
|
||||||
zipcodeLookupDisabled.value = false;
|
|
||||||
notificationStore.addWarning(
|
|
||||||
"Could not find city/state for this zip code. Please enter manually.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reset city/state if zipcode is incomplete
|
|
||||||
if (zipcodeLookupDisabled.value) {
|
|
||||||
formData.value.city = "";
|
|
||||||
formData.value.state = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Customer search
|
|
||||||
const searchCustomers = async (event) => {
|
|
||||||
try {
|
|
||||||
const customers = await Api.getCustomerNames("all");
|
|
||||||
customerSuggestions.value = customers.filter((name) =>
|
|
||||||
name.toLowerCase().includes(event.query.toLowerCase()),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error searching customers:", error);
|
|
||||||
customerSuggestions.value = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCustomerSelect = (event) => {
|
|
||||||
// Store selected customer for contact lookup
|
|
||||||
selectedCustomerData.value = event.value;
|
|
||||||
// Reset contact data when customer changes
|
|
||||||
formData.value.contactName = "";
|
|
||||||
formData.value.phoneNumber = "";
|
|
||||||
formData.value.email = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Contact search
|
|
||||||
const searchContacts = async (event) => {
|
|
||||||
if (!selectedCustomerData.value) {
|
|
||||||
contactSuggestions.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: Implement contact search API method
|
|
||||||
// For now, just allow typing
|
|
||||||
contactSuggestions.value = [event.query];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error searching contacts:", error);
|
|
||||||
contactSuggestions.value = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onContactSelect = (event) => {
|
|
||||||
// TODO: Auto-populate phone and email from selected contact
|
|
||||||
// For now, just set the name
|
|
||||||
formData.value.contactName = event.value;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save/Cancel actions
|
// Save/Cancel actions
|
||||||
@ -579,33 +472,37 @@ const handleSave = async () => {
|
|||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
try {
|
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,
|
||||||
|
firstName: formData.value.firstName,
|
||||||
|
lastName: formData.value.lastName,
|
||||||
|
phoneNumber: formData.value.phoneNumber,
|
||||||
|
email: formData.value.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
if (props.isNew) {
|
||||||
// Create new client
|
|
||||||
const clientData = {
|
|
||||||
customerName: formData.value.customerName,
|
|
||||||
customerType: formData.value.customerType,
|
|
||||||
addressLine1: formData.value.addressLine1,
|
|
||||||
addressLine2: formData.value.addressLine2,
|
|
||||||
zipcode: formData.value.zipcode,
|
|
||||||
city: formData.value.city,
|
|
||||||
state: formData.value.state,
|
|
||||||
contactName: formData.value.contactName,
|
|
||||||
phoneNumber: formData.value.phoneNumber,
|
|
||||||
email: formData.value.email,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Implement API call to create client
|
|
||||||
console.log("Would create client with data:", clientData);
|
|
||||||
|
|
||||||
// For now, just show success and redirect
|
|
||||||
const fullAddress = DataUtils.calculateFullAddress({
|
|
||||||
addressLine1: formData.value.addressLine1,
|
|
||||||
addressLine2: formData.value.addressLine2,
|
|
||||||
city: formData.value.city,
|
|
||||||
state: formData.value.state,
|
|
||||||
pincode: formData.value.zipcode,
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationStore.addSuccess(
|
notificationStore.addSuccess(
|
||||||
`Client ${formData.value.customerName} created successfully!`,
|
`Client ${formData.value.customerName} created successfully!`,
|
||||||
);
|
);
|
||||||
@ -619,12 +516,11 @@ const handleSave = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Update existing client (edit mode)
|
|
||||||
// TODO: Implement API call to update client
|
|
||||||
console.log("Would update client with data:", formData.value);
|
|
||||||
|
|
||||||
notificationStore.addSuccess("Client updated successfully!");
|
notificationStore.addSuccess("Client updated successfully!");
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
|
|
||||||
|
// Reload the client data
|
||||||
|
// Note: Parent component should handle reloading
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving client:", error);
|
console.error("Error saving client:", error);
|
||||||
@ -636,8 +532,8 @@ const handleSave = async () => {
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (props.isNew) {
|
if (props.isNew) {
|
||||||
// Go back for new client
|
// Clear form for new client
|
||||||
router.back();
|
resetForm();
|
||||||
} else {
|
} else {
|
||||||
// Exit edit mode and restore original data
|
// Exit edit mode and restore original data
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { globalSettings } from "./globalSettings";
|
|||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
// Vuetify
|
// Vuetify
|
||||||
|
import "@primeuix/themes/aura";
|
||||||
import "vuetify/styles";
|
import "vuetify/styles";
|
||||||
import { createVuetify } from "vuetify";
|
import { createVuetify } from "vuetify";
|
||||||
import * as components from "vuetify/components";
|
import * as components from "vuetify/components";
|
||||||
|
|||||||
@ -55,3 +55,19 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fix ToggleSwitch z-index so slider is visible but input receives clicks */
|
||||||
|
.p-toggleswitch {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-toggleswitch-slider {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-toggleswitch-input {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user