From 3ea47162c5ecd174133e13ca68b4cf4558023491 Mon Sep 17 00:00:00 2001 From: Casey Wittrock Date: Fri, 7 Nov 2025 15:46:07 -0600 Subject: [PATCH] add whitlist metho for getting status counts --- custom_ui/api/db.py | 65 +++++++++++++++++ frontend/src/api.js | 165 ++++---------------------------------------- 2 files changed, 77 insertions(+), 153 deletions(-) diff --git a/custom_ui/api/db.py b/custom_ui/api/db.py index 1943d7f..01b0287 100644 --- a/custom_ui/api/db.py +++ b/custom_ui/api/db.py @@ -2,6 +2,71 @@ import frappe, json, re from datetime import datetime, date from custom_ui.db_utils import calculate_appointment_scheduled_status, calculate_estimate_sent_status, calculate_payment_recieved_status, calculate_job_status +@frappe.whitelist() +def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None): + # Build base filters for date range if weekly filtering is enabled + base_filters = {} + if weekly and week_start_date and week_end_date: + # Assuming you have a date field to filter by - adjust the field name as needed + # Common options: creation, modified, custom_date_field, etc. + base_filters["creation"] = ["between", [week_start_date, week_end_date]] + + # Helper function to merge base filters with status filters + def get_filters(status_field, status_value): + filters = {status_field: status_value} + filters.update(base_filters) + return filters + + def get_status_total(counts_dicts, status_field): + sum_array = [] + for counts_dict in counts_dicts: + sum_array.append(counts_dict[status_field]) + return sum(sum_array) + + + onsite_meeting_scheduled_status_counts = { + "Not Started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled_status", "Not Started")), + "In Progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled_status", "In Progress")), + "Completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled_status", "Completed")) + } + + estimate_sent_status_counts = { + "Not Started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")), + "In Progress": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "In Progress")), + "Completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed")) + } + + job_status_counts = { + "Not Started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")), + "In Progress": frappe.db.count("Address", filters=get_filters("custom_job_status", "In Progress")), + "Completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed")) + } + + payment_received_status_counts = { + "Not Started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")), + "In Progress": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "In Progress")), + "Completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed")) + } + + status_dicts = [ + onsite_meeting_scheduled_status_counts, + estimate_sent_status_counts, + job_status_counts, + payment_received_status_counts + ] + + return { + "totals": { + "not_started": get_status_total(status_dicts, "Not Started"), + "in_progress": get_status_total(status_dicts, "In Progress"), + "completed": get_status_total(status_dicts, "Completed") + }, + "onsite_meeting_scheduled_status": onsite_meeting_scheduled_status_counts, + "estimate_sent_status": estimate_sent_status_counts, + "job_status": job_status_counts, + "payment_received_status": payment_received_status_counts + } + @frappe.whitelist() def get_clients(options): options = json.loads(options) diff --git a/frontend/src/api.js b/frontend/src/api.js index 07287b7..3179085 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -30,154 +30,6 @@ class Api { static async getClientDetails(options = {}) { return await this.request("custom_ui.api.db.get_clients", { options }); - - // Handle fullName filter by searching in multiple fields - // Since fullName is constructed from customer_name + address_line1 + city + state, - // we need to search across these fields - if (filters.addressTitle && filters.addressTitle.value) { - const searchTerm = filters.addressTitle.value; - // Search in address fields - this is a simplified approach - // In a real implementation, you'd want to join with Customer table and search across all fields - addressFilters.address_line1 = ["like", `%${searchTerm}%`]; - } - - // Add any other custom filters - Object.keys(filters).forEach((key) => { - if (filters[key] && filters[key].value && key !== "fullName") { - // Map other frontend filter names to backend field names if needed - switch ( - key - // Add other filter mappings as needed - ) { - } - } - }); - - try { - // Get total count first for pagination - const totalCount = await this.getDocCount("Address", addressFilters); - - // Get paginated addresses - const addresses = await this.getDocsList( - "Address", - ["*"], - addressFilters, - page, - pageSize, - ); - - const data = []; - const processedData = []; - - // Process each address to build client details - for (const addr of addresses) { - try { - const clientDetail = {}; - - const customer = await this.getDetailedDoc( - "Customer", - addr["custom_customer_to_bill"], - ); - - const quotations = await this.getDocsList("Quotation", [], { - custom_installation_address: addr["name"], - }); - const quoteDetails = - quotations.length > 0 - ? await this.getDetailedDoc("Quotation", quotations[0]["name"]) - : null; - - const jobs = await this.getDocsList("Project", [], { - project_template: "SNW Install", - custom_installation_address: addr["name"], - }); - const jobDetails = - jobs.length > 0 - ? await this.getDetailedDoc("Project", jobs[0]["name"]) - : null; - - clientDetail.customer = customer; - clientDetail.address = addr; - clientDetail.estimate = quoteDetails; - clientDetail.job = jobDetails; - - const totalPaid = quoteDetails - ? quoteDetails.payment_schedule - ? quoteDetails.payment_schedule.reduce( - (sum, payment) => sum + (payment.paid_amount || 0), - 0, - ) - : 0 - : 0; - - const tableRow = { - id: addr.name, // Add unique ID for DataTable - fullName: `${customer.customer_name} - ${addr.address_line1}, ${addr.city} ${addr.state}`, - appointmentStatus: "not started", - estimateStatus: quoteDetails - ? quoteDetails.custom_response == "Accepted" - ? "completed" - : "in progress" - : "not started", - paymentStatus: quoteDetails - ? totalPaid < quoteDetails.grand_total - ? "in progress" - : "completed" - : "not started", - jobStatus: jobDetails - ? jobDetails.status === "Completed" - ? "completed" - : "in progress" - : "not started", - }; - - if (forTable) { - data.push(tableRow); - } else { - data.push(clientDetail); - } - processedData.push(clientDetail); - } catch (error) { - console.error(`Error processing address ${addr.name}:`, error); - // Continue with other addresses even if one fails - } - } - - // Apply client-side sorting if needed (better to do on server) - if (sortField && forTable) { - data.sort((a, b) => { - const aValue = a[sortField] || ""; - const bValue = b[sortField] || ""; - const comparison = aValue.localeCompare(bValue); - return sortOrder === -1 ? -comparison : comparison; - }); - } - - // Since we're applying filters at the database level, use the fetched data as-is - let filteredData = data; - - console.log("DEBUG: API - Fetched Client Details:", { - total: totalCount, - page: page, - pageSize: pageSize, - returned: filteredData.length, - filtersApplied: Object.keys(addressFilters).length > 0, - }); - - // Return paginated response with metadata - return { - data: filteredData, - pagination: { - page: page, - pageSize: pageSize, - total: totalCount, - totalPages: Math.ceil(totalCount / pageSize), - }, - }; - } catch (error) { - console.error("DEBUG: API - Error fetching client details:", error); - throw error; - } } static async getJobDetails() { @@ -236,8 +88,8 @@ class Api { customer: timesheet.customer, totalHours: timesheet.total_hours, status: timesheet.status, - totalPay: timesheet.total_costing_amount - } + totalPay: timesheet.total_costing_amount, + }; console.log("Timesheet Row: ", tableRow); data.push(tableRow); } @@ -346,7 +198,14 @@ class Api { * @param {Object} filters * @returns {Promise} */ - static async getDocsList(doctype, fields = [], filters = {}, page = 0, start=0, pageLength = 0) { + static async getDocsList( + doctype, + fields = [], + filters = {}, + page = 0, + start = 0, + pageLength = 0, + ) { const docs = await frappe.db.get_list(doctype, { fields, filters, @@ -414,7 +273,7 @@ class Api { static async createEstimate(estimateData) { const payload = DataUtils.toSnakeCaseObject(estimateData); - const result = await this.request(FRAPPE_UPSERT_ESTIMATE_METHOD, { data: payload} ); + const result = await this.request(FRAPPE_UPSERT_ESTIMATE_METHOD, { data: payload }); console.log("DEBUG: API - Created Estimate: ", result); return result; } @@ -423,7 +282,7 @@ class Api { const payload = DataUtils.toSnakeCaseObject(jobData); const result = await this.request(FRAPPE_UPSERT_JOB_METHOD, { data: payload }); console.log("DEBUG: API - Created Job: ", result); - return result + return result; } static async createInvoice(invoiceData) {