import ApiUtils from "./apiUtils"; 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"; // 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"; // Job methods const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.get_jobs"; const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.jobs.upsert_job"; // Invoice methods const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.invoices.upsert_invoice"; // Warranty methods const FRAPPE_GET_WARRANTY_CLAIMS_METHOD = "custom_ui.api.db.warranties.get_warranty_claims"; // On-Site Meeting methods const FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD = "custom_ui.api.db.onsite_meetings.get_week_onsite_meetings"; const FRAPPE_GET_ONSITE_MEETINGS_METHOD = "custom_ui.api.db.onsite_meetings.get_onsite_meetings"; // Address methods const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses"; // Client methods const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client"; const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts"; const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_clients_table_data"; const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client"; const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names"; class Api { static async request(frappeMethod, args = {}) { const errorStore = useErrorStore(); args = ApiUtils.toSnakeCaseObject(args); const request = { method: frappeMethod, args }; console.log("DEBUG: API - Request Args: ", request); try { let response = await frappe.call(request); response = ApiUtils.toCamelCaseObject(response); response.method = frappeMethod; console.log("DEBUG: API - Request Response: ", response); if (response.message.status && response.message.status === "error") { throw new Error(response.message.message); } return response.message.data; } catch (error) { console.error("ERROR: API - Request Error: ", error); errorStore.setApiError("Frappe API", error.message || "API request error"); throw error; } } static async getAddressByFullAddress(fullAddress) { return await this.request("custom_ui.api.db.addresses.get_address_by_full_address", { full_address: fullAddress, }); } static async getQuotationItems() { return await this.request("custom_ui.api.db.estimates.get_quotation_items"); } static async getEstimateFromAddress(fullAddress) { return await this.request("custom_ui.api.db.estimates.get_estimate_from_address", { full_address: fullAddress, }); } static async getAddress(fullAddress) { return await this.request("custom_ui.api.db.addresses.get_address", { fullAddress }); } static async getContactsForAddress(fullAddress) { return await this.request("custom_ui.api.db.addresses.get_contacts_for_address", { fullAddress, }); } static async getEstimate(estimateName) { return await this.request("custom_ui.api.db.estimates.get_estimate", { estimate_name: estimateName, }); } static async getEstimateItems() { return await this.request("custom_ui.api.db.estimates.get_estimate_items"); } static async searchAddresses(searchTerm) { const filters = { full_address: ["like", `%${searchTerm}%`], }; return await this.getAddresses(["full_address"], filters); } static async getAddresses(fields = ["*"], filters = {}) { return await this.request(FRAPPE_GET_ADDRESSES_METHOD, { fields, filters }); } static async getUnscheduledOnSiteMeetings() { return await this.request( "custom_ui.api.db.onsite_meetings.get_unscheduled_onsite_meetings", ); } static async getScheduledOnSiteMeetings(fields = ["*"], filters = {}) { return await this.request(FRAPPE_GET_ONSITE_MEETINGS_METHOD, { fields, filters }); } static async getWeekOnSiteMeetings(weekStart, weekEnd) { return await this.request(FRAPPE_GET_WEEK_ONSITE_MEETINGS_METHOD, { weekStart, weekEnd }); } static async updateOnSiteMeeting(name, data) { return await this.request("custom_ui.api.db.onsite_meetings.update_onsite_meeting", { name, data, }); } static async createOnSiteMeeting(address, notes = "") { return await this.request("custom_ui.api.db.onsite_meetings.create_onsite_meeting", { address, notes, }); } static async getClientStatusCounts(params = {}) { return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params); } static async getClientDetails(clientName) { return await this.request(FRAPPE_GET_CLIENT_METHOD, { clientName }); } 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); } return data; } static async getServiceData() { const data = DataUtils.dummyServiceData; 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 = {}; } return data; } static async getWarrantyData() { const data = await this.request(FRAPPE_GET_WARRANTY_CLAIMS_METHOD); 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; } static async getClient(clientName) { return await this.request(FRAPPE_GET_CLIENT_METHOD, { clientName }); } /** * 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 = {}, sortings = []) { const { page = 0, pageSize = 10 } = paginationParams; const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_METHOD, { filters, sortings, page: page + 1, pageSize, }); return result; } static async getPaginatedEstimateDetails(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 estimate options to backend:", options); const result = await this.request(FRAPPE_GET_ESTIMATES_METHOD, { 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(FRAPPE_GET_JOBS_METHOD, { 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(FRAPPE_GET_WARRANTY_CLAIMS_METHOD, { 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(type) { return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { type }); } static async getClientNames(clientName) { return await this.request(FRAPPE_GET_CLIENT_NAMES_METHOD, { searchTerm: clientName }); } static async searchClientNames(searchTerm) { return await this.request("custom_ui.api.db.clients.search_client_names", { searchTerm }); } static async getCompanyNames() { const companies = await this.getDocsList("Company", ["name"]); const companyNames = companies.map((company) => company.name); console.log("DEBUG: API - Fetched Company Names: ", companyNames); return companyNames; } // Create methods static async createClient(clientData) { const result = await this.request(FRAPPE_UPSERT_CLIENT_METHOD, { data: clientData }); console.log("DEBUG: API - Created/Updated Client: ", result); return result; } static async createEstimate(estimateData) { const result = await this.request(FRAPPE_UPSERT_ESTIMATE_METHOD, { data: estimateData }); return result; } static async createJob(jobData) { 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; } static async createInvoice(invoiceData) { const payload = DataUtils.toSnakeCaseObject(invoiceData); const result = await this.request(FRAPPE_UPSERT_INVOICE_METHOD, { data: payload }); console.log("DEBUG: API - Created Invoice: ", result); return result; } static async createWarranty(warrantyData) { const payload = DataUtils.toSnakeCaseObject(warrantyData); const result = await this.request(FRAPPE_UPSERT_INVOICE_METHOD, { data: payload }); console.log("DEBUG: API - Created Warranty: ", 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.map((place) => ({ city: place["place name"], state: place["state abbreviation"], })); } static async getGeocode(address) { const urlSafeAddress = encodeURIComponent(address); const url = `https://nominatim.openstreetmap.org/search?format=jsonv2&q=${urlSafeAddress}`; const response = await this.request(FRAPPE_PROXY_METHOD, { url, method: "GET", headers: { "User-Agent": "FrappeApp/1.0 (+https://yourappdomain.com)" }, }); const { lat, lon } = response[0] || {}; return { latitude: parseFloat(lat), longitude: parseFloat(lon) }; } } export default Api;