From e76bdac6072988231d798cd8fbc33aed51a59cc3 Mon Sep 17 00:00:00 2001 From: Avery <averyl@netflix.com> Date: Fri, 24 Jan 2025 11:44:05 -0800 Subject: [PATCH] Removes experimental features flag from Case Cost and adds Case Costs to the Dashboard (#5719) * Removes experimental features flag from Case Cost and adds Case Costs to the Dashboard * Add Case Cost bar chart module * Remove debugging statements. * chore(deps): bump sass-embedded in /src/dispatch/static/dispatch (#5696) Bumps [sass-embedded](https://github.com/sass/embedded-host-node) from 1.83.1 to 1.83.4. - [Changelog](https://github.com/sass/embedded-host-node/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/embedded-host-node/compare/1.83.1...1.83.4) --- updated-dependencies: - dependency-name: sass-embedded dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Avery <averyl@netflix.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../static/dispatch/src/case/EditSheet.vue | 5 +- .../static/dispatch/src/case/Table.vue | 13 +-- .../dispatch/src/case/type/NewEditSheet.vue | 2 - .../dashboard/case/CaseCostBarChartCard.vue | 108 ++++++++++++++++++ .../src/dashboard/case/CaseDialogFilter.vue | 1 + .../src/dashboard/case/CaseOverview.vue | 31 ++++- 6 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 src/dispatch/static/dispatch/src/dashboard/case/CaseCostBarChartCard.vue diff --git a/src/dispatch/static/dispatch/src/case/EditSheet.vue b/src/dispatch/static/dispatch/src/case/EditSheet.vue index 31d9d2031e50..eafe82062ac0 100644 --- a/src/dispatch/static/dispatch/src/case/EditSheet.vue +++ b/src/dispatch/static/dispatch/src/case/EditSheet.vue @@ -41,7 +41,7 @@ <v-tab key="resources"> Resources </v-tab> <v-tab key="participants"> Participants </v-tab> <v-tab key="timeline"> Timeline </v-tab> - <v-tab v-if="experimental_features" key="costs"> Cost </v-tab> + <v-tab key="costs"> Cost </v-tab> <v-tab key="workflows"> Workflows </v-tab> <v-tab key="entities"> Entities </v-tab> <v-tab key="signals"> Signals </v-tab> @@ -59,7 +59,7 @@ <v-window-item key="timeline"> <case-timeline-tab-v1 /> </v-window-item> - <v-window-item key="costs" v-if="experimental_features"> + <v-window-item key="costs"> <case-costs-tab /> </v-window-item> <v-window-item key="workflow_instances"> @@ -131,7 +131,6 @@ export default { "selected.workflow_instances", "dialogs.showEditSheet", ]), - ...mapFields("auth", ["currentUser.experimental_features"]), }, created() { diff --git a/src/dispatch/static/dispatch/src/case/Table.vue b/src/dispatch/static/dispatch/src/case/Table.vue index 52ab6ff29fb8..c3ab0e504b57 100644 --- a/src/dispatch/static/dispatch/src/case/Table.vue +++ b/src/dispatch/static/dispatch/src/case/Table.vue @@ -84,7 +84,7 @@ {{ item.project.display_name }} </v-chip> </template> - <template #item.case_costs="{ value }" v-if="auth.currentUser.experimental_features"> + <template #item.case_costs="{ value }"> <case-cost-card :case-costs="value" /> </template> <template #item.assignee="{ value }"> @@ -220,7 +220,6 @@ const showCasePage = (e, { item }) => { } function loadHeaders() { - console.log(auth.value.currentUser.experimental_features) return [ { title: "Name", value: "name", align: "left", width: "10%" }, { title: "Title", value: "title", sortable: false }, @@ -230,19 +229,13 @@ function loadHeaders() { { title: "Priority", value: "case_priority.name", sortable: true }, { title: "Project", value: "project.display_name", sortable: true }, { title: "Assignee", value: "assignee", sortable: false }, - { title: "Cost", key: "case_costs", sortable: false, experimental_features: true }, + { title: "Cost", key: "case_costs", sortable: false }, { title: "Reported At", value: "reported_at", sortable: true }, { title: "Closed At", value: "closed_at", sortable: true }, { title: "", key: "data-table-actions", sortable: false, align: "end" }, - ].filter( - (header) => !header.experimental_features || auth.value.currentUser.experimental_features - ) + ] } -watch(auth.value.currentUser.experimental_features, () => { - loadHeaders() -}) - function loadItems({ page, itemsPerPage, sortBy }) { caseManagement.value.table.options.page = page caseManagement.value.table.options.itemsPerPage = itemsPerPage diff --git a/src/dispatch/static/dispatch/src/case/type/NewEditSheet.vue b/src/dispatch/static/dispatch/src/case/type/NewEditSheet.vue index 73641b98e98a..6767f6d60023 100644 --- a/src/dispatch/static/dispatch/src/case/type/NewEditSheet.vue +++ b/src/dispatch/static/dispatch/src/case/type/NewEditSheet.vue @@ -92,7 +92,6 @@ </v-col> <v-col cols="12"> <cost-model-combobox - v-if="experimental_features" :project="project" v-model="cost_model" persistent-hint @@ -213,7 +212,6 @@ export default { ...mapFields("case_type", { default_case_type: "selected.default", }), - ...mapFields("auth", ["currentUser.experimental_features"]), }, methods: { diff --git a/src/dispatch/static/dispatch/src/dashboard/case/CaseCostBarChartCard.vue b/src/dispatch/static/dispatch/src/dashboard/case/CaseCostBarChartCard.vue new file mode 100644 index 000000000000..05f2d1c23a40 --- /dev/null +++ b/src/dispatch/static/dispatch/src/dashboard/case/CaseCostBarChartCard.vue @@ -0,0 +1,108 @@ +<template> + <dashboard-card + :loading="loading" + type="bar" + :options="chartOptions" + :series="series" + title="Cost" + /> +</template> + +<script> +import { forEach, sumBy } from "lodash" +import DashboardCard from "@/dashboard/DashboardCard.vue" +export default { + name: "CaseCostBarChartCard", + + props: { + modelValue: { + type: Object, + default: function () { + return {} + }, + }, + loading: { + type: [String, Boolean], + default: function () { + return false + }, + }, + }, + + components: { + DashboardCard, + }, + + computed: { + chartOptions() { + return { + chart: { + type: "bar", + height: 350, + animations: { + enabled: false, + }, + }, + responsive: [ + { + options: { + legend: { + position: "top", + }, + }, + }, + ], + xaxis: { + categories: this.categoryData || [], + title: { + text: "Month", + }, + }, + yaxis: { + labels: { + show: false, + formatter: function (val) { + var formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumSignificantDigits: 6, + }) + + return formatter.format(val) /* $2,500.00 */ + }, + }, + }, + fill: { + opacity: 1, + }, + legend: { + position: "top", + }, + dataLabels: { + enabled: true, + formatter: function (val) { + var formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumSignificantDigits: 6, + }) + + return formatter.format(val) /* $2,500.00 */ + }, + }, + } + }, + series() { + let series = { name: "cost", data: [] } + forEach(this.modelValue, function (value) { + series.data.push(sumBy(value, "total_cost")) + }) + + return [series] + }, + categoryData() { + return Object.keys(this.modelValue) + }, + }, +} +</script> diff --git a/src/dispatch/static/dispatch/src/dashboard/case/CaseDialogFilter.vue b/src/dispatch/static/dispatch/src/dashboard/case/CaseDialogFilter.vue index 85f9c75b6dc2..cc16341c9471 100644 --- a/src/dispatch/static/dispatch/src/dashboard/case/CaseDialogFilter.vue +++ b/src/dispatch/static/dispatch/src/dashboard/case/CaseDialogFilter.vue @@ -190,6 +190,7 @@ export default { "tags", "title", "triage_at", + "total_cost", ], } diff --git a/src/dispatch/static/dispatch/src/dashboard/case/CaseOverview.vue b/src/dispatch/static/dispatch/src/dashboard/case/CaseOverview.vue index cebf7c2e6b69..08d566688c2e 100644 --- a/src/dispatch/static/dispatch/src/dashboard/case/CaseOverview.vue +++ b/src/dispatch/static/dispatch/src/dashboard/case/CaseOverview.vue @@ -14,30 +14,37 @@ </v-row> <v-row> <!-- Widgets Start --> - <v-col cols="12" sm="6" lg="3"> + <v-col cols="12" sm="6" lg="4"> <stat-widget icon="mdi-domain" :title="toNumberString(totalCases)" sup-title="Cases" /> </v-col> - <v-col cols="12" sm="6" lg="3"> + <v-col cols="12" sm="6" lg="4"> <stat-widget icon="mdi-domain" :title="toNumberString(totalCasesTriaged)" sup-title="Cases Triaged" /> </v-col> - <v-col cols="12" sm="6" lg="3"> + <v-col cols="12" sm="6" lg="4"> <stat-widget icon="mdi-domain" :title="toNumberString(totalCasesEscalated)" sup-title="Cases Escalated" /> </v-col> - <v-col cols="12" sm="6" lg="3"> + <v-col cols="12" sm="6" lg="6"> <stat-widget icon="mdi-clock" :title="toNumberString(totalHours)" sup-title="Total Hours (New to Closed)" /> </v-col> + <v-col cols="12" sm="6" lg="6"> + <stat-widget + icon="mdi-currency-usd" + :title="toUSD(totalCasesCost)" + sup-title="Total Cases Cost" + /> + </v-col> <!-- Widgets Ends --> <!-- Statistics Start --> <v-col cols="12"> @@ -73,6 +80,9 @@ <v-col cols="12" sm="6"> <case-new-closed-average-time-card v-model="groupedItems" :loading="loading" /> </v-col> + <v-col cols="12"> + <case-cost-bar-chart-card v-model="groupedItems" :loading="loading" /> + </v-col> <!-- Statistics Ends --> </v-row> </v-container> @@ -81,11 +91,12 @@ <script> import { groupBy, sumBy } from "lodash" import { mapFields } from "vuex-map-fields" -import { toNumberString } from "@/filters" +import { toNumberString, toUSD } from "@/filters" import differenceInHours from "date-fns/differenceInHours" import parseISO from "date-fns/parseISO" +import CaseCostBarChartCard from "@/dashboard/case/CaseCostBarChartCard.vue" import CaseDialogFilter from "@/dashboard/case/CaseDialogFilter.vue" import CaseEscalatedClosedAverageTimeCard from "@/dashboard/case/CaseEscalatedClosedAverageTimeCard.vue" import CaseNewClosedAverageTimeCard from "@/dashboard/case/CaseNewClosedAverageTimeCard.vue" @@ -100,6 +111,7 @@ export default { name: "CaseDashboard", components: { + CaseCostBarChartCard, CaseDialogFilter, CaseEscalatedClosedAverageTimeCard, CaseNewClosedAverageTimeCard, @@ -122,7 +134,7 @@ export default { }, setup() { - return { toNumberString } + return { toNumberString, toUSD } }, methods: { @@ -228,6 +240,13 @@ export default { return differenceInHours(parseISO(endTime), parseISO(item.reported_at)) }) }, + totalCasesCost() { + let total_cost = sumBy(this.items, "total_cost") + return total_cost ? total_cost : 0 + }, + averageCaseCost() { + return this.totalCasesCost / this.totalCases + }, defaultUserProjects: { get() { let d = null