attempt chart component

This commit is contained in:
Casey Wittrock 2025-11-07 19:05:11 -06:00
parent 6025a9890a
commit 80aae6f09b
7 changed files with 1253 additions and 31 deletions

View File

@ -25,27 +25,31 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N
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"))
"label": "On-Site Meeting Scheduled",
"not_started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "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"))
"label": "Estimate Sent",
"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"))
"label": "Job Status",
"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"))
"label": "Payment Received",
"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 = [
@ -54,18 +58,31 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N
job_status_counts,
payment_received_status_counts
]
categories = []
for status_dict in status_dicts:
category = {
"label": status_dict["label"],
"statuses": [
{
"color": "red",
"label": "Not Started",
"count": status_dict["not_started"]
},
{
"color": "yellow",
"label": "In Progress",
"count": status_dict["in_progress"]
},
{
"color": "green",
"label": "Completed",
"count": status_dict["completed"]
}
]
}
categories.append(category)
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
}
return categories
@frappe.whitelist()
def get_clients(options):

View File

@ -12,10 +12,12 @@
"@mdi/font": "^7.4.47",
"@primeuix/themes": "^1.2.5",
"axios": "^1.12.2",
"chart.js": "^4.5.1",
"frappe-ui": "^0.1.205",
"pinia": "^3.0.3",
"primevue": "^4.4.1",
"vue": "^3.5.22",
"vue-chartjs": "^5.3.3",
"vue-router": "^4.6.3",
"vuetify": "^3.10.7"
},
@ -738,6 +740,12 @@
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0"
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
@ -2351,6 +2359,18 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -4596,6 +4616,16 @@
}
}
},
"node_modules/vue-chartjs": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz",
"integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"vue": "^3.0.0-0 || ^2.7.0"
}
},
"node_modules/vue-router": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",

View File

@ -13,10 +13,12 @@
"@mdi/font": "^7.4.47",
"@primeuix/themes": "^1.2.5",
"axios": "^1.12.2",
"chart.js": "^4.5.1",
"frappe-ui": "^0.1.205",
"pinia": "^3.0.3",
"primevue": "^4.4.1",
"vue": "^3.5.22",
"vue-chartjs": "^5.3.3",
"vue-router": "^4.6.3",
"vuetify": "^3.10.7"
},

View File

@ -6,7 +6,7 @@ const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.upsert_client";
const FRAPPE_UPSERT_ESTIMATE_METHOD = "custom_ui.api.db.upsert_estimate";
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.upsert_job";
const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.upsert_invoice";
const FRAPPE_GET_STATUS_COUNTS_METHOD = "custom_ui.api.db.get_client_status_counts";
const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.get_client_status_counts";
class Api {
static async request(frappeMethod, args = {}) {
@ -29,8 +29,8 @@ class Api {
}
}
static async getStatusCounts() {
return;
static async getClientStatusCounts(params = {}) {
return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params);
}
static async getClientDetails(options = {}) {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,16 @@
<template>
<div class="page-container">
<H2>Client Contact List</H2>
<!-- Status Chart Section -->
<div class="chart-section">
<StatusChart
:statusData="statusCounts"
:onWeekChange="handleWeekChange"
:loading="chartLoading"
/>
</div>
<div id="filter-container" class="filter-container">
<button @click="onClick" id="add-customer-button" class="interaction-button">
Add
@ -19,8 +29,9 @@
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { onMounted, ref, watch, computed } from "vue";
import DataTable from "../common/DataTable.vue";
import StatusChart from "../common/StatusChart.vue";
import Api from "../../api";
import { FilterMatchMode } from "@primevue/core";
import { useLoadingStore } from "../../stores/loading";
@ -36,12 +47,67 @@ const modalStore = useModalStore();
const tableData = ref([]);
const totalRecords = ref(0);
const isLoading = ref(false);
const statusCounts = ref({}); // Start with empty object
const currentWeekParams = ref({});
const chartLoading = ref(true); // Start with loading state
// Computed property to get current filters for the chart
const currentFilters = computed(() => {
return filtersStore.getTableFilters("clients");
});
const onClick = () => {
//frappe.new_doc("Customer");
modalStore.openCreateClient();
};
// Handle week change from chart
const handleWeekChange = async (weekParams) => {
console.log("handleWeekChange called with:", weekParams);
currentWeekParams.value = weekParams;
await refreshStatusCounts();
};
// Refresh status counts with current week and filters
const refreshStatusCounts = async () => {
chartLoading.value = true;
try {
let params = {};
// Only apply weekly filtering if weekParams is provided (not null)
if (currentWeekParams.value) {
params = {
weekly: true,
weekStartDate: currentWeekParams.value.weekStartDate,
weekEndDate: currentWeekParams.value.weekEndDate,
};
console.log("Using weekly filter:", params);
} else {
// No weekly filtering - get all time data
params = {
weekly: false,
};
console.log("Using all-time data (no weekly filter)");
}
// Add current filters to the params
const currentFilters = filtersStore.getTableFilters("clients");
if (currentFilters && Object.keys(currentFilters).length > 0) {
params.filters = currentFilters;
}
const response = await Api.getClientStatusCounts(params);
statusCounts.value = response || {};
console.log("Status counts updated:", statusCounts.value);
} catch (error) {
console.error("Error refreshing status counts:", error);
statusCounts.value = {};
} finally {
chartLoading.value = false;
}
};
const filters = {
addressTitle: { value: null, matchMode: FilterMatchMode.CONTAINS },
};
@ -78,7 +144,6 @@ const handleLazyLoad = async (event) => {
// Get sorting information from filters store first (needed for cache key)
const sorting = filtersStore.getTableSorting("clients");
console.log("Current sorting state:", sorting);
// Get pagination parameters
const paginationParams = {
@ -176,8 +241,15 @@ const handleLazyLoad = async (event) => {
isLoading.value = false;
}
};
// Watch for filters change to update status counts
watch(
() => filtersStore.getTableFilters("clients"),
async () => {
await refreshStatusCounts();
},
{ deep: true },
);
// Load initial data
onMounted(async () => {
// Initialize pagination and filters
paginationStore.initializeTablePagination("clients", { rows: 10 });
@ -189,6 +261,9 @@ onMounted(async () => {
const initialFilters = filtersStore.getTableFilters("clients");
const initialSorting = filtersStore.getTableSorting("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
await handleLazyLoad({
page: initialPagination.page,
rows: initialPagination.rows,
@ -203,4 +278,28 @@ onMounted(async () => {
.page-container {
height: 100%;
}
.chart-section {
margin-bottom: 20px;
}
.filter-container {
margin-bottom: 15px;
}
.interaction-button {
background: #3b82f6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.interaction-button:hover {
background: #2563eb;
}
</style>

View File

@ -1710,7 +1710,6 @@ class DataUtils {
acc[snakeKey] = value;
return acc;
}, {});
console.log("DEBUG: toSnakeCaseObject -> newObj", newObj);
return newObj;
}
@ -1730,7 +1729,6 @@ class DataUtils {
acc[camelKey] = value;
return acc;
}, {});
console.log("DEBUG: toCamelCaseObject -> newObj", newObj);
return newObj;
}
}