From d3818d1985a2534c6096800aae0cb8b1dc6a7755 Mon Sep 17 00:00:00 2001 From: Casey Date: Fri, 16 Jan 2026 09:06:59 -0600 Subject: [PATCH] job creation working --- custom_ui/api/db/clients.py | 10 +- custom_ui/api/db/estimates.py | 18 +- custom_ui/api/db/jobs.py | 5 + custom_ui/api/db/tasks.py | 12 + custom_ui/events/estimate.py | 9 +- custom_ui/events/jobs.py | 23 ++ custom_ui/events/onsite_meeting.py | 1 + custom_ui/events/sales_order.py | 52 +++-- custom_ui/fixtures/doctype.json | 148 ++++++++---- custom_ui/hooks.py | 6 + custom_ui/install.py | 38 ++- custom_ui/services/address_service.py | 77 ++++--- custom_ui/services/client_service.py | 24 +- custom_ui/services/contact_service.py | 17 +- frontend/src/api.js | 6 +- .../clientSubPages/AddressInformationForm.vue | 35 ++- .../clientSubPages/ClientInformationForm.vue | 4 +- .../clientSubPages/ContactInformationForm.vue | 4 +- .../components/clientSubPages/Overview.vue | 5 +- frontend/src/components/pages/Estimate.vue | 218 +++++++++++++----- frontend/src/components/pages/Job.vue | 53 ++++- frontend/src/components/pages/Tasks.vue | 5 +- 22 files changed, 591 insertions(+), 179 deletions(-) diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index 39d5add..0edb984 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -310,7 +310,6 @@ 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") }] } @@ -338,7 +337,7 @@ def upsert_client(data): "last_name": contact_data.get("last_name"), "role": contact_data.get("contact_role", "Other"), "custom_email": contact_data.get("email"), - "is_primary_contact":1 if contact_data.get("is_primary", False) else 0, + "is_primary_contact": 1 if contact_data.get("is_primary", False) else 0, "customer_type": "Lead", "customer_name": client_doc.name, "email_ids": [{ @@ -370,11 +369,13 @@ def upsert_client(data): # Handle address creation address_docs = [] for address in addresses: + is_billing = True if address.get("is_billing_address") else False print("#####DEBUG: Creating address with data:", address) address_doc = AddressService.create_address({ - "address_title": build_address_title(customer_name, address), + "address_title": AddressService.build_address_title(customer_name, address), "address_line1": address.get("address_line1"), "address_line2": address.get("address_line2"), + "address_type": "Billing" if is_billing else "Service", "city": address.get("city"), "state": address.get("state"), "country": "United States", @@ -385,6 +386,9 @@ def upsert_client(data): }) AddressService.link_address_to_customer(address_doc, "Lead", client_doc.name) address_doc.reload() + if is_billing: + client_doc.custom_billing_address = address_doc.name + client_doc.save(ignore_permissions=True) 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) diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 82d2f36..bf7e260 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -37,7 +37,7 @@ def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10): tableRows = [] for estimate in estimates: - full_address = frappe.db.get_value("Address", estimate.get("custom_installation_address"), "full_address") + full_address = frappe.db.get_value("Address", estimate.get("custom_job_address"), "full_address") tableRow = {} tableRow["id"] = estimate["name"] tableRow["address"] = full_address @@ -69,7 +69,7 @@ def get_estimate(estimate_name): estimate = frappe.get_doc("Quotation", estimate_name) est_dict = estimate.as_dict() - address_name = estimate.custom_installation_address or estimate.customer_address + address_name = estimate.custom_job_address or estimate.customer_address if address_name: # Fetch Address Doc address_doc = frappe.get_doc("Address", address_name).as_dict() @@ -386,8 +386,6 @@ def upsert_estimate(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(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 @@ -432,6 +430,12 @@ def upsert_estimate(data): else: print("DEBUG: Creating new estimate") print("DEBUG: Retrieved address name:", data.get("address_name")) + client_doc = ClientService.get_client_or_throw(address_doc.customer_name) + # billing_address = next((addr for addr in address_doc if addr.address_type == "Billing"), None) + # if billing_address: + # print("DEBUG: Found billing address:", billing_address.name) + # else: + # print("DEBUG: No billing address found for client:", client_doc.name) new_estimate = frappe.get_doc({ "doctype": "Quotation", "custom_requires_half_payment": data.get("requires_half_payment", 0), @@ -441,9 +445,9 @@ def upsert_estimate(data): "party_name": data.get("contact_name"), "quotation_to": "Contact", "company": data.get("company"), - "actual_customer_name": address_doc.customer_name, - "customer_type": client_doctype, - "customer_address": data.get("address_name"), + "actual_customer_name": client_doc.name, + "customer_type": address_doc.customer_type, + "customer_address": client_doc.custom_billing_address, "contact_person": data.get("contact_name"), "letter_head": data.get("company"), "custom_project_template": data.get("project_template", None), diff --git a/custom_ui/api/db/jobs.py b/custom_ui/api/db/jobs.py index d27e700..9b7b419 100644 --- a/custom_ui/api/db/jobs.py +++ b/custom_ui/api/db/jobs.py @@ -1,5 +1,6 @@ import frappe, json from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response +from custom_ui.services import AddressService, ClientService # =============================================================================== # JOB MANAGEMENT API METHODS @@ -45,6 +46,10 @@ def get_job(job_id=""): print("DEBUG: Loading Job from database:", job_id) try: project = frappe.get_doc("Project", job_id) + address_doc = AddressService.get_or_throw(project.job_address) + project = project.as_dict() + project["job_address"] = address_doc + project["client"] = ClientService.get_client_or_throw(project.customer) return build_success_response(project) except Exception as e: return build_error_response(str(e), 500) diff --git a/custom_ui/api/db/tasks.py b/custom_ui/api/db/tasks.py index adf282c..e53dd37 100644 --- a/custom_ui/api/db/tasks.py +++ b/custom_ui/api/db/tasks.py @@ -1,6 +1,18 @@ import frappe from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response +from custom_ui.services import DbService +@frappe.whitelist() +def set_task_status(task_name, new_status): + """Set the status of a specific task.""" + try: + task = DbService.get_or_throw("Task", task_name) + task.status = new_status + task.save() + return build_success_response(f"Task {task_name} status updated to {new_status}.") + except Exception as e: + return build_error_response(str(e), 500) + @frappe.whitelist() def get_job_task_list(job_id=""): diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index 40df9ae..f7d212b 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -7,6 +7,9 @@ def after_insert(doc, method): AddressService.append_link_v2( doc.custom_job_address, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template} ) + AddressService.append_link_v2( + doc.custom_job_address, "links", {"link_doctype": "Quotation", "link_name": doc.name} + ) ClientService.append_link_v2( doc.actual_customer_name, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template} ) @@ -74,11 +77,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 + # 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.customer_address = backup new_sales_order.submit() frappe.db.commit() print("DEBUG: Sales Order created successfully:", new_sales_order.name) diff --git a/custom_ui/events/jobs.py b/custom_ui/events/jobs.py index f05774c..aa80dc0 100644 --- a/custom_ui/events/jobs.py +++ b/custom_ui/events/jobs.py @@ -1,5 +1,28 @@ import frappe +from custom_ui.services import AddressService, ClientService + +def after_insert(doc, method): + print("DEBUG: After Insert Triggered for Project") + print("DEBUG: Linking Project to address and Customer") + AddressService.append_link_v2( + doc.job_address, "projects", {"project": doc.name, "project_template": doc.project_template} + ) + AddressService.append_link_v2( + doc.job_address, "links", {"link_doctype": "Project", "link_name": doc.name} + ) + ClientService.append_link_v2( + doc.customer, "projects", {"project": doc.name, "project_template": doc.project_template} + ) + if doc.project_template == "SNW Install": + print("DEBUG: Project template is SNW Install, updating Address status to In Progress") + AddressService.update_value( + doc.job_address, + "job_status", + "In Progress" + ) + + def before_insert(doc, method): # This is where we will add logic to set tasks and other properties of a job based on it's project_template pass \ No newline at end of file diff --git a/custom_ui/events/onsite_meeting.py b/custom_ui/events/onsite_meeting.py index 334a514..32a0262 100644 --- a/custom_ui/events/onsite_meeting.py +++ b/custom_ui/events/onsite_meeting.py @@ -14,6 +14,7 @@ def after_insert(doc, method): print("DEBUG: After Insert Triggered for On-Site Meeting") print("DEBUG: Linking bid meeting to customer and address") AddressService.append_link_v2(doc.address, "onsite_meetings", {"onsite_meeting": doc.name, "project_template": doc.project_template}) + AddressService.append_link_v2(doc.address, "links", {"link_doctype": "On-Site Meeting", "link_name": doc.name}) ClientService.append_link(doc.party_name, "onsite_meetings", "onsite_meeting", doc.name) if doc.project_template == "SNW Install": print("DEBUG: Project template is SNW Install, updating Address status to In Progress") diff --git a/custom_ui/events/sales_order.py b/custom_ui/events/sales_order.py index 1d35125..9c4499c 100644 --- a/custom_ui/events/sales_order.py +++ b/custom_ui/events/sales_order.py @@ -1,5 +1,13 @@ import frappe -from custom_ui.services import DbService +from custom_ui.services import DbService, AddressService, ClientService + +def before_insert(doc, method): + print("DEBUG: before_insert hook triggered for Sales Order:", doc.name) + if doc.custom_project_template == "SNW Install": + print("DEBUG: Sales Order uses SNW Install template, checking for duplicate linked sales orders.") + address_doc = AddressService.get_or_throw(doc.custom_job_address) + if "SNW Install" in [link.project_template for link in address_doc.sales_orders]: + raise frappe.ValidationError("A Sales Order with project template 'SNW Install' is already linked to this address.") def on_submit(doc, method): print("DEBUG: Info from Sales Order") @@ -7,27 +15,39 @@ def on_submit(doc, method): print(doc.company) print(doc.transaction_date) print(doc.customer) + print(doc.custom_job_address) + print(doc.custom_project_template) # Create Invoice and Project from Sales Order try: print("Creating Project from Sales Order", doc.name) - sales_order = frappe.get_doc("Sales Order", doc.name) - if not sales_order.custom_project_template: - return - project_template = DbService.get("Project Template", sales_order.custom_project_template) - new_job = frappe.get_doc({ - "doctype": "Project", - "custom_job_address": sales_order.custom_job_address, - "project_name": f"{sales_order.custom_project_template} - {sales_order.custom_job_address}", - "project_template": project_template.name, - "custom_warranty_duration_days": 90, - "sales_order": sales_order.name - }) - # attatch the job to the sales_order links - new_job.insert() - frappe.db.commit() + if doc.custom_project_template or doc.project_template: + new_job = frappe.get_doc({ + "doctype": "Project", + "custom_job_address": doc.custom_job_address, + "project_name": f"{doc.custom_project_template} - {doc.custom_job_address}", + "project_template": doc.custom_project_template, + "custom_warranty_duration_days": 90, + "customer": doc.customer, + "job_address": doc.custom_job_address, + "sales_order": doc.name + }) + # attatch the job to the sales_order links + new_job.insert() + frappe.db.commit() except Exception as e: print("ERROR creating Project from Sales Order:", str(e)) +def after_insert(doc, method): + print("DEBUG: after_insert hook triggered for Sales Order:", doc.name) + AddressService.append_link_v2( + doc.custom_job_address, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template} + ) + AddressService.append_link_v2( + doc.custom_job_address, "links", {"link_doctype": "Sales Order", "link_name": doc.name} + ) + ClientService.append_link_v2( + doc.customer, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template} + ) def create_sales_invoice_from_sales_order(doc, method): try: diff --git a/custom_ui/fixtures/doctype.json b/custom_ui/fixtures/doctype.json index 5455fe4..1571089 100644 --- a/custom_ui/fixtures/doctype.json +++ b/custom_ui/fixtures/doctype.json @@ -114,8 +114,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.065211", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.476708", "module": "Custom", "name": "Lead Companies Link", "naming_rule": "", @@ -332,8 +332,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.117683", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.527399", "module": "Custom", "name": "Address Project Link", "naming_rule": "", @@ -550,8 +550,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.173958", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.579759", "module": "Custom", "name": "Address Quotation Link", "naming_rule": "", @@ -768,8 +768,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.229451", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.630680", "module": "Custom", "name": "Address On-Site Meeting Link", "naming_rule": "", @@ -901,6 +901,70 @@ "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": "project_template", + "fieldtype": "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": 0, + "in_preview": 0, + "in_standard_filter": 0, + "is_virtual": 0, + "label": "Project Template", + "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": "Project Template", + "parent": "Address Sales Order 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, @@ -922,8 +986,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.280251", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:19:33.624850", "module": "Custom", "name": "Address Sales Order Link", "naming_rule": "", @@ -1076,8 +1140,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.332720", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.729324", "module": "Custom", "name": "Contact Address Link", "naming_rule": "", @@ -1230,8 +1294,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.385343", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.780526", "module": "Custom", "name": "Lead On-Site Meeting Link", "naming_rule": "", @@ -1832,8 +1896,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.457103", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.847539", "module": "Selling", "name": "Quotation Template", "naming_rule": "", @@ -2330,8 +2394,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.529451", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.916692", "module": "Selling", "name": "Quotation Template Item", "naming_rule": "", @@ -2484,8 +2548,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.580626", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:17.965428", "module": "Custom UI", "name": "Customer Company Link", "naming_rule": "", @@ -2638,8 +2702,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.630918", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.018887", "module": "Custom UI", "name": "Customer Address Link", "naming_rule": "", @@ -2792,8 +2856,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.681698", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.066488", "module": "Custom UI", "name": "Customer Contact Link", "naming_rule": "", @@ -2946,8 +3010,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.731378", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.115936", "module": "Custom", "name": "Address Contact Link", "naming_rule": "", @@ -3100,8 +3164,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.783154", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.164485", "module": "Custom", "name": "Customer On-Site Meeting Link", "naming_rule": "", @@ -3254,8 +3318,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.832605", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.212622", "module": "Custom", "name": "Customer Project Link", "naming_rule": "", @@ -3408,8 +3472,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.882511", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.262231", "module": "Custom", "name": "Customer Quotation Link", "naming_rule": "", @@ -3562,8 +3626,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.934072", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.311056", "module": "Custom", "name": "Customer Sales Order Link", "naming_rule": "", @@ -3716,8 +3780,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:57.984126", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.359782", "module": "Custom", "name": "Lead Address Link", "naming_rule": "", @@ -3870,8 +3934,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:58.034758", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.408094", "module": "Custom", "name": "Lead Contact Link", "naming_rule": "", @@ -4024,8 +4088,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:58.086245", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.457175", "module": "Custom", "name": "Lead Quotation Link", "naming_rule": "", @@ -4178,8 +4242,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", - "modified": "2026-01-15 10:05:58.139052", + "migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2", + "modified": "2026-01-16 03:16:18.506817", "module": "Custom", "name": "Address Company Link", "naming_rule": "", diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index c583d46..1759776 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -175,8 +175,14 @@ doc_events = { "on_update_after_submit": "custom_ui.events.estimate.on_update_after_submit" }, "Sales Order": { + "before_insert": "custom_ui.events.sales_order.before_insert", + "after_insert": "custom_ui.events.sales_order.after_insert", "on_submit": "custom_ui.events.sales_order.on_submit" }, + "Project": { + "before_insert": "custom_ui.events.jobs.before_insert", + "after_insert": "custom_ui.events.jobs.after_insert" + }, "Task": { "before_insert": "custom_ui.events.task.before_insert" } diff --git a/custom_ui/install.py b/custom_ui/install.py index b409559..4c89c1b 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -30,7 +30,7 @@ def after_migrate(): frappe.clear_cache(doctype=doctype) frappe.reload_doctype(doctype) - update_address_fields() + # update_address_fields() # build_frontend() @@ -70,6 +70,17 @@ def add_custom_fields(): from frappe.custom.doctype.custom_field.custom_field import create_custom_fields print("\n🔧 Adding custom fields to doctypes...") + + try: + address_meta = frappe.get_meta("Address") + address_type_params = address_meta.get_field("address_type") + if address_type_params and "Service" not in (address_type_params.options or ""): + print(" Adding 'Service' to Address type options...") + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + make_property_setter("Address", "address_type", "options", (address_type_params.options or "") + "\nService", "DocField") + print(" ✅ Added 'Service' to Address address_type options.") + except Exception as e: + print(f" ⚠️ Failed to update Address address_type: {e}") custom_fields = { "Customer": [ @@ -145,6 +156,13 @@ def add_custom_fields(): options="Lead On-Site Meeting Link", insert_after="quotations" ), + dict( + fieldname="custom_billing_address", + label="Custom Address", + fieldtype="Link", + options="Address", + insert_after="customer_name" + ), dict( fieldname="quotations", label="Quotations", @@ -505,6 +523,24 @@ def add_custom_fields(): allow_on_submit=1 ) ], + "Project": [ + dict( + fieldname="job_address", + label="Job Address", + fieldtype="Link", + options="Address", + insert_after="project_name", + description="The address where the job is being performed." + ), + dict( + fieldname="customer", + label="Customer", + fieldtype="Link", + options="Customer", + insert_after="job_address", + description="The customer for whom the project is being executed." + ) + ], "Project Template": [ dict( fieldname="company", diff --git a/custom_ui/services/address_service.py b/custom_ui/services/address_service.py index 14d81d0..38bc14c 100644 --- a/custom_ui/services/address_service.py +++ b/custom_ui/services/address_service.py @@ -1,13 +1,22 @@ import frappe +from frappe.model.document import Document import requests from .contact_service import ContactService, DbService class AddressService: + @staticmethod + def build_address_title(customer_name, address_data) -> str: + """Build a title for the address based on its fields.""" + print(f"DEBUG: Building address title for customer '{customer_name}' with address data: {address_data}") + is_billing = address_data.get("is_billing_address", False) + address_type = "Billing" if is_billing else "Service" + return f"{customer_name} - {address_data.get('address_line1', '')} {address_data.get('city')} - {address_type}" + @staticmethod def build_full_dict( - address_doc, - included_links: list = ["contacts", "on-site meetings", "quotations", "sales orders", "projects", "companies"]) -> dict: + address_doc: Document, + included_links: list = ["contacts", "on-site meetings", "quotations", "sales orders", "projects", "companies"]) -> frappe._dict: """Build a full dictionary representation of an address, including all links. Can optionally exclude links.""" print(f"DEBUG: Building full dict for Address {address_doc.name}") address_dict = address_doc.as_dict() @@ -27,12 +36,12 @@ class AddressService: return address_dict @staticmethod - def get_address_by_full_address(full_address: str): + def get_address_by_full_address(full_address: str) -> Document: """Retrieve an address document by its full_address field. Returns None if not found.""" print(f"DEBUG: Retrieving Address document with full_address: {full_address}") address_name = frappe.db.get_value("Address", {"full_address": full_address}) if address_name: - address_doc = frappe.get_doc("Address", address_name) + address_doc = DbService.get_or_throw("Address", address_name) print("DEBUG: Address document found.") return address_doc print("DEBUG: Address document not found.") @@ -47,18 +56,18 @@ class AddressService: return result @staticmethod - def get(address_name: str): + def get(address_name: str) -> Document: """Retrieve an address document by name. Returns None if not found.""" print(f"DEBUG: Retrieving Address document with name: {address_name}") if AddressService.exists(address_name): - address_doc = frappe.get_doc("Address", address_name) + address_doc = DbService.get_or_throw("Address", address_name) print("DEBUG: Address document found.") return address_doc print("DEBUG: Address document not found.") return None @staticmethod - def get_or_throw(address_name: str): + def get_or_throw(address_name: str) -> Document: """Retrieve an address document by name or throw an error if not found.""" address_doc = AddressService.get(address_name) if address_doc: @@ -66,43 +75,43 @@ class AddressService: raise ValueError(f"Address with name {address_name} does not exist.") @staticmethod - def update_value(docname: str, fieldname: str, value, save: bool = True) -> frappe._dict: + def update_value(doc_name: str, fieldname: str, value, save: bool = True) -> Document: """Update a specific field value of a document.""" - print(f"DEBUG: Updating Address {docname}, setting {fieldname} to {value}") - if AddressService.exists(docname) is False: - raise ValueError(f"Address with name {docname} does not exist.") + print(f"DEBUG: Updating Address {doc_name}, setting {fieldname} to {value}") + if AddressService.exists(doc_name) is False: + raise ValueError(f"Address with name {doc_name} does not exist.") if save: print("DEBUG: Saving updated Address document.") - address_doc = AddressService.get_or_throw(docname) + address_doc = AddressService.get_or_throw(doc_name) setattr(address_doc, fieldname, value) address_doc.save(ignore_permissions=True) else: print("DEBUG: Not saving Address document as save=False.") - frappe.db.set_value("Address", docname, fieldname, value) - print(f"DEBUG: Updated Address {docname}: set {fieldname} to {value}") + frappe.db.set_value("Address", doc_name, fieldname, value) + print(f"DEBUG: Updated Address {doc_name}: set {fieldname} to {value}") return address_doc @staticmethod - def get_value(docname: str, fieldname: str): + def get_value(doc_name: str, fieldname: str) -> any: """Get a specific field value of a document. Returns None if document does not exist.""" - print(f"DEBUG: Getting value of field {fieldname} from Address {docname}") - if not AddressService.exists(docname): + print(f"DEBUG: Getting value of field {fieldname} from Address {doc_name}") + if not AddressService.exists(doc_name): print("DEBUG: Value cannot be retrieved; Address does not exist.") return None - value = frappe.db.get_value("Address", docname, fieldname) + value = frappe.db.get_value("Address", doc_name, fieldname) print(f"DEBUG: Retrieved value: {value}") return value @staticmethod - def get_value_or_throw(docname: str, fieldname: str): + def get_value_or_throw(doc_name: str, fieldname: str) -> any: """Get a specific field value of a document or throw an error if document does not exist.""" - value = AddressService.get_value(docname, fieldname) + value = AddressService.get_value(doc_name, fieldname) if value is not None: return value - raise ValueError(f"Address with name {docname} does not exist.") + raise ValueError(f"Address with name {doc_name} does not exist.") @staticmethod - def create(address_data: dict): + def create(address_data: dict) -> Document: """Create a new address.""" print("DEBUG: Creating new Address with data:", address_data) address = frappe.get_doc({ @@ -114,26 +123,34 @@ class AddressService: return address @staticmethod - def link_address_to_customer(address_doc, customer_type, customer_name): + def link_address_to_customer(address_doc: Document, customer_type: str, customer_name: str): """Link an address to a customer or lead.""" print(f"DEBUG: Linking Address {address_doc.name} to {customer_type} {customer_name}") address_doc.customer_type = customer_type address_doc.customer_name = customer_name + address_doc.append("links", { + "link_doctype": customer_type, + "link_name": customer_name + }) address_doc.save(ignore_permissions=True) print(f"DEBUG: Linked Address {address_doc.name} to {customer_type} {customer_name}") @staticmethod - def link_address_to_contact(address_doc, contact_name): + def link_address_to_contact(address_doc: Document, contact_name: str): """Link an address to a contact.""" print(f"DEBUG: Linking Address {address_doc.name} to Contact {contact_name}") address_doc.append("contacts", { "contact": contact_name }) + address_doc.append("links", { + "link_doctype": "Contact", + "link_name": contact_name + }) address_doc.save(ignore_permissions=True) print(f"DEBUG: Linked Address {address_doc.name} to Contact {contact_name}") @staticmethod - def create_address(address_data): + def create_address(address_data: dict) -> Document: """Create a new address.""" address = frappe.get_doc({ "doctype": "Address", @@ -172,7 +189,7 @@ class AddressService: 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): + def get_county_and_set(address_doc: Document, 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}") @@ -196,7 +213,11 @@ class AddressService: except (KeyError, IndexError): return None - return { + county_info = { "county": county, "county_fips": county_fips - } \ No newline at end of file + } + + AddressService.update_value(address_doc.name, "county", county_info, save) + AddressService.update_value(address_doc.name, "county_fips", county_fips, save) + \ No newline at end of file diff --git a/custom_ui/services/client_service.py b/custom_ui/services/client_service.py index 165b043..3207dd2 100644 --- a/custom_ui/services/client_service.py +++ b/custom_ui/services/client_service.py @@ -1,4 +1,5 @@ import frappe +from frappe.model.document import Document from .db_service import DbService from erpnext.crm.doctype.lead.lead import make_customer from .address_service import AddressService @@ -8,6 +9,12 @@ from .onsite_meeting_service import OnSiteMeetingService class ClientService: + @staticmethod + def get_client_or_throw(client_name: str) -> Document: + """Retrieve a Client document (Customer or Lead) or throw an error if it does not exist.""" + doctype = ClientService.get_client_doctype(client_name) + return DbService.get_or_throw(doctype, client_name) + @staticmethod def get_client_doctype(client_name: str) -> str: """Determine if the client is a Customer or Lead.""" @@ -56,8 +63,9 @@ class ClientService: update_quotations: bool = True, update_addresses: bool = True, update_contacts: bool = True, - update_onsite_meetings: bool = True - ): + update_onsite_meetings: bool = True, + update_companies: bool = True + ) -> Document: """Convert a Lead to a Customer.""" print(f"DEBUG: Converting Lead {lead_name} to Customer") try: @@ -68,7 +76,7 @@ class ClientService: 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'}") - + customer_doc.custom_billing_address = lead_doc.custom_billing_address print("DEBUG: Calling customer_doc.insert()") customer_doc.insert(ignore_permissions=True) print(f"DEBUG: Customer inserted successfully: {customer_doc.name}") @@ -128,6 +136,16 @@ class ClientService: 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") + if update_companies: + print(f"DEBUG: Updating companies. Count: {len(lead_doc.get('companies', []))}") + for company in lead_doc.get("companies", []): + try: + print(f"DEBUG: Processing company: {company.get('company')}") + ClientService.append_link_v2(customer_doc.name, "companies", {"company": company.get("company")}) + print(f"DEBUG: Linked company {company.get('company')} to customer") + except Exception as e: + print(f"ERROR: Failed to link company {company.get('company')}: {str(e)}") + frappe.log_error(f"Company linking error: {str(e)}", "convert_lead_to_customer") print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}") return customer_doc diff --git a/custom_ui/services/contact_service.py b/custom_ui/services/contact_service.py index 1f3b1cc..7cb3e28 100644 --- a/custom_ui/services/contact_service.py +++ b/custom_ui/services/contact_service.py @@ -1,10 +1,11 @@ import frappe +from frappe.model.document import Document from .db_service import DbService class ContactService: @staticmethod - def create(data: dict): + def create(data: dict) -> Document: """Create a new contact.""" print("DEBUG: Creating new Contact with data:", data) contact = frappe.get_doc({ @@ -16,25 +17,33 @@ class ContactService: return contact @staticmethod - def link_contact_to_customer(contact_doc, customer_type, customer_name): + def link_contact_to_customer(contact_doc: Document, customer_type: str, customer_name: str): """Link a contact to a customer or lead.""" print(f"DEBUG: Linking Contact {contact_doc.name} to {customer_type} {customer_name}") contact_doc.customer_type = customer_type contact_doc.customer_name = customer_name + contact_doc.append("links", { + "link_doctype": customer_type, + "link_name": customer_name + }) contact_doc.save(ignore_permissions=True) print(f"DEBUG: Linked Contact {contact_doc.name} to {customer_type} {customer_name}") @staticmethod - def link_contact_to_address(contact_doc, address_name): + def link_contact_to_address(contact_doc: Document, address_name: str): """Link an address to a contact.""" print(f"DEBUG: Linking Address {address_name} to Contact {contact_doc.name}") contact_doc.append("addresses", { "address": address_name }) + contact_doc.append("links", { + "link_doctype": "Address", + "link_name": address_name + }) contact_doc.save(ignore_permissions=True) print(f"DEBUG: Linked Address {address_name} to Contact {contact_doc.name}") @staticmethod - def get_or_throw(contact_name: str): + def get_or_throw(contact_name: str) -> Document: """Retrieve a Contact document or throw an error if it does not exist.""" return DbService.get_or_throw("Contact", contact_name) \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 2699610..a6bb5e8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -394,7 +394,11 @@ class Api { static async getTaskStatusOptions() { console.log("DEBUG: API - Loading Task Status options form the backend."); const result = await this.request(FRAPPE_GET_TASKS_STATUS_OPTIONS, {}); - return result + return result; + } + + static async setTaskStatus(taskName, newStatus) { + return await this.request(FRAPPE_SET_TASK_STATUS_METHOD, { taskName, newStatus }); } // ============================================================================ diff --git a/frontend/src/components/clientSubPages/AddressInformationForm.vue b/frontend/src/components/clientSubPages/AddressInformationForm.vue index 4d0ee9a..622bfb9 100644 --- a/frontend/src/components/clientSubPages/AddressInformationForm.vue +++ b/frontend/src/components/clientSubPages/AddressInformationForm.vue @@ -55,6 +55,7 @@ :id="`isBilling-${index}`" v-model="address.isBillingAddress" :disabled="isSubmitting" + @change="handleBillingChange(index)" style="margin-top: 0" /> @@ -251,6 +252,36 @@ const formatAddressLine = (index, field, event) => { localFormData.value.addresses[index][field] = formatted; }; +const handleBillingChange = (selectedIndex) => { + // If the selected address is now checked as billing + if (localFormData.value.addresses[selectedIndex].isBillingAddress) { + // Uncheck all other addresses + localFormData.value.addresses.forEach((addr, idx) => { + if (idx !== selectedIndex) { + addr.isBillingAddress = false; + } + }); + + // Auto-select all contacts + if (contactOptions.value.length > 0) { + localFormData.value.addresses[selectedIndex].contacts = contactOptions.value.map( + (opt) => opt.value, + ); + } + + // Auto-select primary contact + if (localFormData.value.contacts && localFormData.value.contacts.length > 0) { + const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary); + if (primaryIndex !== -1) { + localFormData.value.addresses[selectedIndex].primaryContact = primaryIndex; + } else { + // Fallback to first contact if no primary found + localFormData.value.addresses[selectedIndex].primaryContact = 0; + } + } + } +}; + const handleZipcodeInput = async (index, event) => { const input = event.target.value; @@ -299,7 +330,7 @@ const handleZipcodeInput = async (index, event) => { .form-section { background: var(--surface-card); border-radius: 6px; - padding: 1rem; + padding: 0.75rem; border: 1px solid var(--surface-border); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); transition: box-shadow 0.2s ease; @@ -313,7 +344,7 @@ const handleZipcodeInput = async (index, event) => { display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--surface-border); } diff --git a/frontend/src/components/clientSubPages/ClientInformationForm.vue b/frontend/src/components/clientSubPages/ClientInformationForm.vue index cb249c7..899a986 100644 --- a/frontend/src/components/clientSubPages/ClientInformationForm.vue +++ b/frontend/src/components/clientSubPages/ClientInformationForm.vue @@ -254,7 +254,7 @@ defineExpose({ .form-section { background: var(--surface-card); border-radius: 6px; - padding: 1rem; + padding: 0.75rem; border: 1px solid var(--surface-border); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); transition: box-shadow 0.2s ease; @@ -268,7 +268,7 @@ defineExpose({ display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--surface-border); } diff --git a/frontend/src/components/clientSubPages/ContactInformationForm.vue b/frontend/src/components/clientSubPages/ContactInformationForm.vue index 5f6285e..8e0bf57 100644 --- a/frontend/src/components/clientSubPages/ContactInformationForm.vue +++ b/frontend/src/components/clientSubPages/ContactInformationForm.vue @@ -285,7 +285,7 @@ defineExpose({}); .form-section { background: var(--surface-card); border-radius: 6px; - padding: 1rem; + padding: 0.75rem; border: 1px solid var(--surface-border); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); transition: box-shadow 0.2s ease; @@ -299,7 +299,7 @@ defineExpose({}); display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 2px solid var(--surface-border); } diff --git a/frontend/src/components/clientSubPages/Overview.vue b/frontend/src/components/clientSubPages/Overview.vue index 2fa0ccb..a84f507 100644 --- a/frontend/src/components/clientSubPages/Overview.vue +++ b/frontend/src/components/clientSubPages/Overview.vue @@ -1074,8 +1074,9 @@ const handleCancel = () => { .status-cards { display: flex; - flex-wrap: wrap; - gap: 1rem; + flex-direction: column; + gap: 0.5rem; + width: 100%; } .status-card { diff --git a/frontend/src/components/pages/Estimate.vue b/frontend/src/components/pages/Estimate.vue index ffb9339..e2e6a7a 100644 --- a/frontend/src/components/pages/Estimate.vue +++ b/frontend/src/components/pages/Estimate.vue @@ -50,19 +50,30 @@ placeholder="Select a contact" :disabled="!formData.address || !isEditable" fluid - /> + > + +
Email: {{ selectedContact.emailId || "N/A" }}
Phone: {{ selectedContact.phone || "N/A" }}
Primary Contact: - {{ selectedContact.isPrimaryContact ? "Yes" : "No" }} + {{ selectedAddress?.primaryContact === selectedContact.name ? "Yes" : "No" }}
- +
{{ item.itemName }} - +
+ Quantity + +
Price: ${{ (item.standardRate || 0).toFixed(2) }} -
-
- - -
-
-
Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountAmount || 0)).toFixed(2) }} @@ -202,7 +220,7 @@ />
-

