merge conflicts
This commit is contained in:
commit
120bcb150f
@ -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."""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import frappe
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def start_payment(invoice_name: str):
|
||||
|
||||
pass
|
||||
72
custom_ui/api/public/payments.py
Normal file
72
custom_ui/api/public/payments.py
Normal file
@ -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."
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
from .service_appointment_service import ServiceAppointmentService
|
||||
from .stripe_service import StripeService
|
||||
29
custom_ui/services/payment_service.py
Normal file
29
custom_ui/services/payment_service.py
Normal file
@ -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()
|
||||
|
||||
7
custom_ui/services/sales_order_service.py
Normal file
7
custom_ui/services/sales_order_service.py
Normal file
@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
class SalesOrderService:
|
||||
|
||||
@staticmethod
|
||||
def apply_advance_payment(sales_order_name: str, payment_entry_doc):
|
||||
pass
|
||||
93
custom_ui/services/stripe_service.py
Normal file
93
custom_ui/services/stripe_service.py
Normal file
@ -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
|
||||
|
||||
|
||||
71
custom_ui/templates/already-paid-half-payment.html
Normal file
71
custom_ui/templates/already-paid-half-payment.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment Already Completed</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.message-container {
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
}
|
||||
.message-icon {
|
||||
font-size: 5rem;
|
||||
color: #74b9ff;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.message-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
.message-text {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
}
|
||||
.contact-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border-left: 5px solid #74b9ff;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message-container">
|
||||
<div class="message-icon">ℹ️</div>
|
||||
<h1 class="message-title">Payment Already Completed</h1>
|
||||
<p class="message-text">The half down payment for this sales order has already been paid.</p>
|
||||
<div class="contact-info">
|
||||
<strong>If you have any questions:</strong><br>
|
||||
Please contact our sales team for assistance or clarification regarding your order.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
90
custom_ui/templates/email/downpayment.html
Normal file
90
custom_ui/templates/email/downpayment.html
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Down Payment Required</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.header h1 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.payment-details {
|
||||
background-color: #ecf0f1;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.payment-details h2 {
|
||||
margin-top: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: #3498db;
|
||||
color: #ffffff;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Thank You for Confirming Your Quote</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear Valued Customer,</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<div class="payment-details">
|
||||
<h2>Payment Details</h2>
|
||||
<p><strong>Sales Order Number:</strong> {{ sales_order_number }}</p>
|
||||
<p><strong>Down Payment Amount:</strong> ${{ total_amount }}</p>
|
||||
</div>
|
||||
<p>Please click the button below to make your secure payment through our payment processor:</p>
|
||||
<a href="https://yourdomain.com/downpayment?so={{ sales_order_number }}&amount={{ total_amount }}" class="cta-button">Make Payment</a>
|
||||
<p>If you have any questions or need assistance, feel free to contact us. We're here to help!</p>
|
||||
<p>Best regards,<br>The Team at {{ company_name }}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated email. Please do not reply directly.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
71
custom_ui/templates/no-half-payment.html
Normal file
71
custom_ui/templates/no-half-payment.html
Normal file
@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>No Down Payment Required</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.message-container {
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
}
|
||||
.message-icon {
|
||||
font-size: 5rem;
|
||||
color: #4ecdc4;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.message-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
.message-text {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
}
|
||||
.contact-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border-left: 5px solid #4ecdc4;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message-container">
|
||||
<div class="message-icon">✅</div>
|
||||
<h1 class="message-title">No Down Payment Required</h1>
|
||||
<p class="message-text">This sales order does not require any down payment.</p>
|
||||
<div class="contact-info">
|
||||
<strong>If you have any questions:</strong><br>
|
||||
Please contact our sales team for assistance or clarification regarding your order.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
86
custom_ui/templates/public-error.html
Normal file
86
custom_ui/templates/public-error.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error - Something Went Wrong</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.error-container {
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
}
|
||||
.error-icon {
|
||||
font-size: 5rem;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 30px;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
.error-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
.error-message {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 30px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
}
|
||||
.error-details {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border-left: 5px solid #ff6b6b;
|
||||
text-align: left;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 1rem;
|
||||
color: #333;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h1 class="error-title">Oops, something went wrong!</h1>
|
||||
<p class="error-message">We're sorry, but an error occurred while processing your request. Please try again later or contact support if the problem persists.</p>
|
||||
{% if error_message %}
|
||||
<div class="error-details">
|
||||
<strong>Error Details:</strong><br>
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -4,6 +4,26 @@
|
||||
<!-- Lead Badge -->
|
||||
<div v-if="isLead" class="lead-badge-container">
|
||||
<Badge value="LEAD" severity="warn" size="large" />
|
||||
<div class="action-buttons">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@click="addAddress"
|
||||
>
|
||||
<v-icon left size="small">mdi-map-marker-plus</v-icon>
|
||||
Add Address
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@click="addContact"
|
||||
>
|
||||
<v-icon left size="small">mdi-account-plus</v-icon>
|
||||
Add Contact
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Name (only show for Company type) -->
|
||||
@ -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
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.general-client-info {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.lead-badge-container {
|
||||
display: inline-flex;
|
||||
margin-left: 0.5rem;
|
||||
vertical-align: middle;
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1rem 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
display: inline-flex;
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
min-height: 2rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.info-section label {
|
||||
@ -182,12 +223,18 @@ const formattedCreationDate = computed(() => {
|
||||
color: var(--text-color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
justify-self: start;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
justify-self: start;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-value.large {
|
||||
@ -197,30 +244,36 @@ const formattedCreationDate = computed(() => {
|
||||
}
|
||||
|
||||
.companies-list {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.primary-contact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
grid-column: span 1;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface-ground);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contact-item i {
|
||||
@ -234,23 +287,29 @@ const formattedCreationDate = computed(() => {
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
grid-column: span 1;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface-ground);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.stat-item i {
|
||||
@ -259,14 +318,18 @@ const formattedCreationDate = computed(() => {
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.95rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
<div class="details-grid">
|
||||
<!-- Address Information -->
|
||||
<div class="detail-section full-width">
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<h4>Address</h4>
|
||||
@ -31,69 +31,95 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contacts Section -->
|
||||
<div class="detail-section full-width">
|
||||
<!-- Associated Companies -->
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-building"></i>
|
||||
<h4>Companies</h4>
|
||||
</div>
|
||||
<div v-if="associatedCompanies.length > 0" class="companies-list">
|
||||
<div
|
||||
v-for="company in associatedCompanies"
|
||||
:key="company"
|
||||
class="company-item"
|
||||
>
|
||||
<i class="pi pi-building"></i>
|
||||
<span>{{ company }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<i class="pi pi-building"></i>
|
||||
<p>No companies associated</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Contact -->
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-user"></i>
|
||||
<h4>Primary Contact</h4>
|
||||
</div>
|
||||
<div v-if="primaryContact" class="contact-card primary">
|
||||
<div class="contact-badge">
|
||||
<Badge value="Primary" severity="success" />
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<h5>{{ primaryContactName }}</h5>
|
||||
<div class="contact-details">
|
||||
<div class="contact-detail">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>{{ primaryContactEmail }}</span>
|
||||
</div>
|
||||
<div class="contact-detail">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>{{ primaryContactPhone }}</span>
|
||||
</div>
|
||||
<div v-if="primaryContact.role" class="contact-detail">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span>{{ primaryContact.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<i class="pi pi-user-minus"></i>
|
||||
<p>No primary contact</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other Contacts -->
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-users"></i>
|
||||
<h4>Contacts</h4>
|
||||
<h4>Other Contacts</h4>
|
||||
</div>
|
||||
|
||||
<!-- Display Mode -->
|
||||
<div v-if="!editMode" class="contacts-display">
|
||||
<template v-if="addressContacts.length > 0">
|
||||
<!-- Primary Contact -->
|
||||
<div v-if="primaryContact" class="contact-card primary">
|
||||
<div class="contact-badge">
|
||||
<Badge value="Primary" severity="success" />
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<h5>{{ primaryContactName }}</h5>
|
||||
<div class="contact-details">
|
||||
<div class="contact-detail">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>{{ primaryContactEmail }}</span>
|
||||
</div>
|
||||
<div class="contact-detail">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>{{ primaryContactPhone }}</span>
|
||||
</div>
|
||||
<div v-if="primaryContact.role" class="contact-detail">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span>{{ primaryContact.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="otherContacts.length > 0" class="contacts-grid">
|
||||
<div
|
||||
v-for="contact in otherContacts"
|
||||
:key="contact.name"
|
||||
class="contact-card small"
|
||||
>
|
||||
<div class="contact-info-compact">
|
||||
<span class="contact-name">{{ getContactName(contact) }}</span>
|
||||
<span class="contact-email">{{ getContactEmail(contact) }}</span>
|
||||
<span class="contact-phone">{{ getContactPhone(contact) }}</span>
|
||||
<span v-if="contact.role" class="contact-role">{{ contact.role }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Other Contacts -->
|
||||
<div v-if="otherContacts.length > 0" class="other-contacts">
|
||||
<h6>Other Contacts</h6>
|
||||
<div class="contacts-grid">
|
||||
<div
|
||||
v-for="contact in otherContacts"
|
||||
:key="contact.name"
|
||||
class="contact-card small"
|
||||
>
|
||||
<div class="contact-info-compact">
|
||||
<span class="contact-name">{{ getContactName(contact) }}</span>
|
||||
<span class="contact-email">{{ getContactEmail(contact) }}</span>
|
||||
<span class="contact-phone">{{ getContactPhone(contact) }}</span>
|
||||
<span v-if="contact.role" class="contact-role">{{ contact.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="empty-state">
|
||||
<i class="pi pi-user-minus"></i>
|
||||
<p>No contacts associated with this address</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<i class="pi pi-user-minus"></i>
|
||||
<p>No other contacts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Mode -->
|
||||
<div v-else class="contacts-edit">
|
||||
<!-- Edit Mode -->
|
||||
<div v-if="editMode" class="detail-section full-width">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-pencil"></i>
|
||||
<h4>Edit Contacts</h4>
|
||||
</div>
|
||||
<div class="contacts-edit">
|
||||
<div class="edit-instructions">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<span>Select contacts to associate with this address. One must be marked as primary.</span>
|
||||
@ -133,28 +159,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Companies Section -->
|
||||
<div class="detail-section full-width">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-building"></i>
|
||||
<h4>Associated Companies</h4>
|
||||
</div>
|
||||
<div v-if="associatedCompanies.length > 0" class="companies-list">
|
||||
<div
|
||||
v-for="company in associatedCompanies"
|
||||
:key="company"
|
||||
class="company-item"
|
||||
>
|
||||
<i class="pi pi-building"></i>
|
||||
<span>{{ company }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<i class="pi pi-building"></i>
|
||||
<p>No companies associated with this address</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Section -->
|
||||
<div class="detail-section full-width">
|
||||
<div class="section-header">
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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.`,
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user