attempt chart component
This commit is contained in:
parent
6025a9890a
commit
80aae6f09b
@ -25,27 +25,31 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N
|
|||||||
|
|
||||||
|
|
||||||
onsite_meeting_scheduled_status_counts = {
|
onsite_meeting_scheduled_status_counts = {
|
||||||
"Not Started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled_status", "Not Started")),
|
"label": "On-Site Meeting Scheduled",
|
||||||
"In Progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled_status", "In Progress")),
|
"not_started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Not Started")),
|
||||||
"Completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled_status", "Completed"))
|
"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 = {
|
estimate_sent_status_counts = {
|
||||||
"Not Started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")),
|
"label": "Estimate Sent",
|
||||||
"In Progress": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "In Progress")),
|
"not_started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")),
|
||||||
"Completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed"))
|
"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 = {
|
job_status_counts = {
|
||||||
"Not Started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")),
|
"label": "Job Status",
|
||||||
"In Progress": frappe.db.count("Address", filters=get_filters("custom_job_status", "In Progress")),
|
"not_started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")),
|
||||||
"Completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed"))
|
"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 = {
|
payment_received_status_counts = {
|
||||||
"Not Started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")),
|
"label": "Payment Received",
|
||||||
"In Progress": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "In Progress")),
|
"not_started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")),
|
||||||
"Completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed"))
|
"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 = [
|
status_dicts = [
|
||||||
@ -54,18 +58,31 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N
|
|||||||
job_status_counts,
|
job_status_counts,
|
||||||
payment_received_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 {
|
return categories
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_clients(options):
|
def get_clients(options):
|
||||||
|
|||||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@ -12,10 +12,12 @@
|
|||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"@primeuix/themes": "^1.2.5",
|
"@primeuix/themes": "^1.2.5",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"frappe-ui": "^0.1.205",
|
"frappe-ui": "^0.1.205",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"primevue": "^4.4.1",
|
"primevue": "^4.4.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"vuetify": "^3.10.7"
|
"vuetify": "^3.10.7"
|
||||||
},
|
},
|
||||||
@ -738,6 +740,12 @@
|
|||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/@mdi/font": {
|
||||||
"version": "7.4.47",
|
"version": "7.4.47",
|
||||||
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||||
@ -2351,6 +2359,18 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"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": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.6.3",
|
"version": "4.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
||||||
|
|||||||
@ -13,10 +13,12 @@
|
|||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"@primeuix/themes": "^1.2.5",
|
"@primeuix/themes": "^1.2.5",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"frappe-ui": "^0.1.205",
|
"frappe-ui": "^0.1.205",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"primevue": "^4.4.1",
|
"primevue": "^4.4.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"vuetify": "^3.10.7"
|
"vuetify": "^3.10.7"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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_ESTIMATE_METHOD = "custom_ui.api.db.upsert_estimate";
|
||||||
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.upsert_job";
|
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.upsert_job";
|
||||||
const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.upsert_invoice";
|
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 {
|
class Api {
|
||||||
static async request(frappeMethod, args = {}) {
|
static async request(frappeMethod, args = {}) {
|
||||||
@ -29,8 +29,8 @@ class Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getStatusCounts() {
|
static async getClientStatusCounts(params = {}) {
|
||||||
return;
|
return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getClientDetails(options = {}) {
|
static async getClientDetails(options = {}) {
|
||||||
|
|||||||
1076
frontend/src/components/common/StatusChart.vue
Normal file
1076
frontend/src/components/common/StatusChart.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<H2>Client Contact List</H2>
|
<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">
|
<div id="filter-container" class="filter-container">
|
||||||
<button @click="onClick" id="add-customer-button" class="interaction-button">
|
<button @click="onClick" id="add-customer-button" class="interaction-button">
|
||||||
Add
|
Add
|
||||||
@ -19,8 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref, watch, computed } from "vue";
|
||||||
import DataTable from "../common/DataTable.vue";
|
import DataTable from "../common/DataTable.vue";
|
||||||
|
import StatusChart from "../common/StatusChart.vue";
|
||||||
import Api from "../../api";
|
import Api from "../../api";
|
||||||
import { FilterMatchMode } from "@primevue/core";
|
import { FilterMatchMode } from "@primevue/core";
|
||||||
import { useLoadingStore } from "../../stores/loading";
|
import { useLoadingStore } from "../../stores/loading";
|
||||||
@ -36,12 +47,67 @@ const modalStore = useModalStore();
|
|||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
const totalRecords = ref(0);
|
const totalRecords = ref(0);
|
||||||
const isLoading = ref(false);
|
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 = () => {
|
const onClick = () => {
|
||||||
//frappe.new_doc("Customer");
|
//frappe.new_doc("Customer");
|
||||||
modalStore.openCreateClient();
|
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 = {
|
const filters = {
|
||||||
addressTitle: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
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)
|
// Get sorting information from filters store first (needed for cache key)
|
||||||
const sorting = filtersStore.getTableSorting("clients");
|
const sorting = filtersStore.getTableSorting("clients");
|
||||||
console.log("Current sorting state:", sorting);
|
|
||||||
|
|
||||||
// Get pagination parameters
|
// Get pagination parameters
|
||||||
const paginationParams = {
|
const paginationParams = {
|
||||||
@ -176,8 +241,15 @@ const handleLazyLoad = async (event) => {
|
|||||||
isLoading.value = false;
|
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 () => {
|
onMounted(async () => {
|
||||||
// Initialize pagination and filters
|
// Initialize pagination and filters
|
||||||
paginationStore.initializeTablePagination("clients", { rows: 10 });
|
paginationStore.initializeTablePagination("clients", { rows: 10 });
|
||||||
@ -189,6 +261,9 @@ onMounted(async () => {
|
|||||||
const initialFilters = filtersStore.getTableFilters("clients");
|
const initialFilters = filtersStore.getTableFilters("clients");
|
||||||
const initialSorting = filtersStore.getTableSorting("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({
|
await handleLazyLoad({
|
||||||
page: initialPagination.page,
|
page: initialPagination.page,
|
||||||
rows: initialPagination.rows,
|
rows: initialPagination.rows,
|
||||||
@ -203,4 +278,28 @@ onMounted(async () => {
|
|||||||
.page-container {
|
.page-container {
|
||||||
height: 100%;
|
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>
|
</style>
|
||||||
|
|||||||
@ -1710,7 +1710,6 @@ class DataUtils {
|
|||||||
acc[snakeKey] = value;
|
acc[snakeKey] = value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
console.log("DEBUG: toSnakeCaseObject -> newObj", newObj);
|
|
||||||
return newObj;
|
return newObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1730,7 +1729,6 @@ class DataUtils {
|
|||||||
acc[camelKey] = value;
|
acc[camelKey] = value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
console.log("DEBUG: toCamelCaseObject -> newObj", newObj);
|
|
||||||
return newObj;
|
return newObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user