massage data

This commit is contained in:
Casey Wittrock 2025-11-06 18:24:19 -06:00
parent 616fa1be79
commit 60d3f35988
7 changed files with 174 additions and 48 deletions

View File

@ -1,4 +1,5 @@
import frappe, json import frappe, json
from custom_ui.db_utils import calculate_appointment_scheduled_status, calculate_estimate_sent_status, calculate_payment_recieved_status, calculate_job_scheduled_status
@frappe.whitelist() @frappe.whitelist()
def get_clients(options): def get_clients(options):
@ -8,11 +9,13 @@ def get_clients(options):
"filters": {}, "filters": {},
"sorting": {}, "sorting": {},
"page": 1, "page": 1,
"page_size": 10 "page_size": 10,
"for_table": False
} }
options = {**defaultOptions, **options} options = {**defaultOptions, **options}
clients = [] clients = []
tableRows = []
count = frappe.db.count("Address", filters=options["filters"]) count = frappe.db.count("Address", filters=options["filters"])
print("DEBUG: Total addresses count:", count) print("DEBUG: Total addresses count:", count)
@ -28,29 +31,58 @@ def get_clients(options):
for address in addresses: for address in addresses:
client = {} client = {}
tableRow = {}
print("DEBUG: Processing address:", address) print("DEBUG: Processing address:", address)
on_site_meetings = frappe.db.get_all(
"On-Site Meeting",
fields=["*"],
filters={"address": address["address_title"]}
)
sales_invvoices = frappe.db.get_all(
"Sales Invoice",
fields=["*"],
filters={"custom_installation_address": address["address_title"]}
)
quotations = frappe.db.get_all( quotations = frappe.db.get_all(
"Quotation", "Quotation",
fields=["*"], fields=["*"],
filters={"custom_installation_address": address["name"]} filters={"custom_installation_address": address["address_title"]}
) )
jobs = frappe.db.get_all("Project", jobs = frappe.db.get_all(
fields=["*"], "Project",
filters={"custom_installation_address": address["name"], fields=["*"],
"project_template": "SNW Install"}) filters={
"custom_installation_address": address["address_title"],
"project_template": "SNW Install"
}
)
tableRow["id"] = address["name"]
tableRow["address_name"] = address.get("address_title", "")
tableRow["appointment_scheduled_status"] = calculate_appointment_scheduled_status(on_site_meetings[0]) if on_site_meetings else "Not Started"
tableRow["estimate_sent_status"] = calculate_estimate_sent_status(quotations[0]) if quotations else "Not Started"
tableRow["payment_received_status"] = calculate_payment_recieved_status(sales_invvoices[0]) if sales_invvoices else "Not Started"
tableRow["job_scheduled_status"] = calculate_job_scheduled_status(jobs[0]) if jobs else "Not Started"
tableRows.append(tableRow)
client["address"] = address client["address"] = address
client["on_site_meetings"] = [] client["on_site_meetings"] = on_site_meetings
client["jobs"] = jobs client["jobs"] = jobs
client["quotations"] = quotations client["quotations"] = quotations
clients.append(client) clients.append(client)
return { return {
"count": count, "pagination": {
"page": options["page"], "total": count,
"page_size": options["page_size"], "page": options["page"],
"clients": clients "page_size": options["page_size"],
"total_pages": (count + options["page_size"] - 1) // options["page_size"]
},
"data": tableRows if options["for_table"] else clients
} }
@frappe.whitelist() @frappe.whitelist()

28
custom_ui/db_utils.py Normal file
View File

@ -0,0 +1,28 @@
def calculate_appointment_scheduled_status(on_site_meeting):
if not on_site_meeting:
return "Not Started"
# if on_site_meeting["end_time"] < today:
# return "In Progress"
return "Completed"
def calculate_estimate_sent_status(quotation):
if not quotation:
return "Not Started"
if quotation["custom_sent"] == 1:
return "Completed"
return "In Progress"
def calculate_payment_recieved_status(sales_invoice):
if not sales_invoice:
return "Not Started"
if sales_invoice and sales_invoice["status"] == "Paid":
return "Completed"
return "In Progress"
def calculate_job_scheduled_status(project):
if not project:
return "Not Started"
if not project["start_time"]:
return "In Progress"
return "Completed"