Customer Response:

@@ -296,6 +314,27 @@
+ + + + + + route.query.address || ""); const nameQuery = computed(() => route.query.name || ""); @@ -406,6 +447,7 @@ const selectedTemplate = ref(null); const showAddressModal = ref(false); const showAddItemModal = ref(false); const showConfirmationModal = ref(false); +const showDownPaymentWarningModal = ref(false); const showResponseModal = ref(false); const showSaveTemplateModal = ref(false); const addressSearchResults = ref([]); @@ -459,10 +501,20 @@ const fetchTemplates = async () => { 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(); + + // Find template by name (ID) or templateName (Label) + const matchedTemplate = templates.value.find(t => + t.name === templateParam || t.templateName === templateParam + ); + + if (matchedTemplate) { + console.log("DEBUG: Found matched template:", matchedTemplate); + selectedTemplate.value = matchedTemplate.name; + // Trigger template change to load items and project template + onTemplateChange(); + } else { + console.log("DEBUG: No matching template found for param:", templateParam); + } } } catch (error) { console.error("Error fetching templates:", error); @@ -580,8 +632,10 @@ const selectAddress = async (address) => { contactOptions.value = contacts.value.map((c) => ({ label: `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.name, value: c.name, + email: c.emailId, + phone: c.phone || c.mobileNo })); - const primary = contacts.value.find((c) => c.isPrimaryContact); + const primary = contacts.value.find((c) => c.name === selectedAddress.value.primaryContact); console.log("DEBUG: Selected address contacts:", contacts.value); const existingContactName = estimate.value ? contacts.value.find((c) => c.fullName === estimate.value.partyName)?.name || "" : null; // Check for contact query param, then existing contact, then primary, then first contact @@ -721,6 +775,19 @@ const getResponseText = (response) => { return "No response yet"; }; +const initiateSendEstimate = () => { + if (!formData.requiresHalfPayment) { + showDownPaymentWarningModal.value = true; + } else { + showConfirmationModal.value = true; + } +}; + +const proceedFromWarning = () => { + showDownPaymentWarningModal.value = false; + showConfirmationModal.value = true; +}; + const confirmAndSendEstimate = async () => { loadingStore.setLoading(true, "Sending estimate..."); const updatedEstimate = await Api.sendEstimateEmail(estimate.value.name); @@ -1265,5 +1332,38 @@ onMounted(async () => { .field-group { margin-bottom: 1rem; } + +.help-icon { + margin-left: 0.5rem; + font-size: 0.9rem; + color: #2196f3; + cursor: help; +} + +.input-wrapper { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.input-label { + font-size: 0.8rem; + color: #666; + font-weight: 500; +} + +.contact-option { + display: flex; + flex-direction: column; +} + +.contact-name { + font-weight: 500; +} + +.contact-detail { + font-size: 0.85rem; + color: #666; +} diff --git a/frontend/src/components/pages/Job.vue b/frontend/src/components/pages/Job.vue index 8cbc8a1..65e33a1 100644 --- a/frontend/src/components/pages/Job.vue +++ b/frontend/src/components/pages/Job.vue @@ -37,6 +37,7 @@ [ + { + label: "Set Status", + rowAction: true, + type: "menu", + menuItems: statusOptions.value.map((option) => ({ + label: option, + command: async (rowData) => { + console.log("Setting status for row:", rowData, "to:", option); + try { + await Api.setTaskStatus(rowData.id, option); + + // Find and update the row in the table data + const rowIndex = tableData.value.findIndex((row) => row.id === rowData.id); + if (rowIndex >= 0) { + // Update reactively + tableData.value[rowIndex].status = option; + notifications.addSuccess(`Status updated to ${option}`); + } + } catch (error) { + console.error("Error updating status:", error); + notifications.addError("Failed to update status"); + } + }, + })), + layout: { + priority: "menu", + }, + }, +]); + const handleLazyLoad = async (event) => { console.log("Task list on Job Page - handling lazy load:", event); try { @@ -191,6 +232,16 @@ const handleLazyLoad = async (event) => { onMounted(async () => { console.log("DEBUG: Query params:", route.query); + + try { + const optionsResult = await Api.getTaskStatusOptions(); + if (optionsResult && optionsResult.length > 0) { + statusOptions.value = optionsResult; + } + } catch (error) { + console.error("Error loading task status options:", error); + } + if (jobIdQuery.value) { // Viewing existing Job try { diff --git a/frontend/src/components/pages/Tasks.vue b/frontend/src/components/pages/Tasks.vue index f7a6d24..1e9515e 100644 --- a/frontend/src/components/pages/Tasks.vue +++ b/frontend/src/components/pages/Tasks.vue @@ -40,8 +40,7 @@ const showCompleted = ref(false); const statusOptions = ref([ "Open", "Working", - "Pending", - "Review", + "Pending Review", "Overdue", "Completed", "Cancelled", @@ -92,7 +91,7 @@ const tableActions = [ console.log("Setting status for row:", rowData, "to:", option); try { // Uncomment when API is ready - // await Api.setTaskStatus(rowData.id, option); + await Api.setTaskStatus(rowData.id, option); // Find and update the row in the table data const rowIndex = tableData.value.findIndex(row => row.id === rowData.id);