import DataUtils from "./utils"; const ZIPPOPOTAMUS_BASE_URL = "https://api.zippopotam.us/us"; const FRAPPE_PROXY_METHOD = "custom_ui.api.proxy.request"; const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.upsert_client"; const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.upsert_estimate"; class Api { static async request(frappeMethod, args = {}) { args = DataUtils.toSnakeCaseObject(args); console.log("DEBUG: API - Request Args: ", { method: frappeMethod, args }); try { let response = await frappe.call({ method: frappeMethod, args: { ...args, }, }); response = DataUtils.toCamelCaseObject(response); console.log("DEBUG: API - Request Response: ", response); return response.message; } catch (error) { console.error("DEBUG: API - Request Error: ", error); // Re-throw the error so calling code can handle it throw error; } } 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() { const projects = await this.getDocsList("Project"); const data = []; for (let prj of projects) { let project = await this.getDetailedDoc("Project", prj.name); const tableRow = { name: project.name, customInstallationAddress: project.custom_installation_address, customer: project.customer, status: project.status, percentComplete: project.percent_complete, }; data.push(tableRow); } console.log("DEBUG: API - getJobDetails result: ", data); return data; } static async getServiceData() { const data = DataUtils.dummyServiceData; console.log("DEBUG: API - getServiceData result: ", data); return data; } static async getRouteData() { const data = DataUtils.dummyRouteData; //const data = []; const routes = getDocList("Pre-Built Routes"); for (const rt of routes) { route = getDetailedDoc("Pre-Built Routes", rt.name); let tableRow = {}; } console.log("DEBUG: API - getRouteData result: ", data); return data; } static async getWarrantyData() { const data = DataUtils.dummyWarrantyData; console.log("DEBUG: API - getWarrantyData result: ", data); return data; } static async getTimesheetData() { //const data = DataUtils.dummyTimesheetData; const data = []; const timesheets = await this.getDocsList("Timesheet"); for (const ts of timesheets) { const timesheet = await this.getDetailedDoc("Timesheet", ts.name); const tableRow = { timesheetId: timesheet.name, employee: timesheet.employee_name, date: timesheet.date, customer: timesheet.customer, totalHours: timesheet.total_hours, status: timesheet.status, totalPay: timesheet.total_costing_amount } console.log("Timesheet Row: ", tableRow); data.push(tableRow); } console.log("DEBUG: API - getTimesheetData result: ", data); return data; } /** * Get paginated client data with filtering and sorting * @param {Object} paginationParams - Pagination parameters from store * @param {Object} filters - Filter parameters from store * @param {Object} sorting - Sorting parameters from store (optional) * @returns {Promise<{data: Array, pagination: Object}>} */ static async getPaginatedClientDetails(paginationParams = {}, filters = {}, sorting = null) { const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams; // Use sorting from the dedicated sorting parameter first, then fall back to pagination params const actualSortField = sorting?.field || sortField; const actualSortOrder = sorting?.order || sortOrder; const options = { page: page + 1, // Backend expects 1-based pages page_size: pageSize, filters, sorting: actualSortField && actualSortOrder ? `${actualSortField} ${actualSortOrder === -1 ? "desc" : "asc"}` : null, for_table: true, }; console.log("DEBUG: API - Sending options to backend:", options); const result = await this.request("custom_ui.api.db.get_clients", { options }); return result; } /** * Get paginated job data with filtering and sorting * @param {Object} paginationParams - Pagination parameters from store * @param {Object} filters - Filter parameters from store * @param {Object} sorting - Sorting parameters from store (optional) * @returns {Promise<{data: Array, pagination: Object}>} */ static async getPaginatedJobDetails(paginationParams = {}, filters = {}, sorting = null) { const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams; // Use sorting from the dedicated sorting parameter first, then fall back to pagination params const actualSortField = sorting?.field || sortField; const actualSortOrder = sorting?.order || sortOrder; const options = { page: page + 1, // Backend expects 1-based pages page_size: pageSize, filters, sorting: actualSortField && actualSortOrder ? `${actualSortField} ${actualSortOrder === -1 ? "desc" : "asc"}` : null, for_table: true, }; console.log("DEBUG: API - Sending job options to backend:", options); const result = await this.request("custom_ui.api.db.get_jobs", { options }); return result; } /** * Get paginated warranty claims data with filtering and sorting * @param {Object} paginationParams - Pagination parameters from store * @param {Object} filters - Filter parameters from store * @param {Object} sorting - Sorting parameters from store (optional) * @returns {Promise<{data: Array, pagination: Object}>} */ static async getPaginatedWarrantyData(paginationParams = {}, filters = {}, sorting = null) { const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams; // Use sorting from the dedicated sorting parameter first, then fall back to pagination params const actualSortField = sorting?.field || sortField; const actualSortOrder = sorting?.order || sortOrder; const options = { page: page + 1, // Backend expects 1-based pages page_size: pageSize, filters, sorting: actualSortField && actualSortOrder ? `${actualSortField} ${actualSortOrder === -1 ? "desc" : "asc"}` : null, for_table: true, }; console.log("DEBUG: API - Sending warranty options to backend:", options); const result = await this.request("custom_ui.api.db.get_warranty_claims", { options }); return result; } /** * Fetch a list of documents from a specific doctype. * * @param {String} doctype * @param {string[]} fields * @param {Object} filters * @returns {Promise} */ static async getDocsList(doctype, fields = [], filters = {}, page = 0, start=0, pageLength = 0) { const docs = await frappe.db.get_list(doctype, { fields, filters, start: start, limit: pageLength, }); console.log( `DEBUG: API - Fetched ${doctype} list (page ${page + 1}, start ${start}): `, docs, ); return docs; } /** * Fetch a detailed document by doctype and name. * * @param {String} doctype * @param {String} name * @param {Object} filters * @returns {Promise} */ static async getDetailedDoc(doctype, name, filters = {}) { const doc = await frappe.db.get_doc(doctype, name, filters); console.log(`DEBUG: API - Fetched Detailed ${doctype}: `, doc); return doc; } static async getDocCount(doctype, filters = {}) { const count = await frappe.db.count(doctype, filters); console.log(`DEBUG: API - Counted ${doctype}: `, count); return count; } static async createDoc(doctype, data) { const doc = await frappe.db.insert({ ...data, doctype, }); console.log(`DEBUG: API - Created ${doctype}: `, doc); return doc; } static async getCustomerNames() { const customers = await this.getDocsList("Customer", ["name"]); const customerNames = customers.map((customer) => customer.name); console.log("DEBUG: API - Fetched Customer Names: ", customerNames); return customerNames; } static async getCompanyNames() { const companies = await this.getDocsList("Company", ["name"]); console.log("DEBUG: API - Fetched Company Names: ", companyNames); return companyNames; } // Create methods static async createClient(clientData) { const payload = DataUtils.toSnakeCaseObject(clientData); const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: payload }); console.log("DEBUG: API - Created/Updated Client: ", result); return result; } static async createEstimate(estimateData) { const payload = DataUtils.toSnakeCaseObject(estimateData); const result = await this.request(FRAPPE_UPSERT_ESTIMATE_METHOD, { data: payload} ); console.log("DEBUG: API - Created Estimate: ", result); return result; } // External API calls /** * Fetch a list of places (city/state) by zipcode using Zippopotamus API. * * @param {String} zipcode * @returns {Promise} */ static async getCityStateByZip(zipcode) { const url = `${ZIPPOPOTAMUS_BASE_URL}/${zipcode}`; const response = await this.request(FRAPPE_PROXY_METHOD, { url, method: "GET" }); const { places } = response || {}; if (!places || places.length === 0) { throw new Error(`No location data found for zip code ${zipcode}`); } return places; } } export default Api;