From e730de3887f1702ec890751bd88a520deab20a1d Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 16:08:26 -0500 Subject: [PATCH 01/11] Changed the hardcoded ToDo for the first category name. --- frontend/src/components/common/TodoChart.vue | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/components/common/TodoChart.vue b/frontend/src/components/common/TodoChart.vue index b55b655..776aca2 100644 --- a/frontend/src/components/common/TodoChart.vue +++ b/frontend/src/components/common/TodoChart.vue @@ -36,9 +36,6 @@ const props = defineProps({ }, }); -//Constants -//const categories = ["To-do", "Completed"]; - //Reactive data const centerData = ref(null); const hoveredSegment = ref(null); @@ -113,7 +110,7 @@ const updateCenterData = () => { }; } else { centerData.value = { - label: "To-do", + label: props.categories.labels[0], value: todos, percentage: null, }; From 2fb82917b440cc0ea147890d536fc20c9fa2229d Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 16:10:49 -0500 Subject: [PATCH 02/11] Added due date logic for tasks due filters. --- custom_ui/api/db/tasks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_ui/api/db/tasks.py b/custom_ui/api/db/tasks.py index dd6835f..a08ba9b 100644 --- a/custom_ui/api/db/tasks.py +++ b/custom_ui/api/db/tasks.py @@ -1,4 +1,5 @@ import frappe +import datetime 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 @@ -46,23 +47,26 @@ def get_task_status_options(): def get_tasks_due(subject_filter, current_company): """Return the number of items due today of the type of subject_filter""" try: - + today = datetime.date.today() due_filters = { 'subject': ['like', f'%{subject_filter}%'], 'status': ['not in', ["Template", "Completed", "Cancelled"]], 'company': current_company, + 'exp_end_date': today, # Add due date filter here } completed_filters = { 'subject': ['like', f'%{subject_filter}%'], 'status': ['not in', ["Template", "Cancelled"]], 'company': current_company, + 'exp_end_date': today, # Add due date filter here } overdue_filters = { 'subject': ['like', f'%{subject_filter}%'], 'status': ['not in', ["Template", "Completed", "Cancelled"]], 'company': current_company, + 'exp_end_date': ["<", today] # Add overdue date filtering here } due_count = frappe.db.count("Task", filters=due_filters) From 91e4d47d48b5090dc68065b86adc4e54ebe5a80a Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 16:43:43 -0500 Subject: [PATCH 03/11] Added Invoice Status Select to Project DocType. --- custom_ui/install.py | 132 +++++++++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/custom_ui/install.py b/custom_ui/install.py index ab74967..fa53153 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -31,7 +31,7 @@ def after_migrate(): for doctype in doctypes_to_refresh: frappe.clear_cache(doctype=doctype) frappe.reload_doctype(doctype) - + check_and_create_holiday_list() # update_address_fields() @@ -56,7 +56,7 @@ def build_frontend(): frappe.log_error(message="No frontend directory found for custom_ui", title="Frontend Build Skipped") print(f"āš ļø Frontend directory does not exist. Skipping build. Path was {frontend_path}") return - + dist_path = os.path.join(app_root, "custom_ui", "public", "dist") should_build = True @@ -69,10 +69,10 @@ def build_frontend(): except subprocess.CalledProcessError as e: frappe.log_error(message=str(e), title="Frontend Build Failed") print(f"\nāŒ Frontend build failed: {e}\n") - + def add_custom_fields(): from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - + print("\nšŸ”§ Adding custom fields to doctypes...") try: @@ -85,7 +85,7 @@ def add_custom_fields(): print(" āœ… Added 'Service' to Address address_type options.") except Exception as e: print(f" āš ļø Failed to update Address address_type: {e}") - + custom_fields = { "Customer": [ dict( @@ -267,7 +267,7 @@ def add_custom_fields(): insert_after="full_address" ), dict( - fieldname="longitude", + fieldname="longitude", label="Longitude", fieldtype="Float", precision=8, @@ -277,7 +277,7 @@ def add_custom_fields(): fieldname="onsite_meeting_scheduled", label="On-Site Meeting Scheduled", fieldtype="Select", - options="Not Started\nIn Progress\nCompleted", + options="Not Started\nIn Progress\nCompleted", default="Not Started", insert_after="longitude" ), @@ -291,7 +291,7 @@ def add_custom_fields(): ), dict( fieldname="job_status", - label="Job Status", + label="Job Status", fieldtype="Select", options="Not Started\nIn Progress\nCompleted", default="Not Started", @@ -302,7 +302,7 @@ def add_custom_fields(): label="Payment Received Status", fieldtype="Select", options="Not Started\nIn Progress\nCompleted", - default="Not Started", + default="Not Started", insert_after="job_status" ), dict( @@ -588,7 +588,15 @@ def add_custom_fields(): fieldtype="Check", default=0, insert_after="expected_end_time" - ) + ), + dict( + fieldname="invoice_status", + label="Invoice Status", + fieldtype="Select", + default="Not Ready", + options="Not Ready\nReady to Invoice\nInvoice Created\nInvoice Sent", + insert_after="is_scheduled" + ), ], "Project Template": [ dict( @@ -616,15 +624,15 @@ def add_custom_fields(): ) ] } - + print("šŸ”§ Custom fields to check per doctype:") for key, value in custom_fields.items(): print(f" • {key}: {len(value)} fields") print(f" Total fields to check: {sum(len(v) for v in custom_fields.values())}\n") - + missing_fields = [] fields_to_update = [] - + for doctype, field_options in custom_fields.items(): meta = frappe.get_meta(doctype) for field_spec in field_options: @@ -637,7 +645,7 @@ def add_custom_fields(): if frappe.db.exists("Custom Field", custom_field_name): custom_field_doc = frappe.get_doc("Custom Field", custom_field_name) needs_update = False - + # Compare important properties for key, desired_value in field_spec.items(): if key == "fieldname": @@ -646,10 +654,10 @@ def add_custom_fields(): if current_value != desired_value: needs_update = True break - + if needs_update: fields_to_update.append((doctype, fieldname, field_spec)) - + if missing_fields: print("\nāŒ Missing custom fields:") for entry in missing_fields: @@ -658,36 +666,36 @@ def add_custom_fields(): missing_field_specs = build_missing_field_specs(custom_fields, missing_fields) create_custom_fields(missing_field_specs) print("āœ… Missing custom fields created.") - + if fields_to_update: print("\nšŸ”§ Updating custom fields with mismatched specs:") for doctype, fieldname, field_spec in fields_to_update: print(f" • {doctype}: {fieldname}") custom_field_name = f"{doctype}-{fieldname}" custom_field_doc = frappe.get_doc("Custom Field", custom_field_name) - + # Update all properties from field_spec for key, value in field_spec.items(): if key != "fieldname": setattr(custom_field_doc, key, value) - + custom_field_doc.save(ignore_permissions=True) - + frappe.db.commit() print("āœ… Custom fields updated.") - + if not missing_fields and not fields_to_update: print("āœ… All custom fields verified.") - + def update_onsite_meeting_fields(): """Update On-Site Meeting doctype fields to make start_time and end_time optional.""" print("\nšŸ”§ Updating On-Site Meeting doctype fields...") - + try: # Get the doctype doctype = frappe.get_doc("DocType", "On-Site Meeting") - + # Find and update start_time and end_time fields updated_fields = [] for field in doctype.fields: @@ -695,20 +703,20 @@ def update_onsite_meeting_fields(): if field.reqd == 1: field.reqd = 0 updated_fields.append(field.fieldname) - + if updated_fields: # Save the doctype doctype.save(ignore_permissions=True) print(f"āœ… Updated fields: {', '.join(updated_fields)} (set to not required)") else: print("āœ… Fields already configured correctly") - + print("šŸ”§ On-Site Meeting field update complete.\n") except Exception as e: print(f"āŒ Error updating On-Site Meeting fields: {str(e)}") frappe.log_error(message=str(e), title="On-Site Meeting Field Update Failed") # Don't raise - this is not critical enough to stop migration - + def update_address_fields(): quotations = frappe.get_all("Quotation", pluck="name") addresses = frappe.get_all("Address", pluck="name") @@ -728,9 +736,9 @@ def update_address_fields(): combined_doctypes.append({"doctype": "Address", "name": address}) for task in tasks: combined_doctypes.append({"doctype": "Task", "name": task}) - + print(f"\nšŸ“ Updating field values for {total_addresses} addresses, {total_quotations} quotations, {total_sales_orders} sales orders, and {total_tasks} tasks...") - + # Field update counters field_counters = { 'quotation_addresses_updated': 0, @@ -751,7 +759,7 @@ def update_address_fields(): 'contacts_updated': 0, 'tasks_updated': 0 } - + onsite_meta = frappe.get_meta("On-Site Meeting") onsite_status_field = "custom_status" if onsite_meta.has_field("custom_status") else "status" @@ -761,7 +769,7 @@ def update_address_fields(): bar_length = 30 filled_length = int(bar_length * index // total_doctypes) bar = 'ā–ˆ' * filled_length + 'ā–‘' * (bar_length - filled_length) - + # Print a three-line, refreshing progress block without adding new lines each loop progress_line = f"šŸ“Š Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_doctypes})" counters_line = f" Fields updated: {field_counters['total_field_updates']} | DocTypes updated: {field_counters['addresses_updated'] + field_counters['quotations_updated'] + field_counters['sales_orders_updated'] + field_counters['customers_updated']}" @@ -792,7 +800,7 @@ def update_address_fields(): custom_installation_address = getattr(quotation_doc, 'custom_installation_address', None) custom_job_address = getattr(quotation_doc, 'custom_job_address', None) custom_project_template = getattr(quotation_doc, 'custom_project_template', None) - + updates = {} if custom_installation_address and not custom_job_address: updates['custom_job_address'] = custom_installation_address @@ -802,15 +810,15 @@ def update_address_fields(): updates['custom_project_template'] = "SNW Install" field_counters[f"{dict_field}_project_templates_updated"] += 1 field_counters['total_field_updates'] += 1 - + if updates: frappe.db.set_value(doc['doctype'], doc['name'], updates) field_counters[f"{dict_field}s_updated"] += 1 - + if doc['doctype'] == "Address": address_doc = frappe.get_doc("Address", doc['name']) updates = {} - + # Use getattr with default values instead of direct attribute access if not getattr(address_doc, 'full_address', None): address_parts_1 = [ @@ -822,57 +830,57 @@ def update_address_fields(): address_doc.state or "", address_doc.pincode or "", ] - + full_address = ", ".join([ " ".join(filter(None, address_parts_1)), " ".join(filter(None, address_parts_2)) - ]).strip() + ]).strip() updates['full_address'] = full_address field_counters['full_address'] += 1 field_counters['total_field_updates'] += 1 - + onsite_meeting = "Not Started" estimate_sent = "Not Started" job_status = "Not Started" payment_received = "Not Started" - - + + onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address_doc.address_title}) if onsite_meetings and onsite_meetings[0]: status_value = onsite_meetings[0].get(onsite_status_field) onsite_meeting = "Completed" if status_value == "Completed" else "In Progress" - + estimates = frappe.get_all("Quotation", fields=["custom_sent", "docstatus", "custom_response"], filters={"custom_job_address": address_doc.address_title}) if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]: estimate_sent = "Completed" elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]): estimate_sent = "In Progress" - + jobs = frappe.get_all("Project", fields=["status"], filters={"custom_installation_address": address_doc.address_title, "project_template": "SNW Install"}) if jobs and jobs[0] and jobs[0]["status"] == "Completed": job_status = "Completed" elif jobs and jobs[0]: job_status = "In Progress" - + sales_invoices = frappe.get_all("Sales Invoice", fields=["outstanding_amount"], filters={"custom_installation_address": address_doc.address_title}) # payments = frappe.get_all("Payment Entry", filters={"custom_installation_address": address_doc.address_title}) if sales_invoices and sales_invoices[0] and sales_invoices[0]["outstanding_amount"] == 0: payment_received = "Completed" elif sales_invoices and sales_invoices[0]: payment_received = "In Progress" - + customer_name = getattr(address_doc, 'custom_customer_to_bill', None) links = address_doc.get("links", []) customer_links = [link for link in links if link.link_doctype == "Customer"] needs_link_update = False - + if customer_name and frappe.db.exists("Customer", customer_name): customer_doc = frappe.get_doc("Customer", customer_name) - + # Check if address needs link update if not customer_links: needs_link_update = True - + if not address_doc.name in [link.address_name for link in customer_doc.get("custom_select_address", [])]: customer_doc.append("custom_select_address", { "address_name": address_doc.name @@ -880,7 +888,7 @@ def update_address_fields(): customer_doc.save(ignore_permissions=True) field_counters['customers_updated'] += 1 field_counters['total_field_updates'] += 1 - + if getattr(address_doc, 'custom_onsite_meeting_scheduled', None) != onsite_meeting: updates['custom_onsite_meeting_scheduled'] = onsite_meeting field_counters['custom_onsite_meeting_scheduled'] += 1 @@ -897,11 +905,11 @@ def update_address_fields(): updates['custom_payment_received_status'] = payment_received field_counters['custom_payment_received_status'] += 1 field_counters['total_field_updates'] += 1 - + if updates: frappe.db.set_value("Address", doc['name'], updates) field_counters['addresses_updated'] += 1 - + # Handle address links after db.set_value to avoid timestamp mismatch if needs_link_update: # Reload the document to get the latest version @@ -922,13 +930,13 @@ def update_address_fields(): frappe.db.set_value("Task", doc["name"], "custom_property", project_address if project_address else alt_project_address) field_counters['tasks_updated'] += 1 field_counters['total_field_updates'] += 1 - - - + + + # Commit every 100 records to avoid long transactions if index % 100 == 0: frappe.db.commit() - + # Print completion summary print(f"\n\nāœ… DocType field value update completed!") print(f"šŸ“Š Summary:") @@ -950,7 +958,7 @@ def update_address_fields(): print(f" • Sales Order Addresses Updated: {field_counters['sales_order_addresses_updated']:,}") print(f" • Sales Order Project Templates Updated: {field_counters['sales_order_project_templates_updated']:,}") print("šŸ“ DocType field value updates complete.\n") - + def build_missing_field_specs(custom_fields, missing_fields): missing_field_specs = {} for entry in missing_fields: @@ -960,14 +968,14 @@ def build_missing_field_specs(custom_fields, missing_fields): if field_spec["fieldname"] == fieldname: missing_field_specs[doctype].append(field_spec) break - + return missing_field_specs def check_and_create_holiday_list(year=2026, country="US", weekly_off="Sunday"): """Check if Holiday List for the given year exists, if not create it.""" print(f"\nšŸ”§ Checking for Holiday List for {country} in {year}...") holiday_list_name = f"{country} Holidays {year}" - + if frappe.db.exists("Holiday List", holiday_list_name): print(f"āœ… Holiday List '{holiday_list_name}' already exists.") return @@ -999,16 +1007,16 @@ def check_and_create_holiday_list(year=2026, country="US", weekly_off="Sunday"): # hl.make_holiday_entries() frappe.db.commit() print(f"āœ… Holiday List '{holiday_list_name}' created successfully.") - + def get_all_sundays(year): sundays = [] d = date(year, 1, 1) - + while d.weekday() != 6: d += timedelta(days=1) - + while d.year == year: sundays.append(d) d += timedelta(days=7) - - return sundays \ No newline at end of file + + return sundays From 81f3489a24b7dce62f19bddd8354d8a46c8ac454 Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:05:30 -0500 Subject: [PATCH 04/11] Added Estimate dial API calls. --- custom_ui/api/db/estimates.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 25149e3..c3608b4 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -15,7 +15,7 @@ def get_estimate_table_data_v2(filters={}, sortings=[], page=1, page_size=10): """Get paginated estimate table data with filtering and sorting.""" print("DEBUG: Raw estimate options received:", filters, sortings, page, page_size) filters, sortings, page, page_size = DbUtils.process_query_conditions(filters, sortings, page, page_size) - + @frappe.whitelist() def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10): @@ -151,7 +151,7 @@ def send_estimate_email(estimate_name): print("DEBUG: Sending estimate email for:", estimate_name) quotation = frappe.get_doc("Quotation", estimate_name) - + if not DbService.exists("Contact", quotation.contact_person): return build_error_response("No email found for the customer.", 400) party = ContactService.get_or_throw(quotation.contact_person) @@ -414,7 +414,7 @@ def upsert_estimate(data): # estimate.customer_address = data.get("address_name") # estimate.letter_head = data.get("company") # estimate.from_onsite_meeting = data.get("onsite_meeting", None) - + # Clear existing items and add new ones estimate.items = [] for item in data.get("items", []): @@ -481,6 +481,31 @@ def upsert_estimate(data): print(f"DEBUG: Error in upsert_estimate: {str(e)}") return build_error_response(str(e), 500) + +@frappe.whitelist() +def get_unapproved_estimates_count(company): + """Get the number of unapproved estimates.""" + try: + draft_filters = {'status': "Draft", "company": company} + submitted_filters = {'status': "Submitted", "company": company} + draft_count = frappe.db.count("Quotation", filters=draft_filters) + submitted_count = frappe.db.count("Quotation", filters=submitted_filters) + return build_success_response([draft_count, submitted_count]) + except Exception as e: + return build_error_response(str(e), 500) + + +@frappe.whitelist() +def get_estimates_half_down_count(company): + """Get the number unpaid half-down estimates.""" + try: + filters = {'requires_half_payment': True, 'company': company} + count = frappe.db.count("Quotation", filters=filters) + return build_success_response([count]) + except Exception as e: + return build_error_response(str(e), 500) + + def get_estimate_history(estimate_name): """Get the history of changes for a specific estimate.""" pass From 37bd0f60a32a37481090abe640e18584e086cd82 Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:06:17 -0500 Subject: [PATCH 05/11] Added invoice dial API calls and a call to create an invoice from a sales order from a Job. --- custom_ui/api/db/invoices.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/custom_ui/api/db/invoices.py b/custom_ui/api/db/invoices.py index 4b7c3ef..b024808 100644 --- a/custom_ui/api/db/invoices.py +++ b/custom_ui/api/db/invoices.py @@ -1,11 +1,36 @@ 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 erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice # =============================================================================== # INVOICES API METHODS # =============================================================================== +@frappe.whitelist() +def create_invoice_for_job(job_name): + """Create the invoice from a sales order of a job.""" + try: + project = frappe.get_doc("Project", job_name) + sales_order = project.sales_order + invoice = make_sales_invoice(sales_order) + invoice.save() + return build_success_response(invoice.as_dict()) + except Exception as e: + return build_error_response(str(e), 500) + + +@frappe.whitelist() +def get_invoices_late_count(): + """Return Due, 30-day late, 90-day late, and Lien-worthy late accounts.""" + try: + dummy_result = [10, 4, 5, 1] + print("DEBUG: DUMMY RESULT:", dummy_result) + return build_success_response(dummy_result) + except Exception as e: + return build_error_response(str(e), 500) + + @frappe.whitelist() def get_invoice_table_data(filters={}, sortings=[], page=1, page_size=10): """Get paginated invoice table data with filtering and sorting support.""" @@ -18,7 +43,7 @@ def get_invoice_table_data(filters={}, sortings=[], page=1, page_size=10): else: count = frappe.db.count("Sales Invoice", filters=processed_filters) - print(f"DEBUG: Number of invoice returned: {count}") + print(f"DEBUG: Number of invoices returned: {count}") invoices = frappe.db.get_all( "Sales Invoice", From 00f6d69482d62c87c253370a9f7d3e4b3230e2a7 Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:07:02 -0500 Subject: [PATCH 06/11] Added API calls for the Job Dials and a column for the Invoice Status in the datatable. --- custom_ui/api/db/jobs.py | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/custom_ui/api/db/jobs.py b/custom_ui/api/db/jobs.py index 16bd48a..d296fe9 100644 --- a/custom_ui/api/db/jobs.py +++ b/custom_ui/api/db/jobs.py @@ -7,6 +7,64 @@ from frappe.utils import getdate # JOB MANAGEMENT API METHODS # =============================================================================== + +@frappe.whitelist() +def get_jobs_in_queue_count(company): + try: + filters = { + 'company': company, + 'is_scheduled': True, + } + count = frappe.db.count("Project", filters=filters) + return build_success_response([count]) + except Exception as e: + return build_error_response(str(e), 500) + + +@frappe.whitelist() +def get_jobs_in_progress_count(company): + try: + today = getdate() + filters = { + 'company': company, + 'invoice_status': 'Not Ready', + 'expected_start_date': ['<=', today], + 'expected_end_date': ['>=', today], + } + count = frappe.db.count("Project", filters=filters) + return build_success_response([count]) + except Exception as e: + return build_error_response(str(e), 500) + + +@frappe.whitelist() +def get_jobs_late_count(company): + try: + today = getdate() + filters = { + 'company': company, + 'invoice_status': 'Not Ready', + 'expected_end_date': ['<', today] + } + count = frappe.db.count("Project", filters=filters) + return build_success_response([count]) + except Exception as e: + return build_error_response(str(e), 500) + + +@frappe.whitelist() +def get_jobs_to_invoice_count(company): + try: + filters = { + 'company': company, + 'invoice_status': 'Ready to Invoice', + } + count = frappe.db.count("Project", filters=filters) + return build_success_response([count]) + except Exception as e: + return build_error_response(str(e), 500) + + @frappe.whitelist() def get_job_templates(company=None): """Get list of job (project) templates.""" @@ -19,6 +77,7 @@ def get_job_templates(company=None): except Exception as e: return build_error_response(str(e), 500) + @frappe.whitelist() def create_job_from_sales_order(sales_order_name): """Create a Job (Project) from a given Sales Order""" @@ -152,9 +211,10 @@ def get_jobs_table_data(filters={}, sortings=[], page=1, page_size=10): tableRow = {} tableRow["id"] = project["name"] tableRow["name"] = project["name"] - tableRow["installation_address"] = project.get("custom_installation_address", "") + tableRow["job_address"] = project["job_address"] tableRow["customer"] = project.get("customer", "") tableRow["status"] = project.get("status", "") + tableRow["invoice_status"] = project.get("invoice_status") tableRow["percent_complete"] = project.get("percent_complete", 0) tableRows.append(tableRow) @@ -203,7 +263,7 @@ def get_projects_for_calendar(start_date, end_date, company=None, project_templa filters["project_template"] = ["in", project_templates] unscheduled_filters = filters.copy() unscheduled_filters["is_scheduled"] = 0 - + # add to filter for if expected_start_date is between start_date and end_date OR expected_end_date is between start_date and end_date filters["expected_start_date"] = ["<=", getdate(end_date)] filters["expected_end_date"] = [">=", getdate(start_date)] @@ -239,4 +299,4 @@ def update_job_scheduled_dates(job_name: str, new_start_date: str = None, new_en project.save() return build_success_response(project.as_dict()) except Exception as e: - return build_error_response(str(e), 500) \ No newline at end of file + return build_error_response(str(e), 500) From fbc51301f30c3d6921fec2d1308a4829f46fc94f Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:08:55 -0500 Subject: [PATCH 07/11] Added an API call for on site meetings for a dial. --- custom_ui/api/db/on_site_meetings.py | 15 +++++ custom_ui/fixtures/doctype.json | 92 ++++++++++++++-------------- 2 files changed, 61 insertions(+), 46 deletions(-) create mode 100644 custom_ui/api/db/on_site_meetings.py diff --git a/custom_ui/api/db/on_site_meetings.py b/custom_ui/api/db/on_site_meetings.py new file mode 100644 index 0000000..fc6d7fa --- /dev/null +++ b/custom_ui/api/db/on_site_meetings.py @@ -0,0 +1,15 @@ +import frappe +from custom_ui.db_utils import build_success_response, build_error_response + + +@frappe.whitelist() +def get_incomplete_bids(company): + print("Getting Incomplete Bids") + try: + filters = {'status': "Unscheduled", 'company': company} + count = frappe.db.count("On-Site Meeting", filters=filters) + print("Incomplete Bids:", count) + return build_success_response([count]) + except Exception as e: + return build_error_response(str(e), 500) + diff --git a/custom_ui/fixtures/doctype.json b/custom_ui/fixtures/doctype.json index ec218e4..2bc4b86 100644 --- a/custom_ui/fixtures/doctype.json +++ b/custom_ui/fixtures/doctype.json @@ -178,8 +178,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:23.661458", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:38.816045", "module": "Custom UI", "name": "Customer Task Link", "naming_rule": "", @@ -396,8 +396,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:23.730085", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:38.886150", "module": "Custom UI", "name": "Address Task Link", "naming_rule": "", @@ -550,8 +550,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:23.799282", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:38.953132", "module": "Custom", "name": "Lead Companies Link", "naming_rule": "", @@ -768,8 +768,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:23.867486", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.023393", "module": "Custom", "name": "Address Project Link", "naming_rule": "", @@ -986,8 +986,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:23.934979", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.093123", "module": "Custom", "name": "Address Quotation Link", "naming_rule": "", @@ -1204,8 +1204,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.001835", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.163081", "module": "Custom", "name": "Address On-Site Meeting Link", "naming_rule": "", @@ -1422,8 +1422,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.069255", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.236784", "module": "Custom", "name": "Address Sales Order Link", "naming_rule": "", @@ -1576,8 +1576,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.137117", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.305461", "module": "Custom", "name": "Contact Address Link", "naming_rule": "", @@ -1730,8 +1730,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.201857", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.376523", "module": "Custom", "name": "Lead On-Site Meeting Link", "naming_rule": "", @@ -2332,8 +2332,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.288443", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.472141", "module": "Selling", "name": "Quotation Template", "naming_rule": "", @@ -2830,8 +2830,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.372116", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.556506", "module": "Selling", "name": "Quotation Template Item", "naming_rule": "", @@ -2984,8 +2984,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.439961", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.624602", "module": "Custom UI", "name": "Customer Company Link", "naming_rule": "", @@ -3138,8 +3138,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.506463", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.690715", "module": "Custom UI", "name": "Customer Address Link", "naming_rule": "", @@ -3292,8 +3292,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.575135", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.758626", "module": "Custom UI", "name": "Customer Contact Link", "naming_rule": "", @@ -3446,8 +3446,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.644548", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.825642", "module": "Custom", "name": "Address Contact Link", "naming_rule": "", @@ -3600,8 +3600,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.712407", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.896330", "module": "Custom", "name": "Customer On-Site Meeting Link", "naming_rule": "", @@ -3754,8 +3754,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.786735", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:39.967073", "module": "Custom", "name": "Customer Project Link", "naming_rule": "", @@ -3908,8 +3908,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.856324", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:40.037175", "module": "Custom", "name": "Customer Quotation Link", "naming_rule": "", @@ -4062,8 +4062,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.924254", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:40.106626", "module": "Custom", "name": "Customer Sales Order Link", "naming_rule": "", @@ -4216,8 +4216,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:24.990458", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:40.174670", "module": "Custom", "name": "Lead Address Link", "naming_rule": "", @@ -4370,8 +4370,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:25.058368", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:40.246628", "module": "Custom", "name": "Lead Contact Link", "naming_rule": "", @@ -4524,8 +4524,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:25.124946", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:40.318639", "module": "Custom", "name": "Lead Quotation Link", "naming_rule": "", @@ -4678,8 +4678,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5a00edceb2f575ccfc93ed99c01bc0b7", - "modified": "2026-01-22 11:05:25.193024", + "migration_hash": "c40d7d206c7d4601cdb57fde7ab57c12", + "modified": "2026-01-24 11:42:40.390663", "module": "Custom", "name": "Address Company Link", "naming_rule": "", From bbe679cb4c1b7f85d910d0bec16bb617244dd6a9 Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:09:24 -0500 Subject: [PATCH 08/11] API javascript connectors. --- frontend/src/api.js | 53 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/frontend/src/api.js b/frontend/src/api.js index 9c71024..55d9149 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -5,6 +5,8 @@ import { useErrorStore } from "./stores/errors"; const ZIPPOPOTAMUS_BASE_URL = "https://api.zippopotam.us/us"; // Proxy method for external API calls const FRAPPE_PROXY_METHOD = "custom_ui.api.proxy.request"; +// On-Site Meeting methods +const FRAPPE_GET_INCOMPLETE_BIDS_METHOD = "custom_ui.api.db.on_site_meetings.get_incomplete_bids"; // Estimate methods const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.estimates.upsert_estimate"; const FRAPPE_GET_ESTIMATES_METHOD = "custom_ui.api.db.estimates.get_estimate_table_data"; @@ -14,7 +16,7 @@ const FRAPPE_LOCK_ESTIMATE_METHOD = "custom_ui.api.db.estimates.lock_estimate"; const FRAPPE_ESTIMATE_UPDATE_RESPONSE_METHOD = "custom_ui.api.db.estimates.manual_response"; const FRAPPE_GET_ESTIMATE_TEMPLATES_METHOD = "custom_ui.api.db.estimates.get_estimate_templates"; const FRAPPE_CREATE_ESTIMATE_TEMPLATE_METHOD = "custom_ui.api.db.estimates.create_estimate_template"; -const FRAPPE_GET_UNAPPROVED_ESTIMATES_COUNT_METHOD = "custom_ui.api.db.estimates.get_unnaproved_estimates_count"; +const FRAPPE_GET_UNAPPROVED_ESTIMATES_COUNT_METHOD = "custom_ui.api.db.estimates.get_unapproved_estimates_count"; const FRAPPE_GET_ESTIMATES_HALF_DOWN_COUNT_METHOD = "custom_ui.api.db.estimates.get_estimates_half_down_count"; // Job methods const FRAPPE_GET_JOB_METHOD = "custom_ui.api.db.jobs.get_job"; @@ -26,6 +28,10 @@ const FRAPPE_GET_INSTALL_PROJECTS_METHOD = "custom_ui.api.db.jobs.get_install_pr const FRAPPE_GET_JOBS_FOR_CALENDAR_METHOD = "custom_ui.api.db.jobs.get_projects_for_calendar"; const FRAPPE_GET_JOB_TEMPLATES_METHOD = "custom_ui.api.db.jobs.get_job_templates"; const FRAPPE_UPDATE_JOB_SCHEDULED_DATES_METHOD = "custom_ui.api.db.jobs.update_job_scheduled_dates"; +const FRAPPE_GET_JOBS_IN_QUEUE_METHOD = "custom_ui.api.db.jobs.get_jobs_in_queue_count"; +const FRAPPE_GET_JOBS_IN_PROGRESS_METHOD = "custom_ui.api.db.jobs.get_jobs_in_progress_count"; +const FRAPPE_GET_JOBS_LATE_METHOD = "custom_ui.api.db.jobs.get_jobs_late_count"; +const FRAPPE_GET_JOBS_TO_INVOICE_METHOD = "custom_ui.api.db.jobs.get_jobs_to_invoice_count"; // Task methods const FRAPPE_GET_TASKS_METHOD = "custom_ui.api.db.tasks.get_tasks_table_data"; const FRAPPE_GET_TASKS_STATUS_OPTIONS = "custom_ui.api.db.tasks.get_task_status_options"; @@ -34,6 +40,8 @@ const FRAPPE_GET_TASKS_DUE_METHOD = "custom_ui.api.db.tasks.get_tasks_due"; // Invoice methods const FRAPPE_GET_INVOICES_METHOD = "custom_ui.api.db.invoices.get_invoice_table_data"; const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.invoices.upsert_invoice"; +const FRAPPE_GET_INVOICES_LATE_METHOD = "custom_ui.api.db.invoices.get_invoices_late_count"; +const FRAPPE_CREATE_INVOICE_FOR_JOB = "custom_ui.api.db.invoices.create_invoice_for_job"; // Warranty methods const FRAPPE_GET_WARRANTY_CLAIMS_METHOD = "custom_ui.api.db.warranties.get_warranty_claims"; // On-Site Meeting methods @@ -240,6 +248,11 @@ class Api { return result; } + static async getIncompleteBidsCount(currentCompany) { + const result = await this.request(FRAPPE_GET_INCOMPLETE_BIDS_METHOD, { company: currentCompany }); + return result; + } + static async createEstimate(estimateData) { const result = await this.request(FRAPPE_UPSERT_ESTIMATE_METHOD, { data: estimateData }); return result; @@ -265,12 +278,12 @@ class Api { return await this.request(FRAPPE_CREATE_ESTIMATE_TEMPLATE_METHOD, { data }); } - static async getUnapprovedEstimatesCount() { - return await this.request(FRAPPE_GET_UNAPPROVED_ESTIMATES_COUNT_METHOD, {}); + static async getUnapprovedEstimatesCount(currentCompany) { + return await this.request(FRAPPE_GET_UNAPPROVED_ESTIMATES_COUNT_METHOD, {company: currentCompany}); } - static async getEstimatesHalfDownCount() { - return await this.request(FRAPPE_GET_ESTIMATES_HALF_DOWN_COUNT_METHOD, {}); + static async getEstimatesHalfDownCount(currentCompany) { + return await this.request(FRAPPE_GET_ESTIMATES_HALF_DOWN_COUNT_METHOD, {company: currentCompany}); } // ============================================================================ @@ -342,6 +355,26 @@ class Api { }); } + static async getJobsInQueueCount(currentCompany) { + return await this.request(FRAPPE_GET_JOBS_IN_QUEUE_METHOD, {company: currentCompany}); + } + + static async getJobsInProgressCount(currentCompany) { + return await this.request(FRAPPE_GET_JOBS_IN_PROGRESS_METHOD, {company: currentCompany}); + } + + static async getJobsLateCount(currentCompany) { + return await this.request(FRAPPE_GET_JOBS_LATE_METHOD, {company: currentCompany}); + } + + static async getJobsToInvoiceCount(currentCompany) { + return await this.request(FRAPPE_GET_JOBS_TO_INVOICE_METHOD, {company: currentCompany}); + } + + static async setJobCompleted(jobName) { + return await this.request(FRAPPE_SET_JOB_COMPLETE_METHOD, {jobName: jobName}); + } + static async getJob(jobName) { if (frappe.db.exists("Project", jobName)) { const result = await this.request(FRAPPE_GET_JOB_METHOD, { jobId: jobName }) @@ -491,6 +524,16 @@ class Api { return result; } + static async getInvoicesLateCount(currentCompany) { + const result = await this.request(FRAPPE_GET_INVOICES_LATE_METHOD, { company: currentCompany }); + return result; + } + + static async createInvoiceForJob(jobName) { + const result = await this.request(FRAPPE_CREATE_INVOICE_FOR_JOB, { jobName: jobName }); + return result; + } + // ============================================================================ // WARRANTY METHODS // ============================================================================ From c682ee8ccba13a922a58645f8b25f17131c8497c Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:10:42 -0500 Subject: [PATCH 09/11] Reading real data from API for dials. --- frontend/src/components/pages/Home.vue | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/pages/Home.vue b/frontend/src/components/pages/Home.vue index 48afc0f..ff40426 100644 --- a/frontend/src/components/pages/Home.vue +++ b/frontend/src/components/pages/Home.vue @@ -350,14 +350,14 @@ const chartData = ref({ hydroseed: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, permitFinalizations: {labels: ["Todo", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, warranties: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, - bids: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, - estimates: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, - halfDown: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, fifteenDayFollowups: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, lateBalances: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, backflows: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, machines: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, deliveries: {labels: ["To-do", "Completed", "Overdue"], data: [0, 0, 0], colors: defaultColors}, + bids: {labels: ["Unscheduled"], data: [0], colors: ['red']}, + estimates: {labels: ["Draft", "Submitted"], data: [0, 0], colors: ['orange', 'blue']}, + halfDown: {labels: ["Unpaid"], data: [0], colors: ['red']}, }); const notifications = useNotificationStore(); @@ -376,11 +376,17 @@ const navigateTo = (path) => { const loadChartData = async() => { chartData.value.locates.data = await Api.getTasksDue("Locate", companyStore.currentCompany); - chartData.value.permits.data = await Api.getTasksDue("Permit", companyStore.currentCompany); + chartData.value.permits.data = await Api.getTasksDue("Permit(s)", companyStore.currentCompany); chartData.value.curbing.data = await Api.getTasksDue("Curbing", companyStore.currentCompany); chartData.value.hydroseed.data = await Api.getTasksDue("Hydroseed", companyStore.currentCompany); + chartData.value.permitFinalizations.data = await Api.getTasksDue("Permit Close-out", companyStore.currentCompany); + chartData.value.warranties.data = await Api.getTasksDue("Warranty", companyStore.currentCompany); + chartData.value.fifteenDayFollowups.data = await Api.getTasksDue("15-Day QA", companyStore.currentCompany); + chartData.value.backflows.data = await Api.getTasksDue("Backflow", companyStore.currentCompany); //Uncomment below when we can check if half-down payments have/can been paid //chartData.value.estimates.data = await Api.getEstimatesHalfDownCount(); + chartData.value.bids.data = await Api.getIncompleteBidsCount(companyStore.currentCompany); + chartData.value.estimates.data = await Api.getUnapprovedEstimatesCount(companyStore.currentCompany); }; onMounted(async() => { @@ -389,7 +395,6 @@ onMounted(async() => { }); watch(() => companyStore.currentCompany, async (newCompany, oldCompany) => { - console.log("Wathcing for new Company"); await loadChartData(); }); From 5ed964b49d6a15a95df4992156610dcd50ec046e Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:11:05 -0500 Subject: [PATCH 10/11] Added a button to create an invoice. VERY PLACEHOLDER. --- frontend/src/components/pages/Job.vue | 29 +++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/pages/Job.vue b/frontend/src/components/pages/Job.vue index 5b500a6..2bdef8c 100644 --- a/frontend/src/components/pages/Job.vue +++ b/frontend/src/components/pages/Job.vue @@ -13,7 +13,7 @@ @@ -32,6 +32,26 @@ +
+ + + + +
[ }, ]); +const createInvoiceForJob = async () => { + console.log(job); + await Api.createInvoiceForJob(job.value.name); +} + const handleLazyLoad = async (event) => { console.log("Task list on Job Page - handling lazy load:", event); try { @@ -232,7 +257,7 @@ const handleLazyLoad = async (event) => { onMounted(async () => { console.log("DEBUG: Query params:", route.query); - + try { const optionsResult = await Api.getTaskStatusOptions(); if (optionsResult && optionsResult.length > 0) { From 5e192a61e18b8d89b41e216e69844d4b432a929a Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Sat, 24 Jan 2026 17:12:22 -0500 Subject: [PATCH 11/11] Reading data from APIs for dials. --- frontend/src/components/pages/Estimates.vue | 36 +++++++++++++----- frontend/src/components/pages/Invoices.vue | 27 ++++++++++--- frontend/src/components/pages/Jobs.vue | 42 ++++++++++++++++----- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/pages/Estimates.vue b/frontend/src/components/pages/Estimates.vue index c83ef9d..86dc4ce 100644 --- a/frontend/src/components/pages/Estimates.vue +++ b/frontend/src/components/pages/Estimates.vue @@ -15,8 +15,7 @@