@burdenoff/microfe-planmagnet 2026.1002.1 → 2026.1002.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-operations.d.ts +2533 -546
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +5654 -71
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +180 -162
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +100 -84
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
- package/dist/planmagnet/board/BulkActions.js +106 -55
- package/dist/planmagnet/board/BulkActions.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +10 -5
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +10 -9
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
- package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
- package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
- package/dist/planmagnet/index.d.ts +1 -1
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +4 -4
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +133 -131
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +5 -5
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +6 -6
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +107 -82
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js +174 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js +350 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js +168 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +160 -156
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts +29 -0
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts.map +1 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js +373 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js.map +1 -0
- package/dist/planmagnet/product/ProductControls.js +1 -1
- package/dist/planmagnet/product/ProductControls.js.map +1 -1
- package/dist/planmagnet/product/ProductFeatureControls.d.ts +39 -0
- package/dist/planmagnet/product/ProductFeatureControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureControls.js +315 -0
- package/dist/planmagnet/product/ProductFeatureControls.js.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts +4 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js +192 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts +52 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.js +417 -0
- package/dist/planmagnet/product/ProductLineControls.js.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts +4 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js +129 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts +4 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js +41 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js.map +1 -0
- package/dist/planmagnet/product/hierarchy.d.ts +127 -0
- package/dist/planmagnet/product/hierarchy.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchy.js +173 -0
- package/dist/planmagnet/product/hierarchy.js.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts +35 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.js +440 -0
- package/dist/planmagnet/product/hierarchyOperations.js.map +1 -0
- package/dist/planmagnet/product/refusal.d.ts +14 -0
- package/dist/planmagnet/product/refusal.d.ts.map +1 -0
- package/dist/planmagnet/product/refusal.js +10 -0
- package/dist/planmagnet/product/refusal.js.map +1 -0
- package/dist/planmagnet/product/routes.d.ts +10 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -1
- package/dist/planmagnet/product/routes.js +2 -2
- package/dist/planmagnet/product/routes.js.map +1 -1
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
- package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
- package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
- package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillChips.js +57 -0
- package/dist/planmagnet/skills/SkillChips.js.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
- package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
- package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillOperations.js +44 -0
- package/dist/planmagnet/skills/skillOperations.js.map +1 -0
- package/dist/planmagnet/skills/skillTags.d.ts +103 -0
- package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillTags.js +115 -0
- package/dist/planmagnet/skills/skillTags.js.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
- package/dist/shared/utils/i18n.d.ts +3 -0
- package/dist/shared/utils/i18n.d.ts.map +1 -1
- package/dist/shared/utils/i18n.js +1 -1
- package/dist/shared/utils/i18n.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useTr as t } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { isForbidden as n } from "../utils/graphqlErrors.js";
|
|
4
|
+
import { ErrorLine as r } from "../controls/ConfirmButton.js";
|
|
5
|
+
import { METRIC_TYPES as i, pageStateFromSearch as a, windowInput as o } from "../satisfaction/satisfaction.js";
|
|
6
|
+
import { SatisfactionFigureCards as s } from "../satisfaction/SatisfactionFigures.js";
|
|
7
|
+
import { CUSTOMER_SATISFACTION_SUMMARY as c, SATISFACTION_QUERIES as l } from "../satisfaction/satisfactionOperations.js";
|
|
8
|
+
import { ProductSelect as ee, ProjectSelect as te } from "../controls/Pickers.js";
|
|
9
|
+
import { writeUrlParam as u } from "../utils/urlParam.js";
|
|
10
|
+
import { SatisfactionReleaseSelect as d, useSatisfactionReleaseOptions as f } from "../satisfaction/SatisfactionReleaseSelect.js";
|
|
11
|
+
import { SatisfactionImportDialog as p } from "../satisfaction/SatisfactionImportDialog.js";
|
|
12
|
+
import { SatisfactionResponseDialog as m } from "../satisfaction/SatisfactionResponseDialog.js";
|
|
13
|
+
import { SatisfactionResponsesTable as h } from "../satisfaction/SatisfactionResponsesTable.js";
|
|
14
|
+
import { SatisfactionWindowPicker as g } from "../satisfaction/SatisfactionWindowPicker.js";
|
|
15
|
+
import { useSatisfactionAccess as _ } from "../satisfaction/useSatisfactionAccess.js";
|
|
16
|
+
import { useEffect as v, useMemo as y, useState as b } from "react";
|
|
17
|
+
import { Info as x, Loader2 as S, MessageSquareHeart as C, X as ne } from "lucide-react";
|
|
18
|
+
import { useLocation as re } from "react-router";
|
|
19
|
+
import { useApolloClient as ie, useQuery as w } from "@apollo/client/react";
|
|
20
|
+
import { Fragment as T, jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
21
|
+
import { Button as O, Card as k, CardContent as A, CardHeader as j, CardTitle as M } from "@burdenoff/fe-libs/ui";
|
|
22
|
+
import { clearGraphqlFetchResponseCache as N } from "@burdenoff/fe-libs/shared/graphql";
|
|
23
|
+
//#region src/planmagnet/pages/ProductsCustomerSatisfactionPagePage.tsx
|
|
24
|
+
function P() {
|
|
25
|
+
let P = t(), { workspaceId: F } = e(), I = _(), L = ie(), { releases: R } = f(), { search: ae } = re(), [z, B] = b(() => a(ae, /* @__PURE__ */ new Date())), [V, H] = b(() => o(z.window) ?? { preset: "LAST_90_DAYS" }), [U, W] = b(null), G = (e) => {
|
|
26
|
+
let t = {
|
|
27
|
+
...z,
|
|
28
|
+
...e
|
|
29
|
+
};
|
|
30
|
+
B(t);
|
|
31
|
+
let n = o(t.window);
|
|
32
|
+
n && H(n);
|
|
33
|
+
};
|
|
34
|
+
v(() => {
|
|
35
|
+
let e = V.preset;
|
|
36
|
+
u("window", e === "LAST_90_DAYS" ? null : e), u("from", e === "CUSTOM" ? V.from ?? null : null), u("to", e === "CUSTOM" ? V.to ?? null : null), u("product", z.productId || null), u("release", z.releaseId || null), u("project", z.projectId || null), u("metric", z.metricType || null);
|
|
37
|
+
}, [
|
|
38
|
+
V,
|
|
39
|
+
z.productId,
|
|
40
|
+
z.releaseId,
|
|
41
|
+
z.projectId,
|
|
42
|
+
z.metricType
|
|
43
|
+
]);
|
|
44
|
+
let K = y(() => ({
|
|
45
|
+
...z.productId ? { productId: z.productId } : {},
|
|
46
|
+
...z.releaseId ? { releaseId: z.releaseId } : {},
|
|
47
|
+
...z.projectId ? { projectId: z.projectId } : {}
|
|
48
|
+
}), [
|
|
49
|
+
z.productId,
|
|
50
|
+
z.releaseId,
|
|
51
|
+
z.projectId
|
|
52
|
+
]), q = y(() => ({
|
|
53
|
+
...K,
|
|
54
|
+
...z.metricType ? { metricType: z.metricType } : {},
|
|
55
|
+
window: V
|
|
56
|
+
}), [
|
|
57
|
+
K,
|
|
58
|
+
z.metricType,
|
|
59
|
+
V
|
|
60
|
+
]), J = {
|
|
61
|
+
productId: z.productId,
|
|
62
|
+
releaseId: z.releaseId,
|
|
63
|
+
projectId: z.projectId
|
|
64
|
+
}, Y = !!(z.productId || z.releaseId || z.projectId), { data: X, previousData: oe, loading: Z, error: Q } = w(c, {
|
|
65
|
+
variables: {
|
|
66
|
+
workspaceId: F ?? "",
|
|
67
|
+
filter: {
|
|
68
|
+
...K,
|
|
69
|
+
window: V
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
skip: !F
|
|
73
|
+
}), $ = (X ?? oe)?.planmagnetCustomerSatisfaction ?? null, se = Z && !X && !!$, ce = () => {
|
|
74
|
+
N(), L.refetchQueries({ include: l });
|
|
75
|
+
};
|
|
76
|
+
return F ? /* @__PURE__ */ D("div", {
|
|
77
|
+
className: "space-y-6 p-6",
|
|
78
|
+
"data-testid": "satisfaction-page",
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ D("div", {
|
|
81
|
+
className: "flex flex-wrap items-start justify-between gap-4",
|
|
82
|
+
children: [/* @__PURE__ */ D("div", {
|
|
83
|
+
className: "max-w-3xl",
|
|
84
|
+
children: [/* @__PURE__ */ D("h1", {
|
|
85
|
+
className: "flex items-center gap-2 text-2xl font-semibold",
|
|
86
|
+
children: [/* @__PURE__ */ E(C, {
|
|
87
|
+
className: "h-6 w-6",
|
|
88
|
+
"aria-hidden": !0
|
|
89
|
+
}), P("planmagnet.satisfaction.title", "Customer satisfaction")]
|
|
90
|
+
}), /* @__PURE__ */ E("p", {
|
|
91
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
92
|
+
children: P("planmagnet.satisfaction.subtitle", "CSAT and NPS from what customers said about your products, releases and projects. Responses are entered by your team or imported from a CSV file.")
|
|
93
|
+
})]
|
|
94
|
+
}), I.canCreate && /* @__PURE__ */ D("div", {
|
|
95
|
+
className: "flex flex-wrap items-center gap-2",
|
|
96
|
+
children: [/* @__PURE__ */ E(p, {
|
|
97
|
+
links: J,
|
|
98
|
+
onImported: (e) => W({
|
|
99
|
+
tone: "status",
|
|
100
|
+
message: P("planmagnet.satisfaction.notice.imported", "Import done — new responses: {{created}}; already imported and skipped: {{skipped}}.", {
|
|
101
|
+
created: e.createdCount,
|
|
102
|
+
skipped: e.skippedCount
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}), /* @__PURE__ */ E(m, {
|
|
106
|
+
links: J,
|
|
107
|
+
onSaved: () => W({
|
|
108
|
+
tone: "status",
|
|
109
|
+
message: P("planmagnet.satisfaction.notice.created", "The response was added.")
|
|
110
|
+
})
|
|
111
|
+
})]
|
|
112
|
+
})]
|
|
113
|
+
}),
|
|
114
|
+
I.readOnly && /* @__PURE__ */ D("p", {
|
|
115
|
+
className: "flex items-center gap-1.5 text-xs text-muted-foreground",
|
|
116
|
+
"data-testid": "satisfaction-read-only",
|
|
117
|
+
children: [/* @__PURE__ */ E(x, {
|
|
118
|
+
className: "h-3.5 w-3.5 shrink-0",
|
|
119
|
+
"aria-hidden": !0
|
|
120
|
+
}), P("planmagnet.satisfaction.readOnly", "You can read customer satisfaction. Adding, importing, editing and deleting responses is for product managers.")]
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ D("div", {
|
|
123
|
+
className: "space-y-3 rounded-lg border p-3",
|
|
124
|
+
"data-testid": "satisfaction-filters",
|
|
125
|
+
children: [/* @__PURE__ */ E(g, {
|
|
126
|
+
value: z.window,
|
|
127
|
+
onChange: (e) => G({ window: e }),
|
|
128
|
+
current: $?.window ?? null
|
|
129
|
+
}), /* @__PURE__ */ D("div", {
|
|
130
|
+
className: "flex flex-col gap-2 sm:flex-row sm:items-center",
|
|
131
|
+
children: [/* @__PURE__ */ D("div", {
|
|
132
|
+
className: "grid flex-1 gap-2 sm:grid-cols-3",
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("label", {
|
|
135
|
+
htmlFor: "satisfaction-filter-product",
|
|
136
|
+
className: "sr-only",
|
|
137
|
+
children: P("planmagnet.satisfaction.filters.product", "Product")
|
|
138
|
+
}), /* @__PURE__ */ E(ee, {
|
|
139
|
+
id: "satisfaction-filter-product",
|
|
140
|
+
value: z.productId,
|
|
141
|
+
noneLabel: P("planmagnet.satisfaction.filters.allProducts", "All products"),
|
|
142
|
+
onChange: (e) => {
|
|
143
|
+
let t = R.find((e) => e.id === z.releaseId);
|
|
144
|
+
G({
|
|
145
|
+
productId: e,
|
|
146
|
+
releaseId: !t || !t.productId || !e || t.productId === e ? z.releaseId : ""
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
})] }),
|
|
150
|
+
/* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("label", {
|
|
151
|
+
htmlFor: "satisfaction-filter-release",
|
|
152
|
+
className: "sr-only",
|
|
153
|
+
children: P("planmagnet.satisfaction.filters.release", "Release")
|
|
154
|
+
}), /* @__PURE__ */ E(d, {
|
|
155
|
+
id: "satisfaction-filter-release",
|
|
156
|
+
value: z.releaseId,
|
|
157
|
+
productId: z.productId,
|
|
158
|
+
noneLabel: P("planmagnet.satisfaction.filters.allReleases", "All releases"),
|
|
159
|
+
onChange: (e) => G({ releaseId: e })
|
|
160
|
+
})] }),
|
|
161
|
+
/* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("label", {
|
|
162
|
+
htmlFor: "satisfaction-filter-project",
|
|
163
|
+
className: "sr-only",
|
|
164
|
+
children: P("planmagnet.satisfaction.filters.project", "Project")
|
|
165
|
+
}), /* @__PURE__ */ E(te, {
|
|
166
|
+
id: "satisfaction-filter-project",
|
|
167
|
+
value: z.projectId,
|
|
168
|
+
noneLabel: P("planmagnet.satisfaction.filters.allProjects", "All projects"),
|
|
169
|
+
onChange: (e) => G({ projectId: e })
|
|
170
|
+
})] })
|
|
171
|
+
]
|
|
172
|
+
}), Y && /* @__PURE__ */ D(O, {
|
|
173
|
+
size: "sm",
|
|
174
|
+
variant: "ghost",
|
|
175
|
+
onClick: () => G({
|
|
176
|
+
productId: "",
|
|
177
|
+
releaseId: "",
|
|
178
|
+
projectId: ""
|
|
179
|
+
}),
|
|
180
|
+
"data-testid": "satisfaction-filters-clear",
|
|
181
|
+
children: [/* @__PURE__ */ E(ne, { className: "mr-1 h-4 w-4" }), P("planmagnet.satisfaction.filters.clear", "Clear filters")]
|
|
182
|
+
})]
|
|
183
|
+
})]
|
|
184
|
+
}),
|
|
185
|
+
z.productId && /* @__PURE__ */ E("p", {
|
|
186
|
+
className: "-mt-3 text-xs text-muted-foreground",
|
|
187
|
+
children: P("planmagnet.satisfaction.filters.productNote", "A product counts its own responses and those about its releases that name no product of their own.")
|
|
188
|
+
}),
|
|
189
|
+
U && /* @__PURE__ */ D("div", {
|
|
190
|
+
role: U.tone,
|
|
191
|
+
className: `flex items-start justify-between gap-3 rounded-md p-3 text-sm ${U.tone === "alert" ? "bg-status-error-bg-subtle text-status-error-text" : "bg-muted"}`,
|
|
192
|
+
"data-testid": "satisfaction-notice",
|
|
193
|
+
children: [/* @__PURE__ */ E("span", { children: U.message }), /* @__PURE__ */ E("button", {
|
|
194
|
+
type: "button",
|
|
195
|
+
className: "shrink-0 text-xs text-muted-foreground hover:underline",
|
|
196
|
+
onClick: () => W(null),
|
|
197
|
+
children: P("planmagnet.satisfaction.dismiss", "Dismiss")
|
|
198
|
+
})]
|
|
199
|
+
}),
|
|
200
|
+
Z && !$ ? /* @__PURE__ */ D("div", {
|
|
201
|
+
className: "flex items-center gap-2 text-sm text-muted-foreground",
|
|
202
|
+
children: [/* @__PURE__ */ E(S, { className: "h-4 w-4 animate-spin" }), P("planmagnet.satisfaction.loading", "Loading…")]
|
|
203
|
+
}) : Q && !$ ? /* @__PURE__ */ E(r, { message: n(Q) ? P("planmagnet.satisfaction.errors.readForbidden", "Your role cannot see customer satisfaction in this workspace.") : P("planmagnet.satisfaction.errors.load", "Customer satisfaction could not be loaded: {{message}}", { message: Q.message }) }) : $ ? /* @__PURE__ */ D(T, { children: [
|
|
204
|
+
Q && /* @__PURE__ */ E(r, { message: P("planmagnet.satisfaction.errors.refresh", "The figures could not be refreshed: {{message}}", { message: Q.message }) }),
|
|
205
|
+
se && /* @__PURE__ */ D("p", {
|
|
206
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
207
|
+
role: "status",
|
|
208
|
+
children: [/* @__PURE__ */ E(S, {
|
|
209
|
+
className: "h-3.5 w-3.5 animate-spin",
|
|
210
|
+
"aria-hidden": !0
|
|
211
|
+
}), P("planmagnet.satisfaction.updating", "Updating the figures…")]
|
|
212
|
+
}),
|
|
213
|
+
/* @__PURE__ */ E(s, { summary: $ })
|
|
214
|
+
] }) : null,
|
|
215
|
+
/* @__PURE__ */ D(k, { children: [/* @__PURE__ */ D(j, {
|
|
216
|
+
className: "flex flex-row flex-wrap items-center justify-between gap-2 space-y-0",
|
|
217
|
+
children: [/* @__PURE__ */ E(M, { children: P("planmagnet.satisfaction.list.title", "Responses") }), /* @__PURE__ */ E("div", {
|
|
218
|
+
className: "flex items-center gap-1 rounded-md border p-0.5",
|
|
219
|
+
role: "group",
|
|
220
|
+
"aria-label": P("planmagnet.satisfaction.list.metricFilter", "Metric"),
|
|
221
|
+
children: ["", ...i].map((e) => /* @__PURE__ */ E("button", {
|
|
222
|
+
type: "button",
|
|
223
|
+
"aria-pressed": z.metricType === e,
|
|
224
|
+
onClick: () => G({ metricType: e }),
|
|
225
|
+
"data-testid": `satisfaction-metric-${e || "ALL"}`,
|
|
226
|
+
className: `rounded px-2.5 py-1 text-xs ${z.metricType === e ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
|
|
227
|
+
children: e || P("planmagnet.satisfaction.list.allMetrics", "All")
|
|
228
|
+
}, e || "all"))
|
|
229
|
+
})]
|
|
230
|
+
}), /* @__PURE__ */ E(A, { children: /* @__PURE__ */ E(h, {
|
|
231
|
+
workspaceId: F,
|
|
232
|
+
filter: q,
|
|
233
|
+
access: I,
|
|
234
|
+
links: J,
|
|
235
|
+
filtered: Y || !!z.metricType,
|
|
236
|
+
onNotice: (e, t) => W({
|
|
237
|
+
message: e,
|
|
238
|
+
tone: t
|
|
239
|
+
}),
|
|
240
|
+
onStale: ce
|
|
241
|
+
}, JSON.stringify(q)) })] })
|
|
242
|
+
]
|
|
243
|
+
}) : /* @__PURE__ */ E("div", {
|
|
244
|
+
className: "p-6 text-sm text-muted-foreground",
|
|
245
|
+
children: P("planmagnet.satisfaction.noWorkspace", "Choose a workspace to see customer satisfaction.")
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
export { P as ProductsCustomerSatisfactionPagePage };
|
|
250
|
+
|
|
251
|
+
//# sourceMappingURL=ProductsCustomerSatisfactionPagePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductsCustomerSatisfactionPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsCustomerSatisfactionPagePage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useLocation } from 'react-router';\nimport { useApolloClient, useQuery } from '@apollo/client/react';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\nimport { Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { Info, Loader2, MessageSquareHeart, X } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { ProductSelect, ProjectSelect } from '../controls/Pickers';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { writeUrlParam } from '../utils/urlParam';\nimport { SatisfactionFigureCards } from '../satisfaction/SatisfactionFigures';\nimport { SatisfactionImportDialog } from '../satisfaction/SatisfactionImportDialog';\nimport {\n SatisfactionReleaseSelect,\n useSatisfactionReleaseOptions,\n} from '../satisfaction/SatisfactionReleaseSelect';\nimport { SatisfactionResponseDialog } from '../satisfaction/SatisfactionResponseDialog';\nimport {\n SatisfactionResponsesTable,\n type ResponsesFilter,\n} from '../satisfaction/SatisfactionResponsesTable';\nimport { SatisfactionWindowPicker } from '../satisfaction/SatisfactionWindowPicker';\nimport {\n DEFAULT_WINDOW_PRESET,\n METRIC_TYPES,\n pageStateFromSearch,\n windowInput,\n type SatisfactionPageState,\n type SatisfactionWindowInput,\n} from '../satisfaction/satisfaction';\nimport {\n CUSTOMER_SATISFACTION_SUMMARY,\n SATISFACTION_QUERIES,\n type CustomerSatisfactionSummaryData,\n} from '../satisfaction/satisfactionOperations';\nimport { useSatisfactionAccess } from '../satisfaction/useSatisfactionAccess';\n\n/**\n * Customer satisfaction (ANR-21): CSAT — the default delivery KPI — and NPS from customers' answers about products,\n * releases and projects, with the responses behind them. Answers are entered one at a time by team members or\n * imported from a CSV file; product managers add, edit, delete and import, everyone else reads.\n *\n * Every figure shows its response count and its window, and a metric with fewer than five responses reads \"Not enough\n * responses\" — never 0. The window (trailing 30 / 90 / 180 / 365 days, 90 by default, or custom UTC days) and the\n * product / release / project filters apply to the figures and the list alike, and live in the URL so a view is\n * linkable and the product, release and delivery-KPI cards can open it filtered.\n */\n\ntype Notice = { message: string; tone: 'status' | 'alert' };\n\nexport function ProductsCustomerSatisfactionPagePage() {\n const tr = useTr();\n const { workspaceId } = usePlanMagnet();\n const access = useSatisfactionAccess();\n const client = useApolloClient();\n const { releases } = useSatisfactionReleaseOptions();\n // The router's location, not window.location: a link from a product, release or delivery card arrives through the\n // host's navigate(). Later changes are written back with replaceState (utils/urlParam.ts), never as history entries.\n const { search } = useLocation();\n const [state, setState] = useState<SatisfactionPageState>(() =>\n pageStateFromSearch(search, new Date())\n );\n // The last window the service can take: a custom pair being typed never reaches a query half-done.\n const [lastWindow, setLastWindow] = useState<SatisfactionWindowInput>(\n () => windowInput(state.window) ?? { preset: DEFAULT_WINDOW_PRESET }\n );\n // What the last write did — kept above the figures and the list, which a refetch redraws.\n const [notice, setNotice] = useState<Notice | null>(null);\n\n const change = (patch: Partial<SatisfactionPageState>) => {\n const next = { ...state, ...patch };\n setState(next);\n const valid = windowInput(next.window);\n if (valid) setLastWindow(valid);\n };\n\n // The URL carries the view (see pageStateFromSearch); the default window is left out.\n useEffect(() => {\n const preset = lastWindow.preset;\n writeUrlParam('window', preset === DEFAULT_WINDOW_PRESET ? null : preset);\n writeUrlParam('from', preset === 'CUSTOM' ? (lastWindow.from ?? null) : null);\n writeUrlParam('to', preset === 'CUSTOM' ? (lastWindow.to ?? null) : null);\n writeUrlParam('product', state.productId || null);\n writeUrlParam('release', state.releaseId || null);\n writeUrlParam('project', state.projectId || null);\n writeUrlParam('metric', state.metricType || null);\n }, [lastWindow, state.productId, state.releaseId, state.projectId, state.metricType]);\n\n const scopeFilter = useMemo(\n () => ({\n ...(state.productId ? { productId: state.productId } : {}),\n ...(state.releaseId ? { releaseId: state.releaseId } : {}),\n ...(state.projectId ? { projectId: state.projectId } : {}),\n }),\n [state.productId, state.releaseId, state.projectId]\n );\n const responsesFilter: ResponsesFilter = useMemo(\n () => ({\n ...scopeFilter,\n ...(state.metricType ? { metricType: state.metricType } : {}),\n window: lastWindow,\n }),\n [scopeFilter, state.metricType, lastWindow]\n );\n const links = {\n productId: state.productId,\n releaseId: state.releaseId,\n projectId: state.projectId,\n };\n const scoped = !!(state.productId || state.releaseId || state.projectId);\n\n const { data, previousData, loading, error } = useQuery<CustomerSatisfactionSummaryData>(\n CUSTOMER_SATISFACTION_SUMMARY,\n {\n variables: { workspaceId: workspaceId ?? '', filter: { ...scopeFilter, window: lastWindow } },\n skip: !workspaceId,\n }\n );\n // A new window or filter keeps the figures on screen (each states its own window) until the new ones arrive.\n const summary = (data ?? previousData)?.planmagnetCustomerSatisfaction ?? null;\n const updating = loading && !data && !!summary;\n\n // A response deleted elsewhere: drop the 15 s fetch replay, then re-read what is on screen.\n const refreshAfterStale = () => {\n clearGraphqlFetchResponseCache();\n void client.refetchQueries({ include: SATISFACTION_QUERIES });\n };\n\n const onProduct = (productId: string) => {\n const release = releases.find((r) => r.id === state.releaseId);\n // A release of another product cannot be about this one: the release filter goes with the old product.\n const keepRelease =\n !release || !release.productId || !productId || release.productId === productId;\n change({ productId, releaseId: keepRelease ? state.releaseId : '' });\n };\n\n if (!workspaceId) {\n return (\n <div className=\"p-6 text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.satisfaction.noWorkspace',\n 'Choose a workspace to see customer satisfaction.'\n )}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"satisfaction-page\">\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div className=\"max-w-3xl\">\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <MessageSquareHeart className=\"h-6 w-6\" aria-hidden={true} />\n {tr('planmagnet.satisfaction.title', 'Customer satisfaction')}\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {tr(\n 'planmagnet.satisfaction.subtitle',\n 'CSAT and NPS from what customers said about your products, releases and projects. Responses are entered by your team or imported from a CSV file.'\n )}\n </p>\n </div>\n {access.canCreate && (\n <div className=\"flex flex-wrap items-center gap-2\">\n <SatisfactionImportDialog\n links={links}\n onImported={(result) =>\n setNotice({\n tone: 'status',\n message: tr(\n 'planmagnet.satisfaction.notice.imported',\n 'Import done — new responses: {{created}}; already imported and skipped: {{skipped}}.',\n { created: result.createdCount, skipped: result.skippedCount }\n ),\n })\n }\n />\n <SatisfactionResponseDialog\n links={links}\n onSaved={() =>\n setNotice({\n tone: 'status',\n message: tr('planmagnet.satisfaction.notice.created', 'The response was added.'),\n })\n }\n />\n </div>\n )}\n </div>\n\n {access.readOnly && (\n <p\n className=\"flex items-center gap-1.5 text-xs text-muted-foreground\"\n data-testid=\"satisfaction-read-only\"\n >\n <Info className=\"h-3.5 w-3.5 shrink-0\" aria-hidden={true} />\n {tr(\n 'planmagnet.satisfaction.readOnly',\n 'You can read customer satisfaction. Adding, importing, editing and deleting responses is for product managers.'\n )}\n </p>\n )}\n\n <div className=\"space-y-3 rounded-lg border p-3\" data-testid=\"satisfaction-filters\">\n <SatisfactionWindowPicker\n value={state.window}\n onChange={(window) => change({ window })}\n current={summary?.window ?? null}\n />\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"grid flex-1 gap-2 sm:grid-cols-3\">\n <div>\n <label htmlFor=\"satisfaction-filter-product\" className=\"sr-only\">\n {tr('planmagnet.satisfaction.filters.product', 'Product')}\n </label>\n <ProductSelect\n id=\"satisfaction-filter-product\"\n value={state.productId}\n noneLabel={tr('planmagnet.satisfaction.filters.allProducts', 'All products')}\n onChange={onProduct}\n />\n </div>\n <div>\n <label htmlFor=\"satisfaction-filter-release\" className=\"sr-only\">\n {tr('planmagnet.satisfaction.filters.release', 'Release')}\n </label>\n <SatisfactionReleaseSelect\n id=\"satisfaction-filter-release\"\n value={state.releaseId}\n productId={state.productId}\n noneLabel={tr('planmagnet.satisfaction.filters.allReleases', 'All releases')}\n onChange={(releaseId) => change({ releaseId })}\n />\n </div>\n <div>\n <label htmlFor=\"satisfaction-filter-project\" className=\"sr-only\">\n {tr('planmagnet.satisfaction.filters.project', 'Project')}\n </label>\n <ProjectSelect\n id=\"satisfaction-filter-project\"\n value={state.projectId}\n noneLabel={tr('planmagnet.satisfaction.filters.allProjects', 'All projects')}\n onChange={(projectId) => change({ projectId })}\n />\n </div>\n </div>\n {scoped && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => change({ productId: '', releaseId: '', projectId: '' })}\n data-testid=\"satisfaction-filters-clear\"\n >\n <X className=\"mr-1 h-4 w-4\" />\n {tr('planmagnet.satisfaction.filters.clear', 'Clear filters')}\n </Button>\n )}\n </div>\n </div>\n {state.productId && (\n <p className=\"-mt-3 text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.satisfaction.filters.productNote',\n 'A product counts its own responses and those about its releases that name no product of their own.'\n )}\n </p>\n )}\n\n {notice && (\n <div\n role={notice.tone}\n className={`flex items-start justify-between gap-3 rounded-md p-3 text-sm ${\n notice.tone === 'alert'\n ? 'bg-status-error-bg-subtle text-status-error-text'\n : 'bg-muted'\n }`}\n data-testid=\"satisfaction-notice\"\n >\n <span>{notice.message}</span>\n <button\n type=\"button\"\n className=\"shrink-0 text-xs text-muted-foreground hover:underline\"\n onClick={() => setNotice(null)}\n >\n {tr('planmagnet.satisfaction.dismiss', 'Dismiss')}\n </button>\n </div>\n )}\n\n {loading && !summary ? (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr('planmagnet.satisfaction.loading', 'Loading…')}\n </div>\n ) : error && !summary ? (\n <ErrorLine\n message={\n isForbidden(error)\n ? tr(\n 'planmagnet.satisfaction.errors.readForbidden',\n 'Your role cannot see customer satisfaction in this workspace.'\n )\n : tr(\n 'planmagnet.satisfaction.errors.load',\n 'Customer satisfaction could not be loaded: {{message}}',\n { message: error.message }\n )\n }\n />\n ) : summary ? (\n <>\n {error && (\n <ErrorLine\n message={tr(\n 'planmagnet.satisfaction.errors.refresh',\n 'The figures could not be refreshed: {{message}}',\n { message: error.message }\n )}\n />\n )}\n {updating && (\n <p className=\"flex items-center gap-2 text-xs text-muted-foreground\" role=\"status\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" aria-hidden={true} />\n {tr('planmagnet.satisfaction.updating', 'Updating the figures…')}\n </p>\n )}\n <SatisfactionFigureCards summary={summary} />\n </>\n ) : null}\n\n <Card>\n <CardHeader className=\"flex flex-row flex-wrap items-center justify-between gap-2 space-y-0\">\n <CardTitle>{tr('planmagnet.satisfaction.list.title', 'Responses')}</CardTitle>\n <div\n className=\"flex items-center gap-1 rounded-md border p-0.5\"\n role=\"group\"\n aria-label={tr('planmagnet.satisfaction.list.metricFilter', 'Metric')}\n >\n {(['', ...METRIC_TYPES] as const).map((metric) => (\n <button\n key={metric || 'all'}\n type=\"button\"\n aria-pressed={state.metricType === metric}\n onClick={() => change({ metricType: metric })}\n data-testid={`satisfaction-metric-${metric || 'ALL'}`}\n className={`rounded px-2.5 py-1 text-xs ${\n state.metricType === metric\n ? 'bg-primary text-primary-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n >\n {metric || tr('planmagnet.satisfaction.list.allMetrics', 'All')}\n </button>\n ))}\n </div>\n </CardHeader>\n <CardContent>\n <SatisfactionResponsesTable\n key={JSON.stringify(responsesFilter)}\n workspaceId={workspaceId}\n filter={responsesFilter}\n access={access}\n links={links}\n filtered={scoped || !!state.metricType}\n onNotice={(message, tone) => setNotice({ message, tone })}\n onStale={refreshAfterStale}\n />\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -52,7 +52,7 @@ function v() {
|
|
|
52
52
|
let S = v?.planmagnetFeatureRequest;
|
|
53
53
|
if (!S) return /* @__PURE__ */ d("div", {
|
|
54
54
|
className: "p-6 text-muted-foreground",
|
|
55
|
-
children: "Feature not found."
|
|
55
|
+
children: "Feature request not found."
|
|
56
56
|
});
|
|
57
57
|
let C = S.votes ?? [], w = S.competitors?.length ?? 0, T = {};
|
|
58
58
|
for (let e of C) T[e.weight] = (T[e.weight] || 0) + 1;
|
|
@@ -72,7 +72,7 @@ function v() {
|
|
|
72
72
|
children: [
|
|
73
73
|
/* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("h1", {
|
|
74
74
|
className: "text-2xl font-semibold",
|
|
75
|
-
children: "Feature
|
|
75
|
+
children: "Feature request analytics"
|
|
76
76
|
}), /* @__PURE__ */ f("p", {
|
|
77
77
|
className: "text-sm text-muted-foreground",
|
|
78
78
|
children: [
|
|
@@ -186,7 +186,7 @@ function v() {
|
|
|
186
186
|
}),
|
|
187
187
|
/* @__PURE__ */ f("div", {
|
|
188
188
|
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
189
|
-
children: [/* @__PURE__ */ f(p, { children: [/* @__PURE__ */ d(h, { children: /* @__PURE__ */ d(g, { children: "All
|
|
189
|
+
children: [/* @__PURE__ */ f(p, { children: [/* @__PURE__ */ d(h, { children: /* @__PURE__ */ d(g, { children: "All feature requests - status distribution" }) }), /* @__PURE__ */ d(m, {
|
|
190
190
|
className: "space-y-3",
|
|
191
191
|
children: Object.entries(A).map(([e, t]) => {
|
|
192
192
|
let n = x?.all ?? 0, r = n > 0 ? Math.round(t / n * 100) : 0;
|
|
@@ -220,7 +220,7 @@ function v() {
|
|
|
220
220
|
})
|
|
221
221
|
})] }, e);
|
|
222
222
|
})
|
|
223
|
-
})] }), /* @__PURE__ */ f(p, { children: [/* @__PURE__ */ d(h, { children: /* @__PURE__ */ d(g, { children: "
|
|
223
|
+
})] }), /* @__PURE__ */ f(p, { children: [/* @__PURE__ */ d(h, { children: /* @__PURE__ */ d(g, { children: "Request impact" }) }), /* @__PURE__ */ f(m, {
|
|
224
224
|
className: "space-y-3",
|
|
225
225
|
children: [/* @__PURE__ */ f("div", {
|
|
226
226
|
className: "text-center p-4 bg-muted rounded-md",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsFeatureAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsFeatureAnalyticsPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { TrendingUp, ThumbsUp, Users, BarChart3, Target, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { countLabel, useFeatureCounts } from '../hooks/useCounts';\n\nconst GET_FEATURE_REQUEST_ANALYTICS = gql`\n query GetFeatureRequestAnalytics($id: ID!) {\n planmagnetFeatureRequest(id: $id) {\n id\n title\n description\n status\n businessValue\n effortEstimate\n priorityScore\n voteCount\n voteRank\n submittedBy\n tags\n createdAt\n updatedAt\n votes {\n id\n voterId\n weight\n comment\n createdAt\n }\n competitors {\n id\n name\n }\n }\n }\n`;\n\ninterface VoteNode {\n id: string;\n voterId: string;\n weight: number;\n comment: string | null;\n createdAt: string;\n}\n\nexport function ProductsFeatureAnalyticsPagePage() {\n const params = useParams<{ id: string }>();\n const featureId = params.id ?? '';\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_FEATURE_REQUEST_ANALYTICS, {\n variables: { id: featureId },\n skip: !featureId,\n });\n\n // Rank and distribution cover EVERY request, from the server (BOFF-7995: both were computed over the 100\n // newest in the browser, so a workspace with more read \"of 100\", and an older request had no rank).\n const featureCounts = useFeatureCounts();\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load analytics: {error.message}</div>;\n }\n\n const feature = data?.planmagnetFeatureRequest;\n if (!feature) return <div className=\"p-6 text-muted-foreground\">Feature not found.</div>;\n\n const votes: VoteNode[] = feature.votes ?? [];\n const competitorCount = feature.competitors?.length ?? 0;\n\n // Vote distribution by weight\n const votesByWeight: Record<number, number> = {};\n for (const v of votes) {\n votesByWeight[v.weight] = (votesByWeight[v.weight] || 0) + 1;\n }\n const maxVoteWeight = Math.max(...Object.values(votesByWeight), 1);\n\n // Vote timeline - group by creation date\n const voteTimeline: { date: string; count: number }[] = [];\n const voteDateMap: Record<string, number> = {};\n for (const v of votes) {\n const date = new Date(v.createdAt).toLocaleDateString();\n voteDateMap[date] = (voteDateMap[date] || 0) + 1;\n }\n for (const [date, count] of Object.entries(voteDateMap)) {\n voteTimeline.push({ date, count });\n }\n voteTimeline.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());\n const maxTimelineCount = Math.max(...voteTimeline.map((d) => d.count), 1);\n\n const statusDistribution: Record<string, number> = featureCounts\n ? Object.fromEntries(Object.entries(featureCounts.byStatus).filter(([, n]) => n > 0))\n : {};\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Feature Analytics</h1>\n <p className=\"text-sm text-muted-foreground\">\n {feature.title} - {feature.status.toLowerCase().replace('_', ' ')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Target className=\"h-4 w-4\" />\n Priority Score\n </div>\n <p className=\"text-2xl font-bold mt-1\">{feature.priorityScore ?? 0}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <ThumbsUp className=\"h-4 w-4\" />\n Total Votes\n </div>\n <p className=\"text-2xl font-bold mt-1\">{feature.voteCount}</p>\n <p className=\"text-xs text-green-600 mt-1\">\n Rank #{feature.voteRank} of {countLabel(featureCounts?.all)}\n </p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Users className=\"h-4 w-4\" />\n Competitors\n </div>\n <p className=\"text-2xl font-bold mt-1\">{competitorCount}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <TrendingUp className=\"h-4 w-4\" />\n Business Value\n </div>\n <p className=\"text-2xl font-bold mt-1\">{feature.businessValue ?? 0}</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n Vote Timeline\n </CardTitle>\n </CardHeader>\n <CardContent>\n {voteTimeline.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-8\">\n No vote data available\n </p>\n ) : (\n <div className=\"flex items-end gap-1 h-48\">\n {voteTimeline.map((d) => (\n <div key={d.date} className=\"flex-1 flex flex-col items-center gap-1\">\n <div className=\"w-full flex justify-center\">\n <div\n className=\"w-5 bg-primary rounded-t transition-all\"\n style={{ height: `${(d.count / maxTimelineCount) * 160}px` }}\n />\n </div>\n <span className=\"text-[10px] text-muted-foreground truncate max-w-[40px]\">\n {d.date}\n </span>\n </div>\n ))}\n </div>\n )}\n <p className=\"text-xs text-muted-foreground text-center mt-2\">Votes over time</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Vote Weight Distribution</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {Object.entries(votesByWeight).length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-8\">No votes recorded</p>\n ) : (\n Object.entries(votesByWeight)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([weight, count]) => (\n <div key={weight}>\n <div className=\"flex justify-between text-sm mb-1\">\n <span>Weight {weight}</span>\n <span className=\"font-semibold\">{count} votes</span>\n </div>\n <div className=\"w-full h-3 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-primary\"\n style={{ width: `${(count / maxVoteWeight) * 100}%` }}\n />\n </div>\n </div>\n ))\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle>All Features - Status Distribution</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(statusDistribution).map(([status, count]) => {\n const total = featureCounts?.all ?? 0;\n const pct = total > 0 ? Math.round((count / total) * 100) : 0;\n const colors: Record<string, string> = {\n SUBMITTED: 'bg-gray-400',\n UNDER_REVIEW: 'bg-yellow-400',\n PLANNED: 'bg-blue-400',\n IN_PROGRESS: 'bg-purple-400',\n COMPLETED: 'bg-green-400',\n DECLINED: 'bg-red-400',\n DUPLICATE: 'bg-orange-400',\n };\n return (\n <div key={status}>\n <div className=\"flex justify-between text-sm mb-1\">\n <span className=\"capitalize\">{status.toLowerCase().replace('_', ' ')}</span>\n <span className=\"font-medium\">\n {count} ({pct}%)\n </span>\n </div>\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${colors[status] ?? 'bg-gray-300'}`}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n })}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Feature Impact</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"text-center p-4 bg-muted rounded-md\">\n <p className=\"text-3xl font-bold text-primary\">{feature.voteCount}</p>\n <p className=\"text-sm text-muted-foreground\">total votes received</p>\n </div>\n <div className=\"grid grid-cols-2 gap-2 text-center\">\n <div className=\"p-3 border rounded-md\">\n <p className=\"text-lg font-semibold\">{feature.businessValue ?? 0}</p>\n <p className=\"text-xs text-muted-foreground\">business value</p>\n </div>\n <div className=\"p-3 border rounded-md\">\n <p className=\"text-lg font-semibold\">{feature.priorityScore ?? 0}</p>\n <p className=\"text-xs text-muted-foreground\">priority score</p>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProductsFeatureAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsFeatureAnalyticsPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { TrendingUp, ThumbsUp, Users, BarChart3, Target, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { countLabel, useFeatureCounts } from '../hooks/useCounts';\n\nconst GET_FEATURE_REQUEST_ANALYTICS = gql`\n query GetFeatureRequestAnalytics($id: ID!) {\n planmagnetFeatureRequest(id: $id) {\n id\n title\n description\n status\n businessValue\n effortEstimate\n priorityScore\n voteCount\n voteRank\n submittedBy\n tags\n createdAt\n updatedAt\n votes {\n id\n voterId\n weight\n comment\n createdAt\n }\n competitors {\n id\n name\n }\n }\n }\n`;\n\ninterface VoteNode {\n id: string;\n voterId: string;\n weight: number;\n comment: string | null;\n createdAt: string;\n}\n\nexport function ProductsFeatureAnalyticsPagePage() {\n const params = useParams<{ id: string }>();\n const featureId = params.id ?? '';\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_FEATURE_REQUEST_ANALYTICS, {\n variables: { id: featureId },\n skip: !featureId,\n });\n\n // Rank and distribution cover EVERY request, from the server (BOFF-7995: both were computed over the 100\n // newest in the browser, so a workspace with more read \"of 100\", and an older request had no rank).\n const featureCounts = useFeatureCounts();\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load analytics: {error.message}</div>;\n }\n\n const feature = data?.planmagnetFeatureRequest;\n if (!feature) return <div className=\"p-6 text-muted-foreground\">Feature request not found.</div>;\n\n const votes: VoteNode[] = feature.votes ?? [];\n const competitorCount = feature.competitors?.length ?? 0;\n\n // Vote distribution by weight\n const votesByWeight: Record<number, number> = {};\n for (const v of votes) {\n votesByWeight[v.weight] = (votesByWeight[v.weight] || 0) + 1;\n }\n const maxVoteWeight = Math.max(...Object.values(votesByWeight), 1);\n\n // Vote timeline - group by creation date\n const voteTimeline: { date: string; count: number }[] = [];\n const voteDateMap: Record<string, number> = {};\n for (const v of votes) {\n const date = new Date(v.createdAt).toLocaleDateString();\n voteDateMap[date] = (voteDateMap[date] || 0) + 1;\n }\n for (const [date, count] of Object.entries(voteDateMap)) {\n voteTimeline.push({ date, count });\n }\n voteTimeline.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());\n const maxTimelineCount = Math.max(...voteTimeline.map((d) => d.count), 1);\n\n const statusDistribution: Record<string, number> = featureCounts\n ? Object.fromEntries(Object.entries(featureCounts.byStatus).filter(([, n]) => n > 0))\n : {};\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Feature request analytics</h1>\n <p className=\"text-sm text-muted-foreground\">\n {feature.title} - {feature.status.toLowerCase().replace('_', ' ')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Target className=\"h-4 w-4\" />\n Priority Score\n </div>\n <p className=\"text-2xl font-bold mt-1\">{feature.priorityScore ?? 0}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <ThumbsUp className=\"h-4 w-4\" />\n Total Votes\n </div>\n <p className=\"text-2xl font-bold mt-1\">{feature.voteCount}</p>\n <p className=\"text-xs text-green-600 mt-1\">\n Rank #{feature.voteRank} of {countLabel(featureCounts?.all)}\n </p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Users className=\"h-4 w-4\" />\n Competitors\n </div>\n <p className=\"text-2xl font-bold mt-1\">{competitorCount}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <TrendingUp className=\"h-4 w-4\" />\n Business Value\n </div>\n <p className=\"text-2xl font-bold mt-1\">{feature.businessValue ?? 0}</p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n Vote Timeline\n </CardTitle>\n </CardHeader>\n <CardContent>\n {voteTimeline.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-8\">\n No vote data available\n </p>\n ) : (\n <div className=\"flex items-end gap-1 h-48\">\n {voteTimeline.map((d) => (\n <div key={d.date} className=\"flex-1 flex flex-col items-center gap-1\">\n <div className=\"w-full flex justify-center\">\n <div\n className=\"w-5 bg-primary rounded-t transition-all\"\n style={{ height: `${(d.count / maxTimelineCount) * 160}px` }}\n />\n </div>\n <span className=\"text-[10px] text-muted-foreground truncate max-w-[40px]\">\n {d.date}\n </span>\n </div>\n ))}\n </div>\n )}\n <p className=\"text-xs text-muted-foreground text-center mt-2\">Votes over time</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Vote Weight Distribution</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {Object.entries(votesByWeight).length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-8\">No votes recorded</p>\n ) : (\n Object.entries(votesByWeight)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([weight, count]) => (\n <div key={weight}>\n <div className=\"flex justify-between text-sm mb-1\">\n <span>Weight {weight}</span>\n <span className=\"font-semibold\">{count} votes</span>\n </div>\n <div className=\"w-full h-3 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-primary\"\n style={{ width: `${(count / maxVoteWeight) * 100}%` }}\n />\n </div>\n </div>\n ))\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle>All feature requests - status distribution</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(statusDistribution).map(([status, count]) => {\n const total = featureCounts?.all ?? 0;\n const pct = total > 0 ? Math.round((count / total) * 100) : 0;\n const colors: Record<string, string> = {\n SUBMITTED: 'bg-gray-400',\n UNDER_REVIEW: 'bg-yellow-400',\n PLANNED: 'bg-blue-400',\n IN_PROGRESS: 'bg-purple-400',\n COMPLETED: 'bg-green-400',\n DECLINED: 'bg-red-400',\n DUPLICATE: 'bg-orange-400',\n };\n return (\n <div key={status}>\n <div className=\"flex justify-between text-sm mb-1\">\n <span className=\"capitalize\">{status.toLowerCase().replace('_', ' ')}</span>\n <span className=\"font-medium\">\n {count} ({pct}%)\n </span>\n </div>\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${colors[status] ?? 'bg-gray-300'}`}\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n })}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Request impact</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"text-center p-4 bg-muted rounded-md\">\n <p className=\"text-3xl font-bold text-primary\">{feature.voteCount}</p>\n <p className=\"text-sm text-muted-foreground\">total votes received</p>\n </div>\n <div className=\"grid grid-cols-2 gap-2 text-center\">\n <div className=\"p-3 border rounded-md\">\n <p className=\"text-lg font-semibold\">{feature.businessValue ?? 0}</p>\n <p className=\"text-xs text-muted-foreground\">business value</p>\n </div>\n <div className=\"p-3 border rounded-md\">\n <p className=\"text-lg font-semibold\">{feature.priorityScore ?? 0}</p>\n <p className=\"text-xs text-muted-foreground\">priority score</p>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"AAsGA,wBAAgB,6BAA6B,4CA8R5C"}
|