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