diff --git a/custom_ui/api/db.py b/custom_ui/api/db.py index 6593df3..cda2b9c 100644 --- a/custom_ui/api/db.py +++ b/custom_ui/api/db.py @@ -1,5 +1,6 @@ import frappe, json, re from datetime import datetime, date +from custom_ui.db_utils import process_filters, process_sorting @frappe.whitelist() def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None): @@ -89,6 +90,7 @@ def get_client(client_name): ]) projects = [frappe.get_doc("Project", proj["name"]) for proj in project_names] + projects_data = [] customer = frappe.get_doc("Customer", customer_name) # get all associated data as needed return { @@ -99,110 +101,67 @@ def get_client(client_name): @frappe.whitelist() -def get_clients_table_data(options): - options = json.loads(options) - print("DEBUG: Raw options received:", options) - defaultOptions = { - "filters": {}, - "sorting": {}, - "page": 1, - "page_size": 10, - "for_table": False - } - options = {**defaultOptions, **options} - print("DEBUG: Final options:", options) - - # Map frontend field names to backend field names - def map_field_name(frontend_field): - field_mapping = { - "customerName": "customer_name", - "addressTitle": "address_title", - "addressName": "address_title", # Legacy support - "appointmentScheduledStatus": "address_title", # These are computed fields, sort by address_title - "estimateSentStatus": "address_title", - "paymentReceivedStatus": "address_title", - "jobStatus": "address_title" - } - return field_mapping.get(frontend_field, frontend_field) - - # Process filters from PrimeVue format to Frappe format - processed_filters = {} - if options["filters"]: - for field_name, filter_obj in options["filters"].items(): - if isinstance(filter_obj, dict) and "value" in filter_obj: - if filter_obj["value"] is not None and filter_obj["value"] != "": - # Map frontend field names to backend field names - if field_name == "customer_name": - mapped_field_name = "custom_customer_to_bill" - else: - mapped_field_name = field_name - - # Handle different match modes - match_mode = filter_obj.get("matchMode", "contains") - if isinstance(match_mode, str): - match_mode = match_mode.lower() - - if match_mode in ("contains", "contains"): - processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"] - elif match_mode in ("startswith", "startsWith"): - processed_filters[mapped_field_name] = ["like", f"{filter_obj['value']}%"] - elif match_mode in ("endswith", "endsWith"): - processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}"] - elif match_mode in ("equals", "equals"): - processed_filters[mapped_field_name] = filter_obj["value"] - else: - # Default to contains - processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"] - - # Process sorting - order_by = None - if options.get("sorting") and options["sorting"]: - sorting_str = options["sorting"] - if sorting_str and sorting_str.strip(): - # Parse "field_name asc/desc" format - parts = sorting_str.strip().split() - if len(parts) >= 2: - sort_field = parts[0] - sort_direction = parts[1].lower() - # Map frontend field to backend field - backend_sort_field = map_field_name(sort_field) - order_by = f"{backend_sort_field} {sort_direction}" - else: - order_by = "modified desc" - +def get_clients_table_data(filters = {}, sorting = [], page=1, page_size=10): + print("DEBUG: Raw client table options received:", { + "filters": filters, + "sorting": sorting, + "page": page, + "page_size": page_size + }) + page = int(page) + page_size = int(page_size) + processed_filters = process_filters(filters) print("DEBUG: Processed filters:", processed_filters) - print("DEBUG: Order by:", order_by) + + + processed_sortings = process_sorting(sorting) + print("DEBUG: Order by:", processed_sortings) count = frappe.db.count("Address", filters=processed_filters) print("DEBUG: Total addresses count:", count) - - addresses = frappe.db.get_all( + print("DEBUG: Filters (repr):", repr(processed_filters), "Type:", type(processed_filters)) + print("DEBUG: Order by (repr):", repr(processed_sortings), "Type:", type(processed_sortings)) + address_names = frappe.db.get_all( "Address", - fields=["name", "custom_customer_to_bill", "address_title", "custom_onsite_meeting_scheduled", "custom_estimate_sent_status", "custom_job_status", "custom_payment_received_status"], - filters=processed_filters, - limit=options["page_size"], - start=(options["page"] - 1) * options["page_size"], - order_by=order_by + fields=["name"], + filters=processed_filters if isinstance(processed_filters, dict) else None, + or_filters=processed_filters if isinstance(processed_filters, list) else None, + limit=page_size, + start=(page - 1) * page_size, + order_by=processed_sortings ) + print("DEBUG: Retrieved address names:", address_names) + + addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names] rows = [] for address in addresses: + print("DEBUG: Processing address:", address) + links = address.links + print("DEBUG: Address links:", links) + customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None + print("DEBUG: Extracted customer links:", customer_links) + customer_name = customer_links[0].link_name if customer_links else address["custom_customer_to_bill"] tableRow = {} tableRow["id"] = address["name"] - tableRow["customer_name"] = address["custom_customer_to_bill"] - tableRow["address_title"] = address["address_title"] - tableRow["appointment_scheduled_status"] = address["custom_onsite_meeting_scheduled"] - tableRow["estimate_sent_status"] = address["custom_estimate_sent_status"] - tableRow["job_status"] = address["custom_job_status"] - tableRow["payment_received_status"] = address["custom_payment_received_status"] + tableRow["customer_name"] = customer_name + tableRow["address"] = ( + f"{address['address_line1']}" + f"{' ' + address['address_line2'] if address['address_line2'] else ''} " + f"{address['city']}, {address['state']} {address['pincode']}" + ) + tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled + tableRow["estimate_sent_status"] = address.custom_estimate_sent_status + tableRow["job_status"] = address.custom_job_status + tableRow["payment_received_status"] = address.custom_payment_received_status rows.append(tableRow) return { "pagination": { "total": count, - "page": options["page"], - "page_size": options["page_size"], - "total_pages": (count + options["page_size"] - 1) // options["page_size"] + "page": page, + "page_size": page_size, + "total_pages": (count + page_size - 1) // page_size }, "data": rows } @@ -289,17 +248,6 @@ def get_jobs(options): jobs = [] tableRows = [] - # Map frontend field names to backend field names for Project doctype - def map_job_field_name(frontend_field): - field_mapping = { - "name": "name", - "customInstallationAddress": "custom_installation_address", - "customer": "customer", - "status": "status", - "percentComplete": "percent_complete" - } - return field_mapping.get(frontend_field, frontend_field) - # Process filters from PrimeVue format to Frappe format processed_filters = {} if options["filters"]: diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index ecf4701..52bb1d3 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -1,27 +1,74 @@ -def calculate_appointment_scheduled_status(on_site_meeting): - if not on_site_meeting: - return "Not Started" - return "Completed" +import json -def calculate_estimate_sent_status(quotation): - if not quotation: - return "Not Started" - if quotation["custom_sent"] == 1: - return "Completed" - return "In Progress" +def map_field_name(frontend_field): + field_mapping = { + "customer_name": "custom_customer_to_bill", + "address": "address_line1", + "appointment_scheduled_status": "custom_onsite_meeting_scheduled", + "estimate_sent_status": "custom_estimate_sent_status", + "payment_received_status": "custom_payment_received_status", + "job_status": "custom_job_status", + "installation_address": "custom_installation_address", + } + return field_mapping.get(frontend_field, frontend_field) -def calculate_payment_recieved_status(sales_invoice, payment_entries): - if not sales_invoice: - return "Not Started" - payment_sum = sum(entry["paid_amount"] for entry in payment_entries) if payment_entries else 0 - if (sales_invoice and sales_invoice["status"] == "Paid") or sales_invoice["grand_total"] <= payment_sum: - return "Completed" - return "In Progress" +def process_filters(filters): + processed_filters = {} + if filters: + filters = json.loads(filters) if isinstance(filters, str) else filters + for field_name, filter_obj in filters.items(): + if isinstance(filter_obj, dict) and "value" in filter_obj: + if filter_obj["value"] is not None and filter_obj["value"] != "": + # Map frontend field names to backend field names + address_fields = ["address_line1", "address_line2", "city", "state", "pincode"] if field_name == "address" else [] + + mapped_field_name = map_field_name(field_name) -def calculate_job_status(project, tasks=[]): - if not project or not tasks: - return "Not Started" - if any(task["status"] != "Completed" for task in tasks): - return "In Progress" - return "Completed" \ No newline at end of file + # Handle different match modes + match_mode = filter_obj.get("match_mode", "contains") + if isinstance(match_mode, str): + match_mode = match_mode.lower() + # Special handling for address to search accross multiple fields + if address_fields: + address_filters = [] + for addr_field in address_fields: + if match_mode in ("contains", "contains"): + address_filters.append([addr_field, "like", f"%{filter_obj['value']}%"]) + elif match_mode in ("startswith", "starts_with"): + address_filters.append([addr_field, "like", f"{filter_obj['value']}%"]) + elif match_mode in ("endswith", "ends_with"): + address_filters.append([addr_field, "like", f"%{filter_obj['value']}"]) + elif match_mode in ("equals", "equals"): + address_filters.append([addr_field, "=", filter_obj["value"]]) + else: + address_filters.append([addr_field, "like", f"%{filter_obj['value']}%"]) + processed_filters = address_filters + + continue # Skip the rest of the loop for address field + + if match_mode in ("contains", "contains"): + processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"] + elif match_mode in ("startswith", "starts_with"): + processed_filters[mapped_field_name] = ["like", f"{filter_obj['value']}%"] + elif match_mode in ("endswith", "ends_with"): + processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}"] + elif match_mode in ("equals", "equals"): + processed_filters[mapped_field_name] = filter_obj["value"] + else: + # Default to contains + processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"] + return processed_filters + +def process_sorting(sortings): + sortings = json.loads(sortings) if isinstance(sortings, str) else sortings + order_by = "" + if sortings and len(sortings) > 0: + for sorting in sortings: + mapped_field = map_field_name(sorting[0].strip()) + sort_direction = sorting[1].strip().lower() + order_by += f"{mapped_field} {sort_direction}, " + order_by = order_by.rstrip(", ") + else: + order_by = "modified desc" + return order_by \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index c549762..771720c 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -32,7 +32,7 @@ class Api { } static async getClientDetails(clientName) { - return await this.request(FRAPPE_GET_CLIENT_DETAILS_METHOD); + return await this.request(FRAPPE_GET_CLIENT_DETAILS_METHOD, { clientName }); } static async getJobDetails() { @@ -107,24 +107,14 @@ class Api { * @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, + static async getPaginatedClientDetails(paginationParams = {}, filters = {}, sorting = []) { + const { page = 0, pageSize = 10 } = paginationParams; + const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_METHOD, { filters, - sorting: - actualSortField && actualSortOrder - ? `${actualSortField} ${actualSortOrder === -1 ? "desc" : "asc"}` - : null, - for_table: true, - }; - const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_METHOD, { options }); + sorting, + page: page + 1, + pageSize, + }); return result; } diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue index b6eb1d5..12c5387 100644 --- a/frontend/src/components/common/DataTable.vue +++ b/frontend/src/components/common/DataTable.vue @@ -223,7 +223,7 @@ :lazy="lazy" :totalRecords="lazy ? totalRecords : getFilteredDataLength" @page="handlePage" - @sort="triggerLazyLoad" + @sort="handleSort" @filter="handleFilter" sortMode="single" removableSort @@ -823,6 +823,52 @@ const handlePage = (event) => { }; // Handle sorting events +const handleSort = (event) => { + console.log("Sort event received:", { + sortField: event.sortField, + sortOrder: event.sortOrder, + type: typeof event.sortOrder, + event: event, + }); + + try { + // Handle removable sort - when sortOrder is null/undefined, clear sorting + if (event.sortOrder === null || event.sortOrder === undefined) { + console.log("Clearing sort due to removable sort"); + filtersStore.updateTableSorting(props.tableName, null, null); + } else { + // Update the stored sorting state immediately + filtersStore.updateTableSorting(props.tableName, event.sortField, event.sortOrder); + } + + // Reset to first page when sorting changes + currentPageState.value = { page: 0, first: 0 }; + selectedPageJump.value = ""; // Reset page jump dropdown + + if (props.lazy) { + paginationStore.clearTableCache(props.tableName); + paginationStore.resetToFirstPage(props.tableName); + + // Trigger lazy load with sort information (or no sort if cleared) + triggerLazyLoad({ + sortField: event.sortField || null, + sortOrder: event.sortOrder || null, + page: 0, + first: 0, + }); + } else { + // For non-lazy tables, trigger data refresh + triggerDataRefresh(); + } + + emit("sort-change", event); + } catch (error) { + console.error("Error in handleSort:", error); + console.error("Event that caused error:", event); + console.error("Stack trace:", error.stack); + } +}; + // Handle filter events const handleFilter = (event) => { console.log("Filter event:", event); @@ -844,9 +890,9 @@ const triggerLazyLoad = (event = {}) => { const paginationParams = paginationStore.getPaginationParams(props.tableName); const filters = filtersStore.getTableFilters(props.tableName); - // Use sort information from the event if provided (from DataTable sort event) - // Otherwise fall back to stored sorting state - const storedSorting = filtersStore.getTableSorting(props.tableName); + // Get current sorting state from store + const primarySortField = filtersStore.getPrimarySortField(props.tableName); + const primarySortOrder = filtersStore.getPrimarySortOrder(props.tableName); const lazyEvent = { first: event.first !== undefined ? event.first : paginationParams.offset, @@ -855,31 +901,14 @@ const triggerLazyLoad = (event = {}) => { sortField: event.sortField !== undefined ? event.sortField - : storedSorting.field || paginationParams.sortField, + : primarySortField || paginationParams.sortField, sortOrder: event.sortOrder !== undefined ? event.sortOrder - : storedSorting.order || paginationParams.sortOrder, + : primarySortOrder || paginationParams.sortOrder, filters: { ...filters, ...(event.filters || {}) }, }; - // If this is a sort event, update the stored sorting state - if (event.sortField !== undefined || event.sortOrder !== undefined) { - filtersStore.updateTableSorting( - props.tableName, - lazyEvent.sortField, - lazyEvent.sortOrder, - ); - - // Reset to first page when sorting changes - currentPageState.value = { page: 0, first: 0 }; - selectedPageJump.value = ""; // Reset page jump dropdown - paginationStore.clearTableCache(props.tableName); - paginationStore.resetToFirstPage(props.tableName); - lazyEvent.page = 0; - lazyEvent.first = 0; - } - console.log("Triggering lazy load with:", lazyEvent); emit("lazy-load", lazyEvent); } @@ -888,11 +917,12 @@ const triggerLazyLoad = (event = {}) => { // Trigger data refresh for non-lazy tables when filters change const triggerDataRefresh = () => { const filters = filtersStore.getTableFilters(props.tableName); - const sorting = filtersStore.getTableSorting(props.tableName); + const primarySortField = filtersStore.getPrimarySortField(props.tableName); + const primarySortOrder = filtersStore.getPrimarySortOrder(props.tableName); const refreshEvent = { filters: filters, - sortField: sorting.field, - sortOrder: sorting.order, + sortField: primarySortField, + sortOrder: primarySortOrder, page: currentPageState.value.page, first: currentPageState.value.first, rows: currentRows.value, diff --git a/frontend/src/components/pages/Clients.vue b/frontend/src/components/pages/Clients.vue index 6509eff..b28797c 100644 --- a/frontend/src/components/pages/Clients.vue +++ b/frontend/src/components/pages/Clients.vue @@ -103,7 +103,7 @@ const refreshStatusCounts = async () => { const filters = { customerName: { value: null, matchMode: FilterMatchMode.CONTAINS }, - addressTitle: { value: null, matchMode: FilterMatchMode.CONTAINS }, + address: { value: null, matchMode: FilterMatchMode.CONTAINS }, }; const columns = [ @@ -112,11 +112,11 @@ const columns = [ fieldName: "customerName", type: "text", sortable: true, - filterable: true + filterable: true, }, { label: "Address", - fieldName: "addressTitle", + fieldName: "address", type: "text", sortable: true, filterable: true, @@ -143,7 +143,7 @@ const columns = [ label: "Job Status", fieldName: "jobStatus", type: "status", - sortable: true + sortable: true, }, ]; @@ -187,7 +187,7 @@ const tableActions = [ // icon: "pi pi-download", // requiresMultipleSelection: true, // Bulk action - operates on selected rows // layout: { - // position: "right", + // position: "right"., // variant: "filled", // }, // }, @@ -229,18 +229,25 @@ const handleLazyLoad = async (event) => { try { isLoading.value = true; - // Get sorting information from filters store first (needed for cache key) - const sorting = filtersStore.getTableSorting("clients"); + // If this is a sort event, update the store first + if (event.sortField !== undefined && event.sortOrder !== undefined) { + console.log("Sort event detected - updating store with:", { + sortField: event.sortField, + sortOrder: event.sortOrder, + orderType: typeof event.sortOrder, + }); + filtersStore.updateTableSorting("clients", event.sortField, event.sortOrder); + } + + // Get sorting information from filters store in backend format + const sortingArray = filtersStore.getTableSortingForBackend("clients"); + console.log("Current sorting array for backend:", sortingArray); // Get pagination parameters const paginationParams = { page: event.page || 0, pageSize: event.rows || 10, - sortField: event.sortField, - sortOrder: event.sortOrder, - }; - - // Get filters (convert PrimeVue format to API format) + }; // Get filters (convert PrimeVue format to API format) const filters = {}; if (event.filters) { Object.keys(event.filters).forEach((key) => { @@ -252,18 +259,22 @@ const handleLazyLoad = async (event) => { // Clear cache when filters or sorting are active to ensure fresh data const hasActiveFilters = Object.keys(filters).length > 0; - const hasActiveSorting = paginationParams.sortField && paginationParams.sortOrder; + const hasActiveSorting = event.sortField && event.sortOrder; if (hasActiveFilters || hasActiveSorting) { paginationStore.clearTableCache("clients"); } + // For cache key, use primary sort field/order for compatibility + const primarySortField = filtersStore.getPrimarySortField("clients") || event.sortField; + const primarySortOrder = filtersStore.getPrimarySortOrder("clients") || event.sortOrder; + // Check cache first const cachedData = paginationStore.getCachedPage( "clients", paginationParams.page, paginationParams.pageSize, - sorting.field || paginationParams.sortField, - sorting.order || paginationParams.sortOrder, + primarySortField, + primarySortOrder, filters, ); @@ -281,10 +292,20 @@ const handleLazyLoad = async (event) => { return; } - // Call API with pagination, filters, and sorting - const result = await Api.getPaginatedClientDetails(paginationParams, filters, sorting); + // Call API with pagination, filters, and sorting in backend format + console.log("Making API call with:", { + paginationParams, + filters, + sortingArray, + }); - console.log(result); + const result = await Api.getPaginatedClientDetails( + paginationParams, + filters, + sortingArray, + ); + + console.log("API response:", result); // Update local state - extract from pagination structure tableData.value = result.data; @@ -292,13 +313,13 @@ const handleLazyLoad = async (event) => { // Update pagination store with new total paginationStore.setTotalRecords("clients", result.pagination.total); - // Cache the result + // Cache the result using primary sort for compatibility paginationStore.setCachedPage( "clients", paginationParams.page, paginationParams.pageSize, - sorting.field || paginationParams.sortField, - sorting.order || paginationParams.sortOrder, + primarySortField, + primarySortOrder, filters, { records: result.data, @@ -332,7 +353,8 @@ onMounted(async () => { // Load first page const initialPagination = paginationStore.getTablePagination("clients"); const initialFilters = filtersStore.getTableFilters("clients"); - const initialSorting = filtersStore.getTableSorting("clients"); + const primarySortField = filtersStore.getPrimarySortField("clients"); + const primarySortOrder = filtersStore.getPrimarySortOrder("clients"); // Don't load initial status counts here - let the chart component handle it // The chart will emit the initial week parameters and trigger refreshStatusCounts @@ -341,8 +363,8 @@ onMounted(async () => { page: initialPagination.page, rows: initialPagination.rows, first: initialPagination.first, - sortField: initialSorting.field || initialPagination.sortField, - sortOrder: initialSorting.order || initialPagination.sortOrder, + sortField: primarySortField || initialPagination.sortField, + sortOrder: primarySortOrder || initialPagination.sortOrder, filters: initialFilters, }); }); diff --git a/frontend/src/stores/filters.js b/frontend/src/stores/filters.js index a4b9cf6..86b331c 100644 --- a/frontend/src/stores/filters.js +++ b/frontend/src/stores/filters.js @@ -5,7 +5,7 @@ export const useFiltersStore = defineStore("filters", { state: () => ({ // Store filters by table/component name tableFilters: {}, - // Store sorting by table/component name + // Store sorting by table/component name - now supports multiple sorts as array tableSorting: {}, }), actions: { @@ -16,7 +16,43 @@ export const useFiltersStore = defineStore("filters", { // Generic method to get sorting for a specific table getTableSorting(tableName) { - return this.tableSorting[tableName] || { field: null, order: null }; + return this.tableSorting[tableName] || []; + }, + + // Get sorting in backend format: [["field", "asc/desc"], ["field", "asc/desc"]] + getTableSortingForBackend(tableName) { + const sorting = this.tableSorting[tableName] || []; + console.log("getTableSortingForBackend - raw sorting:", sorting); + + const result = sorting.map((sort) => { + const direction = sort.order === 1 ? "asc" : "desc"; + console.log("Converting sort:", { + field: sort.field, + order: sort.order, + direction, + }); + return [sort.field, direction]; + }); + + console.log("getTableSortingForBackend result:", result); + return result; + }, + + // Get primary sort field for compatibility with PrimeVue + getPrimarySortField(tableName) { + const sorting = this.tableSorting[tableName] || []; + return sorting.length > 0 ? sorting[0].field : null; + }, + + // Get primary sort order for compatibility with PrimeVue + getPrimarySortOrder(tableName) { + const sorting = this.tableSorting[tableName] || []; + if (sorting.length > 0) { + const order = sorting[0].order; + console.log("getPrimarySortOrder returning:", order, typeof order); + return order; + } + return null; }, // Generic method to update a specific filter @@ -37,13 +73,62 @@ export const useFiltersStore = defineStore("filters", { } }, - // Generic method to update sorting for a table + // Generic method to update sorting for a table (supports single sort from PrimeVue) updateTableSorting(tableName, field, order) { if (!this.tableSorting[tableName]) { - this.tableSorting[tableName] = { field: null, order: null }; + this.tableSorting[tableName] = []; } - this.tableSorting[tableName].field = field; - this.tableSorting[tableName].order = order; + + // Clear sorting if no field provided or order is null/undefined + if (!field || order === null || order === undefined) { + console.log("Clearing sort for table:", tableName); + this.tableSorting[tableName] = []; + return; + } + + // Ensure order is a number (PrimeVue uses 1 for asc, -1 for desc) + let numericOrder = order; + if (typeof order === "string") { + numericOrder = order.toLowerCase() === "asc" ? 1 : -1; + } else if (typeof order === "number") { + // Ensure it's 1 or -1 + numericOrder = order > 0 ? 1 : -1; + } else { + console.warn("Invalid sort order type:", typeof order, order); + return; + } + + console.log("updateTableSorting called with:", { + tableName, + field, + order, + numericOrder, + }); + + // Replace existing sort with new single sort (PrimeVue behavior) + this.tableSorting[tableName] = [{ field, order: numericOrder }]; + }, + + // Method to add or update a specific sort field (for multi-sort support) + addTableSort(tableName, field, order) { + if (!this.tableSorting[tableName]) { + this.tableSorting[tableName] = []; + } + + // Remove existing sort for this field + this.tableSorting[tableName] = this.tableSorting[tableName].filter( + (sort) => sort.field !== field, + ); + + // Add new sort if field and order provided + if (field && order) { + this.tableSorting[tableName].push({ field, order }); + } + }, + + // Method to set multiple sorts at once + setTableSorting(tableName, sortArray) { + this.tableSorting[tableName] = sortArray || []; }, // Method to clear all filters for a table @@ -57,9 +142,7 @@ export const useFiltersStore = defineStore("filters", { // Method to clear sorting for a table clearTableSorting(tableName) { - if (this.tableSorting[tableName]) { - this.tableSorting[tableName] = { field: null, order: null }; - } + this.tableSorting[tableName] = []; }, // Method to clear both filters and sorting for a table @@ -87,7 +170,7 @@ export const useFiltersStore = defineStore("filters", { // Method to initialize sorting for a table initializeTableSorting(tableName) { if (!this.tableSorting[tableName]) { - this.tableSorting[tableName] = { field: null, order: null }; + this.tableSorting[tableName] = []; } }, @@ -102,7 +185,7 @@ export const useFiltersStore = defineStore("filters", { // Method to check if sorting is active isSortingActive(tableName) { const sorting = this.getTableSorting(tableName); - return sorting.field !== null && sorting.order !== null; + return sorting.length > 0; }, // Method to get all table state (filters + sorting) diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 3e33878..d30fb35 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -1696,8 +1696,21 @@ class DataUtils { ]; static toSnakeCaseObject(obj) { + console.log("Converting to snake case:", obj); const newObj = Object.entries(obj).reduce((acc, [key, value]) => { const snakeKey = key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase()); + if (key === "sorting") { + value = value + ? value.map((item) => { + const [field, order] = item; + const snakeField = field.replace( + /[A-Z]/g, + (match) => "_" + match.toLowerCase(), + ); + return [snakeField, order]; + }) + : value; + } if (Array.isArray(value)) { value = value.map((item) => { return Object.prototype.toString.call(item) === "[object Object]"