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/custom_ui/api/payments.py b/custom_ui/api/payments.py deleted file mode 100644 index 476dfe6..0000000 --- a/custom_ui/api/payments.py +++ /dev/null @@ -1,6 +0,0 @@ -import frappe - -@frappe.whitelist(allow_guest=True) -def start_payment(invoice_name: str): - - pass \ No newline at end of file diff --git a/custom_ui/api/public/payments.py b/custom_ui/api/public/payments.py new file mode 100644 index 0000000..b369087 --- /dev/null +++ b/custom_ui/api/public/payments.py @@ -0,0 +1,72 @@ +import frappe +import json +from frappe.utils.data import flt +from custom_ui.services import DbService, StripeService + +@frappe.whitelist(allow_guest=True) +def half_down_stripe_payment(sales_order): + """Public endpoint for initiating a half-down advance payment for a sales order.""" + if not DbService.exists("Sales Order", sales_order): + frappe.throw("Sales Order does not exist.") + so = DbService.get_or_throw("Sales Order", sales_order) + if not so.requires_half_payment: + frappe.throw("This sales order does not require a half-down payment.") + if so.docstatus != 1: + frappe.throw("Sales Order must be submitted to proceed with payment.") + if so.custom_halfdown_is_paid or so.advanced_paid >= so.custom_halfdown_amount: + frappe.throw("Half-down payment has already been made for this sales order.") + stripe_session = StripeService.create_checkout_session( + company=so.company, + amount=so.custom_halfdown_amount, + service=so.custom_project_template, + sales_order=so.name, + for_advance_payment=True + ) + return frappe.redirect(stripe_session.url) + +@frappe.whitelist(allow_guest=True) +def stripe_webhook(): + """Endpoint to handle Stripe webhooks.""" + payload = frappe.request.get_data() + sig_header = frappe.request.headers.get('Stripe-Signature') + session, metadata = StripeService.get_session_and_metadata(payload, sig_header) + + if DbService.exists("Payment Entry", {"reference_no": session.id}): + raise frappe.ValidationError("Payment Entry already exists for this session.") + + reference_doctype = "Sales Invoice" + + if metadata.get("payment_type") == "advance": + reference_doctype = "Sales Order" + elif metadata.get("payment_type") != "full": + raise frappe.ValidationError("Invalid payment type in metadata.") + + amount_paid = flt(session.amount_total) / 100 + currency = session.currency.upper() + reference_doc = frappe.get_doc(reference_doctype, metadata.get("order_num")) + + pe = frappe.get_doc({ + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "mode_of_payment": "Stripe", + "party": reference_doc.customer, + "party_name": reference_doc.customer, + "paid_to": metadata.get("company"), + "reference_no": session.id, + "reference_date": frappe.utils.nowdate(), + "reference_doctype": reference_doctype, + "reference_name": reference_doc.name, + "paid_amount": amount_paid, + "paid_currency": currency, + }) + + pe.insert() + pe.submit() + return "Payment Entry created and submitted successfully." + + + + + + \ No newline at end of file diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index cfae092..d6465a1 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -31,6 +31,7 @@ def before_insert(doc, method): print("DEBUG: CHECKING CUSTOMER NAME") print(doc.actual_customer_name) print("Quotation_to:", doc.quotation_to) + doc.customer_address = frappe.get_value(doc.customer_type, doc.actual_customer_name, "customer_billing_address") # print("Party_type:", doc.party_type) if doc.custom_project_template == "SNW Install": print("DEBUG: Quotation uses SNW Install template, making sure no duplicate linked estimates.") @@ -53,25 +54,27 @@ def before_submit(doc, method): def on_update_after_submit(doc, method): print("DEBUG: on_update_after_submit hook triggered for Quotation:", doc.name) print("DEBUG: Current custom_current_status:", doc.custom_current_status) + if doc.custom_current_status == "Won": + print("DEBUG: Quotation is already marked as Won, no action needed.") + return if doc.custom_current_status == "Estimate Accepted": doc.custom_current_status = "Won" print("DEBUG: Quotation marked as Won, updating current status.") if doc.customer_type == "Lead": print("DEBUG: Customer is a Lead, converting to Customer and updating Quotation.") new_customer = ClientService.convert_lead_to_customer(doc.actual_customer_name) - doc.actual_customer_name = new_customer.name doc.customer_type = "Customer" + doc.actual_customer_name = new_customer.name new_customer.reload() - ClientService.append_link_v2( - new_customer.name, "quotations", {"quotation": doc.name} - ) doc.save() print("DEBUG: Creating Sales Order from accepted Estimate") new_sales_order = make_sales_order(doc.name) - new_sales_order.custom_requires_half_payment = doc.requires_half_payment + new_sales_order.requires_half_payment = doc.requires_half_payment new_sales_order.customer = doc.actual_customer_name + new_sales_order.customer_name = doc.actual_customer_name + new_sales_order.customer_address = doc.customer_address # new_sales_order.custom_installation_address = doc.custom_installation_address - # new_sales_order.custom_job_address = doc.custom_job_address + new_sales_order.custom_job_address = doc.custom_job_address new_sales_order.payment_schedule = [] print("DEBUG: Setting payment schedule for Sales Order") new_sales_order.set_payment_schedule() diff --git a/custom_ui/events/jobs.py b/custom_ui/events/jobs.py index 378018e..de0d9ff 100644 --- a/custom_ui/events/jobs.py +++ b/custom_ui/events/jobs.py @@ -56,7 +56,9 @@ def after_insert(doc, method): def before_insert(doc, method): # This is where we will add logic to set tasks and other properties of a job based on it's project_template - pass + if doc.requires_half_payment: + print("DEBUG: Project requires half payment, setting flag.") + doc.ready_to_schedule = 0 def before_save(doc, method): print("DEBUG: Before Save Triggered for Project:", doc.name) diff --git a/custom_ui/events/sales_order.py b/custom_ui/events/sales_order.py index 1fffdb4..d8d659f 100644 --- a/custom_ui/events/sales_order.py +++ b/custom_ui/events/sales_order.py @@ -2,32 +2,42 @@ import frappe from custom_ui.services import DbService, AddressService, ClientService -def on_save(doc, method): - print("DEBUG: on_save hook triggered for Sales Order", doc.name) - if doc.advance_paid >= doc.grand_total/2: - if doc.project and doc.half_down_required: - print("DEBUG: Advance payments exceed required threshold of half down, setting project half down paid.") - project = frappe.get_doc("Project", doc.project) - project.is_half_down_paid = True +def before_save(doc, method): + print("DEBUG: before_save hook triggered for Sales Order", doc.name) + if doc.docstatus == 1: + if doc.requires_half_payment: + half_down_is_paid = doc.custom_halfdown_is_paid or doc.advance_paid >= doc.custom_halfdown_amount or doc.advance_paid >= doc.grand_total / 2 + if half_down_is_paid and not doc.custom_halfdown_is_paid: + doc.custom_halfdown_is_paid = 1 def before_insert(doc, method): + print("DEBUG: Before Insert triggered for Sales Order: ", doc.name) + if doc.custom_project_template == "SNW Install": + print("DEBUG: Sales Order uses SNW Install template, checking for duplicates.") + address_doc = AddressService.get_or_throw(doc.custom_job_address) + for link in address_doc.sales_orders: + if link.project_template == "SNW Install": + raise frappe.ValidationError("A Sales Order with project template 'SNW Install' is already linked to this address.") print("DEBUG: before_insert hook triggered for Sales Order") - # if doc.custom_project_template == "SNW Install": - # print("DEBUG: Sales Order uses SNW Install template, checking for duplicate linked sales orders.") - # address_doc = AddressService.get_or_throw(doc.custom_job_address) - # if "SNW Install" in [link.project_template for link in address_doc.sales_orders]: - # raise frappe.ValidationError("A Sales Order with project template 'SNW Install' is already linked to this address.") + if doc.requires_half_payment: + print("DEBUG: Sales Order requires half payment, calculating half-down amount.") + half_down_amount = doc.grand_total / 2 + doc.custom_halfdown_amount = half_down_amount + print("DEBUG: Half-down amount set to:", half_down_amount) + def on_submit(doc, method): + print("DEBUG: on_submit hook triggered for Sales Order:", doc.name) print("DEBUG: Info from Sales Order") - print(doc.custom_installation_address) - print(doc.company) - print(doc.transaction_date) - print(doc.customer) - print(doc.custom_job_address) - print(doc.custom_project_template) + print(f"Sales Order Name: {doc.name}") + print(f"Grand Total: {doc.grand_total}") + print(f"Company: {doc.company}") + print(f"Requires Half Payment: {doc.requires_half_payment}") + print(f"Customer: {doc.customer}") + print(f"Job Address: {doc.custom_job_address}") + print(f"Project Template: {doc.custom_project_template}") # Create Invoice and Project from Sales Order try: print("Creating Project from Sales Order", doc.name) @@ -62,6 +72,15 @@ def after_insert(doc, method): doc.customer, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template} ) +def on_update_after_submit(doc, method): + print("DEBUG: on_update_after_submit hook triggered for Sales Order:", doc.name) + if doc.requires_half_payment and doc.custom_halfdown_is_paid: + project_is_scheduable = frappe.get_value("Project", doc.project, "ready_to_schedule") + if not project_is_scheduable: + print("DEBUG: Half-down payment made, setting Project to ready to schedule.") + frappe.set_value("Project", doc.project, "ready_to_schedule", 1) + + def create_sales_invoice_from_sales_order(doc, method): pass diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index 9bd60cf..6cfcf77 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -179,15 +179,18 @@ doc_events = { "on_update_after_submit": "custom_ui.events.estimate.on_update_after_submit" }, "Sales Order": { + "before_save": "custom_ui.events.sales_order.before_save", "before_insert": "custom_ui.events.sales_order.before_insert", "after_insert": "custom_ui.events.sales_order.after_insert", - "on_submit": "custom_ui.events.sales_order.on_submit" + "on_submit": "custom_ui.events.sales_order.on_submit", + "on_update_after_submit": "custom_ui.events.sales_order.on_update_after_submit" }, "Project": { "before_insert": "custom_ui.events.jobs.before_insert", "after_insert": "custom_ui.events.jobs.after_insert", "before_save": "custom_ui.events.jobs.before_save", - "on_update": "custom_ui.events.jobs.after_save" + "on_update": "custom_ui.events.jobs.after_save", + "after_save": "custom_ui.events.jobs.after_save" }, "Task": { "before_insert": "custom_ui.events.task.before_insert", diff --git a/custom_ui/services/__init__.py b/custom_ui/services/__init__.py index 1fb0a17..a1d4631 100644 --- a/custom_ui/services/__init__.py +++ b/custom_ui/services/__init__.py @@ -5,4 +5,5 @@ from .client_service import ClientService from .estimate_service import EstimateService from .onsite_meeting_service import OnSiteMeetingService from .task_service import TaskService -from .service_appointment_service import ServiceAppointmentService \ No newline at end of file +from .service_appointment_service import ServiceAppointmentService +from .stripe_service import StripeService \ No newline at end of file diff --git a/custom_ui/services/payment_service.py b/custom_ui/services/payment_service.py new file mode 100644 index 0000000..ca447f1 --- /dev/null +++ b/custom_ui/services/payment_service.py @@ -0,0 +1,29 @@ +import frappe +from custom_ui.services import DbService + +class PaymentService: + + @staticmethod + def create_payment_entry(reference_doctype: str, reference_doc_name: str, data: dict) -> frappe._dict: + """Create a Payment Entry document based on the reference document.""" + print(f"DEBUG: Creating Payment Entry for {reference_doctype} {reference_doc_name} with data: {data}") + reference_doc = DbService.get_or_throw(reference_doctype, reference_doc_name) + pe = frappe.get_doc({ + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "mode_of_payment": data.get("mode_of_payment", "Stripe"), + "party": reference_doc.customer, + "party_name": reference_doc.customer, + "paid_to": data.get("paid_to"), + "reference_no": data.get("reference_no"), + "reference_date": data.get("reference_date", frappe.utils.nowdate()), + "reference_doctype": reference_doctype, + "reference_name": reference_doc.name, + "paid_amount": data.get("paid_amount"), + "paid_currency": data.get("paid_currency"), + }) + pe.insert() + print(f"DEBUG: Created Payment Entry with name: {pe.name}") + return pe.as_dict() + \ No newline at end of file diff --git a/custom_ui/services/sales_order_service.py b/custom_ui/services/sales_order_service.py new file mode 100644 index 0000000..9730618 --- /dev/null +++ b/custom_ui/services/sales_order_service.py @@ -0,0 +1,7 @@ +import frappe + +class SalesOrderService: + + @staticmethod + def apply_advance_payment(sales_order_name: str, payment_entry_doc): + pass \ No newline at end of file diff --git a/custom_ui/services/stripe_service.py b/custom_ui/services/stripe_service.py new file mode 100644 index 0000000..7f02c57 --- /dev/null +++ b/custom_ui/services/stripe_service.py @@ -0,0 +1,93 @@ +import frappe +import stripe +import json +from custom_ui.services import DbService +from frappe.utils import get_url + +class StripeService: + + @staticmethod + def get_stripe_settings(company: str): + """Fetch Stripe settings for a given company.""" + settings_name = frappe.get_all("Stripe Settings", pluck="name", filters={"company": company}) + if not settings_name: + frappe.throw(f"Stripe Settings not found for company: {company}") + settings = frappe.get_doc("Stripe Settings", settings_name[0]) if settings_name else None + return settings + + @staticmethod + def get_api_key(company: str) -> str: + """Retrieve the Stripe API key for the specified company.""" + settings = StripeService.get_stripe_settings(company) + return settings.secret_key + + @staticmethod + def get_webhook_secret(company: str) -> str: + """Retrieve the Stripe webhook secret for the specified company.""" + settings = StripeService.get_stripe_settings(company) + if not settings.webhook_secret: + frappe.throw(f"Stripe Webhook Secret not configured for company: {company}") + return settings.webhook_secret + + @staticmethod + def create_checkout_session(company: str, amount: float, service: str, order_num: str, currency: str = "usd", for_advance_payment: bool = False, line_items: list | None = None) -> stripe.checkout.Session: + """Create a Stripe Checkout Session. order_num is a Sales Order name if for_advance_payment is True, otherwise it is a Sales Invoice name.""" + stripe.api_key = StripeService.get_api_key(company) + + line_items = line_items if line_items and not for_advance_payment else [{ + "price_data": { + "currency": currency.lower(), + "product_data": { + "name": f"Advance payment for {company}{' - ' + service if service else ''}" + }, + "unit_amount": int(amount * 100), + }, + "quantity": 1, + }] + + session = stripe.checkout.Session.create( + mode="payment", + payment_method_types=["card"], + line_items=line_items, + metadata={ + "order_num": order_num, + "company": company, + "payment_type": "advance" if for_advance_payment else "full" + }, + success_url=f"{get_url()}/payment-success?session_id={{CHECKOUT_SESSION_ID}}", + cancel_url=f"{get_url()}/payment-cancelled", + ) + + return session + + @staticmethod + def get_event(payload: bytes, sig_header: str, company: str = None) -> stripe.Event: + company = company if company else json.loads(payload).get("data", {}).get("object", {}).get("metadata", {}).get("company") + if not company: + frappe.throw("Company information missing in webhook payload.") + try: + event = stripe.Webhook.construct_event( + payload=payload, + sig_header=sig_header, + secret=StripeService.get_webhook_secret(company), + api_key=StripeService.get_api_key(company) + ) + except ValueError as e: + frappe.throw(f"Invalid payload: {str(e)}") + except stripe.error.SignatureVerificationError as e: + frappe.throw(f"Invalid signature: {str(e)}") + + return event + + @staticmethod + def get_session_and_metadata(payload: bytes, sig_header: str, company: str = None) -> tuple[stripe.checkout.Session, dict]: + """Retrieve the Stripe Checkout Session and its metadata from a webhook payload.""" + event = StripeService.get_event(payload, sig_header, company) + if event.type != "checkout.session.completed": + frappe.throw(f"Unhandled event type: {event.type}") + session = event.data.object + metadata = session["metadata"] if "metadata" in session else {} + + return session, metadata + + \ No newline at end of file diff --git a/custom_ui/templates/already-paid-half-payment.html b/custom_ui/templates/already-paid-half-payment.html new file mode 100644 index 0000000..68876c6 --- /dev/null +++ b/custom_ui/templates/already-paid-half-payment.html @@ -0,0 +1,71 @@ + + + + + + Payment Already Completed + + + + +
+
ℹ️
+

