From 8452f577878fdd2a863fcfec8e158e0e19940506 Mon Sep 17 00:00:00 2001 From: Casey Date: Tue, 27 Jan 2026 16:34:06 -0600 Subject: [PATCH] add templates, update styles --- custom_ui/api/db/addresses.py | 29 ++- custom_ui/api/db/service_appointments.py | 3 + .../clientView/GeneralClientInfo.vue | 115 +++++++-- .../components/clientView/PropertyDetails.vue | 241 +++++++++--------- frontend/src/components/pages/Client.vue | 37 ++- templates/downpayment/downpayment.html | 90 +++++++ 6 files changed, 362 insertions(+), 153 deletions(-) create mode 100644 templates/downpayment/downpayment.html diff --git a/custom_ui/api/db/addresses.py b/custom_ui/api/db/addresses.py index 3d3fe96..bb99ff7 100644 --- a/custom_ui/api/db/addresses.py +++ b/custom_ui/api/db/addresses.py @@ -1,7 +1,7 @@ import frappe import json from custom_ui.db_utils import build_error_response, build_success_response -from custom_ui.services import ClientService, AddressService +from custom_ui.services import ClientService, AddressService, ContactService @frappe.whitelist() def get_address_by_full_address(full_address): @@ -35,6 +35,33 @@ def get_address(address_name): # except Exception as e: # return build_error_response(str(e), 500) +@frappe.whitelist() +def create_address(address_data, company, customer_name): + """Create a new address.""" + print(f"DEBUG: create_address called with address_data: {address_data}, company: {company}, customer_name: {customer_name}") + if isinstance(address_data, str): + address_data = json.loads(address_data) + customer_doctype = ClientService.get_client_doctype(customer_name) + address_data["customer_name"] = customer_name + address_data["customer_type"] = customer_doctype + address_data["address_title"] = AddressService.build_address_title(customer_name, address_data) + address_data["address_type"] = "Service" + address_data["custom_billing_address"] = 0 + address_data["is_service_address"] = 1 + address_data["country"] = "United States" + address_data["companies"] = [{ "company": company }] + print(f"DEBUG: Final address_data before creation: {address_data}") + try: + address_doc = AddressService.create_address(address_data) + for contact in address_data.get("contacts", []): + AddressService.link_address_to_contact(address_doc, contact) + contact_doc = ContactService.get_or_throw(contact) + ContactService.link_contact_to_address(contact_doc, address_doc) + ClientService.append_link_v2(customer_name, "properties", {"address": address_doc.name}) + return build_success_response(address_doc.as_dict()) + except Exception as e: + return build_error_response(str(e), 500) + @frappe.whitelist() def get_addresses(fields=["*"], filters={}): """Get addresses with optional filtering.""" diff --git a/custom_ui/api/db/service_appointments.py b/custom_ui/api/db/service_appointments.py index 6cb864c..2ac4b63 100644 --- a/custom_ui/api/db/service_appointments.py +++ b/custom_ui/api/db/service_appointments.py @@ -20,6 +20,9 @@ def get_service_appointments(companies, filters={}): ServiceAppointmentService.get_full_dict(name) for name in service_appointment_names ] + + "is_half_down_paid" + return build_success_response(service_appointments) except Exception as e: return build_error_response(str(e), 500) diff --git a/frontend/src/components/clientView/GeneralClientInfo.vue b/frontend/src/components/clientView/GeneralClientInfo.vue index 2505590..2b27689 100644 --- a/frontend/src/components/clientView/GeneralClientInfo.vue +++ b/frontend/src/components/clientView/GeneralClientInfo.vue @@ -4,6 +4,26 @@
+
+ + mdi-map-marker-plus + Add Address + + + mdi-account-plus + Add Contact + +
@@ -145,35 +165,56 @@ const formattedCreationDate = computed(() => { day: "numeric", }); }); + +// Placeholder methods for adding address and contact +const addAddress = () => { + console.log("Add Address modal would open here"); + // TODO: Open add address modal +}; + +const addContact = () => { + console.log("Add Contact modal would open here"); + // TODO: Open add contact modal +}; diff --git a/frontend/src/components/clientView/PropertyDetails.vue b/frontend/src/components/clientView/PropertyDetails.vue index b9c06af..b0c4cfa 100644 --- a/frontend/src/components/clientView/PropertyDetails.vue +++ b/frontend/src/components/clientView/PropertyDetails.vue @@ -4,7 +4,7 @@
-
+

