From 8ebd77540c1c68f294cdc9841c06061f76398bf8 Mon Sep 17 00:00:00 2001 From: Casey Date: Sat, 14 Feb 2026 08:47:53 -0600 Subject: [PATCH] added ability to link address/contacts if they already exist --- custom_ui/api/db/addresses.py | 26 ++ custom_ui/api/db/clients.py | 74 +++-- custom_ui/api/db/contacts.py | 19 ++ frontend/src/api.js | 20 ++ .../clientSubPages/AddressInformationForm.vue | 91 +++--- .../clientSubPages/ClientInformationForm.vue | 134 +++++---- .../clientSubPages/ContactInformationForm.vue | 27 +- frontend/src/components/pages/Client.vue | 269 +++++++++++++----- frontend/src/components/pages/Estimate.vue | 11 - frontend/src/components/pages/Job.vue | 3 +- .../Screenshot from 2026-02-14 08-33-17.png | Bin 0 -> 107232 bytes 11 files changed, 450 insertions(+), 224 deletions(-) create mode 100644 frontend/src/components/pages/Screenshot from 2026-02-14 08-33-17.png diff --git a/custom_ui/api/db/addresses.py b/custom_ui/api/db/addresses.py index bb99ff7..5881ba0 100644 --- a/custom_ui/api/db/addresses.py +++ b/custom_ui/api/db/addresses.py @@ -3,6 +3,32 @@ import json from custom_ui.db_utils import build_error_response, build_success_response from custom_ui.services import ClientService, AddressService, ContactService +@frappe.whitelist() +def check_addresses_exist(addresses): + """Check if any of the provided addresses already exist in the system.""" + if isinstance(addresses, str): + addresses = json.loads(addresses) + print(f"DEBUG: check_addresses_exist called with addresses: {addresses}") + existing_addresses = [] + for address in addresses: + filters = { + "doctype": "Address", + "address_line1": address.get("address_line1"), + "city": address.get("city"), + # "state": address.get("state"), + "pincode": address.get("pincode") + } + if address.get("address_line2"): + filters["address_line2"] = address.get("address_line2") + print(f"DEBUG: Checking existence for address with filters: {filters}") + if frappe.db.exists(filters): + print("DEBUG: Address exists:", filters) + existing_addresses.append(address) + else: + print("DEBUG: Address does not exist:", filters) + + return build_success_response(existing_addresses) + @frappe.whitelist() def get_address_by_full_address(full_address): """Get address by full_address, including associated contacts.""" diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index c1c6194..e89ecbf 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -9,6 +9,25 @@ from custom_ui.services import AddressService, ContactService, ClientService # CLIENT MANAGEMENT API METHODS # =============================================================================== +@frappe.whitelist() +def check_client_exists(client_name): + """Check if a client exists as either a Customer or a Lead. + Additionally, return a list of potential matches based on the client name.""" + print("DEBUG: check_client_exists called with client_name:", client_name) + try: + exact_customer_match = frappe.db.exists("Customer", client_name) + exact_lead_match = frappe.db.exists("Lead", {"custom_customer_name": client_name}) + customer_matches = frappe.get_all("Customer", pluck="name", filters={"name": ["like", f"%{client_name}%"]}) + lead_matches = frappe.get_all("Lead", pluck="custom_customer_name", filters={"custom_customer_name": ["like", f"%{client_name}%"]}) + # remove duplicates from potential matches between customers and leads + + return build_success_response({ + "exact_match": exact_customer_match or exact_lead_match, + "potential_matches": list(set(customer_matches + lead_matches)) + }) + except Exception as e: + return build_error_response(str(e), 500) + @frappe.whitelist() def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None): """Get counts of clients by status categories with optional weekly filtering.""" @@ -363,15 +382,15 @@ def upsert_client(data): client_doc = check_and_get_client_doc(customer_name) if client_doc: return build_error_response(f"Client with name '{customer_name}' already exists.", 400) - for address in addresses: - if address_exists( - address.get("address_line1"), - address.get("address_line2"), - address.get("city"), - address.get("state"), - address.get("pincode") - ): - return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400) + # for address in addresses: + # if address_exists( + # address.get("address_line1"), + # address.get("address_line2"), + # address.get("city"), + # address.get("state"), + # address.get("pincode") + # ): + # return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400) # Handle customer creation/update @@ -444,25 +463,36 @@ def upsert_client(data): # Handle address creation address_docs = [] for address in addresses: + is_billing = True if address.get("is_billing_address") else False is_service = True if address.get("is_service_address") else False - print("#####DEBUG: Creating address with data:", address) - address_doc = AddressService.create_address({ - "address_title": AddressService.build_address_title(customer_name, address), + address_exists = frappe.db.exists("Address", { "address_line1": address.get("address_line1"), "address_line2": address.get("address_line2"), - "address_type": "Billing" if is_billing else "Service", - "custom_billing_address": is_billing, - "is_service_address": is_service, - "is_primary_address": is_billing, "city": address.get("city"), - "state": address.get("state"), - "country": "United States", - "pincode": address.get("pincode"), - "customer_type": "Lead", - "customer_name": client_doc.name, - "companies": [{ "company": data.get("company_name") }] + "pincode": address.get("pincode") }) + address_doc = None + if address_exists: + address_doc = frappe.get_doc("Address", address_exists) + else: + print("#####DEBUG: Creating address with data:", address) + address_doc = AddressService.create_address({ + "address_title": AddressService.build_address_title(customer_name, address), + "address_line1": address.get("address_line1"), + "address_line2": address.get("address_line2"), + "address_type": "Billing" if is_billing else "Service", + "custom_billing_address": is_billing, + "is_service_address": is_service, + "is_primary_address": is_billing, + "city": address.get("city"), + "state": address.get("state"), + "country": "United States", + "pincode": address.get("pincode"), + "customer_type": "Lead", + "customer_name": client_doc.name, + "companies": [{ "company": data.get("company_name") }] + }) AddressService.link_address_to_customer(address_doc, "Lead", client_doc.name) address_doc.reload() if is_billing: diff --git a/custom_ui/api/db/contacts.py b/custom_ui/api/db/contacts.py index d64b26f..adf1c1e 100644 --- a/custom_ui/api/db/contacts.py +++ b/custom_ui/api/db/contacts.py @@ -1,4 +1,23 @@ import frappe +import json +from custom_ui.db_utils import build_error_response, build_success_response + +@frappe.whitelist() +def check_contacts_exist(contacts): + """Check if any of the provided contacts already exist in the system.""" + if isinstance(contacts, str): + contacts = json.loads(contacts) + print(f"DEBUG: check_contacts_exist called with contacts: {contacts}") + existing_contacts = [] + for contact in contacts: + if frappe.db.exists("Contact", { + "first_name": contact.get("first_name"), + "last_name": contact.get("last_name"), + "email_id": contact.get("email"), + "phone": contact.get("phone_number") + }): + existing_contacts.append(contact) + return build_success_response(existing_contacts) def existing_contact_name(first_name: str, last_name: str, email: str, phone: str) -> str: """Check if a contact exists based on provided details.""" diff --git a/frontend/src/api.js b/frontend/src/api.js index 1b3b9e1..a2eb5d1 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -56,7 +56,10 @@ const FRAPPE_GET_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.g const FRAPPE_GET_ONSITE_MEETINGS_METHOD = "custom_ui.api.db.bid_meetings.get_bid_meetings"; const FRAPPE_SUBMIT_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.submit_bid_meeting_note_form"; // Address methods +const FRAPPE_CHECK_ADDRESSES_EXIST_METHOD = "custom_ui.api.db.addresses.check_addresses_exist"; const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses"; +// Contact methods +const FRAPPE_CHECK_CONTACTS_EXIST_METHOD = "custom_ui.api.db.contacts.check_contacts_exist"; // Client methods const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client"; const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts"; @@ -64,6 +67,7 @@ const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_client const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_clients_table_data_v2"; const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2"; const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names"; +const FRAPPE_CHECK_CLIENT_EXISTS_METHOD = "custom_ui.api.db.clients.check_client_exists"; // Employee methods const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees"; const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized"; @@ -105,6 +109,10 @@ class Api { // CLIENT METHODS // ============================================================================ + static async checkCustomerExists(clientName) { + return await this.request(FRAPPE_CHECK_CLIENT_EXISTS_METHOD, { clientName }); + } + static async getClientStatusCounts(params = {}) { return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params); } @@ -644,10 +652,22 @@ class Api { return result; } + // ============================================================================ + // CONTACT METHODS + // ============================================================================ + + static async checkContactsExist(contacts) { + return await this.request(FRAPPE_CHECK_CONTACTS_EXIST_METHOD, { contacts }); + } + // ============================================================================ // ADDRESS METHODS // ============================================================================ + static async checkAddressesExist(addresses) { + return await this.request(FRAPPE_CHECK_ADDRESSES_EXIST_METHOD, { addresses }); + } + static async getAddressByFullAddress(fullAddress) { return await this.request("custom_ui.api.db.addresses.get_address_by_full_address", { full_address: fullAddress, diff --git a/frontend/src/components/clientSubPages/AddressInformationForm.vue b/frontend/src/components/clientSubPages/AddressInformationForm.vue index 632979d..ac3f229 100644 --- a/frontend/src/components/clientSubPages/AddressInformationForm.vue +++ b/frontend/src/components/clientSubPages/AddressInformationForm.vue @@ -9,6 +9,7 @@ v-for="(address, index) in localFormData.addresses" :key="index" class="address-item" + :class="{ 'existing-highlight': isExistingAddress(address) }" >
@@ -169,6 +170,10 @@ const props = defineProps({ type: Boolean, default: false, }, + existingAddresses: { + type: Array, + default: () => [], + }, }); const emit = defineEmits(["update:formData"]); @@ -310,48 +315,53 @@ const handleServiceChange = (selectedIndex) => { } }; +const getFullAddress = (address) => { + return `${address.addressLine1 || ''} ${address.addressLine2 || ''} ${address.city || ''} ${address.state || ''} ${address.pincode || ''}`.trim().replace(/\s+/g, ' '); +}; + const handleZipcodeInput = async (index, 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.addresses[index].pincode = digitsOnly; - - // Reset city/state if zipcode is not complete - if (digitsOnly.length < 5 && localFormData.value.addresses[index].zipcodeLookupDisabled) { - localFormData.value.addresses[index].city = ""; - localFormData.value.addresses[index].state = ""; - localFormData.value.addresses[index].zipcodeLookupDisabled = false; - } - - // Fetch city/state when 5 digits entered - if (digitsOnly.length === 5) { + const value = event.target.value; + localFormData.value.addresses[index].pincode = value; + if (value.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.addresses[index].city = places[0]["city"]; - localFormData.value.addresses[index].state = places[0]["state"]; - localFormData.value.addresses[index].zipcodeLookupDisabled = true; - notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`); + const zipInfo = await Api.getCityStateByZip(value); + console.log("Zipcode lookup result:", zipInfo); + if (zipInfo && zipInfo.length > 0) { + localFormData.value.addresses[index].city = zipInfo[0].city; + localFormData.value.addresses[index].state = zipInfo[0].state; + localFormData.value.addresses[index].zipcodeLookupDisabled = false; + } else { + throw new Error("No data returned"); } } catch (error) { - // Enable manual entry if lookup fails - localFormData.value.addresses[index].zipcodeLookupDisabled = false; - notificationStore.addWarning( - "Could not find city/state for this zip code. Please enter manually.", - ); + console.error("Zipcode lookup failed:", error); + localFormData.value.addresses[index].zipcodeLookupDisabled = true; + localFormData.value.addresses[index].city = ''; + localFormData.value.addresses[index].state = ''; + notificationStore.addError("Invalid zipcode or lookup failed"); } + } else { + localFormData.value.addresses[index].zipcodeLookupDisabled = true; + localFormData.value.addresses[index].city = ''; + localFormData.value.addresses[index].state = ''; } }; + +const normalizeAddressString = (s = '') => { + return (s || '') + .toString() + .replace(/,/g, '') + .replace(/\s+/g, ' ') + .trim() + .toLowerCase(); +}; + +const isExistingAddress = (address) => { + const fullAddr = getFullAddress(address); + const normFull = normalizeAddressString(fullAddr); + if (!props.existingAddresses || props.existingAddresses.length === 0) return false; + return props.existingAddresses.some((ea) => normalizeAddressString(ea) === normFull); +}; diff --git a/frontend/src/components/clientSubPages/ClientInformationForm.vue b/frontend/src/components/clientSubPages/ClientInformationForm.vue index 899a986..0564f31 100644 --- a/frontend/src/components/clientSubPages/ClientInformationForm.vue +++ b/frontend/src/components/clientSubPages/ClientInformationForm.vue @@ -28,13 +28,12 @@ class="w-full" />
-
-
- {{ customerName }} - +
+

Here are potential matches for your search. Click on a customer to view their details.

+
+
+ {{ customerName }} + +
@@ -77,12 +79,30 @@
- diff --git a/frontend/src/components/pages/Client.vue b/frontend/src/components/pages/Client.vue index 0de7dfc..907ff41 100644 --- a/frontend/src/components/pages/Client.vue +++ b/frontend/src/components/pages/Client.vue @@ -1,64 +1,124 @@