update
This commit is contained in:
parent
e2746b83bb
commit
1610905a43
@ -9,6 +9,101 @@ from custom_ui.services import AddressService, ContactService, ClientService
|
||||
# CLIENT MANAGEMENT API METHODS
|
||||
# ===============================================================================
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_addresses_contacts(client_name, company_name, addresses=[], contacts=[]):
|
||||
if isinstance(addresses, str):
|
||||
addresses = json.loads(addresses)
|
||||
if isinstance(contacts, str):
|
||||
contacts = json.loads(contacts)
|
||||
print(f"DEBUG: add_addresses_contacts called with client_name: {client_name}, addresses: {addresses}, contacts: {contacts}")
|
||||
try:
|
||||
client_doc = ClientService.get_client_or_throw(client_name)
|
||||
if contacts:
|
||||
contact_docs = [frappe.get_doc("Contact", contact.contact) for contact in client_doc.contacts]
|
||||
for contact in contacts:
|
||||
contact_doc = None
|
||||
if frappe.db.exists("Contact", {"email_id": contact.get("email")}):
|
||||
contact_doc = frappe.get_doc("Contact", {"email_id": contact.get("email")})
|
||||
else:
|
||||
contact_doc = ContactService.create({
|
||||
"first_name": contact.get("first_name"),
|
||||
"last_name": contact.get("last_name"),
|
||||
"email_id": contact.get("email"),
|
||||
"role": contact.get("role"),
|
||||
"phone": contact.get("phone"),
|
||||
"custom_email": contact.get("email"),
|
||||
"is_primary_contact": 0,
|
||||
"customer_type": client_doc.doctype,
|
||||
"customer_name": client_doc.name,
|
||||
"email_ids": [{
|
||||
"email": contact.get("email"),
|
||||
"is_primary": 1
|
||||
}],
|
||||
"phone_nos": [{
|
||||
"phone": contact.get("phone"),
|
||||
"is_primary_phone": 1,
|
||||
"is_primary_mobile_no": 1
|
||||
}]
|
||||
})
|
||||
contact_doc.insert()
|
||||
ClientService.append_link_v2(client_doc.name, "contacts", {"contact": contact_doc.name})
|
||||
ContactService.link_contact_to_customer(contact_doc, client_doc.doctype, client_doc.name)
|
||||
contact_docs.append(contact_doc)
|
||||
address_docs = [frappe.get_doc("Address", link.address) for link in client_doc.properties]
|
||||
for address in addresses:
|
||||
address_doc = None
|
||||
if frappe.db.exists("Address", {
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"city": address.get("city"),
|
||||
# "state": address.get("state"),
|
||||
"pincode": address.get("pincode")
|
||||
}):
|
||||
address_doc = frappe.get_doc("Address", {
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"city": address.get("city"),
|
||||
# "state": address.get("state"),
|
||||
"pincode": address.get("pincode")
|
||||
})
|
||||
else:
|
||||
address_doc = AddressService.create({
|
||||
"address_title": AddressService.build_address_title(customer_name=client_name, address_data=address),
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"city": address.get("city"),
|
||||
"state": address.get("state"),
|
||||
"pincode": address.get("pincode"),
|
||||
"country": "United States",
|
||||
"address_type": "Service",
|
||||
"custom_billing_address": 0,
|
||||
"is_primary_address": 0,
|
||||
"is_service_address": 1,
|
||||
"customer_type": client_doc.doctype,
|
||||
"customer_name": client_doc.name
|
||||
})
|
||||
address_doc.insert()
|
||||
if company_name not in [company.company for company in address_doc.companies]:
|
||||
address_doc.append("companies", {"company": company_name})
|
||||
address_doc.save(ignore_permissions=True)
|
||||
AddressService.link_address_to_customer(address_doc, client_doc.doctype, client_doc.name)
|
||||
for contact_to_link_idx in address.get("contacts", []):
|
||||
contact_doc = contact_docs[contact_to_link_idx]
|
||||
AddressService.link_address_to_contact(address_doc, contact_doc.name)
|
||||
ContactService.link_contact_to_address(contact_doc, address_doc.name)
|
||||
primary_contact = contact_docs[address.get("primary_contact", 0)]
|
||||
AddressService.set_primary_contact(address_doc.name, primary_contact.name)
|
||||
ClientService.append_link_v2(client_doc.name, "properties", {"address": address_doc.name})
|
||||
address_docs.append(address_doc)
|
||||
|
||||
return build_success_response({
|
||||
"contacts": [contact.as_dict() for contact in contact_docs],
|
||||
"addresses": [address.as_dict() for address in address_docs],
|
||||
"message": "Addresses and contacts added successfully."
|
||||
})
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_client_exists(client_name):
|
||||
"""Check if a client exists as either a Customer or a Lead.
|
||||
|
||||
@ -61,7 +61,7 @@ def get_unscheduled_service_appointments(companies):
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_service_appointment_scheduled_dates(service_appointment_name: str, start_date, end_date, crew_lead_name, start_time=None, end_time=None):
|
||||
def update_service_appointment_scheduled_dates(service_appointment_name: str, start_date, end_date, crew_lead_name, skipped_days=[], start_time=None, end_time=None):
|
||||
"""Update scheduled dates for a Service Appointment."""
|
||||
print(f"DEBUG: Updating scheduled dates for Service Appointment {service_appointment_name} to start: {start_date}, end: {end_date}, crew lead: {crew_lead_name}, start time: {start_time}, end time: {end_time}")
|
||||
try:
|
||||
@ -71,7 +71,8 @@ def update_service_appointment_scheduled_dates(service_appointment_name: str, st
|
||||
start_date,
|
||||
end_date,
|
||||
start_time,
|
||||
end_time
|
||||
end_time,
|
||||
skip_days=skipped_days
|
||||
)
|
||||
return build_success_response(updated_service_appointment.as_dict())
|
||||
except Exception as e:
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation - VS",
|
||||
"fax": null,
|
||||
"is_group": 0,
|
||||
"modified": "2026-02-15 00:56:31.933618",
|
||||
"modified": "2026-02-17 03:05:48.497419",
|
||||
"monthly_sales_target": 0.0,
|
||||
"name": "Veritas Stone",
|
||||
"old_parent": "",
|
||||
@ -149,7 +149,7 @@
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation - DL",
|
||||
"fax": null,
|
||||
"is_group": 0,
|
||||
"modified": "2026-02-15 00:56:31.935944",
|
||||
"modified": "2026-02-17 03:05:48.503038",
|
||||
"monthly_sales_target": 0.0,
|
||||
"name": "Daniels Landscape Supplies",
|
||||
"old_parent": "",
|
||||
@ -237,7 +237,7 @@
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation - SD",
|
||||
"fax": null,
|
||||
"is_group": 0,
|
||||
"modified": "2026-02-15 00:56:31.938072",
|
||||
"modified": "2026-02-17 03:05:48.505280",
|
||||
"monthly_sales_target": 0.0,
|
||||
"name": "sprinklersnorthwest (Demo)",
|
||||
"old_parent": "",
|
||||
@ -325,7 +325,7 @@
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation - NYC",
|
||||
"fax": null,
|
||||
"is_group": 0,
|
||||
"modified": "2026-02-15 00:56:31.942129",
|
||||
"modified": "2026-02-17 03:05:48.509153",
|
||||
"monthly_sales_target": 0.0,
|
||||
"name": "Nuco Yard Care",
|
||||
"old_parent": "",
|
||||
@ -413,7 +413,7 @@
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation - LF",
|
||||
"fax": null,
|
||||
"is_group": 0,
|
||||
"modified": "2026-02-15 00:56:31.940118",
|
||||
"modified": "2026-02-17 03:05:48.507129",
|
||||
"monthly_sales_target": 0.0,
|
||||
"name": "Lowe Fencing",
|
||||
"old_parent": "",
|
||||
@ -501,7 +501,7 @@
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation - S",
|
||||
"fax": null,
|
||||
"is_group": 0,
|
||||
"modified": "2026-02-15 00:56:31.946270",
|
||||
"modified": "2026-02-17 03:05:48.512257",
|
||||
"monthly_sales_target": 0.0,
|
||||
"name": "Sprinklers Northwest",
|
||||
"old_parent": "",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -33179,5 +33179,351 @@
|
||||
"track_views": 0,
|
||||
"translated_doctype": 0,
|
||||
"website_search_field": null
|
||||
},
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_last_update": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"actions": [],
|
||||
"allow_auto_repeat": 0,
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 1,
|
||||
"app": null,
|
||||
"autoname": null,
|
||||
"beta": 0,
|
||||
"color": null,
|
||||
"colour": null,
|
||||
"custom": 1,
|
||||
"default_email_template": null,
|
||||
"default_print_format": null,
|
||||
"default_view": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"documentation": null,
|
||||
"editable_grid": 1,
|
||||
"email_append_to": 0,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": "Customer",
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "customer_type",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Customer Type",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "Customer\nLead",
|
||||
"parent": "Address Customer Link",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Customer Name",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "customer_type",
|
||||
"parent": "Address Customer Link",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "relation",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Relation",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "Owner\nBuilder",
|
||||
"parent": "Address Customer Link",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_active",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Active",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": null,
|
||||
"parent": "Address Customer Link",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"force_re_route_to_default_view": 0,
|
||||
"grid_page_length": 50,
|
||||
"has_web_view": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": null,
|
||||
"image_field": null,
|
||||
"in_create": 0,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_calendar_and_gantt": 0,
|
||||
"is_published_field": null,
|
||||
"is_submittable": 0,
|
||||
"is_tree": 0,
|
||||
"is_virtual": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": null,
|
||||
"modified": "2026-02-17 08:14:21.379886",
|
||||
"module": "Custom UI",
|
||||
"name": "Address Customer Link",
|
||||
"naming_rule": "",
|
||||
"nsm_parent_field": null,
|
||||
"parent_node": null,
|
||||
"permissions": [],
|
||||
"print_outline": null,
|
||||
"protect_attached_files": 0,
|
||||
"queue_in_background": 0,
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"recipient_account_field": null,
|
||||
"restrict_to_domain": null,
|
||||
"route": null,
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"search_fields": null,
|
||||
"sender_field": null,
|
||||
"sender_name_field": null,
|
||||
"show_name_in_global_search": 0,
|
||||
"show_preview_popup": 0,
|
||||
"show_title_field_in_link": 0,
|
||||
"smallicon": null,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"subject": null,
|
||||
"subject_field": null,
|
||||
"tag_fields": null,
|
||||
"timeline_field": null,
|
||||
"title_field": null,
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0,
|
||||
"translated_doctype": 0,
|
||||
"website_search_field": null
|
||||
}
|
||||
]
|
||||
@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"bid_meeting_note_form": "SNW Install Bid Meeting Notes",
|
||||
"calendar_color": "#CB2929",
|
||||
"calendar_color": "#c1dec5",
|
||||
"company": "Sprinklers Northwest",
|
||||
"custom__complete_method": "Task Weight",
|
||||
"docstatus": 0,
|
||||
"doctype": "Project Template",
|
||||
"item_groups": "SNW-I, SNW-S, SNW-LS",
|
||||
"modified": "2026-02-15 01:31:39.325004",
|
||||
"modified": "2026-02-16 03:59:53.719382",
|
||||
"name": "SNW Install",
|
||||
"project_type": "External",
|
||||
"tasks": [
|
||||
|
||||
@ -95,22 +95,6 @@
|
||||
"row_name": null,
|
||||
"value": "[\"weight\", \"description\", \"base_date\", \"offset_days\", \"skip_weekends\", \"skip_holidays\", \"logic_key\", \"offset_direction\", \"title\", \"days\", \"calculate_from\", \"trigger\", \"task_type_calculate_from\", \"work_type\", \"no_due_date\", \"triggering_doctype\", \"custom_completion_trigger\", \"custom_completion_trigger_doctype\", \"target_percent\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Contact",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocField",
|
||||
"field_name": "email_id",
|
||||
"is_system_generated": 0,
|
||||
"modified": "2025-01-06 14:08:26.445944",
|
||||
"module": null,
|
||||
"name": "Contact-email_id-options",
|
||||
"property": "options",
|
||||
"property_type": "Text",
|
||||
"row_name": null,
|
||||
"value": "email_id"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Pre-Built Routes",
|
||||
@ -10415,22 +10399,6 @@
|
||||
"row_name": null,
|
||||
"value": "None"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Customer",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2025-02-05 06:03:40.318065",
|
||||
"module": null,
|
||||
"name": "Customer-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"basic_info\", \"naming_series\", \"salutation\", \"customer_name\", \"customer_type\", \"customer_group\", \"custom_appointment_date\", \"column_break0\", \"territory\", \"gender\", \"lead_name\", \"opportunity_name\", \"account_manager\", \"image\", \"defaults_tab\", \"default_currency\", \"default_bank_account\", \"column_break_14\", \"default_price_list\", \"custom_previous_year_price\", \"internal_customer_section\", \"is_internal_customer\", \"represents_company\", \"column_break_70\", \"companies\", \"more_info\", \"market_segment\", \"industry\", \"customer_pos_id\", \"website\", \"language\", \"column_break_45\", \"customer_details\", \"contact_and_address_tab\", \"address_contacts\", \"address_html\", \"column_break1\", \"contact_html\", \"custom_related_addresses\", \"custom_select_address\", \"custom_associated_contacts\", \"custom_add_contacts\", \"custom_primary_billing_and_contact_details\", \"custom_billing_address\", \"custom_column_break_q0puw\", \"custom_billing_contact\", \"primary_address_and_contact_detail\", \"column_break_26\", \"primary_address\", \"customer_primary_address\", \"column_break_nwor\", \"customer_primary_contact\", \"mobile_no\", \"email_id\", \"tax_tab\", \"taxation_section\", \"tax_id\", \"column_break_21\", \"tax_category\", \"tax_withholding_category\", \"accounting_tab\", \"credit_limit_section\", \"payment_terms\", \"credit_limits\", \"default_receivable_accounts\", \"accounts\", \"loyalty_points_tab\", \"loyalty_program\", \"column_break_54\", \"loyalty_program_tier\", \"sales_team_tab\", \"sales_team\", \"sales_team_section\", \"default_sales_partner\", \"column_break_66\", \"default_commission_rate\", \"settings_tab\", \"so_required\", \"dn_required\", \"exempt_from_sales_tax\", \"column_break_53\", \"is_frozen\", \"disabled\", \"portal_users_tab\", \"portal_users\", \"dashboard_tab\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "SMS Log",
|
||||
@ -12751,22 +12719,6 @@
|
||||
"row_name": null,
|
||||
"value": "format:{full_address)-#-{MM}-{YYYY}-{####}"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-01-21 03:31:53.827100",
|
||||
"module": null,
|
||||
"name": "Address-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"address_details\", \"custom_column_break_vqa4d\", \"custom_column_break_jw2ty\", \"custom_installationservice_address\", \"custom_billing_address\", \"is_shipping_address\", \"is_primary_address\", \"custom_is_compnay_address\", \"custom_column_break_ky1zo\", \"custom_estimate_sent_status\", \"custom_onsite_meeting_scheduled\", \"custom_job_status\", \"custom_payment_received_status\", \"custom_section_break_fvgdt\", \"address_title\", \"primary_contact\", \"address_type\", \"address_line1\", \"address_line2\", \"custom_linked_city\", \"custom_subdivision\", \"is_your_company_address\", \"custom_column_break_3mo7x\", \"state\", \"city\", \"pincode\", \"county\", \"country\", \"full_address\", \"latitude\", \"longitude\", \"onsite_meeting_scheduled\", \"estimate_sent_status\", \"job_status\", \"payment_received_status\", \"custom_column_break_rrto0\", \"custom_customer_to_bill\", \"lead_name\", \"customer_type\", \"customer_name\", \"contacts\", \"companies\", \"quotations\", \"onsite_meetings\", \"projects\", \"sales_orders\", \"tasks\", \"custom_contact_name\", \"phone\", \"email_id\", \"fax\", \"tax_category\", \"disabled\", \"custom_section_break_aecpx\", \"column_break0\", \"custom_show_irrigation_district\", \"custom_google_map\", \"custom_latitude\", \"custom_longitude\", \"custom_address_for_coordinates\", \"linked_with\", \"custom_linked_contacts\", \"links\", \"custom_column_break_9cbvb\", \"custom_linked_companies\", \"custom_irrigation\", \"custom_upcoming_services\", \"custom_service_type\", \"custom_service_route\", \"custom_confirmation_status\", \"custom_backflow_test_form_filed\", \"custom_column_break_j79td\", \"custom_technician_assigned\", \"custom_scheduled_date\", \"custom_column_break_sqplk\", \"custom_test_route\", \"custom_tech\", \"custom_column_break_wcs7g\", \"custom_section_break_zruvq\", \"custom_irrigation_district\", \"custom_serial_\", \"custom_makemodel_\", \"custom_column_break_djjw3\", \"custom_backflow_location\", \"custom_shutoff_location\", \"custom_valve_boxes\", \"custom_timer_type_and_location\", \"custom_column_break_slusf\", \"custom_section_break_5d1cf\", \"custom_installed_by_sprinklers_nw\", \"custom_column_break_th7rq\", \"custom_installed_for\", \"custom_install_month\", \"custom_install_year\", \"custom_column_break_4itse\", \"custom_section_break_xfdtv\", \"custom_backflow_test_report\", \"custom_column_break_oxppn\", \"custom_photo_attachment\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Project Template",
|
||||
@ -15119,38 +15071,6 @@
|
||||
"row_name": null,
|
||||
"value": "format:{custom_customer_name}-#-{YYYY}-{MM}-{####}"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-01-26 02:35:09.522811",
|
||||
"module": null,
|
||||
"name": "Address-main-links_order",
|
||||
"property": "links_order",
|
||||
"property_type": "Small Text",
|
||||
"row_name": null,
|
||||
"value": "[\"21ddd8462e\", \"c26b89d0d3\", \"ee207f2316\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-01-26 02:35:09.598292",
|
||||
"module": null,
|
||||
"name": "Address-main-states_order",
|
||||
"property": "states_order",
|
||||
"property_type": "Small Text",
|
||||
"row_name": null,
|
||||
"value": "[\"62m56h85vo\", \"62m5uugrvr\", \"62m57bgpkf\", \"62m5fgrjb0\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Contact",
|
||||
@ -15167,22 +15087,6 @@
|
||||
"row_name": null,
|
||||
"value": "Expression"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Contact",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-01-26 02:40:01.427255",
|
||||
"module": null,
|
||||
"name": "Contact-main-autoname",
|
||||
"property": "autoname",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "format:{full-name}-#-{MM}-{YYYY}-{####}"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Contact",
|
||||
@ -15262,5 +15166,117 @@
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"custom_column_break_k7sgq\", \"custom_installation_address\", \"naming_series\", \"project_name\", \"job_address\", \"status\", \"custom_warranty_duration_days\", \"custom_warranty_expiration_date\", \"custom_warranty_information\", \"project_type\", \"percent_complete_method\", \"percent_complete\", \"column_break_5\", \"project_template\", \"expected_start_date\", \"expected_start_time\", \"expected_end_date\", \"expected_end_time\", \"requires_half_payment\", \"is_half_down_paid\", \"is_scheduled\", \"invoice_status\", \"custom_completion_date\", \"priority\", \"custom_foreman\", \"custom_hidden_fields\", \"department\", \"service_appointment\", \"tasks\", \"ready_to_schedule\", \"is_active\", \"custom_address\", \"custom_section_break_lgkpd\", \"custom_workflow_related_custom_fields__landry\", \"custom_permit_status\", \"custom_utlity_locate_status\", \"custom_crew_scheduling\", \"customer_details\", \"customer\", \"column_break_14\", \"sales_order\", \"users_section\", \"users\", \"copied_from\", \"section_break0\", \"notes\", \"section_break_18\", \"actual_start_date\", \"actual_start_time\", \"actual_time\", \"column_break_20\", \"actual_end_date\", \"actual_end_time\", \"project_details\", \"estimated_costing\", \"total_costing_amount\", \"total_expense_claim\", \"total_purchase_cost\", \"company\", \"column_break_28\", \"total_sales_amount\", \"total_billable_amount\", \"total_billed_amount\", \"total_consumed_material_cost\", \"cost_center\", \"margin\", \"gross_margin\", \"column_break_37\", \"per_gross_margin\", \"monitor_progress\", \"collect_progress\", \"holiday_list\", \"frequency\", \"from_time\", \"to_time\", \"first_email\", \"second_email\", \"daily_time_to_send\", \"day_to_send\", \"weekly_time_to_send\", \"column_break_45\", \"subject\", \"message\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Customer",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 03:51:08.179106",
|
||||
"module": null,
|
||||
"name": "Customer-main-naming_rule",
|
||||
"property": "naming_rule",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "Expression"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Customer",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 03:51:08.313125",
|
||||
"module": null,
|
||||
"name": "Customer-main-autoname",
|
||||
"property": "autoname",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "format:{customer_name}-CUST-{MM}-{YYYY}-{#####}"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Customer",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 03:51:08.370590",
|
||||
"module": null,
|
||||
"name": "Customer-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"basic_info\", \"naming_series\", \"salutation\", \"customer_name\", \"from_lead\", \"properties\", \"contacts\", \"primary_contact\", \"customer_type\", \"customer_group\", \"custom_appointment_date\", \"column_break0\", \"territory\", \"gender\", \"lead_name\", \"opportunity_name\", \"prospect_name\", \"account_manager\", \"image\", \"defaults_tab\", \"default_currency\", \"default_bank_account\", \"column_break_14\", \"default_price_list\", \"custom_previous_year_price\", \"internal_customer_section\", \"is_internal_customer\", \"represents_company\", \"column_break_70\", \"companies\", \"quotations\", \"onsite_meetings\", \"projects\", \"tasks\", \"sales_orders\", \"more_info\", \"market_segment\", \"industry\", \"customer_pos_id\", \"website\", \"language\", \"column_break_45\", \"customer_details\", \"contact_and_address_tab\", \"address_contacts\", \"address_html\", \"column_break1\", \"contact_html\", \"custom_related_addresses\", \"custom_select_address\", \"custom_associated_contacts\", \"custom_add_contacts\", \"custom_primary_billing_and_contact_details\", \"custom_billing_address\", \"custom_column_break_q0puw\", \"custom_billing_contact\", \"primary_address_and_contact_detail\", \"column_break_26\", \"primary_address\", \"customer_primary_address\", \"column_break_nwor\", \"customer_primary_contact\", \"mobile_no\", \"email_id\", \"first_name\", \"last_name\", \"tax_tab\", \"taxation_section\", \"tax_id\", \"column_break_21\", \"tax_category\", \"tax_withholding_category\", \"accounting_tab\", \"credit_limit_section\", \"payment_terms\", \"credit_limits\", \"default_receivable_accounts\", \"accounts\", \"loyalty_points_tab\", \"loyalty_program\", \"column_break_54\", \"loyalty_program_tier\", \"sales_team_tab\", \"sales_team\", \"sales_team_section\", \"default_sales_partner\", \"column_break_66\", \"default_commission_rate\", \"settings_tab\", \"so_required\", \"dn_required\", \"exempt_from_sales_tax\", \"column_break_53\", \"is_frozen\", \"disabled\", \"portal_users_tab\", \"portal_users\", \"dashboard_tab\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Contact",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 03:53:14.660505",
|
||||
"module": null,
|
||||
"name": "Contact-main-autoname",
|
||||
"property": "autoname",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "format:{full_name}-CONT-{MM}-{YYYY}-{####}"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 08:16:54.459462",
|
||||
"module": null,
|
||||
"name": "Address-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"address_details\", \"custom_column_break_vqa4d\", \"custom_column_break_jw2ty\", \"custom_installationservice_address\", \"custom_billing_address\", \"is_shipping_address\", \"is_primary_address\", \"custom_is_compnay_address\", \"custom_column_break_ky1zo\", \"custom_estimate_sent_status\", \"custom_onsite_meeting_scheduled\", \"custom_job_status\", \"custom_payment_received_status\", \"custom_section_break_fvgdt\", \"address_title\", \"primary_contact\", \"address_type\", \"address_line1\", \"address_line2\", \"custom_linked_city\", \"custom_subdivision\", \"is_your_company_address\", \"custom_column_break_3mo7x\", \"state\", \"city\", \"pincode\", \"county\", \"country\", \"full_address\", \"latitude\", \"longitude\", \"onsite_meeting_scheduled\", \"estimate_sent_status\", \"job_status\", \"payment_received_status\", \"custom_column_break_rrto0\", \"custom_customer_to_bill\", \"lead_name\", \"customer_type\", \"customer_name\", \"contacts\", \"companies\", \"quotations\", \"onsite_meetings\", \"projects\", \"sales_orders\", \"tasks\", \"custom_contact_name\", \"phone\", \"email_id\", \"fax\", \"tax_category\", \"disabled\", \"is_service_address\", \"customers\", \"custom_section_break_aecpx\", \"column_break0\", \"custom_show_irrigation_district\", \"custom_google_map\", \"custom_latitude\", \"custom_longitude\", \"custom_address_for_coordinates\", \"linked_with\", \"custom_linked_contacts\", \"links\", \"custom_column_break_9cbvb\", \"custom_linked_companies\", \"custom_irrigation\", \"custom_upcoming_services\", \"custom_service_type\", \"custom_service_route\", \"custom_confirmation_status\", \"custom_backflow_test_form_filed\", \"custom_column_break_j79td\", \"custom_technician_assigned\", \"custom_scheduled_date\", \"custom_column_break_sqplk\", \"custom_test_route\", \"custom_tech\", \"custom_column_break_wcs7g\", \"custom_section_break_zruvq\", \"custom_irrigation_district\", \"custom_serial_\", \"custom_makemodel_\", \"custom_column_break_djjw3\", \"custom_backflow_location\", \"custom_shutoff_location\", \"custom_valve_boxes\", \"custom_timer_type_and_location\", \"custom_column_break_slusf\", \"custom_section_break_5d1cf\", \"custom_installed_by_sprinklers_nw\", \"custom_column_break_th7rq\", \"custom_installed_for\", \"custom_install_month\", \"custom_install_year\", \"custom_column_break_4itse\", \"custom_section_break_xfdtv\", \"custom_backflow_test_report\", \"custom_column_break_oxppn\", \"custom_photo_attachment\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 08:16:54.562064",
|
||||
"module": null,
|
||||
"name": "Address-main-links_order",
|
||||
"property": "links_order",
|
||||
"property_type": "Small Text",
|
||||
"row_name": null,
|
||||
"value": "[\"21ddd8462e\", \"c26b89d0d3\", \"ee207f2316\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-17 08:16:54.619557",
|
||||
"module": null,
|
||||
"name": "Address-main-states_order",
|
||||
"property": "states_order",
|
||||
"property_type": "Small Text",
|
||||
"row_name": null,
|
||||
"value": "[\"62m56h85vo\", \"62m5uugrvr\", \"62m57bgpkf\", \"62m5fgrjb0\"]"
|
||||
}
|
||||
]
|
||||
@ -324,31 +324,6 @@
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Permits required prior to installation start.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-02-08 01:48:15.012387",
|
||||
"name": "Permit",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Permit",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 7.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
@ -424,6 +399,31 @@
|
||||
"weight": 25.0,
|
||||
"work_type": "Labor"
|
||||
},
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Permits required prior to installation start.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-02-08 01:48:15.012387",
|
||||
"name": "Permit",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Permit",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 7.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
|
||||
20
custom_ui/services/db_restore_service.py
Normal file
20
custom_ui/services/db_restore_service.py
Normal file
@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
|
||||
class DBRestoreService:
|
||||
@staticmethod
|
||||
def massage_customer_address_contact_links():
|
||||
"""Fixes the links between Customer, Address, and Contacts from legacy data that may have been imported without proper linking."""
|
||||
# use emojis in print statments to make it more fun and visually distinct in the logs
|
||||
print("DEBUG: 🛠️ Starting to massage customer, address, and contact links")
|
||||
all_addresses = frappe.get_all("Address", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_addresses)} addresses to process")
|
||||
all_customers = frappe.get_all("Customer", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_customers)} customers to process")
|
||||
all_leads = frappe.get_all("Lead", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_leads)} leads to process")
|
||||
all_contacts = frappe.get_all("Contact", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_contacts)} contacts to process")
|
||||
|
||||
# query all customer doctypes that don't have an empty array for custom_select_address. This field is a child table so get_all cannot be used. We need to use a custom sql query
|
||||
# the child table is a doctype called "Custom "
|
||||
# print(f"DEBUG: Found {len(customers_with_addresses)} customers with addresses to process")
|
||||
@ -24,6 +24,7 @@ class ServiceAppointmentService:
|
||||
service_appointment["service_address"] = AddressService.get_or_throw(service_appointment["service_address"]).as_dict()
|
||||
service_appointment["customer"] = ClientService.get_client_or_throw(service_appointment["customer"]).as_dict()
|
||||
service_appointment["project"] = DbService.get_or_throw("Project", service_appointment["project"]).as_dict()
|
||||
service_appointment["color"] = frappe.get_value("Project Template", service_appointment["project_template"], "calendar_color")
|
||||
|
||||
return service_appointment
|
||||
|
||||
@ -43,20 +44,21 @@ class ServiceAppointmentService:
|
||||
if end_time:
|
||||
service_appointment.expected_end_time = end_time
|
||||
if skip_days:
|
||||
current_skip_days
|
||||
print(f"DEBUG: Updating skip days for Service Appointment {service_appointment_name}. Current skip days: {[skip_day.date for skip_day in service_appointment.skip_days]}, New skip days: {skip_days}")
|
||||
# Compare skip_days with the current skip_days and remove/add as needed
|
||||
current_skip_days = set([skip_day.date for skip_day in service_appointment.skip_days])
|
||||
new_skip_days = set(skip_days)
|
||||
current_skip_days = [skip_day.date for skip_day in service_appointment.skip_days]
|
||||
# Remove skip days that are no longer needed
|
||||
for skip_day in current_skip_days - new_skip_days:
|
||||
skip_day_doc = service_appointment.skip_days.find(lambda d: d.date == skip_day)
|
||||
if skip_day_doc:
|
||||
service_appointment.skip_days.remove(skip_day_doc.name)
|
||||
print(f"DEBUG: Removed skip day {skip_day} from Service Appointment {service_appointment_name}")
|
||||
for skip_day in current_skip_days:
|
||||
if skip_day not in skip_days:
|
||||
skip_day_doc = service_appointment.skip_days.find(lambda d: d.date == skip_day)
|
||||
if skip_day_doc:
|
||||
service_appointment.skip_days.remove(skip_day_doc.name)
|
||||
print(f"DEBUG: Removed skip day {skip_day} from Service Appointment {service_appointment_name}")
|
||||
# Add new skip days
|
||||
for skip_day in new_skip_days - current_skip_days:
|
||||
service_appointment.append("skip_days", {"date": skip_day})
|
||||
print(f"DEBUG: Added new skip day {skip_day} for Service Appointment {service_appointment_name}")
|
||||
for skip_day in skip_days:
|
||||
if skip_day not in current_skip_days:
|
||||
service_appointment.append("skip_days", {"date": skip_day["date"]})
|
||||
print(f"DEBUG: Added new skip day {skip_day} for Service Appointment {service_appointment_name}")
|
||||
service_appointment.save()
|
||||
print(f"DEBUG: Updated scheduled dates for Service Appointment {service_appointment_name}")
|
||||
return service_appointment
|
||||
|
||||
@ -68,6 +68,7 @@ const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_cli
|
||||
const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2";
|
||||
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
||||
const FRAPPE_CHECK_CLIENT_EXISTS_METHOD = "custom_ui.api.db.clients.check_client_exists";
|
||||
const FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD = "custom_ui.api.db.clients.add_addresses_contacts";
|
||||
// Employee methods
|
||||
const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
|
||||
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
|
||||
@ -81,7 +82,7 @@ const FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD = "custom_ui.api.
|
||||
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_status";
|
||||
class Api {
|
||||
// ============================================================================
|
||||
// CORE REQUEST METHOPD
|
||||
// CORE REQUEST METHOD
|
||||
// ============================================================================
|
||||
|
||||
static async request(frappeMethod, args = {}) {
|
||||
@ -183,6 +184,10 @@ class Api {
|
||||
return result;
|
||||
}
|
||||
|
||||
static async addAddressesAndContacts(clientName, companyName, addresses = [], contacts = []) {
|
||||
return await this.request(FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD, { clientName, companyName, addresses, contacts });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ON-SITE MEETING METHODS
|
||||
// ============================================================================
|
||||
|
||||
@ -57,7 +57,9 @@
|
||||
<v-card-title class="text-subtitle-1 py-2">
|
||||
Select Project Templates
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
if (isHoliday(date)) {
|
||||
classes.push('holiday');
|
||||
}
|
||||
<v-card-text class="pa-2">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="toggleAllTemplates">
|
||||
@ -203,7 +205,7 @@
|
||||
<div class="calendar-section">
|
||||
<div class="weekly-calendar">
|
||||
<!-- Days Header -->
|
||||
<div class="calendar-header-row" :style="{ gridTemplateColumns: `150px repeat(7, 1fr)` }">
|
||||
<div class="calendar-header-row" :style="{ gridTemplateColumns: `150px repeat(7, 140px)` }">
|
||||
<div class="crew-column-header">Crew</div>
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
@ -218,7 +220,7 @@
|
||||
|
||||
<!-- Foremen Grid -->
|
||||
<div class="calendar-grid">
|
||||
<div v-for="foreman in visibleForemen" :key="foreman.name" class="foreman-row" :style="{ gridTemplateColumns: `150px repeat(7, 1fr)` }">
|
||||
<div v-for="foreman in visibleForemen" :key="foreman.name" class="foreman-row" :style="{ gridTemplateColumns: `150px repeat(7, 140px)` }">
|
||||
<!-- Foreman/Crew Column -->
|
||||
<div class="crew-column">
|
||||
<div class="crew-name">{{ foreman.employeeName }}</div>
|
||||
@ -230,67 +232,65 @@
|
||||
v-for="day in weekDays"
|
||||
:key="`${foreman.name}-${day.date}`"
|
||||
class="day-cell"
|
||||
:class="{
|
||||
'today': isToday(day.date),
|
||||
'holiday': isHoliday(day.date),
|
||||
'sunday': isSunday(day.date),
|
||||
'drag-over': isDragOver && dragOverCell?.foremanId === foreman.name && dragOverCell?.date === day.date,
|
||||
'has-skipped-jobs': getSkippedJobsForCell(foreman.name, day.date).length > 0,
|
||||
}"
|
||||
:class="getCellStyling(foreman.name, day.date).classes"
|
||||
:style="Object.assign({}, getCellStyling(foreman.name, day.date).data.backgroundColor ? { background: getCellStyling(foreman.name, day.date).data.backgroundColor } : {}, getCellStyling(foreman.name, day.date).data.rightBorder ? { borderRight: '2px solid #333' } : {})"
|
||||
:draggable="getCellStyling(foreman.name, day.date).data.jobs.length > 0"
|
||||
@dragstart="handleCellDragStart($event, foreman.name, day.date)"
|
||||
@dragover="handleDragOver($event, foreman.name, day.date)"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop($event, foreman.name, day.date)"
|
||||
@click="skipMode ? handleSkipDayClick(foreman.name, day.date) : null"
|
||||
@click="skipMode ? handleSkipDayClick(foreman.name, day.date) : handleCellClick(foreman.name, day.date, $event)"
|
||||
@mousedown="(event) => handleCellMouseDown(event, foreman.name, day.date)"
|
||||
>
|
||||
<!-- Jobs in this day -->
|
||||
<div
|
||||
v-for="job in getJobsForCell(foreman.name, day.date)"
|
||||
:key="job.name"
|
||||
class="calendar-job"
|
||||
:style="getJobStyle(job, day.date)"
|
||||
:draggable="job.status === 'Scheduled'"
|
||||
@click.stop="skipMode ? handleSkipDayClick(foreman.name, day.date, job) : showEventDetails({ event: job })"
|
||||
@dragstart="job.status === 'Scheduled' ? handleDragStart(job, $event) : null"
|
||||
@dragend="handleDragEnd"
|
||||
@mousedown="(job.status === 'Scheduled' || job.status === 'Started') ? startResize($event, job, day.date) : null"
|
||||
@drop.stop.prevent="handleScheduledDrop(job, $event, foreman.name, day.date)"
|
||||
@dragover.stop.prevent="handleScheduledDragOver(job, $event, foreman.name, day.date)"
|
||||
>
|
||||
<v-icon v-if="jobStartsBeforeWeek(job)" size="small" class="spans-arrow-left">mdi-arrow-left</v-icon>
|
||||
<div class="job-content">
|
||||
<div class="job-title">{{ job.projectTemplate }}</div>
|
||||
<div class="job-address">{{ job.serviceAddress.fullAddress }}</div>
|
||||
</div>
|
||||
<v-icon v-if="jobSpansToNextWeek(job)" size="small" class="spans-arrow-right">mdi-arrow-right</v-icon>
|
||||
<div class="resize-handle"></div>
|
||||
</div>
|
||||
|
||||
<!-- Skipped days -->
|
||||
<div
|
||||
v-for="job in getSkippedJobsForCell(foreman.name, day.date)"
|
||||
:key="`skip-${job.name}`"
|
||||
class="skipped-day"
|
||||
:class="getPriorityClass(job.priority)"
|
||||
>
|
||||
<span>Skipped</span>
|
||||
<button
|
||||
class="remove-skip-btn"
|
||||
@click.stop="handleRemoveSkip(job, day.date)"
|
||||
title="Remove skipped day"
|
||||
>
|
||||
<v-icon size="small">mdi-close</v-icon>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Job content for scheduled cells -->
|
||||
<template v-if="getCellStyling(foreman.name, day.date).data.jobs.length > 0 && !getCellStyling(foreman.name, day.date).data.isSkipped">
|
||||
<div class="job-content">
|
||||
<!-- Start icon/component -->
|
||||
<span v-if="getCellStyling(foreman.name, day.date).data.showStartIcon" class="job-start-icon">
|
||||
<v-icon size="small">mdi-play-circle</v-icon>
|
||||
</span>
|
||||
<!-- End icon/component -->
|
||||
<span v-if="getCellStyling(foreman.name, day.date).data.showEndIcon" class="job-end-icon">
|
||||
<v-icon size="small">mdi-stop-circle</v-icon>
|
||||
</span>
|
||||
<!-- Address info only if showAddress -->
|
||||
<div v-if="getCellStyling(foreman.name, day.date).data.showAddress" class="job-address">
|
||||
{{ getCellStyling(foreman.name, day.date).data.jobs[0].serviceAddress.fullAddress }}
|
||||
</div>
|
||||
<div class="job-title">{{ getCellStyling(foreman.name, day.date).data.jobs[0].projectTemplate }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Resize handle for job end cells -->
|
||||
<div
|
||||
v-if="getCellStyling(foreman.name, day.date).data.showResizeBar"
|
||||
class="resize-handle"
|
||||
@mousedown.stop="(event) => startResize(event, getCellStyling(foreman.name, day.date).data.jobs[0], day.date)"
|
||||
></div>
|
||||
|
||||
<!-- Arrows for jobs spanning weeks -->
|
||||
<v-icon
|
||||
v-if="jobStartsBeforeWeek(getCellStyling(foreman.name, day.date).data.jobs[0])"
|
||||
size="small"
|
||||
class="spans-arrow-left"
|
||||
>mdi-arrow-left</v-icon>
|
||||
<v-icon
|
||||
v-if="jobSpansToNextWeek(getCellStyling(foreman.name, day.date).data.jobs[0])"
|
||||
size="small"
|
||||
class="spans-arrow-right"
|
||||
>mdi-arrow-right</v-icon>
|
||||
</template>
|
||||
|
||||
<!-- Holiday connector line for split jobs -->
|
||||
<template v-if="isHoliday(day.date)">
|
||||
<div
|
||||
v-for="job in getJobsWithConnector(foreman.name, day.date)"
|
||||
:key="`connector-${job.name}`"
|
||||
class="holiday-connector"
|
||||
:class="getPriorityClass(job.priority)"
|
||||
></div>
|
||||
</template>
|
||||
<!-- Skipped day content -->
|
||||
<template v-if="getCellStyling(foreman.name, day.date).data.isSkipped">
|
||||
<span>Skipped</span>
|
||||
<button
|
||||
class="remove-skip-btn"
|
||||
@click.stop="handleRemoveSkip(getCellStyling(foreman.name, day.date).data.skipJob, day.date)"
|
||||
title="Remove skipped day"
|
||||
>
|
||||
<v-icon size="small">mdi-close</v-icon>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -716,24 +716,120 @@ const formatDuration = (minutes) => {
|
||||
return `${hours}h ${mins}m`;
|
||||
};
|
||||
|
||||
// Get jobs for a specific foreman and date
|
||||
const getJobsForCell = (foremanId, date) => {
|
||||
// Don't render jobs on Sunday or holidays
|
||||
if (isSunday(date) || isHoliday(date)) return [];
|
||||
// Get cell styling classes and data for a specific foreman and date
|
||||
const getCellStyling = (foreman, date) => {
|
||||
const classes = [];
|
||||
const data = { jobs: [], isSkipped: false, skipJob: null };
|
||||
|
||||
return scheduledServices.value.filter((job) => {
|
||||
if (job.foreman !== foremanId) return false;
|
||||
|
||||
const jobStart = job.expectedStartDate;
|
||||
const jobEnd = job.expectedEndDate || job.expectedStartDate;
|
||||
|
||||
// Check if this date falls within the job's date range
|
||||
// AND that it's a valid segment start date
|
||||
// AND not in skipDays
|
||||
const segments = getJobSegments(job);
|
||||
const isSkipped = job.skipDays && job.skipDays.some(skip => skip.date === date);
|
||||
return segments.some(seg => seg.start === date) && !isSkipped;
|
||||
// Check if holiday
|
||||
if (isHoliday(date)) {
|
||||
classes.push('holiday');
|
||||
}
|
||||
|
||||
// Check if Sunday
|
||||
if (isSunday(date)) {
|
||||
classes.push('sunday');
|
||||
}
|
||||
|
||||
// Check if today
|
||||
if (isToday(date)) {
|
||||
classes.push('today');
|
||||
}
|
||||
|
||||
// Get jobs for this cell
|
||||
const jobs = scheduledServices.value.filter(job => {
|
||||
if (job.foreman !== foreman) return false;
|
||||
const start = job.expectedStartDate;
|
||||
const end = job.expectedEndDate || start;
|
||||
return date >= start && date <= end;
|
||||
});
|
||||
data.jobs = jobs;
|
||||
|
||||
// Check for skipped days
|
||||
const skippedJobs = jobs.filter(job => {
|
||||
return job.skipDays && job.skipDays.some(skip => skip.date === date);
|
||||
});
|
||||
if (skippedJobs.length > 0) {
|
||||
data.isSkipped = true;
|
||||
data.skipJob = skippedJobs[0]; // Take first one
|
||||
classes.push('skipped');
|
||||
// Add background color for skipped cell
|
||||
data.backgroundColor = (skippedJobs[0].color || '#2196f3') + '40';
|
||||
return { classes, data };
|
||||
}
|
||||
|
||||
// If no jobs, return basic styling
|
||||
if (jobs.length === 0) {
|
||||
classes.push('empty');
|
||||
return { classes, data };
|
||||
}
|
||||
|
||||
// Cell is part of a scheduled job - determine styling
|
||||
const jobForStyling = jobs[0];
|
||||
const startDate = jobForStyling.expectedStartDate;
|
||||
const endDate = jobForStyling.expectedEndDate || startDate;
|
||||
const isStart = date === startDate;
|
||||
const isEnd = date === endDate;
|
||||
|
||||
// Add priority class
|
||||
classes.push(getPriorityClass(jobForStyling.priority));
|
||||
|
||||
// Add job color for all scheduled days
|
||||
data.backgroundColor = jobForStyling.color || '#2196f3';
|
||||
|
||||
// Border styling for start/end
|
||||
if (isStart) {
|
||||
classes.push('job-start');
|
||||
data.showStartIcon = true;
|
||||
data.showAddress = true;
|
||||
} else {
|
||||
classes.push('job-continuation');
|
||||
|
||||
// Check if previous day was skipped
|
||||
const prevDate = addDays(date, -1);
|
||||
const prevSkipped = jobForStyling.skipDays && jobForStyling.skipDays.some(skip => skip.date === prevDate);
|
||||
if (prevSkipped) {
|
||||
classes.push('after-skip');
|
||||
}
|
||||
|
||||
// Check if previous dates are on previous week
|
||||
const weekStart = parseLocalDate(weekStartDate.value);
|
||||
const cellDate = parseLocalDate(date);
|
||||
if (cellDate.getDay() === 1 && cellDate.getTime() > weekStart.getTime()) { // Monday and not first Monday
|
||||
classes.push('after-week-break');
|
||||
}
|
||||
}
|
||||
|
||||
// Right side styling
|
||||
if (isEnd) {
|
||||
classes.push('job-end');
|
||||
data.showEndIcon = true;
|
||||
// Only add right border for end cell
|
||||
data.rightBorder = true;
|
||||
} else {
|
||||
data.rightBorder = false;
|
||||
}
|
||||
|
||||
// Add job-end class on every update
|
||||
classes.push('job-end');
|
||||
|
||||
// Check if next day is skipped
|
||||
const nextDate = addDays(date, 1);
|
||||
const nextSkipped = jobForStyling.skipDays && jobForStyling.skipDays.some(skip => skip.date === nextDate);
|
||||
if (nextSkipped) {
|
||||
classes.push('before-skip');
|
||||
}
|
||||
|
||||
// Check if job continues to next week
|
||||
const weekEnd = parseLocalDate(addDays(weekStartDate.value, 6));
|
||||
const endDateObj = parseLocalDate(endDate);
|
||||
if (endDateObj > weekEnd) {
|
||||
classes.push('continues-next-week');
|
||||
}
|
||||
|
||||
// Only show resize bar on end cell
|
||||
data.showResizeBar = isEnd;
|
||||
return { classes, data };
|
||||
};
|
||||
|
||||
// Get skipped jobs for a specific foreman and date
|
||||
@ -788,15 +884,17 @@ const getJobStyle = (job, currentDate) => {
|
||||
? 'calc(100% - 8px)' // Single day: full width minus padding
|
||||
: `calc(${visualDays * 100}% + ${(visualDays - 1)}px)`; // Multi-day: span cells accounting for borders
|
||||
|
||||
// Get color from project template
|
||||
let backgroundColor = '#2196f3'; // Default color
|
||||
if (job.projectTemplate) {
|
||||
const template = projectTemplates.value.find(t => t.name === job.projectTemplate);
|
||||
if (template && template.calendarColor) {
|
||||
backgroundColor = template.calendarColor;
|
||||
}
|
||||
// Use job.color for cell background if available
|
||||
let backgroundColor = job.color || '#2196f3';
|
||||
// Use higher opacity for skip days
|
||||
if (job.skipDays && job.skipDays.some(skip => skip.date === currentDate)) {
|
||||
backgroundColor = backgroundColor.replace(/\)$/,", 0.25)") || backgroundColor + '40';
|
||||
}
|
||||
const darkerColor = darkenColor(backgroundColor, 20);
|
||||
return {
|
||||
width: widthCalc,
|
||||
zIndex: 10,
|
||||
background: backgroundColor
|
||||
};
|
||||
|
||||
return {
|
||||
width: widthCalc,
|
||||
@ -909,14 +1007,40 @@ const onDateSelected = (date) => {
|
||||
}
|
||||
};
|
||||
|
||||
const showEventDetails = (event) => {
|
||||
// Don't open modal if we just finished resizing
|
||||
if (justFinishedResize.value) {
|
||||
justFinishedResize.value = false;
|
||||
return;
|
||||
const handleCellDragStart = (event, foremanId, date) => {
|
||||
const cellData = getCellStyling(foremanId, date);
|
||||
if (cellData.data.jobs.length > 0 && !cellData.data.isSkipped) {
|
||||
const job = cellData.data.jobs[0];
|
||||
if (job.status === 'Scheduled') {
|
||||
handleDragStart(job, event);
|
||||
} else {
|
||||
// Prevent dragging if not scheduled
|
||||
event.preventDefault();
|
||||
}
|
||||
} else {
|
||||
// Prevent dragging skipped cells or empty cells
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCellMouseDown = (event, foremanId, date) => {
|
||||
const cellData = getCellStyling(foremanId, date);
|
||||
if (cellData.data.jobs.length > 0 && !cellData.data.isSkipped) {
|
||||
const job = cellData.data.jobs[0];
|
||||
if (job.status === 'Scheduled' || job.status === 'Started') {
|
||||
// Check if click is on resize handle area (right edge)
|
||||
const target = event.target;
|
||||
const rect = target.closest('.day-cell').getBoundingClientRect();
|
||||
const clickX = event.clientX;
|
||||
const isNearRightEdge = clickX > rect.right - 20; // 20px from right edge
|
||||
|
||||
if (isNearRightEdge && cellData.classes.includes('job-end')) {
|
||||
event.preventDefault(); // Only prevent default for resize
|
||||
startResize(event, job, date);
|
||||
}
|
||||
// If not near right edge, allow normal drag behavior
|
||||
}
|
||||
}
|
||||
selectedEvent.value = event.event;
|
||||
eventDialog.value = true;
|
||||
};
|
||||
|
||||
const scheduleService = (service) => {
|
||||
@ -933,20 +1057,6 @@ const handleDragStart = (service, event) => {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", service.name);
|
||||
|
||||
// Get the dimensions of the dragged element
|
||||
const dragElement = event.target;
|
||||
const rect = dragElement.getBoundingClientRect();
|
||||
|
||||
// Set the drag image offset to center the element horizontally and position cursor at top
|
||||
const offsetX = rect.width / 2;
|
||||
const offsetY = 10;
|
||||
|
||||
try {
|
||||
event.dataTransfer.setDragImage(dragElement, offsetX, offsetY);
|
||||
} catch (e) {
|
||||
console.log("Could not set custom drag image");
|
||||
}
|
||||
|
||||
// Add visual feedback
|
||||
event.target.style.opacity = '0.5';
|
||||
console.log("Drag started for service:", service.projectTemplate);
|
||||
@ -1315,46 +1425,12 @@ const handleUnscheduledDrop = async (event) => {
|
||||
|
||||
// Allow moving scheduled jobs between days/crews if status is 'Scheduled'
|
||||
const handleScheduledDragOver = (job, event, foremanId, date) => {
|
||||
if (!draggedService.value) return;
|
||||
// Only allow if dragging a scheduled job and target is a valid cell
|
||||
if (draggedService.value.status === 'Scheduled' && job.status === 'Scheduled') {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
// This is now handled by the main handleDragOver function
|
||||
};
|
||||
|
||||
const handleScheduledDrop = async (job, event, foremanId, date) => {
|
||||
if (!draggedService.value) return;
|
||||
// Only allow if dragging a scheduled job and target is a valid cell
|
||||
if (draggedService.value.status !== 'Scheduled') return;
|
||||
// Prevent dropping on same cell
|
||||
if (draggedService.value.name === job.name && draggedService.value.foreman === foremanId && draggedService.value.expectedStartDate === date) return;
|
||||
|
||||
// Prevent dropping on Sunday or holidays
|
||||
if (isSunday(date) || isHoliday(date)) return;
|
||||
|
||||
// Update job's foreman and date
|
||||
const serviceIndex = scheduledServices.value.findIndex(s => s.name === draggedService.value.name);
|
||||
if (serviceIndex !== -1) {
|
||||
try {
|
||||
await Api.updateServiceAppointmentScheduledDates(
|
||||
draggedService.value.name,
|
||||
date,
|
||||
draggedService.value.expectedEndDate,
|
||||
foremanId
|
||||
);
|
||||
scheduledServices.value[serviceIndex] = {
|
||||
...scheduledServices.value[serviceIndex],
|
||||
expectedStartDate: date,
|
||||
foreman: foremanId
|
||||
};
|
||||
notifications.addSuccess('Job moved successfully!');
|
||||
} catch (error) {
|
||||
notifications.addError('Failed to move job');
|
||||
}
|
||||
}
|
||||
isDragOver.value = false;
|
||||
dragOverCell.value = null;
|
||||
draggedService.value = null;
|
||||
// Allow moving scheduled jobs between days/crews if status is 'Scheduled'
|
||||
const handleScheduledDrop = async (event, foremanId, date) => {
|
||||
// This is now handled by the main handleDrop function
|
||||
};
|
||||
|
||||
// Resize functionality
|
||||
@ -1366,28 +1442,28 @@ const startResize = (event, job, date) => {
|
||||
const target = event.target;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const clickX = event.clientX;
|
||||
|
||||
// Check if click is on the resize handle or near right edge
|
||||
const isNearRightEdge = clickX > rect.right - 10;
|
||||
const isResizeHandle = target.classList.contains('resize-handle');
|
||||
|
||||
// Allow repeated extension: remove restriction on job.expectedEndDate
|
||||
// (no early return)
|
||||
if (!isNearRightEdge && !isResizeHandle) return;
|
||||
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
// Set flag immediately to prevent modal from opening
|
||||
justFinishedResize.value = true;
|
||||
|
||||
|
||||
resizingJob.value = job;
|
||||
resizeStartX.value = event.clientX;
|
||||
resizeStartDate.value = date;
|
||||
originalEndDate.value = job.expectedEndDate || job.expectedStartDate;
|
||||
|
||||
|
||||
// Add global mouse move and mouse up listeners
|
||||
document.addEventListener('mousemove', handleResize);
|
||||
document.addEventListener('mouseup', stopResize);
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
document.body.style.cursor = 'ew-resize';
|
||||
};
|
||||
@ -1727,7 +1803,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.weekly-calendar {
|
||||
min-width: 1000px;
|
||||
min-width: 1130px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
@ -1812,15 +1888,20 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
border-right: 1px solid var(--surface-border);
|
||||
border: 1px solid #e0e0e0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
min-height: 80px;
|
||||
max-height: 80px;
|
||||
padding: 4px;
|
||||
overflow: visible; /* Allow multi-day jobs to overflow */
|
||||
width: 100%; /* Ensure cell doesn't expand */
|
||||
overflow: hidden; /* Prevent content from expanding cell */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* Prevent shrinking */
|
||||
box-sizing: border-box; /* Include padding in width calculation */
|
||||
}
|
||||
|
||||
.day-cell:hover {
|
||||
@ -1837,67 +1918,148 @@ onMounted(async () => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.calendar-job {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 68px;
|
||||
height: calc(100% - 8px);
|
||||
max-width: none; /* Allow spanning */
|
||||
}
|
||||
|
||||
.calendar-job:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.calendar-job:hover .resize-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.calendar-job[draggable="true"] {
|
||||
.day-cell[draggable="true"] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.calendar-job[draggable="true"]:active {
|
||||
.day-cell[draggable="true"]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Job styling based on priority */
|
||||
.day-cell.priority-urgent {
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.day-cell.priority-high {
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.day-cell.priority-medium {
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.day-cell.priority-low {
|
||||
background: linear-gradient(135deg, #4caf50, #388e3c);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Job border styling - only for cells that contain jobs */
|
||||
.day-cell.priority-urgent:not(.empty):not(.skipped),
|
||||
.day-cell.priority-high:not(.empty):not(.skipped),
|
||||
.day-cell.priority-medium:not(.empty):not(.skipped),
|
||||
.day-cell.priority-low:not(.empty):not(.skipped) {
|
||||
border-top: 3px solid currentColor;
|
||||
border-bottom: 3px solid currentColor;
|
||||
}
|
||||
|
||||
/* Job start/end border styling */
|
||||
.day-cell.job-start:not(.skipped) {
|
||||
border-left: 4px solid currentColor;
|
||||
}
|
||||
|
||||
.day-cell.job-end:not(.skipped) {
|
||||
border-right: 4px solid currentColor;
|
||||
}
|
||||
|
||||
/* Skip day styling */
|
||||
.day-cell.skipped {
|
||||
border: 2px dotted #ff0000;
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
color: #ff0000;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Special week break borders */
|
||||
.day-cell.after-week-break:not(.skipped) {
|
||||
border-left: 4px double currentColor;
|
||||
}
|
||||
|
||||
.day-cell.continues-next-week:not(.skipped) {
|
||||
border-right: 4px double currentColor;
|
||||
}
|
||||
|
||||
/* Skip day adjacent styling */
|
||||
.day-cell.after-skip:not(.skipped) {
|
||||
border-left: 2px dashed currentColor;
|
||||
}
|
||||
|
||||
.day-cell.before-skip:not(.skipped) {
|
||||
border-right: 2px dashed currentColor;
|
||||
}
|
||||
|
||||
/* Holiday and Sunday styling */
|
||||
.day-cell.holiday {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(255, 193, 7, 0.15),
|
||||
rgba(255, 193, 7, 0.15) 10px,
|
||||
rgba(255, 193, 7, 0.05) 10px,
|
||||
rgba(255, 193, 7, 0.05) 20px
|
||||
);
|
||||
border-left: 3px solid #ffc107;
|
||||
border-right: 3px solid #ffc107;
|
||||
}
|
||||
|
||||
.day-cell.sunday {
|
||||
background-color: rgba(200, 200, 200, 0.1); /* light gray for Sunday */
|
||||
}
|
||||
|
||||
/* Skipped styling */
|
||||
.day-cell.skipped {
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
border-top: 2px dotted #ff0000;
|
||||
border-bottom: 2px dotted #ff0000;
|
||||
color: #ff0000;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Empty cell styling */
|
||||
.day-cell.empty {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.job-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85em;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.95em;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.job-address {
|
||||
font-size: 0.8em;
|
||||
font-size: 0.75em;
|
||||
opacity: 0.9;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
line-height: 1.2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
@ -1921,17 +2083,15 @@ onMounted(async () => {
|
||||
|
||||
.spans-arrow-left {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.spans-arrow-right {
|
||||
position: absolute;
|
||||
right: 25px; /* Position before resize handle */
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
|
||||
@ -174,6 +174,10 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
contactOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
@ -205,7 +209,7 @@ const localFormData = computed({
|
||||
|
||||
const contactOptions = computed(() => {
|
||||
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
||||
return [];
|
||||
return props.contactOptions;
|
||||
}
|
||||
return localFormData.value.contacts.map((contact, index) => ({
|
||||
label: `${contact.firstName || ""} ${contact.lastName || ""}`.trim() || `Contact ${index + 1}`,
|
||||
@ -231,28 +235,6 @@ onMounted(() => {
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const addAddress = () => {
|
||||
localFormData.value.addresses.push({
|
||||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
isBillingAddress: false,
|
||||
isServiceAddress: true,
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
contacts: [],
|
||||
primaryContact: null,
|
||||
zipcodeLookupDisabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
const removeAddress = (index) => {
|
||||
if (localFormData.value.addresses.length > 1) {
|
||||
localFormData.value.addresses.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddressLine = (index, field, event) => {
|
||||
const value = event.target.value;
|
||||
if (!value) return;
|
||||
|
||||
@ -149,24 +149,24 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => {
|
||||
if (!props.formData.contacts || props.formData.contacts.length === 0) {
|
||||
props.formData.contacts = [
|
||||
{
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return props.formData;
|
||||
},
|
||||
get: () => props.formData,
|
||||
set: (value) => emit("update:formData", value),
|
||||
});
|
||||
|
||||
// Ensure at least one contact always exists
|
||||
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
||||
localFormData.value.contacts = [
|
||||
{
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const roleOptions = ref([
|
||||
{ label: "Owner", value: "Owner" },
|
||||
{ label: "Property Manager", value: "Property Manager" },
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<Dialog :visible="visible" @update:visible="val => emit('update:visible', val)" modal :closable="false" :style="{ width: '700px', maxWidth: '95vw' }">
|
||||
<template #header>
|
||||
<span class="modal-title">Add Contact/Address</span>
|
||||
</template>
|
||||
<div class="modal-body">
|
||||
<ContactInformationForm
|
||||
:formData="contactFormData.value"
|
||||
@update:formData="val => contactFormData.value = val"
|
||||
:isSubmitting="isSubmitting"
|
||||
:existingContacts="existingContacts"
|
||||
/>
|
||||
<AddressInformationForm
|
||||
:formData="addressFormData.value"
|
||||
@update:formData="val => addressFormData.value = val"
|
||||
:isSubmitting="isSubmitting"
|
||||
:contactOptions="allContactOptions"
|
||||
:existingAddresses="existingAddresses"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" @click="close" severity="secondary" />
|
||||
<Button label="Create" @click="create" severity="primary" :loading="isSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Button from 'primevue/button';
|
||||
import ContactInformationForm from '../clientSubPages/ContactInformationForm.vue';
|
||||
import AddressInformationForm from '../clientSubPages/AddressInformationForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
clientContacts: { type: Array, default: () => [] },
|
||||
existingContacts: { type: Array, default: () => [] },
|
||||
existingAddresses: { type: Array, default: () => [] },
|
||||
isSubmitting: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(['update:visible', 'created']);
|
||||
|
||||
const contactFormData = ref({ contacts: [] });
|
||||
const addressFormData = ref({ addresses: [], contacts: [] });
|
||||
|
||||
// Keep addressFormData.contacts in sync with new contacts
|
||||
watch(
|
||||
() => contactFormData.value.contacts,
|
||||
(newContacts) => {
|
||||
addressFormData.value.contacts = newContacts || [];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// All contact options = clientContacts + new contacts
|
||||
const allContactOptions = computed(() => {
|
||||
const clientOpts = (props.clientContacts || []).map((c, idx) => ({
|
||||
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
|
||||
value: `client-${idx}`,
|
||||
...c,
|
||||
}));
|
||||
const newOpts = (contactFormData.value.contacts || []).map((c, idx) => ({
|
||||
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
|
||||
value: `new-${idx}`,
|
||||
...c,
|
||||
}));
|
||||
return [...clientOpts, ...newOpts];
|
||||
});
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
function create() {
|
||||
// Dummy create handler
|
||||
console.log('Create clicked', {
|
||||
contacts: contactFormData.value.contacts,
|
||||
addresses: addressFormData.value.addresses,
|
||||
});
|
||||
emit('created', {
|
||||
contacts: contactFormData.value.contacts,
|
||||
addresses: addressFormData.value.addresses,
|
||||
});
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,116 +1,123 @@
|
||||
<template>
|
||||
<div class="general-client-info">
|
||||
<div class="info-grid">
|
||||
<!-- 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) -->
|
||||
<div v-if="clientData.customerType === 'Company'" class="info-section">
|
||||
<label>Company Name</label>
|
||||
<span class="info-value large">{{ displayClientName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client Type -->
|
||||
<div class="info-section">
|
||||
<label>Client Type</label>
|
||||
<span class="info-value">{{ clientData.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Associated Companies -->
|
||||
<div v-if="associatedCompanies.length > 0" class="info-section">
|
||||
<label>Associated Companies</label>
|
||||
<div class="companies-list">
|
||||
<Tag
|
||||
v-for="company in associatedCompanies"
|
||||
:key="company"
|
||||
:value="company"
|
||||
severity="info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Address -->
|
||||
<div v-if="billingAddress" class="info-section">
|
||||
<label>Billing Address</label>
|
||||
<span class="info-value">{{ billingAddress }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Primary Contact Info -->
|
||||
<div v-if="primaryContact" class="info-section primary-contact">
|
||||
<label>{{ clientData.customerType === 'Individual' ? 'Contact Information' : 'Primary Contact' }}</label>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>{{ primaryContact.fullName || primaryContact.name || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>{{ primaryContact.emailId || primaryContact.customEmail || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>{{ primaryContact.phone || primaryContact.mobileNo || "N/A" }}</span>
|
||||
<div>
|
||||
<div class="general-client-info">
|
||||
<div class="info-grid">
|
||||
<!-- Add Contact/Address Button (always visible) -->
|
||||
<div class="lead-badge-container">
|
||||
<template v-if="isLead">
|
||||
<Badge value="LEAD" severity="warn" size="large" />
|
||||
</template>
|
||||
<div class="action-buttons">
|
||||
<Button size="small" variant="outlined" color="primary" @click="openAddModal">
|
||||
<v-icon left size="small">mdi-account-plus</v-icon>
|
||||
Add Contact/Address
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="info-section stats">
|
||||
<label>Overview</label>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<span class="stat-value">{{ addressCount }}</span>
|
||||
<span class="stat-label">Addresses</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-users"></i>
|
||||
<span class="stat-value">{{ contactCount }}</span>
|
||||
<span class="stat-label">Contacts</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span class="stat-value">{{ projectCount }}</span>
|
||||
<span class="stat-label">Projects</span>
|
||||
<!-- Client Name (only show for Company type) -->
|
||||
<div v-if="clientData.customerType === 'Company'" class="info-section">
|
||||
<label>Company Name</label>
|
||||
<span class="info-value large">{{ displayClientName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client Type -->
|
||||
<div class="info-section">
|
||||
<label>Client Type</label>
|
||||
<span class="info-value">{{ clientData.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Associated Companies -->
|
||||
<div v-if="associatedCompanies.length > 0" class="info-section">
|
||||
<label>Associated Companies</label>
|
||||
<div class="companies-list">
|
||||
<Tag
|
||||
v-for="company in associatedCompanies"
|
||||
:key="company"
|
||||
:value="company"
|
||||
severity="info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Date -->
|
||||
<div class="info-section">
|
||||
<label>Created</label>
|
||||
<span class="info-value">{{ formattedCreationDate }}</span>
|
||||
<!-- Billing Address -->
|
||||
<div v-if="billingAddress" class="info-section">
|
||||
<label>Billing Address</label>
|
||||
<span class="info-value">{{ billingAddress }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Primary Contact Info -->
|
||||
<div v-if="primaryContact" class="info-section primary-contact">
|
||||
<label>{{ clientData.customerType === 'Individual' ? 'Contact Information' : 'Primary Contact' }}</label>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>{{ primaryContact.fullName || primaryContact.name || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>{{ primaryContact.emailId || primaryContact.customEmail || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>{{ primaryContact.phone || primaryContact.mobileNo || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="info-section stats">
|
||||
<label>Overview</label>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<span class="stat-value">{{ addressCount }}</span>
|
||||
<span class="stat-label">Addresses</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-users"></i>
|
||||
<span class="stat-value">{{ contactCount }}</span>
|
||||
<span class="stat-label">Contacts</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span class="stat-value">{{ projectCount }}</span>
|
||||
<span class="stat-label">Projects</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Date -->
|
||||
<div class="info-section">
|
||||
<label>Created</label>
|
||||
<span class="info-value">{{ formattedCreationDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddContactAddressModal
|
||||
:visible="showAddModal"
|
||||
@update:visible="showAddModal = $event"
|
||||
:clientContacts="clientData.contacts || []"
|
||||
:existingContacts="clientData.contacts?.map(c => c.fullName || c.name) || []"
|
||||
:existingAddresses="clientData.addresses?.map(a => a.addressLine1) || []"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import Badge from "primevue/badge";
|
||||
import Tag from "primevue/tag";
|
||||
import AddContactAddressModal from './AddContactAddressModal.vue';
|
||||
import Button from 'primevue/button';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showAddModal = ref(false);
|
||||
const openAddModal = () => {
|
||||
showAddModal.value = true;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
clientData: {
|
||||
@ -166,16 +173,8 @@ const formattedCreationDate = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user