big update
This commit is contained in:
parent
73d235b7bc
commit
0380dd10d8
@ -1,7 +1,7 @@
|
||||
import frappe
|
||||
import json
|
||||
from custom_ui.db_utils import build_error_response, build_success_response, process_filters, process_sorting
|
||||
from custom_ui.services import DbService, ClientService, AddressService
|
||||
from custom_ui.services import DbService, ClientService, AddressService, ContactService
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_week_bid_meetings(week_start, week_end):
|
||||
@ -17,8 +17,10 @@ def get_week_bid_meetings(week_start, week_end):
|
||||
order_by="start_time asc"
|
||||
)
|
||||
for meeting in meetings:
|
||||
address_doc = frappe.get_doc("Address", meeting["address"])
|
||||
address_doc = AddressService.get_or_throw(meeting["address"])
|
||||
meeting["address"] = address_doc.as_dict()
|
||||
contact_doc = ContactService.get_or_throw(meeting["contact"]) if meeting.get("contact") else None
|
||||
meeting["contact"] = contact_doc.as_dict() if contact_doc else None
|
||||
return build_success_response(meetings)
|
||||
except Exception as e:
|
||||
frappe.log_error(message=str(e), title="Get Week On-Site Meetings Failed")
|
||||
@ -60,6 +62,13 @@ def get_unscheduled_bid_meetings():
|
||||
filters={"status": "Unscheduled"},
|
||||
order_by="creation desc"
|
||||
)
|
||||
for meeting in meetings:
|
||||
address_doc = AddressService.get_or_throw(meeting["address"])
|
||||
meeting["address"] = address_doc.as_dict()
|
||||
# client_doc = ClientService.get_client_doctype(meeting["party_name"])
|
||||
# meeting["client"] = client_doc.as_dict() if client_doc else None
|
||||
contact_doc = ContactService.get_or_throw(meeting["contact"]) if meeting.get("contact") else None
|
||||
meeting["contact"] = contact_doc.as_dict() if contact_doc else None
|
||||
return build_success_response(meetings)
|
||||
except Exception as e:
|
||||
frappe.log_error(message=str(e), title="Get Unscheduled On-Site Meetings Failed")
|
||||
@ -75,8 +84,11 @@ def get_bid_meeting(name):
|
||||
|
||||
# Get the full address data
|
||||
if meeting_dict.get("address"):
|
||||
address_doc = frappe.get_doc("Address", meeting_dict["address"])
|
||||
address_doc = AddressService.get_or_throw(meeting_dict["address"])
|
||||
meeting_dict["address"] = address_doc.as_dict()
|
||||
if meeting_dict.get("contact"):
|
||||
contact_doc = ContactService.get_or_throw(meeting_dict["contact"])
|
||||
meeting_dict["contact"] = contact_doc.as_dict()
|
||||
|
||||
return build_success_response(meeting_dict)
|
||||
except frappe.DoesNotExistError:
|
||||
@ -127,37 +139,30 @@ def create_bid_meeting(data):
|
||||
@frappe.whitelist()
|
||||
def update_bid_meeting(name, data):
|
||||
"""Update an existing On-Site Meeting."""
|
||||
defualts = {
|
||||
"address": None,
|
||||
"start_time": None,
|
||||
"end_time": None,
|
||||
"notes": None,
|
||||
"assigned_employee": None,
|
||||
"completed_by": None,
|
||||
"contact": None,
|
||||
"status": None
|
||||
}
|
||||
try:
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
|
||||
# Ensure we always have the expected keys so fields can be cleared
|
||||
data = {**defualts, **(data or {})}
|
||||
meeting = frappe.get_doc("On-Site Meeting", name)
|
||||
|
||||
# Only update fields that are explicitly provided in the data
|
||||
for key, value in data.items():
|
||||
# Allow explicitly clearing date/time and assignment fields
|
||||
if key in ["start_time", "end_time", "assigned_employee", "completed_by"] and value is None:
|
||||
meeting.set(key, None)
|
||||
continue
|
||||
|
||||
if value is not None:
|
||||
if key == "address":
|
||||
value = frappe.db.get_value("Address", {"full_address": value}, "name")
|
||||
elif key in ["assigned_employee", "completed_by"]:
|
||||
value = frappe.db.get_value("Employee", {"employee_name": value}, "name")
|
||||
print(f"DEBUG: Updating field '{key}' to value '{value}'")
|
||||
if key == "address" and value is not None:
|
||||
# Convert full address to address name
|
||||
value = frappe.db.get_value("Address", {"full_address": value}, "name")
|
||||
meeting.set(key, value)
|
||||
elif key in ["assigned_employee", "completed_by"] and value is not None:
|
||||
# Convert employee name to employee ID
|
||||
value = frappe.db.get_value("Employee", {"employee_name": value}, "name")
|
||||
meeting.set(key, value)
|
||||
else:
|
||||
# For all other fields, set the value as-is (including None to clear fields)
|
||||
meeting.set(key, value)
|
||||
print(f"DEBUG: Field '{key}' updated to '{meeting.get(key)}'")
|
||||
meeting.save()
|
||||
frappe.db.commit()
|
||||
|
||||
return build_success_response(meeting.as_dict())
|
||||
except frappe.DoesNotExistError:
|
||||
return build_error_response(f"On-Site Meeting '{name}' does not exist.", 404)
|
||||
|
||||
@ -205,19 +205,20 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||
addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names]
|
||||
tableRows = []
|
||||
for address in addresses:
|
||||
is_lead = False
|
||||
is_lead = address.customer_type == "Lead"
|
||||
print("##########IS LEAD:", is_lead)
|
||||
tableRow = {}
|
||||
links = address.links
|
||||
customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None
|
||||
customer_name = address.get("custom_customer_to_bill", None)
|
||||
if not customer_links:
|
||||
customer_links = [link for link in links if link.link_doctype == "Lead"] if links else None
|
||||
is_lead = True if customer_links else False
|
||||
if not customer_name and not customer_links:
|
||||
# customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None
|
||||
customer_name = address.get("customer_name")
|
||||
# if not customer_links:
|
||||
# customer_links = [link for link in links if link.link_doctype == "Lead"] if links else None
|
||||
# is_lead = True if customer_links else False
|
||||
# if not customer_name and not customer_links:
|
||||
# customer_name = frappe.get_value("Lead", address.get("customer_name"), "custom_customer_name")
|
||||
if is_lead:
|
||||
# print("DEBUG: No customer to bill. Customer links found:", customer_links)
|
||||
customer_name = frappe.get_value("Lead", address.get("customer_name"), "custom_customer_name")
|
||||
elif not customer_name and customer_links:
|
||||
print("DEBUG: No customer to bill. Customer links found:", customer_links)
|
||||
customer_name = frappe.get_value("Lead", customer_links[0].link_name, "custom_customer_name") if is_lead else customer_links[0].link_name
|
||||
tableRow["id"] = address["name"]
|
||||
tableRow["customer_name"] = customer_name
|
||||
tableRow["address"] = (
|
||||
@ -225,6 +226,7 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||
f"{' ' + address['address_line2'] if address['address_line2'] else ''} "
|
||||
f"{address['city']}, {address['state']} {address['pincode']}"
|
||||
)
|
||||
print("########IS LEAD @TABLE ROW:", is_lead)
|
||||
tableRow["client_type"] = "Lead" if is_lead else "Customer"
|
||||
# tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
|
||||
# tableRow["estimate_sent_status"] = address.custom_estimate_sent_status
|
||||
@ -308,6 +310,7 @@ def upsert_client(data):
|
||||
"phone": primary_contact.get("phone_number"),
|
||||
"custom_customer_name": customer_name,
|
||||
"customer_type": customer_type,
|
||||
"address_type": "Billing",
|
||||
"companies": [{ "company": data.get("company_name")
|
||||
}]
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ from custom_ui.api.db.general import get_doc_history
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from werkzeug.wrappers import Response
|
||||
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
|
||||
from custom_ui.services import DbService, ClientService, AddressService
|
||||
from custom_ui.services import DbService, ClientService, AddressService, ContactService
|
||||
|
||||
# ===============================================================================
|
||||
# ESTIMATES & INVOICES API METHODS
|
||||
@ -145,17 +145,18 @@ def send_estimate_email(estimate_name):
|
||||
print("DEBUG: Sending estimate email for:", estimate_name)
|
||||
quotation = frappe.get_doc("Quotation", estimate_name)
|
||||
|
||||
party_exists = frappe.db.exists(quotation.quotation_to, quotation.party_name)
|
||||
if not party_exists:
|
||||
|
||||
if not DbService.exists("Contact", quotation.contact_person):
|
||||
return build_error_response("No email found for the customer.", 400)
|
||||
party = frappe.get_doc(quotation.quotation_to, quotation.party_name)
|
||||
party = ContactService.get_or_throw(quotation.contact_person)
|
||||
|
||||
email = None
|
||||
if (getattr(party, 'email_id', None)):
|
||||
email = party.email_id
|
||||
elif (getattr(party, 'contact_ids', None) and len(party.email_ids) > 0):
|
||||
primary = next((e for e in party.email_ids if e.is_primary), None)
|
||||
email = primary.email_id if primary else party.email_ids[0].email_id
|
||||
email = quotation.contact_email or None
|
||||
if not email:
|
||||
if (getattr(party, 'email_id', None)):
|
||||
email = party.email_id
|
||||
elif (getattr(party, 'email_ids', None) and len(party.email_ids) > 0):
|
||||
primary = next((e for e in party.email_ids if e.is_primary), None)
|
||||
email = primary.email_id if primary else party.email_ids[0].email_id
|
||||
|
||||
if not email and quotation.custom_job_address:
|
||||
address = frappe.get_doc("Address", quotation.custom_job_address)
|
||||
@ -383,9 +384,10 @@ def upsert_estimate(data):
|
||||
try:
|
||||
data = json.loads(data) if isinstance(data, str) else data
|
||||
print("DEBUG: Upsert estimate data:", data)
|
||||
|
||||
address_doc = AddressService.get_or_throw(data.get("address_name"))
|
||||
estimate_name = data.get("estimate_name")
|
||||
client_doctype = ClientService.get_client_doctype(data.get("customer"))
|
||||
client_doctype = ClientService.get_client_doctype(address_doc.customer_name)
|
||||
print("DEBUG: Retrieved client doctype:", client_doctype)
|
||||
project_template = data.get("project_template", None)
|
||||
|
||||
# If estimate_name exists, update existing estimate
|
||||
@ -430,17 +432,16 @@ def upsert_estimate(data):
|
||||
else:
|
||||
print("DEBUG: Creating new estimate")
|
||||
print("DEBUG: Retrieved address name:", data.get("address_name"))
|
||||
client_doctype = ClientService.get_client_doctype(data.get("customer"))
|
||||
new_estimate = frappe.get_doc({
|
||||
"doctype": "Quotation",
|
||||
"custom_requires_half_payment": data.get("requires_half_payment", 0),
|
||||
"custom_job_address": data.get("address_name"),
|
||||
"custom_current_status": "Draft",
|
||||
"contact_email": data.get("contact_email"),
|
||||
"party_name": data.get("customer"),
|
||||
"quotation_to": client_doctype,
|
||||
"party_name": data.get("contact_name"),
|
||||
"quotation_to": "Contact",
|
||||
"company": data.get("company"),
|
||||
"customer": data.get("customer"),
|
||||
"actual_customer_name": address_doc.customer_name,
|
||||
"customer_type": client_doctype,
|
||||
"customer_address": data.get("address_name"),
|
||||
"contact_person": data.get("contact_name"),
|
||||
@ -457,9 +458,12 @@ def upsert_estimate(data):
|
||||
"discount_amount": item.get("discount_amount") or item.get("discountAmount", 0),
|
||||
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
||||
})
|
||||
# Iterate through every field and print it out, I need to see if there is any field that is a Dynamic link saying Customer
|
||||
for fieldname, value in new_estimate.as_dict().items():
|
||||
print(f"DEBUG: Field '{fieldname}': {value}")
|
||||
new_estimate.insert()
|
||||
AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
|
||||
ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name)
|
||||
# AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
|
||||
# ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name)
|
||||
print("DEBUG: New estimate created with name:", new_estimate.name)
|
||||
return build_success_response(new_estimate.as_dict())
|
||||
except Exception as e:
|
||||
|
||||
0
custom_ui/events/client.py
Normal file
0
custom_ui/events/client.py
Normal file
@ -5,8 +5,10 @@ from custom_ui.services import DbService, AddressService, ClientService
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: After insert hook triggered for Quotation:", doc.name)
|
||||
AddressService.append_link_v2(
|
||||
doc.custom_job_address,
|
||||
{"quotations": {"quotation": doc.name, "project_template": doc.custom_project_template}}
|
||||
doc.custom_job_address, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template}
|
||||
)
|
||||
ClientService.append_link_v2(
|
||||
doc.actual_customer_name, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template}
|
||||
)
|
||||
template = doc.custom_project_template or "Other"
|
||||
if template == "Other":
|
||||
@ -20,18 +22,24 @@ def after_insert(doc, method):
|
||||
)
|
||||
|
||||
def before_insert(doc, method):
|
||||
print("DEBUG: Before insert hook triggered for Quotation:", doc.name)
|
||||
# if doc.custom_project_template == "SNW Install":
|
||||
# print("DEBUG: Quotation uses SNW Install template, setting initial Address status to 'In Progress'.")
|
||||
# address_doc = AddressService.get_or_throw(doc.custom_job_address)
|
||||
# if "SNW Install" in [link.project_template for link in address_doc.quotations]:
|
||||
# raise frappe.ValidationError("An Estimate with project template 'SNW Install' is already linked to this address.")
|
||||
print("DEBUG: Before insert hook triggered for Quotation:", doc)
|
||||
print("DEBUG: CHECKING CUSTOMER TYPE")
|
||||
print(doc.customer_type)
|
||||
print("DEBUG: CHECKING CUSTOMER NAME")
|
||||
print(doc.actual_customer_name)
|
||||
print("Quotation_to:", doc.quotation_to)
|
||||
# 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.")
|
||||
address_doc = AddressService.get_or_throw(doc.custom_job_address)
|
||||
if "SNW Install" in [link.project_template for link in address_doc.quotations]:
|
||||
raise frappe.ValidationError("An Estimate with project template 'SNW Install' is already linked to this address.")
|
||||
|
||||
def before_submit(doc, method):
|
||||
print("DEBUG: Before submit hook triggered for Quotation:", doc.name)
|
||||
if doc.custom_project_template == "SNW Install":
|
||||
print("DEBUG: Quotation uses SNW Install template.")
|
||||
if doc.custom_current_status == "Estimate Sent":
|
||||
if doc.custom_sent == 1:
|
||||
print("DEBUG: Current status is 'Estimate Sent', updating Address status to 'Sent'.")
|
||||
AddressService.update_value(
|
||||
doc.custom_job_address,
|
||||
@ -47,14 +55,18 @@ def on_update_after_submit(doc, method):
|
||||
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.customer, update_quotations=False)
|
||||
doc.customer = new_customer.name
|
||||
new_customer = ClientService.convert_lead_to_customer(doc.actual_customer_name, update_quotations=False)
|
||||
doc.actual_customer_name = new_customer.name
|
||||
doc.customer_type = "Customer"
|
||||
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.customer = doc.customer
|
||||
new_sales_order.customer = doc.actual_customer_name
|
||||
# new_sales_order.custom_installation_address = doc.custom_installation_address
|
||||
# new_sales_order.custom_job_address = doc.custom_job_address
|
||||
new_sales_order.payment_schedule = []
|
||||
@ -62,8 +74,11 @@ def on_update_after_submit(doc, method):
|
||||
new_sales_order.set_payment_schedule()
|
||||
print("DEBUG: Inserting Sales Order:", new_sales_order.as_dict())
|
||||
new_sales_order.delivery_date = new_sales_order.transaction_date
|
||||
backup = new_sales_order.customer_address
|
||||
new_sales_order.customer_address = None
|
||||
new_sales_order.insert()
|
||||
print("DEBUG: Submitting Sales Order")
|
||||
new_sales_order.customer_address = backup
|
||||
new_sales_order.submit()
|
||||
frappe.db.commit()
|
||||
print("DEBUG: Sales Order created successfully:", new_sales_order.name)
|
||||
|
||||
@ -22,7 +22,7 @@ def after_insert(doc, method):
|
||||
|
||||
def before_save(doc, method):
|
||||
print("DEBUG: Before Save Triggered for On-Site Meeting")
|
||||
if doc.status != "Scheduled" and doc.start_time and doc.end_time:
|
||||
if doc.status != "Scheduled" and doc.start_time and doc.end_time and doc.status != "Completed":
|
||||
print("DEBUG: Meeting has start and end time, setting status to Scheduled")
|
||||
doc.status = "Scheduled"
|
||||
if doc.project_template == "SNW Install":
|
||||
|
||||
@ -114,8 +114,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "084d85c4c6bd8aa7a867582454f1fc78",
|
||||
"modified": "2026-01-13 11:55:00.125952",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.065211",
|
||||
"module": "Custom",
|
||||
"name": "Lead Companies Link",
|
||||
"naming_rule": "",
|
||||
@ -332,8 +332,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 03:13:47.584878",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.117683",
|
||||
"module": "Custom",
|
||||
"name": "Address Project Link",
|
||||
"naming_rule": "",
|
||||
@ -550,8 +550,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 03:12:40.401170",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.173958",
|
||||
"module": "Custom",
|
||||
"name": "Address Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -768,8 +768,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 03:08:11.881037",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.229451",
|
||||
"module": "Custom",
|
||||
"name": "Address On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -922,8 +922,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.392595",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.280251",
|
||||
"module": "Custom",
|
||||
"name": "Address Sales Order Link",
|
||||
"naming_rule": "",
|
||||
@ -1076,8 +1076,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.443423",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.332720",
|
||||
"module": "Custom",
|
||||
"name": "Contact Address Link",
|
||||
"naming_rule": "",
|
||||
@ -1230,8 +1230,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.493149",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.385343",
|
||||
"module": "Custom",
|
||||
"name": "Lead On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -1832,8 +1832,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.563912",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.457103",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Template",
|
||||
"naming_rule": "",
|
||||
@ -2330,8 +2330,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.635350",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.529451",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Template Item",
|
||||
"naming_rule": "",
|
||||
@ -2484,8 +2484,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.685490",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.580626",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Company Link",
|
||||
"naming_rule": "",
|
||||
@ -2638,8 +2638,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.735935",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.630918",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Address Link",
|
||||
"naming_rule": "",
|
||||
@ -2792,8 +2792,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.785648",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.681698",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -2946,8 +2946,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.837258",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.731378",
|
||||
"module": "Custom",
|
||||
"name": "Address Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -3100,8 +3100,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.887850",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.783154",
|
||||
"module": "Custom",
|
||||
"name": "Customer On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -3254,8 +3254,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.938600",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.832605",
|
||||
"module": "Custom",
|
||||
"name": "Customer Project Link",
|
||||
"naming_rule": "",
|
||||
@ -3408,8 +3408,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:38.989639",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.882511",
|
||||
"module": "Custom",
|
||||
"name": "Customer Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -3562,8 +3562,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:39.042414",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.934072",
|
||||
"module": "Custom",
|
||||
"name": "Customer Sales Order Link",
|
||||
"naming_rule": "",
|
||||
@ -3716,8 +3716,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:39.092979",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.984126",
|
||||
"module": "Custom",
|
||||
"name": "Lead Address Link",
|
||||
"naming_rule": "",
|
||||
@ -3870,8 +3870,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:39.144013",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:58.034758",
|
||||
"module": "Custom",
|
||||
"name": "Lead Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -4024,8 +4024,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:39.251239",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:58.086245",
|
||||
"module": "Custom",
|
||||
"name": "Lead Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -4178,8 +4178,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "5f481f64a0f53ad40b09d8b5694265c1",
|
||||
"modified": "2026-01-15 00:40:39.304711",
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:58.139052",
|
||||
"module": "Custom",
|
||||
"name": "Address Company Link",
|
||||
"naming_rule": "",
|
||||
|
||||
@ -462,18 +462,20 @@ def add_custom_fields():
|
||||
insert_after="custom_job_address"
|
||||
),
|
||||
dict(
|
||||
fieldname="customer",
|
||||
fieldname="actual_customer_name",
|
||||
label="Customer",
|
||||
fieldtype="Dynamic Link",
|
||||
options="customer_type",
|
||||
insert_after="from_onsite_meeting"
|
||||
insert_after="from_onsite_meeting",
|
||||
allow_on_submit=1
|
||||
),
|
||||
dict(
|
||||
fieldname="customer_type",
|
||||
label="Customer Type",
|
||||
fieldtype="Select",
|
||||
options="Customer\nLead",
|
||||
insert_after="customer_name"
|
||||
insert_after="customer_name",
|
||||
allow_on_submit=1
|
||||
)
|
||||
],
|
||||
"Sales Order": [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import frappe
|
||||
import requests
|
||||
from .contact_service import ContactService, DbService
|
||||
|
||||
class AddressService:
|
||||
@ -164,6 +165,38 @@ class AddressService:
|
||||
"""Set a link field for an address using a link dictionary."""
|
||||
print(f"DEBUG: Setting link field {field} for Address {address_name} with link data {link}")
|
||||
address_doc = AddressService.get_or_throw(address_name)
|
||||
print("DEBUG: Appending link:", link)
|
||||
address_doc.append(field, link)
|
||||
print("DEBUG: Saving address document after appending link.")
|
||||
address_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}")
|
||||
print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}")
|
||||
|
||||
@staticmethod
|
||||
def get_county_and_set(address_doc, save: bool = False):
|
||||
"""Get the county from the address document and set it if not already set."""
|
||||
if not address_doc.county:
|
||||
print(f"DEBUG: Getting county for Address {address_doc.name}")
|
||||
# Example logic to determine county from address fields
|
||||
# This is a placeholder; actual implementation may vary
|
||||
url = "https://geocoding.geo.cencus.gov/geocoder/geographies/coordinates"
|
||||
params = {
|
||||
"x": address_doc.longitude,
|
||||
"y": address_doc.latitude,
|
||||
"benchmark": "Public_AR_Current",
|
||||
"vintage": "Current_Current",
|
||||
"format": "json"
|
||||
}
|
||||
|
||||
r = requests.get(url, params=params, timeout=10)
|
||||
data = r.json()
|
||||
|
||||
try:
|
||||
county = data['result']['geographies']['Counties'][0]['NAME']
|
||||
county_fips = data['result']['geographies']['Counties'][0]['GEOID']
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
||||
return {
|
||||
"county": county,
|
||||
"county_fips": county_fips
|
||||
}
|
||||
@ -37,7 +37,19 @@ class ClientService:
|
||||
})
|
||||
client_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} to {link_doctype} {link_name}")
|
||||
|
||||
|
||||
@staticmethod
|
||||
def append_link_v2(client_name: str, field: str, link: dict):
|
||||
"""Set a link field for a client (Customer or Lead) using a link dictionary."""
|
||||
print(f"DEBUG: Setting link field {field} for client {client_name} with link data {link}")
|
||||
client_doctype = ClientService.get_client_doctype(client_name)
|
||||
client_doc = DbService.get_or_throw(client_doctype, client_name)
|
||||
print("DEBUG: Appending link:", link)
|
||||
client_doc.append(field, link)
|
||||
print("DEBUG: Saving client document after appending link.")
|
||||
client_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} with link data {link}")
|
||||
|
||||
@staticmethod
|
||||
def convert_lead_to_customer(
|
||||
lead_name: str,
|
||||
@ -48,24 +60,78 @@ class ClientService:
|
||||
):
|
||||
"""Convert a Lead to a Customer."""
|
||||
print(f"DEBUG: Converting Lead {lead_name} to Customer")
|
||||
lead_doc = DbService.get_or_throw("Lead", lead_name)
|
||||
customer_doc = make_customer(lead_doc.name)
|
||||
customer_doc.insert(ignore_permissions=True)
|
||||
if update_addresses:
|
||||
for address in lead_doc.get("addresses", []):
|
||||
address_doc = AddressService.get_or_throw(address.get("address"))
|
||||
AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name)
|
||||
if update_contacts:
|
||||
for contact in lead_doc.get("contacts", []):
|
||||
contact_doc = ContactService.get_or_throw(contact.get("contact"))
|
||||
ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name)
|
||||
if update_quotations:
|
||||
for quotation in lead_doc.get("quotations", []):
|
||||
quotation_doc = EstimateService.get_or_throw(quotation.get("quotation"))
|
||||
EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name)
|
||||
if update_onsite_meetings:
|
||||
for meeting in lead_doc.get("onsite_meetings", []):
|
||||
meeting_doc = OnSiteMeetingService.get_or_throw(meeting.get("onsite_meeting"))
|
||||
OnSiteMeetingService.link_onsite_meeting_to_customer(meeting_doc, "Customer", customer_doc.name)
|
||||
print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}")
|
||||
return customer_doc
|
||||
try:
|
||||
lead_doc = DbService.get_or_throw("Lead", lead_name)
|
||||
print(f"DEBUG: Retrieved Lead document: {lead_doc.name}")
|
||||
|
||||
print("DEBUG: RUNNING make_customer()")
|
||||
customer_doc = make_customer(lead_doc.name)
|
||||
print(f"DEBUG: make_customer() returned document type: {type(customer_doc)}")
|
||||
print(f"DEBUG: Customer doc name: {customer_doc.name if hasattr(customer_doc, 'name') else 'NO NAME'}")
|
||||
|
||||
print("DEBUG: Calling customer_doc.insert()")
|
||||
customer_doc.insert(ignore_permissions=True)
|
||||
print(f"DEBUG: Customer inserted successfully: {customer_doc.name}")
|
||||
|
||||
frappe.db.commit()
|
||||
print("DEBUG: Database committed after customer insert")
|
||||
print("DEBUG: CREATED CUSTOMER:", customer_doc.as_dict())
|
||||
if update_addresses:
|
||||
print("DEBUG: Lead_doc addresses:", lead_doc.get("addresses", []))
|
||||
print(f"DEBUG: Updating addresses. Count: {len(lead_doc.get('properties', []))}")
|
||||
for address in lead_doc.get("properties", []):
|
||||
try:
|
||||
print(f"DEBUG: Processing address: {address.get('address')}")
|
||||
ClientService.append_link_v2(customer_doc.name, "properties", {"address": address.get("address")})
|
||||
address_doc = AddressService.get_or_throw(address.get("address"))
|
||||
AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name)
|
||||
print(f"DEBUG: Linked address {address.get('address')} to customer")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to link address {address.get('address')}: {str(e)}")
|
||||
frappe.log_error(f"Address linking error: {str(e)}", "convert_lead_to_customer")
|
||||
|
||||
if update_contacts:
|
||||
print(f"DEBUG: Updating contacts. Count: {len(lead_doc.get('contacts', []))}")
|
||||
for contact in lead_doc.get("contacts", []):
|
||||
try:
|
||||
print(f"DEBUG: Processing contact: {contact.get('contact')}")
|
||||
ClientService.append_link_v2(customer_doc.name, "contacts", {"contact": contact.get("contact")})
|
||||
contact_doc = ContactService.get_or_throw(contact.get("contact"))
|
||||
ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name)
|
||||
print(f"DEBUG: Linked contact {contact.get('contact')} to customer")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to link contact {contact.get('contact')}: {str(e)}")
|
||||
frappe.log_error(f"Contact linking error: {str(e)}", "convert_lead_to_customer")
|
||||
|
||||
if update_quotations:
|
||||
print(f"DEBUG: Updating quotations. Count: {len(lead_doc.get('quotations', []))}")
|
||||
for quotation in lead_doc.get("quotations", []):
|
||||
try:
|
||||
print(f"DEBUG: Processing quotation: {quotation.get('quotation')}")
|
||||
ClientService.append_link_v2(customer_doc.name, "quotations", {"quotation": quotation.get("quotation")})
|
||||
quotation_doc = EstimateService.get_or_throw(quotation.get("quotation"))
|
||||
EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name)
|
||||
print(f"DEBUG: Linked quotation {quotation.get('quotation')} to customer")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to link quotation {quotation.get('quotation')}: {str(e)}")
|
||||
frappe.log_error(f"Quotation linking error: {str(e)}", "convert_lead_to_customer")
|
||||
|
||||
if update_onsite_meetings:
|
||||
print(f"DEBUG: Updating onsite meetings. Count: {len(lead_doc.get('onsite_meetings', []))}")
|
||||
for meeting in lead_doc.get("onsite_meetings", []):
|
||||
try:
|
||||
print(f"DEBUG: Processing onsite meeting: {meeting.get('onsite_meeting')}")
|
||||
meeting_doc = DbService.get_or_throw("On-Site Meeting",meeting.get("onsite_meeting"))
|
||||
ClientService.append_link_v2(customer_doc.name, "onsite_meetings", {"onsite_meeting": meeting.get("onsite_meeting")})
|
||||
OnSiteMeetingService.link_onsite_meeting_to_customer(meeting_doc, "Customer", customer_doc.name)
|
||||
print(f"DEBUG: Linked onsite meeting {meeting.get('onsite_meeting')} to customer")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to link onsite meeting {meeting.get('onsite_meeting')}: {str(e)}")
|
||||
frappe.log_error(f"Onsite meeting linking error: {str(e)}", "convert_lead_to_customer")
|
||||
print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}")
|
||||
return customer_doc
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Exception in convert_lead_to_customer: {str(e)}")
|
||||
frappe.log_error(f"convert_lead_to_customer failed: {str(e)}", "ClientService")
|
||||
raise
|
||||
@ -173,21 +173,26 @@
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, meeting)"
|
||||
@dragend="handleDragEnd($event)"
|
||||
@click="showMeetingDetails(meeting)"
|
||||
>
|
||||
<v-card-text class="pa-3">
|
||||
<div class="meeting-title">{{ meeting.address }}</div>
|
||||
<div class="meeting-status">
|
||||
<v-card-text class="pa-2">
|
||||
<div class="unscheduled-address">
|
||||
<v-icon size="x-small" class="mr-1">mdi-map-marker</v-icon>
|
||||
{{ meeting.address?.fullAddress || meeting.address }}
|
||||
</div>
|
||||
<div v-if="meeting.contact?.name" class="unscheduled-contact">
|
||||
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
|
||||
{{ meeting.contact.name }}
|
||||
</div>
|
||||
<div v-if="meeting.projectTemplate" class="unscheduled-project">
|
||||
<v-icon size="x-small" class="mr-1">mdi-file-document</v-icon>
|
||||
{{ meeting.projectTemplate }}
|
||||
</div>
|
||||
<div class="unscheduled-status">
|
||||
<v-chip size="x-small" :color="getStatusColor(meeting.status)">
|
||||
{{ meeting.status }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-if="meeting.notes" class="meeting-notes">
|
||||
{{ meeting.notes }}
|
||||
</div>
|
||||
<div v-if="meeting.assigned_employee" class="meeting-employee">
|
||||
<v-icon size="x-small" class="mr-1">mdi-account</v-icon>
|
||||
{{ meeting.assigned_employee }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
@ -876,17 +881,12 @@ const loadWeekMeetings = async () => {
|
||||
? `${startDateTime.getHours().toString().padStart(2, "0")}:${startDateTime.getMinutes().toString().padStart(2, "0")}`
|
||||
: null;
|
||||
|
||||
// Return the full meeting object with calendar-specific fields added
|
||||
return {
|
||||
...meeting, // Keep all original fields
|
||||
id: meeting.name,
|
||||
name: meeting.name,
|
||||
date: date,
|
||||
scheduledTime: time,
|
||||
address: meeting.address,
|
||||
notes: meeting.notes,
|
||||
assigned_employee: meeting.assignedEmployee,
|
||||
status: meeting.status,
|
||||
startTime: meeting.startTime,
|
||||
endTime: meeting.endTime,
|
||||
};
|
||||
})
|
||||
.filter((meeting) => meeting.date && meeting.scheduledTime); // Only include meetings with valid date/time
|
||||
@ -1274,6 +1274,30 @@ watch(
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unscheduled-address,
|
||||
.unscheduled-contact,
|
||||
.unscheduled-project {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.unscheduled-address {
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.unscheduled-status {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.calendar-section {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-map-marker" style="color: var(--primary-color); font-size: 1.2rem;"></i>
|
||||
<h3>Property Address Information</h3>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
@ -10,20 +11,23 @@
|
||||
class="address-item"
|
||||
>
|
||||
<div class="address-header">
|
||||
<h4>Address {{ index + 1 }}</h4>
|
||||
<div class="address-title">
|
||||
<i class="pi pi-home" style="font-size: 0.9rem; color: var(--primary-color);"></i>
|
||||
<h4>Address {{ index + 1 }}</h4>
|
||||
</div>
|
||||
<Button
|
||||
v-if="localFormData.addresses.length > 1"
|
||||
@click="removeAddress(index)"
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Delete"
|
||||
icon="pi pi-trash"
|
||||
class="remove-btn"
|
||||
/>
|
||||
</div>
|
||||
<div class="address-fields">
|
||||
<div class="form-field full-width">
|
||||
<label :for="`address-line1-${index}`">
|
||||
Address Line 1 <span class="required">*</span>
|
||||
<i class="pi pi-map" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Address Line 1 <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`address-line1-${index}`"
|
||||
@ -35,7 +39,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field full-width">
|
||||
<label :for="`address-line2-${index}`">Address Line 2</label>
|
||||
<label :for="`address-line2-${index}`"><i class="pi pi-map" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Address Line 2</label>
|
||||
<InputText
|
||||
:id="`address-line2-${index}`"
|
||||
v-model="address.addressLine2"
|
||||
@ -53,12 +57,12 @@
|
||||
:disabled="isSubmitting"
|
||||
style="margin-top: 0"
|
||||
/>
|
||||
<label :for="`isBilling-${index}`">Is Billing Address</label>
|
||||
<label :for="`isBilling-${index}`"><i class="pi pi-dollar" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Is Billing Address</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label :for="`zipcode-${index}`">
|
||||
Zip Code <span class="required">*</span>
|
||||
<i class="pi pi-hashtag" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Zip Code <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`zipcode-${index}`"
|
||||
@ -72,7 +76,7 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`city-${index}`">
|
||||
City <span class="required">*</span>
|
||||
<i class="pi pi-building" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>City <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`city-${index}`"
|
||||
@ -84,7 +88,7 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`state-${index}`">
|
||||
State <span class="required">*</span>
|
||||
<i class="pi pi-flag" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>State <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`state-${index}`"
|
||||
@ -97,7 +101,7 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label :for="`contacts-${index}`">Assigned Contacts</label>
|
||||
<label :for="`contacts-${index}`"><i class="pi pi-users" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Assigned Contacts</label>
|
||||
<MultiSelect
|
||||
:id="`contacts-${index}`"
|
||||
v-model="address.contacts"
|
||||
@ -111,7 +115,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`primaryContact-${index}`">Primary Contact</label>
|
||||
<label :for="`primaryContact-${index}`"><i class="pi pi-star-fill" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Primary Contact</label>
|
||||
<Select
|
||||
:id="`primaryContact-${index}`"
|
||||
v-model="address.primaryContact"
|
||||
@ -294,51 +298,72 @@ const handleZipcodeInput = async (index, event) => {
|
||||
<style scoped>
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.address-item {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--surface-section);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.address-item:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.address-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.address-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.address-header h4 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -349,18 +374,18 @@ const handleZipcodeInput = async (index, event) => {
|
||||
.address-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@ -370,8 +395,10 @@ const handleZipcodeInput = async (index, event) => {
|
||||
|
||||
.form-field label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color);
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.required {
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
<div>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-user" style="color: var(--primary-color); font-size: 1.2rem;"></i>
|
||||
<h3>Client Information</h3>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="customer-type"> Customer Type <span class="required">*</span> </label>
|
||||
<label for="customer-type"><i class="pi pi-building" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Customer Type <span class="required">*</span></label>
|
||||
<Select
|
||||
id="customer-type"
|
||||
v-model="localFormData.customerType"
|
||||
@ -17,7 +18,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="customer-name"> Customer Name <span class="required">*</span> </label>
|
||||
<label for="customer-name"><i class="pi pi-id-card" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Customer Name <span class="required">*</span></label>
|
||||
<div class="input-with-button">
|
||||
<InputText
|
||||
id="customer-name"
|
||||
@ -27,13 +28,13 @@
|
||||
class="w-full"
|
||||
/>
|
||||
<Button
|
||||
label="Check Client"
|
||||
label="Check"
|
||||
size="small"
|
||||
icon="pi pi-user-check"
|
||||
icon="pi pi-check-circle"
|
||||
class="check-btn"
|
||||
@click="checkCustomerExists"
|
||||
:disabled="isSubmitting"
|
||||
>Check</Button>
|
||||
/>
|
||||
<Button
|
||||
v-if="!isNewClient && !isEditMode"
|
||||
@click="searchCustomers"
|
||||
@ -41,7 +42,7 @@
|
||||
size="small"
|
||||
icon="pi pi-search"
|
||||
class="search-btn"
|
||||
></Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -252,23 +253,30 @@ defineExpose({
|
||||
<style scoped>
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
/*border: 1px solid var(--surface-border);*/
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -289,20 +297,22 @@ defineExpose({
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color);
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.required {
|
||||
@ -311,7 +321,8 @@ defineExpose({
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
@ -396,45 +407,19 @@ defineExpose({
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
border: 1px solid var(--primary-color);
|
||||
color: white;
|
||||
background: var(--primary-color);
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-width: 5rem;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.check-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.check-btn:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
color: white;
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
.search-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.search-btn:hover:not(:disabled) {
|
||||
background: var(--surface-hover);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="pi pi-users" style="color: var(--primary-color); font-size: 1.2rem;"></i>
|
||||
<h3>Contact Information</h3>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
@ -10,7 +11,10 @@
|
||||
class="contact-item"
|
||||
>
|
||||
<div class="contact-header">
|
||||
<h4>Contact {{ index + 1 }}</h4>
|
||||
<div class="contact-title">
|
||||
<i class="pi pi-user" style="font-size: 0.9rem; color: var(--primary-color);"></i>
|
||||
<h4>Contact {{ index + 1 }}</h4>
|
||||
</div>
|
||||
<div class="interactables">
|
||||
<div class="form-field header-row">
|
||||
<input
|
||||
@ -23,7 +27,7 @@
|
||||
@change="setPrimary(index)"
|
||||
/>
|
||||
<label :for="`checkbox-${index}`">
|
||||
Client Primary Contact
|
||||
<i class="pi pi-star-fill" style="font-size: 0.7rem; margin-right: 0.25rem;"></i>Primary
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
@ -31,7 +35,7 @@
|
||||
@click="removeContact(index)"
|
||||
size="small"
|
||||
severity="danger"
|
||||
label="Delete"
|
||||
icon="pi pi-trash"
|
||||
class="remove-btn"
|
||||
/>
|
||||
</div>
|
||||
@ -40,32 +44,32 @@
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label :for="`first-name-${index}`">
|
||||
First Name <span class="required">*</span>
|
||||
<i class="pi pi-user" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>First Name <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`first-name-${index}`"
|
||||
v-model="contact.firstName"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Enter first name"
|
||||
placeholder="First name"
|
||||
class="w-full"
|
||||
@input="formatName(index, 'firstName', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`last-name-${index}`">
|
||||
Last Name <span class="required">*</span>
|
||||
<i class="pi pi-user" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Last Name <span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
:id="`last-name-${index}`"
|
||||
v-model="contact.lastName"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Enter last name"
|
||||
placeholder="Last name"
|
||||
class="w-full"
|
||||
@input="formatName(index, 'lastName', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`contact-role-${index}`">Role</label>
|
||||
<label :for="`contact-role-${index}`"><i class="pi pi-briefcase" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Role</label>
|
||||
<Select
|
||||
:id="`contact-role-${index}`"
|
||||
v-model="contact.contactRole"
|
||||
@ -73,14 +77,14 @@
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:disabled="isSubmitting"
|
||||
placeholder="Select a role"
|
||||
placeholder="Select role"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label :for="`email-${index}`">Email</label>
|
||||
<label :for="`email-${index}`"><i class="pi pi-envelope" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Email</label>
|
||||
<InputText
|
||||
:id="`email-${index}`"
|
||||
v-model="contact.email"
|
||||
@ -91,7 +95,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label :for="`phone-number-${index}`">Phone</label>
|
||||
<label :for="`phone-number-${index}`"><i class="pi pi-phone" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Phone</label>
|
||||
<InputText
|
||||
:id="`phone-number-${index}`"
|
||||
v-model="contact.phoneNumber"
|
||||
@ -280,46 +284,67 @@ defineExpose({});
|
||||
<style scoped>
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--surface-section);
|
||||
min-width: 33%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.contact-item:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.contact-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.contact-header h4 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -334,35 +359,32 @@ defineExpose({});
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.contact-item .form-grid {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-field.full-width {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@ -75,46 +75,69 @@
|
||||
<!-- Display Mode (existing client view) -->
|
||||
<template v-else>
|
||||
<!-- Address Info Card -->
|
||||
<div class="info-card w-half" v-if="selectedAddressData">
|
||||
<h3>General Information</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item full-width">
|
||||
<label>Address Title:</label>
|
||||
<span>{{ selectedAddressData.addressTitle || "N/A" }}</span>
|
||||
<div class="compact-card" v-if="selectedAddressData">
|
||||
<div class="card-header-compact">
|
||||
<i class="pi pi-home"></i>
|
||||
<h4>Property Details</h4>
|
||||
</div>
|
||||
<div class="compact-grid">
|
||||
<div class="compact-item">
|
||||
<i class="pi pi-tag"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Title</span>
|
||||
<span class="item-value">{{ selectedAddressData.addressTitle || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item full-width">
|
||||
<label>Full Address:</label>
|
||||
<span>{{ fullAddress }}</span>
|
||||
<div class="compact-item">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Address</span>
|
||||
<span class="item-value">{{ fullAddress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item full-width">
|
||||
<label>City:</label>
|
||||
<span>{{ selectedAddressData.city || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item full-width">
|
||||
<label>State:</label>
|
||||
<span>{{ selectedAddressData.state || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item full-width">
|
||||
<label>Zip Code:</label>
|
||||
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
|
||||
<div class="compact-row">
|
||||
<div class="compact-item-inline">
|
||||
<i class="pi pi-building"></i>
|
||||
<span class="item-value">{{ selectedAddressData.city || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="compact-item-inline">
|
||||
<i class="pi pi-flag"></i>
|
||||
<span class="item-value">{{ selectedAddressData.state || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="compact-item-inline">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span class="item-value">{{ selectedAddressData.pincode || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Basic Info Card -->
|
||||
<div class="info-card w-half">
|
||||
<h3>Contact Information</h3>
|
||||
<div class="compact-card">
|
||||
<div class="card-header-compact">
|
||||
<i class="pi pi-users"></i>
|
||||
<h4>Contact Information</h4>
|
||||
</div>
|
||||
<template v-if="contactsForAddress.length > 0">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Customer Name:</label>
|
||||
<span>{{ clientData?.customerName || "N/A" }}</span>
|
||||
<div class="compact-grid">
|
||||
<div class="compact-row">
|
||||
<div class="compact-item-inline">
|
||||
<i class="pi pi-user"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Customer</span>
|
||||
<span class="item-value">{{ clientData?.customerName || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compact-item-inline">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Type</span>
|
||||
<span class="item-value">{{ clientData?.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Customer Type:</label>
|
||||
<span>{{ clientData?.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
<div v-if="contactsForAddress.length > 1" class="contact-selector">
|
||||
|
||||
<div v-if="contactsForAddress.length > 1" class="contact-selector-compact">
|
||||
<Dropdown
|
||||
v-model="selectedContactIndex"
|
||||
:options="contactOptions"
|
||||
@ -122,44 +145,76 @@
|
||||
option-value="value"
|
||||
placeholder="Select Contact"
|
||||
class="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Contact Name:</label>
|
||||
<span>{{ contactFullName }}</span>
|
||||
|
||||
<div class="contact-details">
|
||||
<div class="compact-item">
|
||||
<i class="pi pi-user-edit"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Contact</span>
|
||||
<span class="item-value">{{ contactFullName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Phone:</label>
|
||||
<span>{{ primaryContactPhone }}</span>
|
||||
<div class="compact-item">
|
||||
<i class="pi pi-phone"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Phone</span>
|
||||
<span class="item-value">{{ primaryContactPhone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Email:</label>
|
||||
<span>{{ primaryContactEmail }}</span>
|
||||
<div class="compact-item">
|
||||
<i class="pi pi-at"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Email</span>
|
||||
<span class="item-value">{{ primaryContactEmail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Customer Group:</label>
|
||||
<span>{{ clientData?.customerGroup || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Territory:</label>
|
||||
<span>{{ clientData?.territory || "N/A" }}</span>
|
||||
|
||||
<div class="compact-row" v-if="clientData?.customerGroup || clientData?.territory">
|
||||
<div class="compact-item-inline" v-if="clientData?.customerGroup">
|
||||
<i class="pi pi-sitemap"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Group</span>
|
||||
<span class="item-value">{{ clientData.customerGroup }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compact-item-inline" v-if="clientData?.territory">
|
||||
<i class="pi pi-globe"></i>
|
||||
<div class="item-content">
|
||||
<span class="item-label">Territory</span>
|
||||
<span class="item-value">{{ clientData.territory }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>No contacts available for this address.</p>
|
||||
<div class="empty-state">
|
||||
<i class="pi pi-user-minus"></i>
|
||||
<p>No contacts available for this address.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Financials At a Glance -->
|
||||
<div class="info-card w-half">
|
||||
<h3>Open Balances</h3>
|
||||
<span>$3200.00</span>
|
||||
<button class="sidebar-button" @click="navigateTo('/invoices')">
|
||||
Go to Invoices
|
||||
</button>
|
||||
<div class="compact-card financial-card">
|
||||
<div class="card-header-compact">
|
||||
<i class="pi pi-dollar"></i>
|
||||
<h4>Open Balances</h4>
|
||||
</div>
|
||||
<div class="financial-amount">$3,200.00</div>
|
||||
<Button
|
||||
@click="navigateTo('/invoices')"
|
||||
icon="pi pi-arrow-right"
|
||||
label="View Invoices"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -757,36 +812,189 @@ const handleCancel = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
/*padding: 1rem;*/
|
||||
}
|
||||
|
||||
.quick-action-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.install-status-section {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
/* Compact Cards */
|
||||
.compact-card {
|
||||
background: var(--surface-card);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
flex: 1 1 280px;
|
||||
min-width: 280px;
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.compact-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.card-header-compact i {
|
||||
font-size: 1.1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-header-compact h4 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.compact-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.compact-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.compact-item > i {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin-top: 0.15rem;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.compact-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.compact-item-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.compact-item-inline > i {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.compact-item-inline .item-content {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.contact-selector-compact {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--surface-ground);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Financial Card Specific */
|
||||
.financial-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.financial-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
text-align: center;
|
||||
padding: 0.75rem 0;
|
||||
background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 2rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Map Card - Keep Full Size */
|
||||
.map-card {
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.info-card {
|
||||
flex: 1 1 300px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
flex: 1 1 600px;
|
||||
}
|
||||
|
||||
.map-card {
|
||||
flex: 1 1 600px;
|
||||
.map-card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@ -800,8 +1008,16 @@ const handleCancel = () => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-card h3,
|
||||
.map-card h3 {
|
||||
.info-card {
|
||||
background: var(--surface-card);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
flex: 1 1 300px;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.25rem;
|
||||
@ -915,45 +1131,4 @@ const handleCancel = () => {
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/*.w-half {
|
||||
width: 45% !important;
|
||||
}*/
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.overview-container {
|
||||
padding: 0.5rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.map-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.status-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.status-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -110,7 +110,7 @@ const handleBidMeetingClick = () => {
|
||||
|
||||
const handleEstimateClick = () => {
|
||||
if (props.estimateSentStatus === "Not Started") {
|
||||
router.push(`/estimate?new=true&address=${encodeURIComponent(props.fullAddress)}`);
|
||||
router.push(`/estimate?new=true&address=${encodeURIComponent(props.fullAddress)}&template=SNW%20Install`);
|
||||
} else {
|
||||
router.push(`/estimate?name=${encodeURIComponent(props.estimate)}`);
|
||||
}
|
||||
|
||||
@ -1,97 +1,135 @@
|
||||
<template>
|
||||
<Modal :visible="showModal" @update:visible="showModal = $event" :options="modalOptions" @confirm="handleClose">
|
||||
<template #title>Meeting Details</template>
|
||||
<template #title>
|
||||
<div class="modal-header">
|
||||
<i class="pi pi-calendar" style="color: var(--primary-color); font-size: 1.2rem; margin-right: 0.5rem;"></i>
|
||||
Meeting Details
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="meeting" class="meeting-details">
|
||||
<!-- Meeting ID -->
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-identifier</v-icon>
|
||||
<strong>Meeting ID:</strong>
|
||||
<span class="detail-value">{{ meeting.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-map-marker</v-icon>
|
||||
<strong>Address:</strong>
|
||||
<span class="detail-value">{{ meeting.address?.fullAddress || meeting.address }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Contact -->
|
||||
<div class="detail-row" v-if="meeting.contact">
|
||||
<v-icon class="mr-2">mdi-account</v-icon>
|
||||
<strong>Contact:</strong>
|
||||
<span class="detail-value">{{ meeting.contact }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Party Name (Customer) -->
|
||||
<div class="detail-row" v-if="meeting.partyName">
|
||||
<v-icon class="mr-2">mdi-account-group</v-icon>
|
||||
<strong>Customer:</strong>
|
||||
<span class="detail-value">{{ meeting.partyName }} ({{ meeting.partyType }})</span>
|
||||
</div>
|
||||
|
||||
<!-- Project Template -->
|
||||
<div class="detail-row" v-if="meeting.projectTemplate">
|
||||
<v-icon class="mr-2">mdi-folder-outline</v-icon>
|
||||
<strong>Template:</strong>
|
||||
<span class="detail-value">{{ meeting.projectTemplate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Time -->
|
||||
<div class="detail-row" v-if="meeting.startTime">
|
||||
<v-icon class="mr-2">mdi-calendar-clock</v-icon>
|
||||
<strong>Scheduled:</strong>
|
||||
<span class="detail-value">{{ formatDateTime(meeting.startTime) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Duration -->
|
||||
<div class="detail-row" v-if="meeting.startTime && meeting.endTime">
|
||||
<v-icon class="mr-2">mdi-timer</v-icon>
|
||||
<strong>Duration:</strong>
|
||||
<span class="detail-value">{{ calculateDuration(meeting.startTime, meeting.endTime) }} minutes</span>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="detail-row">
|
||||
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
||||
<strong>Status:</strong>
|
||||
<v-chip size="small" :color="getStatusColor(meeting.status)">
|
||||
<!-- Status Badge -->
|
||||
<div class="status-section">
|
||||
<div class="status-badge" :class="`status-${meeting.status?.toLowerCase()}`">
|
||||
<i class="pi pi-circle-fill"></i>
|
||||
{{ meeting.status }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assigned Employee -->
|
||||
<div class="detail-row" v-if="meeting.assignedEmployee">
|
||||
<v-icon class="mr-2">mdi-account-tie</v-icon>
|
||||
<strong>Assigned To:</strong>
|
||||
<span class="detail-value">{{ meeting.assignedEmployee }}</span>
|
||||
<!-- Key Information Grid -->
|
||||
<div class="info-grid">
|
||||
<!-- Customer Name -->
|
||||
<div class="info-card" v-if="customerName">
|
||||
<div class="info-label">
|
||||
<i class="pi pi-user"></i>
|
||||
Customer
|
||||
</div>
|
||||
<div class="info-value">{{ customerName }}</div>
|
||||
<div class="info-meta" v-if="meeting.partyType">{{ meeting.partyType }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Template -->
|
||||
<div class="info-card" v-if="meeting.projectTemplate">
|
||||
<div class="info-label">
|
||||
<i class="pi pi-folder"></i>
|
||||
Project Type
|
||||
</div>
|
||||
<div class="info-value">{{ meeting.projectTemplate }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Time -->
|
||||
<div class="info-card" v-if="meeting.startTime">
|
||||
<div class="info-label">
|
||||
<i class="pi pi-clock"></i>
|
||||
Scheduled
|
||||
</div>
|
||||
<div class="info-value">{{ formatDateTime(meeting.startTime) }}</div>
|
||||
<div class="info-meta" v-if="meeting.endTime">Duration: {{ calculateDuration(meeting.startTime, meeting.endTime) }} min</div>
|
||||
</div>
|
||||
|
||||
<!-- Assigned Employee -->
|
||||
<div class="info-card" v-if="meeting.assignedEmployee">
|
||||
<div class="info-label">
|
||||
<i class="pi pi-user-edit"></i>
|
||||
Assigned To
|
||||
</div>
|
||||
<div class="info-value">{{ meeting.assignedEmployee }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed By -->
|
||||
<div class="detail-row" v-if="meeting.completedBy">
|
||||
<v-icon class="mr-2">mdi-account-check</v-icon>
|
||||
<strong>Completed By:</strong>
|
||||
<span class="detail-value">{{ meeting.completedBy }}</span>
|
||||
<!-- Address Section -->
|
||||
<div class="section-divider">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<span>Location</span>
|
||||
</div>
|
||||
<div class="address-section">
|
||||
<div class="address-text">
|
||||
<strong>{{ addressText }}</strong>
|
||||
<div class="meeting-id">ID: {{ meeting.name }}</div>
|
||||
</div>
|
||||
<div v-if="hasCoordinates" class="map-container">
|
||||
<iframe
|
||||
:src="mapUrl"
|
||||
width="100%"
|
||||
height="200"
|
||||
frameborder="0"
|
||||
style="border: 1px solid var(--surface-border); border-radius: 6px;"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Company -->
|
||||
<div class="detail-row" v-if="meeting.company">
|
||||
<v-icon class="mr-2">mdi-domain</v-icon>
|
||||
<strong>Company:</strong>
|
||||
<span class="detail-value">{{ meeting.company }}</span>
|
||||
<!-- Contact Information -->
|
||||
<div class="section-divider" v-if="contactInfo">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>Contact Information</span>
|
||||
</div>
|
||||
<div class="contact-section" v-if="contactInfo">
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-user"></i>
|
||||
<span class="contact-label">Name:</span>
|
||||
<span class="contact-value">{{ contactInfo.fullName }}</span>
|
||||
</div>
|
||||
<div class="contact-item" v-if="contactInfo.role">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span class="contact-label">Role:</span>
|
||||
<span class="contact-value">{{ contactInfo.role }}</span>
|
||||
</div>
|
||||
<div class="contact-item" v-if="contactInfo.phone">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span class="contact-label">Phone:</span>
|
||||
<a :href="`tel:${contactInfo.phone}`" class="contact-value contact-link">{{ contactInfo.phone }}</a>
|
||||
</div>
|
||||
<div class="contact-item" v-if="contactInfo.email">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span class="contact-label">Email:</span>
|
||||
<a :href="`mailto:${contactInfo.email}`" class="contact-value contact-link">{{ contactInfo.email }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="detail-row" v-if="meeting.notes">
|
||||
<v-icon class="mr-2">mdi-note-text</v-icon>
|
||||
<strong>Notes:</strong>
|
||||
<span class="detail-value">{{ meeting.notes }}</span>
|
||||
<div v-if="meeting.notes" class="notes-section">
|
||||
<div class="section-divider">
|
||||
<i class="pi pi-file-edit"></i>
|
||||
<span>Notes</span>
|
||||
</div>
|
||||
<div class="notes-content">{{ meeting.notes }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info -->
|
||||
<div class="additional-info" v-if="meeting.company || meeting.completedBy">
|
||||
<div class="info-item" v-if="meeting.company">
|
||||
<i class="pi pi-building"></i>
|
||||
<span>{{ meeting.company }}</span>
|
||||
</div>
|
||||
<div class="info-item" v-if="meeting.completedBy">
|
||||
<i class="pi pi-check-circle"></i>
|
||||
<span>Completed by: {{ meeting.completedBy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<v-btn
|
||||
v-if="meeting.status !== 'Completed'"
|
||||
v-if="meeting.status !== 'Completed' && meeting.status !== 'Unscheduled'"
|
||||
@click="handleMarkComplete"
|
||||
color="success"
|
||||
variant="elevated"
|
||||
@ -154,12 +192,75 @@ const showModal = computed({
|
||||
|
||||
// Modal options
|
||||
const modalOptions = computed(() => ({
|
||||
maxWidth: "700px",
|
||||
maxWidth: "800px",
|
||||
showCancelButton: false,
|
||||
confirmButtonText: "Close",
|
||||
confirmButtonColor: "primary",
|
||||
}));
|
||||
|
||||
// Computed properties for data extraction
|
||||
const customerName = computed(() => {
|
||||
if (props.meeting?.address?.customerName) {
|
||||
return props.meeting.address.customerName;
|
||||
}
|
||||
if (props.meeting?.partyName) {
|
||||
return props.meeting.partyName;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const addressText = computed(() => {
|
||||
return props.meeting?.address?.fullAddress || props.meeting?.address || "";
|
||||
});
|
||||
|
||||
const hasCoordinates = computed(() => {
|
||||
const lat = props.meeting?.address?.customLatitude || props.meeting?.address?.latitude;
|
||||
const lon = props.meeting?.address?.customLongitude || props.meeting?.address?.longitude;
|
||||
return lat && lon && parseFloat(lat) !== 0 && parseFloat(lon) !== 0;
|
||||
});
|
||||
|
||||
const mapUrl = computed(() => {
|
||||
if (!hasCoordinates.value) return "";
|
||||
const lat = parseFloat(props.meeting?.address?.customLatitude || props.meeting?.address?.latitude);
|
||||
const lon = parseFloat(props.meeting?.address?.customLongitude || props.meeting?.address?.longitude);
|
||||
const zoom = 15;
|
||||
// Using OpenStreetMap embed with marker
|
||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.01},${lat - 0.01},${lon + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lon}`;
|
||||
});
|
||||
|
||||
const contactInfo = computed(() => {
|
||||
console.log('=== CONTACT DEBUG ===');
|
||||
console.log('Full meeting object:', props.meeting);
|
||||
console.log('Meeting contact value:', props.meeting?.contact);
|
||||
console.log('Contact type:', typeof props.meeting?.contact);
|
||||
|
||||
const contact = props.meeting?.contact;
|
||||
if (!contact) {
|
||||
console.log('No contact found - returning null');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle both string and object contact
|
||||
if (typeof contact === 'string') {
|
||||
console.log('Contact is a string:', contact);
|
||||
return { fullName: contact };
|
||||
}
|
||||
|
||||
// Log the contact object to see what properties are available
|
||||
console.log('Contact object keys:', Object.keys(contact));
|
||||
console.log('Contact object:', contact);
|
||||
|
||||
const contactData = {
|
||||
fullName: contact.name || contact.fullName || contact.contactName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || '',
|
||||
phone: contact.phone || contact.mobileNo || contact.mobile || contact.phoneNos?.[0]?.phone || '',
|
||||
email: contact.emailId || contact.email || contact.emailAddress || contact.emailIds?.[0]?.emailId || '',
|
||||
role: contact.role || contact.designation || '',
|
||||
};
|
||||
|
||||
console.log('Extracted contact data:', contactData);
|
||||
return contactData;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const handleClose = () => {
|
||||
emit("close");
|
||||
@ -204,32 +305,20 @@ const handleCreateEstimate = () => {
|
||||
const addressText = props.meeting.address?.fullAddress || props.meeting.address || "";
|
||||
const template = props.meeting.projectTemplate || "";
|
||||
const fromMeeting = props.meeting.name || "";
|
||||
const contactName = props.meeting.contact?.name || "";
|
||||
|
||||
router.push({
|
||||
path: "/estimate",
|
||||
query: {
|
||||
new: "true",
|
||||
address: addressText,
|
||||
"from-meeting": fromMeeting,
|
||||
template: template,
|
||||
from: fromMeeting,
|
||||
contact: contactName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTimeStr) => {
|
||||
if (!dateTimeStr) return "";
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString("en-US", {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
const calculateDuration = (startTime, endTime) => {
|
||||
if (!startTime || !endTime) return 0;
|
||||
const start = new Date(startTime);
|
||||
@ -238,28 +327,9 @@ const calculateDuration = (startTime, endTime) => {
|
||||
return Math.round(diffMs / (1000 * 60)); // Convert to minutes
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const statusColors = {
|
||||
Unscheduled: "warning",
|
||||
Scheduled: "info",
|
||||
Completed: "success",
|
||||
Cancelled: "error",
|
||||
};
|
||||
return statusColors[status] || "default";
|
||||
};
|
||||
|
||||
const formatTimeDisplay = (time) => {
|
||||
if (!time) return "";
|
||||
const [hours, minutes] = time.split(":").map(Number);
|
||||
const displayHour = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours;
|
||||
const ampm = hours >= 12 ? "PM" : "AM";
|
||||
return `${displayHour}:${minutes.toString().padStart(2, "0")} ${ampm}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return "";
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
const formatDateTime = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
return new Date(dateString).toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
|
||||
@ -370,6 +370,9 @@ const company = useCompanyStore();
|
||||
|
||||
const addressQuery = computed(() => route.query.address || "");
|
||||
const nameQuery = computed(() => route.query.name || "");
|
||||
const templateQuery = computed(() => route.query.template || "");
|
||||
const fromMeetingQuery = computed(() => route.query["from-meeting"] || "");
|
||||
const contactQuery = computed(() => route.query.contact || "");
|
||||
const isNew = computed(() => route.query.new === "true");
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
@ -383,6 +386,7 @@ const formData = reactive({
|
||||
estimateName: null,
|
||||
requiresHalfPayment: false,
|
||||
projectTemplate: null,
|
||||
fromMeeting: null,
|
||||
});
|
||||
|
||||
const selectedAddress = ref(null);
|
||||
@ -450,6 +454,16 @@ const fetchTemplates = async () => {
|
||||
try {
|
||||
const result = await Api.getEstimateTemplates(company.currentCompany);
|
||||
templates.value = result;
|
||||
|
||||
// Check if template query param exists and set it after templates are loaded
|
||||
const templateParam = route.query.template;
|
||||
if (templateParam) {
|
||||
console.log("DEBUG: Setting template from query param:", templateParam);
|
||||
console.log("DEBUG: Available templates:", templates.value.map(t => t.name));
|
||||
selectedTemplate.value = templateParam;
|
||||
// Trigger template change to load items and project template
|
||||
onTemplateChange();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
notificationStore.addNotification("Failed to fetch templates", "error");
|
||||
@ -570,7 +584,13 @@ const selectAddress = async (address) => {
|
||||
const primary = contacts.value.find((c) => c.isPrimaryContact);
|
||||
console.log("DEBUG: Selected address contacts:", contacts.value);
|
||||
const existingContactName = estimate.value ? contacts.value.find((c) => c.fullName === estimate.value.partyName)?.name || "" : null;
|
||||
formData.contact = estimate.value ? existingContactName : primary ? primary.name : contacts.value[0]?.name || "";
|
||||
// Check for contact query param, then existing contact, then primary, then first contact
|
||||
if (contactQuery.value) {
|
||||
const contactFromQuery = contacts.value.find((c) => c.name === contactQuery.value);
|
||||
formData.contact = contactFromQuery ? contactFromQuery.name : (primary ? primary.name : contacts.value[0]?.name || "");
|
||||
} else {
|
||||
formData.contact = estimate.value ? existingContactName : primary ? primary.name : contacts.value[0]?.name || "";
|
||||
}
|
||||
showAddressModal.value = false;
|
||||
};
|
||||
|
||||
@ -651,6 +671,7 @@ const saveDraft = async () => {
|
||||
estimateName: formData.estimateName,
|
||||
requiresHalfPayment: formData.requiresHalfPayment,
|
||||
projectTemplate: formData.projectTemplate,
|
||||
fromMeeting: formData.fromMeeting,
|
||||
company: company.currentCompany
|
||||
};
|
||||
estimate.value = await Api.createEstimate(data);
|
||||
@ -819,6 +840,8 @@ watch(
|
||||
formData.contact = estimate.value.contactPerson;
|
||||
selectedContact.value = contacts.value.find((c) => c.name === estimate.value.contactPerson) || null;
|
||||
|
||||
formData.projectTemplate = estimate.value.customProjectTemplate || estimate.value.custom_project_template || null;
|
||||
|
||||
if (estimate.value.items && estimate.value.items.length > 0) {
|
||||
selectedItems.value = estimate.value.items.map(item => {
|
||||
const fullItem = quotationItems.value.find(qi => qi.itemCode === item.itemCode);
|
||||
@ -857,10 +880,15 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error("Error loading quotation items:", error);
|
||||
}
|
||||
fetchProjectTemplates();
|
||||
await fetchProjectTemplates();
|
||||
|
||||
if (isNew.value) {
|
||||
fetchTemplates();
|
||||
await fetchTemplates();
|
||||
|
||||
// Handle from-meeting query parameter
|
||||
if (fromMeetingQuery.value) {
|
||||
formData.fromMeeting = fromMeetingQuery.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (addressQuery.value && isNew.value) {
|
||||
@ -891,6 +919,8 @@ onMounted(async () => {
|
||||
formData.contact = estimate.value.contactPerson;
|
||||
selectedContact.value = contacts.value.find((c) => c.name === estimate.value.contactPerson) || null;
|
||||
|
||||
formData.projectTemplate = estimate.value.customProjectTemplate || estimate.value.custom_project_template || null;
|
||||
|
||||
// Populate items from the estimate
|
||||
if (estimate.value.items && estimate.value.items.length > 0) {
|
||||
selectedItems.value = estimate.value.items.map(item => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user