View File

@ -246,23 +246,30 @@ class Api {
* Get paginated client data with filtering and sorting * Get paginated client data with filtering and sorting
* @param {Object} paginationParams - Pagination parameters from store * @param {Object} paginationParams - Pagination parameters from store
* @param {Object} filters - Filter parameters from store * @param {Object} filters - Filter parameters from store
* @returns {Promise<{data: Array, totalRecords: number}>} * @returns {Promise<{data: Array, pagination: Object}>}
*/ */
static async getPaginatedClientDetails(paginationParams = {}, filters = {}) { static async getPaginatedClientDetails(paginationParams = {}, filters = {}) {
const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams; const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams;
const options = { const options = {
page: page + 1, page: page + 1, // Backend expects 1-based pages
pageSize, page_size: pageSize,
filters, filters,
sortField, sorting: sortField ? `${sortField} ${sortOrder === -1 ? "desc" : "asc"}` : null,
sortOrder, for_table: true,
}; };
const result = await this.getClientDetails(options); const result = await this.request("custom_ui.api.db.get_clients", { options });
// Transform the response to match what the frontend expects
return { return {
...result, data: result.data,
pagination: {
total: result.pagination.total,
page: result.pagination.page,
pageSize: result.pagination.page_size,
totalPages: result.pagination.total_pages,
},
}; };
} }

View File

@ -1,6 +1,10 @@
<template lang="html"> <template lang="html">
<!-- Filter Controls Panel --> <!-- Filter Controls Panel -->
<div v-if="hasFilters" class="filter-controls-panel mb-3 p-3 bg-light rounded"> <div
v-if="hasFilters"
:key="`filter-controls-${tableName}`"
class="filter-controls-panel mb-3 p-3 bg-light rounded"
>
<div class="row g-3 align-items-end"> <div class="row g-3 align-items-end">
<div v-for="col in filterableColumns" :key="col.fieldName" class="col-md-4 col-lg-3"> <div v-for="col in filterableColumns" :key="col.fieldName" class="col-md-4 col-lg-3">
<label :for="`filter-${col.fieldName}`" class="form-label small fw-semibold"> <label :for="`filter-${col.fieldName}`" class="form-label small fw-semibold">
@ -41,7 +45,11 @@
</div> </div>
<!-- Page Jump Controls --> <!-- Page Jump Controls -->
<div v-if="totalPages > 1" class="page-controls-panel mb-3 p-2 bg-light rounded"> <div
v-if="totalPages > 1"
:key="`page-controls-${totalPages}-${getPageInfo().total}`"
class="page-controls-panel mb-3 p-2 bg-light rounded"
>
<div class="row g-3 align-items-center"> <div class="row g-3 align-items-center">
<div class="col-auto"> <div class="col-auto">
<small class="text-muted">Quick navigation:</small> <small class="text-muted">Quick navigation:</small>
@ -278,6 +286,28 @@ const filterRef = computed({
}, },
}); });
// Watch for changes in total records to update page controls reactivity
watch(
() => props.totalRecords,
(newTotal) => {
if (props.lazy && newTotal !== undefined) {
// Force reactivity update for page controls
selectedPageJump.value = "";
}
},
);
// Watch for data changes to update page controls for non-lazy tables
watch(
() => props.data,
(newData) => {
if (!props.lazy && newData) {
// Force reactivity update for page controls
selectedPageJump.value = "";
}
},
);
// Watch for filter changes to sync match mode changes // Watch for filter changes to sync match mode changes
watch( watch(
filterRef, filterRef,

View File

@ -44,26 +44,31 @@ const onClick = () => {
}; };
const filters = { const filters = {
fullName: { value: null, matchMode: FilterMatchMode.CONTAINS }, addressName: { value: null, matchMode: FilterMatchMode.CONTAINS },
}; };
const columns = [ const columns = [
{ {
label: "Name", label: "Name",
fieldName: "fullName", fieldName: "addressName",
type: "text", type: "text",
sortable: true, sortable: true,
filterable: true, filterable: true,
}, },
{ {
label: "Appt. Scheduled", label: "Appt. Scheduled",
fieldName: "appointmentStatus", fieldName: "appointmentScheduledStatus",
type: "status", type: "status",
sortable: true, sortable: true,
}, },
{ label: "Estimate Sent", fieldName: "estimateStatus", type: "status", sortable: true }, { label: "Estimate Sent", fieldName: "estimateSentStatus", type: "status", sortable: true },
{ label: "Payment Received", fieldName: "paymentStatus", type: "status", sortable: true }, {
{ label: "Job Status", fieldName: "jobStatus", type: "status", sortable: true }, label: "Payment Received",
fieldName: "paymentReceivedStatus",
type: "status",
sortable: true,
},
{ label: "Job Status", fieldName: "jobScheduledStatus", type: "status", sortable: true },
]; ];
// Handle lazy loading events from DataTable // Handle lazy loading events from DataTable
const handleLazyLoad = async (event) => { const handleLazyLoad = async (event) => {
@ -125,12 +130,19 @@ const handleLazyLoad = async (event) => {
// Call API with pagination and filters // Call API with pagination and filters
const result = await Api.getPaginatedClientDetails(paginationParams, filters); const result = await Api.getPaginatedClientDetails(paginationParams, filters);
// Update local state // Update local state - extract from pagination structure
tableData.value = result.data; tableData.value = result.data;
totalRecords.value = result.totalRecords; totalRecords.value = result.pagination.total;
// Update pagination store with new total // Update pagination store with new total
paginationStore.setTotalRecords("clients", result.totalRecords); paginationStore.setTotalRecords("clients", result.pagination.total);
console.log("Updated pagination state:", {
tableData: tableData.value.length,
totalRecords: totalRecords.value,
storeTotal: paginationStore.getTablePagination("clients").totalRecords,
storeTotalPages: paginationStore.getTotalPages("clients"),
});
// Cache the result // Cache the result
paginationStore.setCachedPage( paginationStore.setCachedPage(
@ -142,13 +154,13 @@ const handleLazyLoad = async (event) => {
filters, filters,
{ {
records: result.data, records: result.data,
totalRecords: result.totalRecords, totalRecords: result.pagination.total,
}, },
); );
console.log("Loaded from API:", { console.log("Loaded from API:", {
records: result.data.length, records: result.data.length,
total: result.totalRecords, total: result.pagination.total,
page: paginationParams.page + 1, page: paginationParams.page + 1,
}); });
} catch (error) { } catch (error) {

View File

@ -6,7 +6,7 @@ export const useFiltersStore = defineStore("filters", {
// Store filters by table/component name // Store filters by table/component name
tableFilters: { tableFilters: {
clients: { clients: {
fullName: { value: null, matchMode: FilterMatchMode.CONTAINS }, addressName: { value: null, matchMode: FilterMatchMode.CONTAINS },
}, },
jobs: { jobs: {
customer: { value: null, matchMode: FilterMatchMode.CONTAINS }, customer: { value: null, matchMode: FilterMatchMode.CONTAINS },
@ -25,8 +25,8 @@ export const useFiltersStore = defineStore("filters", {
routes: { routes: {
technician: { value: null, matchMode: FilterMatchMode.CONTAINS }, technician: { value: null, matchMode: FilterMatchMode.CONTAINS },
routeId: { value: null, matchMode: FilterMatchMode.CONTAINS }, routeId: { value: null, matchMode: FilterMatchMode.CONTAINS },
} },
} },
}), }),
actions: { actions: {
// Generic method to get filters for a specific table // Generic method to get filters for a specific table
@ -42,7 +42,7 @@ export const useFiltersStore = defineStore("filters", {
if (!this.tableFilters[tableName][fieldName]) { if (!this.tableFilters[tableName][fieldName]) {
this.tableFilters[tableName][fieldName] = { this.tableFilters[tableName][fieldName] = {
value: null, value: null,
matchMode: FilterMatchMode.CONTAINS matchMode: FilterMatchMode.CONTAINS,
}; };
} }
this.tableFilters[tableName][fieldName].value = value; this.tableFilters[tableName][fieldName].value = value;
@ -55,7 +55,7 @@ export const useFiltersStore = defineStore("filters", {
// Method to clear all filters for a table // Method to clear all filters for a table
clearTableFilters(tableName) { clearTableFilters(tableName) {
if (this.tableFilters[tableName]) { if (this.tableFilters[tableName]) {
Object.keys(this.tableFilters[tableName]).forEach(key => { Object.keys(this.tableFilters[tableName]).forEach((key) => {
this.tableFilters[tableName][key].value = null; this.tableFilters[tableName][key].value = null;
}); });
} }
@ -67,11 +67,11 @@ export const useFiltersStore = defineStore("filters", {
this.tableFilters[tableName] = {}; this.tableFilters[tableName] = {};
} }
columns.forEach(column => { columns.forEach((column) => {
if (column.filterable && !this.tableFilters[tableName][column.fieldName]) { if (column.filterable && !this.tableFilters[tableName][column.fieldName]) {
this.tableFilters[tableName][column.fieldName] = { this.tableFilters[tableName][column.fieldName] = {
value: null, value: null,
matchMode: FilterMatchMode.CONTAINS matchMode: FilterMatchMode.CONTAINS,
}; };
} }
}); });
@ -79,12 +79,12 @@ export const useFiltersStore = defineStore("filters", {
// Legacy method for backward compatibility // Legacy method for backward compatibility
setClientNameFilter(filterValue) { setClientNameFilter(filterValue) {
this.updateTableFilter('clients', 'fullName', filterValue); this.updateTableFilter("clients", "addressName", filterValue);
}, },
// Getter for legacy compatibility // Getter for legacy compatibility
get clientNameFilter() { get clientNameFilter() {
return this.tableFilters?.clients?.fullName?.value || ""; return this.tableFilters?.clients?.addressName?.value || "";
} },
}, },
}); });

View File

@ -1698,7 +1698,15 @@ class DataUtils {
static toSnakeCaseObject(obj) { static toSnakeCaseObject(obj) {
const newObj = Object.entries(obj).reduce((acc, [key, value]) => { const newObj = Object.entries(obj).reduce((acc, [key, value]) => {
const snakeKey = key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase()); const snakeKey = key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase());
value = Object.prototype.toString.call(value) === "[object Object]" ? this.toSnakeCaseObject(value) : value; if (Array.isArray(value)) {
value = value.map((item) => {
return Object.prototype.toString.call(item) === "[object Object]"
? this.toSnakeCaseObject(item)
: item;
});
} else if (Object.prototype.toString.call(value) === "[object Object]") {
value = this.toSnakeCaseObject(value);
}
acc[snakeKey] = value; acc[snakeKey] = value;
return acc; return acc;
}, {}); }, {});
@ -1709,7 +1717,16 @@ class DataUtils {
static toCamelCaseObject(obj) { static toCamelCaseObject(obj) {
const newObj = Object.entries(obj).reduce((acc, [key, value]) => { const newObj = Object.entries(obj).reduce((acc, [key, value]) => {
const camelKey = key.replace(/_([a-z])/g, (match, p1) => p1.toUpperCase()); const camelKey = key.replace(/_([a-z])/g, (match, p1) => p1.toUpperCase());
value = Object.prototype.toString.call(value) === "[object Object]" ? this.toCamelCaseObject(value) : value; // check if value is an array
if (Array.isArray(value)) {
value = value.map((item) => {
return Object.prototype.toString.call(item) === "[object Object]"
? this.toCamelCaseObject(item)
: item;
});
} else if (Object.prototype.toString.call(value) === "[object Object]") {
value = this.toCamelCaseObject(value);
}
acc[camelKey] = value; acc[camelKey] = value;
return acc; return acc;
}, {}); }, {});