Address

@@ -31,69 +31,95 @@
- -
+ +
+
+ +

Companies

+
+
+
+ + {{ company }} +
+
+
+ +

No companies associated

+
+
+ + +
+
+ +

Primary Contact

+
+
+
+ +
+
+
{{ primaryContactName }}
+
+
+ + {{ primaryContactEmail }} +
+
+ + {{ primaryContactPhone }} +
+
+ + {{ primaryContact.role }} +
+
+
+
+
+ +

No primary contact

+
+
+ + +
-

Contacts

+

Other Contacts

- - -
- - +
+
+ +

No other contacts

+
+
- -
+ +
+
+ +

Edit Contacts

+
+
Select contacts to associate with this address. One must be marked as primary. @@ -133,28 +159,6 @@
- -
-
- -

Associated Companies

-
-
-
- - {{ company }} -
-
-
- -

No companies associated with this address

-
-
-
@@ -327,52 +331,53 @@ const emitChanges = () => { .property-details { background: var(--surface-card); border-radius: 12px; - padding: 1rem; + padding: 0.75rem; border: 1px solid var(--surface-border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); margin-bottom: 1rem; } .property-details > h3 { - margin: 0 0 1rem 0; - font-size: 1.5rem; + margin: 0 0 0.75rem 0; + font-size: 1.25rem; font-weight: 600; color: var(--text-color); } .details-grid { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 1fr 1fr; gap: 1rem; + align-items: start; } .detail-section { background: var(--surface-ground); border-radius: 8px; - padding: 1rem; + padding: 0.75rem; } .detail-section.full-width { - width: 100%; + grid-column: span 2; } .section-header { display: flex; align-items: center; - gap: 0.75rem; - margin-bottom: 0.75rem; - padding-bottom: 0.75rem; - border-bottom: 2px solid var(--surface-border); + gap: 0.5rem; + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--surface-border); } .section-header i { - font-size: 1.25rem; + font-size: 1rem; color: var(--primary-color); } .section-header h4 { margin: 0; - font-size: 1.1rem; + font-size: 1rem; font-weight: 600; color: var(--text-color); } @@ -380,11 +385,11 @@ const emitChanges = () => { .address-info { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.5rem; } .full-address { - font-size: 1.1rem; + font-size: 1rem; font-weight: 500; color: var(--text-color); margin: 0; @@ -400,13 +405,13 @@ const emitChanges = () => { .contacts-display { display: flex; flex-direction: column; - gap: 1.5rem; + gap: 1rem; } .contact-card { background: var(--surface-card); - border-radius: 8px; - padding: 1.25rem; + border-radius: 6px; + padding: 0.75rem; border: 1px solid var(--surface-border); } @@ -416,12 +421,12 @@ const emitChanges = () => { } .contact-badge { - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; } .contact-info h5 { - margin: 0 0 0.75rem 0; - font-size: 1.25rem; + margin: 0 0 0.5rem 0; + font-size: 1.1rem; font-weight: 600; color: var(--text-color); } @@ -429,30 +434,30 @@ const emitChanges = () => { .contact-details { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.25rem; } .contact-detail { display: flex; align-items: center; - gap: 0.75rem; + gap: 0.5rem; } .contact-detail i { - font-size: 1rem; + font-size: 0.9rem; color: var(--primary-color); - min-width: 20px; + min-width: 18px; } .contact-detail span { - font-size: 0.95rem; + font-size: 0.9rem; color: var(--text-color); } /* Other Contacts */ .other-contacts h6 { - margin: 0 0 1rem 0; - font-size: 0.9rem; + margin: 0 0 0.75rem 0; + font-size: 0.85rem; font-weight: 600; color: var(--text-color-secondary); text-transform: uppercase; @@ -461,12 +466,12 @@ const emitChanges = () => { .contacts-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; } .contact-card.small { - padding: 1rem; + padding: 0.75rem; } .contact-info-compact { @@ -581,27 +586,27 @@ const emitChanges = () => { .companies-list { display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.5rem; } .company-item { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.75rem; + gap: 0.5rem; + padding: 0.5rem; background: var(--surface-card); - border-radius: 6px; + border-radius: 4px; border: 1px solid var(--surface-border); } .company-item i { - font-size: 1rem; + font-size: 0.9rem; color: var(--primary-color); - min-width: 20px; + min-width: 18px; } .company-item span { - font-size: 0.95rem; + font-size: 0.9rem; color: var(--text-color); font-weight: 500; } diff --git a/frontend/src/components/pages/Client.vue b/frontend/src/components/pages/Client.vue index 82c7765..3fd1132 100644 --- a/frontend/src/components/pages/Client.vue +++ b/frontend/src/components/pages/Client.vue @@ -196,6 +196,16 @@ const getClient = async (name) => { } else if (selectedAddress.value) { // geocode.value = await Api.getGeocode(selectedAddress.value); } + + // Check if client is associated with current company + if (companyStore.currentCompany && client.value.companies) { + const clientHasCompany = client.value.companies.some(company => company.company === companyStore.currentCompany); + if (!clientHasCompany) { + notificationStore.addWarning( + `The selected company is not linked to this client.`, + ); + } + } } catch (error) { console.error("Error fetching client data in Client.vue: ", error.message || error); } finally { @@ -251,15 +261,26 @@ watch( () => companyStore.currentCompany, (newCompany) => { console.log("############# Company changed to:", newCompany); - let companyIsPresent = false - for (company of selectedAddressData.value.companies || []) { - console.log("Checking address company:", company); - if (company.company === newCompany) { - companyIsPresent = true; - break; - } + if (!newCompany || !client.value.customerName) return; + + // Check if client is associated with the company + let clientHasCompany = false; + if (client.value.companies) { + clientHasCompany = client.value.companies.some(company => company.company === newCompany); } - if (!companyIsPresent) { + + // Check if selected address is associated with the company + let addressHasCompany = false; + if (selectedAddressData.value?.companies) { + addressHasCompany = selectedAddressData.value.companies.some(company => company.company === newCompany); + } + + // Show warnings for missing associations + if (!clientHasCompany) { + notificationStore.addWarning( + `The selected company is not linked to this client.`, + ); + } else if (!addressHasCompany) { notificationStore.addWarning( `The selected company is not linked to this address.`, ); diff --git a/templates/downpayment/downpayment.html b/templates/downpayment/downpayment.html new file mode 100644 index 0000000..136424c --- /dev/null +++ b/templates/downpayment/downpayment.html @@ -0,0 +1,90 @@ + + + + + + Down Payment Required + + + +
+
+

Thank You for Confirming Your Quote

+
+
+

Dear Valued Customer,

+

Thank you for accepting our quote for services at {{ company_name }}. We're excited to work with you and appreciate your trust in our team.

+

To proceed with scheduling your service, a half down payment is required. This helps us secure the necessary resources and ensures everything is prepared for your appointment.

+
+

Payment Details

+

Sales Order Number: {{ sales_order_number }}

+

Down Payment Amount: ${{ total_amount }}

+
+

Please click the button below to make your secure payment through our payment processor:

+ Make Payment +

If you have any questions or need assistance, feel free to contact us. We're here to help!

+

Best regards,
The Team at {{ company_name }}

+
+ +
+ + \ No newline at end of file