Payment Already Completed

+

The half down payment for this sales order has already been paid.

+
+ If you have any questions:
+ Please contact our sales team for assistance or clarification regarding your order. +
+
+ + \ No newline at end of file diff --git a/custom_ui/templates/email/downpayment.html b/custom_ui/templates/email/downpayment.html new file mode 100644 index 0000000..136424c --- /dev/null +++ b/custom_ui/templates/email/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 diff --git a/custom_ui/templates/no-half-payment.html b/custom_ui/templates/no-half-payment.html new file mode 100644 index 0000000..632ba49 --- /dev/null +++ b/custom_ui/templates/no-half-payment.html @@ -0,0 +1,71 @@ + + + + + + No Down Payment Required + + + + +
+
+

No Down Payment Required

+

This sales order does not require any down payment.

+
+ If you have any questions:
+ Please contact our sales team for assistance or clarification regarding your order. +
+
+ + \ No newline at end of file diff --git a/custom_ui/templates/public-error.html b/custom_ui/templates/public-error.html new file mode 100644 index 0000000..47cef95 --- /dev/null +++ b/custom_ui/templates/public-error.html @@ -0,0 +1,86 @@ + + + + + + Error - Something Went Wrong + + + + +
+
⚠️
+

Oops, something went wrong!

+

We're sorry, but an error occurred while processing your request. Please try again later or contact support if the problem persists.

+ {% if error_message %} +
+ Error Details:
+ {{ error_message }} +
+ {% endif %} +
+ + \ No newline at end of file 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.`, );