@burdenoff/microfe-planmagnet 2026.1001.7 → 2026.1001.8
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/planmagnet/hooks/useLiveRefresh.d.ts +33 -2
- package/dist/planmagnet/hooks/useLiveRefresh.d.ts.map +1 -1
- package/dist/planmagnet/hooks/useLiveRefresh.js +71 -42
- package/dist/planmagnet/hooks/useLiveRefresh.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +153 -117
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +173 -138
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.d.ts.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +171 -153
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { useFeatureCounts as t } from "../hooks/useCounts.js";
|
|
3
|
+
import { useLiveRefresh as n } from "../hooks/useLiveRefresh.js";
|
|
4
|
+
import { LiveStatus as r } from "../controls/LiveStatus.js";
|
|
5
|
+
import { planningFocusLabel as i } from "../roadmaps/planningFocus.js";
|
|
6
|
+
import { useState as a } from "react";
|
|
7
|
+
import { AlertTriangle as o, BarChart3 as s, CheckCircle as c, Loader2 as l, Target as u, TrendingUp as d } from "lucide-react";
|
|
8
|
+
import { useParams as f } from "react-router";
|
|
9
|
+
import { gql as p } from "@apollo/client";
|
|
10
|
+
import { useQuery as m } from "@apollo/client/react";
|
|
11
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
12
|
+
import { Button as _, Card as v, CardContent as y, CardHeader as b, CardTitle as x } from "@burdenoff/fe-libs/ui";
|
|
9
13
|
//#region src/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.tsx
|
|
10
|
-
var
|
|
14
|
+
var S = ["GetRoadmapAnalytics"], C = p`
|
|
11
15
|
query GetRoadmapAnalytics($id: ID!) {
|
|
12
16
|
planmagnetRoadmap(id: $id) {
|
|
13
17
|
id
|
|
@@ -30,130 +34,162 @@ var _ = l`
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
`;
|
|
33
|
-
function
|
|
34
|
-
let
|
|
35
|
-
variables: { id:
|
|
36
|
-
skip: !
|
|
37
|
-
}),
|
|
38
|
-
|
|
37
|
+
function w() {
|
|
38
|
+
let p = f().id ?? "", w = e(), { data: T, loading: E, error: D } = m(C, {
|
|
39
|
+
variables: { id: p },
|
|
40
|
+
skip: !p
|
|
41
|
+
}), O = t(), [k, A] = a(!1), j = n({
|
|
42
|
+
kind: "roadmap",
|
|
43
|
+
roadmapId: p,
|
|
44
|
+
queries: S,
|
|
45
|
+
onEvent: (e) => {
|
|
46
|
+
e.kind === "DELETED" && e.id === p && A(!0);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (k) return /* @__PURE__ */ g("div", {
|
|
50
|
+
className: "space-y-3 p-6",
|
|
51
|
+
"data-testid": "roadmap-deleted",
|
|
52
|
+
children: [/* @__PURE__ */ h("p", {
|
|
53
|
+
className: "text-sm text-muted-foreground",
|
|
54
|
+
children: "This roadmap was deleted."
|
|
55
|
+
}), /* @__PURE__ */ h(_, {
|
|
56
|
+
size: "sm",
|
|
57
|
+
variant: "ghost",
|
|
58
|
+
onClick: () => w("/products/roadmaps-page"),
|
|
59
|
+
children: "Back to roadmaps"
|
|
60
|
+
})]
|
|
61
|
+
});
|
|
62
|
+
if (E && !T) return /* @__PURE__ */ h("div", {
|
|
39
63
|
className: "flex items-center justify-center p-12",
|
|
40
|
-
children: /* @__PURE__ */
|
|
64
|
+
children: /* @__PURE__ */ h(l, { className: "h-8 w-8 animate-spin text-muted-foreground" })
|
|
41
65
|
});
|
|
42
|
-
if (
|
|
66
|
+
if (D) return /* @__PURE__ */ g("div", {
|
|
43
67
|
className: "p-6 text-destructive",
|
|
44
|
-
children: ["Failed to load analytics: ",
|
|
68
|
+
children: ["Failed to load analytics: ", D.message]
|
|
45
69
|
});
|
|
46
|
-
let
|
|
47
|
-
if (!
|
|
48
|
-
className: "p-6
|
|
49
|
-
|
|
70
|
+
let M = T?.planmagnetRoadmap;
|
|
71
|
+
if (!M) return /* @__PURE__ */ g("div", {
|
|
72
|
+
className: "space-y-3 p-6",
|
|
73
|
+
"data-testid": "roadmap-not-found",
|
|
74
|
+
children: [/* @__PURE__ */ h("p", {
|
|
75
|
+
className: "text-sm text-muted-foreground",
|
|
76
|
+
children: "This roadmap does not exist in this workspace, or it was deleted."
|
|
77
|
+
}), /* @__PURE__ */ h(_, {
|
|
78
|
+
size: "sm",
|
|
79
|
+
variant: "ghost",
|
|
80
|
+
onClick: () => w("/products/roadmaps-page"),
|
|
81
|
+
children: "Back to roadmaps"
|
|
82
|
+
})]
|
|
50
83
|
});
|
|
51
|
-
let
|
|
52
|
-
for (let e of
|
|
53
|
-
let
|
|
54
|
-
for (let e of
|
|
84
|
+
let N = M.items ?? [], P = (e) => e.status === "COMPLETED" || e.status === "DONE", F = N.filter((e) => e.status !== "CANCELLED"), I = F.filter(P), L = F.length > 0 ? Math.round(I.length / F.length * 100) : 0, R = {};
|
|
85
|
+
for (let e of N) R[e.status] = (R[e.status] || 0) + 1;
|
|
86
|
+
let z = O ? Object.fromEntries(Object.entries(O.byStatus).filter(([, e]) => e > 0)) : {}, B = {};
|
|
87
|
+
for (let e of N) {
|
|
55
88
|
let t = e.swimlane ?? "Uncategorized";
|
|
56
|
-
|
|
89
|
+
B[t] = (B[t] || 0) + 1;
|
|
57
90
|
}
|
|
58
|
-
let
|
|
59
|
-
for (let e of
|
|
91
|
+
let V = /* @__PURE__ */ new Date(), H = F.filter((e) => e.endDate && new Date(e.endDate) < V && !P(e)), U = F.filter((e) => !P(e)), W = U.length > 0 ? Math.round(U.filter((e) => !H.includes(e) && e.status !== "AT_RISK" && e.status !== "DELAYED").length / U.length * 100) : null, G = {};
|
|
92
|
+
for (let e of F) if (e.endDate) {
|
|
60
93
|
let t = e.endDate.slice(0, 7);
|
|
61
|
-
|
|
94
|
+
G[t] || (G[t] = {
|
|
62
95
|
planned: 0,
|
|
63
96
|
completed: 0
|
|
64
|
-
}),
|
|
97
|
+
}), G[t].planned += 1, P(e) && (G[t].completed += 1);
|
|
65
98
|
}
|
|
66
|
-
let
|
|
99
|
+
let K = Object.entries(G).sort(([e], [t]) => e.localeCompare(t)).slice(-6).map(([e, t]) => ({
|
|
67
100
|
month: (/* @__PURE__ */ new Date(`${e}-01T00:00:00Z`)).toLocaleDateString("en-US", {
|
|
68
101
|
month: "short",
|
|
69
102
|
year: "2-digit",
|
|
70
103
|
timeZone: "UTC"
|
|
71
104
|
}),
|
|
72
105
|
...t
|
|
73
|
-
})),
|
|
74
|
-
return /* @__PURE__ */
|
|
106
|
+
})), q = Math.max(...K.map((e) => Math.max(e.planned, e.completed)), 1);
|
|
107
|
+
return /* @__PURE__ */ g("div", {
|
|
75
108
|
className: "space-y-6 p-6",
|
|
76
109
|
children: [
|
|
77
|
-
/* @__PURE__ */
|
|
78
|
-
className: "
|
|
79
|
-
children: "
|
|
80
|
-
|
|
110
|
+
/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ g("div", {
|
|
111
|
+
className: "flex items-center gap-2",
|
|
112
|
+
children: [/* @__PURE__ */ h("h1", {
|
|
113
|
+
className: "text-2xl font-semibold",
|
|
114
|
+
children: "Roadmap Analytics"
|
|
115
|
+
}), /* @__PURE__ */ h(r, { mode: j.mode })]
|
|
116
|
+
}), /* @__PURE__ */ g("p", {
|
|
81
117
|
className: "text-sm text-muted-foreground",
|
|
82
118
|
children: [
|
|
83
|
-
|
|
119
|
+
M.name,
|
|
84
120
|
" - ",
|
|
85
|
-
|
|
121
|
+
i(M.planningFocus)
|
|
86
122
|
]
|
|
87
123
|
})] }),
|
|
88
|
-
/* @__PURE__ */
|
|
124
|
+
/* @__PURE__ */ g("div", {
|
|
89
125
|
className: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
|
90
126
|
children: [
|
|
91
|
-
/* @__PURE__ */
|
|
127
|
+
/* @__PURE__ */ h(v, { children: /* @__PURE__ */ g(y, {
|
|
92
128
|
className: "pt-4",
|
|
93
129
|
children: [
|
|
94
|
-
/* @__PURE__ */
|
|
130
|
+
/* @__PURE__ */ g("div", {
|
|
95
131
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
96
|
-
children: [/* @__PURE__ */
|
|
132
|
+
children: [/* @__PURE__ */ h(u, { className: "h-4 w-4" }), "Open items on track"]
|
|
97
133
|
}),
|
|
98
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ h("p", {
|
|
99
135
|
className: "text-2xl font-bold mt-1",
|
|
100
|
-
children:
|
|
136
|
+
children: W === null ? "—" : `${W}%`
|
|
101
137
|
}),
|
|
102
|
-
/* @__PURE__ */
|
|
138
|
+
/* @__PURE__ */ h("div", {
|
|
103
139
|
className: "w-full h-2 rounded-full bg-muted mt-2 overflow-hidden",
|
|
104
|
-
children: /* @__PURE__ */
|
|
140
|
+
children: /* @__PURE__ */ h("div", {
|
|
105
141
|
className: "h-full rounded-full bg-success",
|
|
106
|
-
style: { width: `${
|
|
142
|
+
style: { width: `${W ?? 0}%` }
|
|
107
143
|
})
|
|
108
144
|
})
|
|
109
145
|
]
|
|
110
146
|
}) }),
|
|
111
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ h(v, { children: /* @__PURE__ */ g(y, {
|
|
112
148
|
className: "pt-4",
|
|
113
149
|
children: [
|
|
114
|
-
/* @__PURE__ */
|
|
150
|
+
/* @__PURE__ */ g("div", {
|
|
115
151
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
116
|
-
children: [/* @__PURE__ */ d
|
|
152
|
+
children: [/* @__PURE__ */ h(d, { className: "h-4 w-4" }), "Progress"]
|
|
117
153
|
}),
|
|
118
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ g("p", {
|
|
119
155
|
className: "text-2xl font-bold mt-1",
|
|
120
|
-
children: [
|
|
156
|
+
children: [L, "%"]
|
|
121
157
|
}),
|
|
122
|
-
/* @__PURE__ */
|
|
158
|
+
/* @__PURE__ */ g("p", {
|
|
123
159
|
className: "text-xs text-muted-foreground mt-1",
|
|
124
|
-
children: [
|
|
160
|
+
children: [N.length, " items total"]
|
|
125
161
|
})
|
|
126
162
|
]
|
|
127
163
|
}) }),
|
|
128
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ h(v, { children: /* @__PURE__ */ g(y, {
|
|
129
165
|
className: "pt-4",
|
|
130
166
|
children: [
|
|
131
|
-
/* @__PURE__ */
|
|
167
|
+
/* @__PURE__ */ g("div", {
|
|
132
168
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
133
|
-
children: [/* @__PURE__ */
|
|
169
|
+
children: [/* @__PURE__ */ h(c, { className: "h-4 w-4" }), "Completed"]
|
|
134
170
|
}),
|
|
135
|
-
/* @__PURE__ */
|
|
171
|
+
/* @__PURE__ */ h("p", {
|
|
136
172
|
className: "text-2xl font-bold mt-1",
|
|
137
|
-
children:
|
|
173
|
+
children: I.length
|
|
138
174
|
}),
|
|
139
|
-
/* @__PURE__ */
|
|
175
|
+
/* @__PURE__ */ h("p", {
|
|
140
176
|
className: "text-xs text-muted-foreground mt-1",
|
|
141
177
|
children: "items completed"
|
|
142
178
|
})
|
|
143
179
|
]
|
|
144
180
|
}) }),
|
|
145
|
-
/* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ h(v, { children: /* @__PURE__ */ g(y, {
|
|
146
182
|
className: "pt-4",
|
|
147
183
|
children: [
|
|
148
|
-
/* @__PURE__ */
|
|
184
|
+
/* @__PURE__ */ g("div", {
|
|
149
185
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
150
|
-
children: [/* @__PURE__ */
|
|
186
|
+
children: [/* @__PURE__ */ h(o, { className: "h-4 w-4" }), "Overdue"]
|
|
151
187
|
}),
|
|
152
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ h("p", {
|
|
153
189
|
className: "text-2xl font-bold mt-1",
|
|
154
|
-
children:
|
|
190
|
+
children: H.length
|
|
155
191
|
}),
|
|
156
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ h("p", {
|
|
157
193
|
className: "text-xs text-muted-foreground mt-1",
|
|
158
194
|
children: "items past end date"
|
|
159
195
|
})
|
|
@@ -161,53 +197,53 @@ function v() {
|
|
|
161
197
|
}) })
|
|
162
198
|
]
|
|
163
199
|
}),
|
|
164
|
-
/* @__PURE__ */
|
|
200
|
+
/* @__PURE__ */ g("div", {
|
|
165
201
|
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
166
|
-
children: [/* @__PURE__ */
|
|
202
|
+
children: [/* @__PURE__ */ g(v, { children: [/* @__PURE__ */ h(b, { children: /* @__PURE__ */ g(x, {
|
|
167
203
|
className: "flex items-center gap-2",
|
|
168
|
-
children: [/* @__PURE__ */
|
|
169
|
-
}) }), /* @__PURE__ */
|
|
204
|
+
children: [/* @__PURE__ */ h(s, { className: "h-4 w-4" }), "Item Velocity"]
|
|
205
|
+
}) }), /* @__PURE__ */ g(y, { children: [K.length === 0 ? /* @__PURE__ */ h("p", {
|
|
170
206
|
className: "text-sm text-muted-foreground text-center py-8",
|
|
171
207
|
children: "No timeline data available"
|
|
172
|
-
}) : /* @__PURE__ */
|
|
208
|
+
}) : /* @__PURE__ */ h("div", {
|
|
173
209
|
className: "flex items-end gap-2 h-48",
|
|
174
|
-
children:
|
|
210
|
+
children: K.map((e) => /* @__PURE__ */ g("div", {
|
|
175
211
|
className: "flex-1 flex flex-col items-center gap-1",
|
|
176
|
-
children: [/* @__PURE__ */
|
|
212
|
+
children: [/* @__PURE__ */ g("div", {
|
|
177
213
|
className: "flex gap-0.5 items-end w-full justify-center h-40",
|
|
178
|
-
children: [/* @__PURE__ */
|
|
214
|
+
children: [/* @__PURE__ */ h("div", {
|
|
179
215
|
className: "w-3 bg-primary/30 rounded-t",
|
|
180
|
-
style: { height: `${e.planned /
|
|
216
|
+
style: { height: `${e.planned / q * 100}%` },
|
|
181
217
|
title: `Planned: ${e.planned}`
|
|
182
|
-
}), /* @__PURE__ */
|
|
218
|
+
}), /* @__PURE__ */ h("div", {
|
|
183
219
|
className: "w-3 bg-primary rounded-t",
|
|
184
|
-
style: { height: `${e.completed /
|
|
220
|
+
style: { height: `${e.completed / q * 100}%` },
|
|
185
221
|
title: `Completed: ${e.completed}`
|
|
186
222
|
})]
|
|
187
|
-
}), /* @__PURE__ */
|
|
223
|
+
}), /* @__PURE__ */ h("span", {
|
|
188
224
|
className: "text-xs text-muted-foreground",
|
|
189
225
|
children: e.month
|
|
190
226
|
})]
|
|
191
227
|
}, e.month))
|
|
192
|
-
}), /* @__PURE__ */
|
|
228
|
+
}), /* @__PURE__ */ g("div", {
|
|
193
229
|
className: "flex gap-4 justify-center mt-3 text-xs",
|
|
194
|
-
children: [/* @__PURE__ */
|
|
230
|
+
children: [/* @__PURE__ */ g("span", {
|
|
195
231
|
className: "flex items-center gap-1",
|
|
196
|
-
children: [/* @__PURE__ */
|
|
197
|
-
}), /* @__PURE__ */
|
|
232
|
+
children: [/* @__PURE__ */ h("span", { className: "h-2 w-2 rounded bg-primary/30" }), "Planned"]
|
|
233
|
+
}), /* @__PURE__ */ g("span", {
|
|
198
234
|
className: "flex items-center gap-1",
|
|
199
|
-
children: [/* @__PURE__ */
|
|
235
|
+
children: [/* @__PURE__ */ h("span", { className: "h-2 w-2 rounded bg-primary" }), "Completed"]
|
|
200
236
|
})]
|
|
201
|
-
})] })] }), /* @__PURE__ */
|
|
237
|
+
})] })] }), /* @__PURE__ */ g(v, { children: [/* @__PURE__ */ h(b, { children: /* @__PURE__ */ h(x, { children: "Item Status Breakdown" }) }), /* @__PURE__ */ h(y, {
|
|
202
238
|
className: "space-y-3",
|
|
203
|
-
children: Object.entries(
|
|
204
|
-
let n =
|
|
205
|
-
return /* @__PURE__ */
|
|
239
|
+
children: Object.entries(R).map(([e, t]) => {
|
|
240
|
+
let n = N.length, r = n > 0 ? Math.round(t / n * 100) : 0;
|
|
241
|
+
return /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ g("div", {
|
|
206
242
|
className: "flex justify-between text-sm mb-1",
|
|
207
|
-
children: [/* @__PURE__ */
|
|
243
|
+
children: [/* @__PURE__ */ h("span", {
|
|
208
244
|
className: "capitalize",
|
|
209
245
|
children: e.toLowerCase().replace("_", " ")
|
|
210
|
-
}), /* @__PURE__ */
|
|
246
|
+
}), /* @__PURE__ */ g("span", {
|
|
211
247
|
className: "font-medium",
|
|
212
248
|
children: [
|
|
213
249
|
t,
|
|
@@ -216,9 +252,9 @@ function v() {
|
|
|
216
252
|
"%)"
|
|
217
253
|
]
|
|
218
254
|
})]
|
|
219
|
-
}), /* @__PURE__ */
|
|
255
|
+
}), /* @__PURE__ */ h("div", {
|
|
220
256
|
className: "w-full h-2 rounded-full bg-muted overflow-hidden",
|
|
221
|
-
children: /* @__PURE__ */
|
|
257
|
+
children: /* @__PURE__ */ h("div", {
|
|
222
258
|
className: `h-full rounded-full ${{
|
|
223
259
|
TODO: "bg-gray-400",
|
|
224
260
|
PLANNED: "bg-yellow-400",
|
|
@@ -233,21 +269,21 @@ function v() {
|
|
|
233
269
|
})
|
|
234
270
|
})] })]
|
|
235
271
|
}),
|
|
236
|
-
/* @__PURE__ */
|
|
272
|
+
/* @__PURE__ */ g("div", {
|
|
237
273
|
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
238
|
-
children: [/* @__PURE__ */
|
|
274
|
+
children: [/* @__PURE__ */ g(v, { children: [/* @__PURE__ */ h(b, { children: /* @__PURE__ */ h(x, { children: "Feature request status (workspace)" }) }), /* @__PURE__ */ h(y, {
|
|
239
275
|
className: "space-y-3",
|
|
240
|
-
children: Object.entries(
|
|
276
|
+
children: Object.entries(z).length === 0 ? /* @__PURE__ */ h("p", {
|
|
241
277
|
className: "text-sm text-muted-foreground text-center py-4",
|
|
242
278
|
children: "No feature request data"
|
|
243
|
-
}) : Object.entries(
|
|
244
|
-
let n =
|
|
245
|
-
return /* @__PURE__ */
|
|
279
|
+
}) : Object.entries(z).map(([e, t]) => {
|
|
280
|
+
let n = O?.all ?? 0, r = n > 0 ? Math.round(t / n * 100) : 0;
|
|
281
|
+
return /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ g("div", {
|
|
246
282
|
className: "flex justify-between text-sm mb-1",
|
|
247
|
-
children: [/* @__PURE__ */
|
|
283
|
+
children: [/* @__PURE__ */ h("span", {
|
|
248
284
|
className: "capitalize",
|
|
249
285
|
children: e.toLowerCase().replace("_", " ")
|
|
250
|
-
}), /* @__PURE__ */
|
|
286
|
+
}), /* @__PURE__ */ g("span", {
|
|
251
287
|
className: "font-medium",
|
|
252
288
|
children: [
|
|
253
289
|
t,
|
|
@@ -256,9 +292,9 @@ function v() {
|
|
|
256
292
|
"%)"
|
|
257
293
|
]
|
|
258
294
|
})]
|
|
259
|
-
}), /* @__PURE__ */
|
|
295
|
+
}), /* @__PURE__ */ h("div", {
|
|
260
296
|
className: "w-full h-2 rounded-full bg-muted overflow-hidden",
|
|
261
|
-
children: /* @__PURE__ */
|
|
297
|
+
children: /* @__PURE__ */ h("div", {
|
|
262
298
|
className: `h-full rounded-full ${{
|
|
263
299
|
SUBMITTED: "bg-gray-400",
|
|
264
300
|
UNDER_REVIEW: "bg-yellow-400",
|
|
@@ -271,16 +307,16 @@ function v() {
|
|
|
271
307
|
})
|
|
272
308
|
})] }, e);
|
|
273
309
|
})
|
|
274
|
-
})] }), /* @__PURE__ */
|
|
310
|
+
})] }), /* @__PURE__ */ g(v, { children: [/* @__PURE__ */ h(b, { children: /* @__PURE__ */ h(x, { children: "Swimlane Distribution" }) }), /* @__PURE__ */ h(y, {
|
|
275
311
|
className: "space-y-3",
|
|
276
|
-
children: Object.entries(
|
|
312
|
+
children: Object.entries(B).length === 0 ? /* @__PURE__ */ h("p", {
|
|
277
313
|
className: "text-sm text-muted-foreground text-center py-4",
|
|
278
314
|
children: "No swimlane data"
|
|
279
|
-
}) : Object.entries(
|
|
280
|
-
let n =
|
|
281
|
-
return /* @__PURE__ */
|
|
315
|
+
}) : Object.entries(B).map(([e, t]) => {
|
|
316
|
+
let n = N.length, r = n > 0 ? Math.round(t / n * 100) : 0;
|
|
317
|
+
return /* @__PURE__ */ g("div", { children: [/* @__PURE__ */ g("div", {
|
|
282
318
|
className: "flex justify-between text-sm mb-1",
|
|
283
|
-
children: [/* @__PURE__ */
|
|
319
|
+
children: [/* @__PURE__ */ h("span", { children: e }), /* @__PURE__ */ g("span", {
|
|
284
320
|
className: "font-medium",
|
|
285
321
|
children: [
|
|
286
322
|
t,
|
|
@@ -289,9 +325,9 @@ function v() {
|
|
|
289
325
|
"%)"
|
|
290
326
|
]
|
|
291
327
|
})]
|
|
292
|
-
}), /* @__PURE__ */
|
|
328
|
+
}), /* @__PURE__ */ h("div", {
|
|
293
329
|
className: "w-full h-2 rounded-full bg-muted overflow-hidden",
|
|
294
|
-
children: /* @__PURE__ */
|
|
330
|
+
children: /* @__PURE__ */ h("div", {
|
|
295
331
|
className: "h-full rounded-full bg-primary",
|
|
296
332
|
style: { width: `${r}%` }
|
|
297
333
|
})
|
|
@@ -299,17 +335,17 @@ function v() {
|
|
|
299
335
|
})
|
|
300
336
|
})] })]
|
|
301
337
|
}),
|
|
302
|
-
|
|
338
|
+
H.length > 0 && /* @__PURE__ */ g(v, { children: [/* @__PURE__ */ h(b, { children: /* @__PURE__ */ h(x, { children: "Overdue Items" }) }), /* @__PURE__ */ h(y, { children: /* @__PURE__ */ h("div", {
|
|
303
339
|
className: "space-y-3",
|
|
304
|
-
children:
|
|
340
|
+
children: H.map((e) => /* @__PURE__ */ g("div", {
|
|
305
341
|
className: "flex items-center justify-between border rounded-md p-3",
|
|
306
|
-
children: [/* @__PURE__ */
|
|
342
|
+
children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
|
|
307
343
|
className: "text-sm font-medium",
|
|
308
344
|
children: e.title
|
|
309
|
-
}), /* @__PURE__ */
|
|
345
|
+
}), /* @__PURE__ */ g("p", {
|
|
310
346
|
className: "text-xs text-muted-foreground",
|
|
311
347
|
children: ["Due: ", e.endDate ? new Date(e.endDate).toLocaleDateString() : "N/A"]
|
|
312
|
-
})] }), /* @__PURE__ */
|
|
348
|
+
})] }), /* @__PURE__ */ h("span", {
|
|
313
349
|
className: "text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800 capitalize",
|
|
314
350
|
children: e.status.toLowerCase().replace("_", " ")
|
|
315
351
|
})]
|
|
@@ -319,6 +355,6 @@ function v() {
|
|
|
319
355
|
});
|
|
320
356
|
}
|
|
321
357
|
//#endregion
|
|
322
|
-
export {
|
|
358
|
+
export { w as ProductsRoadmapAnalyticsPagePage };
|
|
323
359
|
|
|
324
360
|
//# sourceMappingURL=ProductsRoadmapAnalyticsPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsRoadmapAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.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, Target, CheckCircle, AlertTriangle, BarChart3, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { useFeatureCounts } from '../hooks/useCounts';\nimport { planningFocusLabel } from '../roadmaps/planningFocus';\n\nconst GET_ROADMAP_ANALYTICS = gql`\n query GetRoadmapAnalytics($id: ID!) {\n planmagnetRoadmap(id: $id) {\n id\n name\n description\n type\n planningFocus\n isPublic\n ownerId\n createdAt\n updatedAt\n items {\n id\n title\n status\n startDate\n endDate\n swimlane\n }\n }\n }\n`;\n\ninterface RoadmapItemNode {\n id: string;\n title: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n swimlane: string | null;\n}\n\nexport function ProductsRoadmapAnalyticsPagePage() {\n const params = useParams<{ id: string }>();\n const roadmapId = 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_ROADMAP_ANALYTICS, {\n variables: { id: roadmapId },\n skip: !roadmapId,\n });\n\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 roadmap = data?.planmagnetRoadmap;\n if (!roadmap) return <div className=\"p-6 text-muted-foreground\">Roadmap not found.</div>;\n\n const items: RoadmapItemNode[] = roadmap.items ?? [];\n\n // BOFF-7994: cancelled items no longer need doing — out of the progress denominator and out of \"overdue\".\n const isDone = (i: RoadmapItemNode) => i.status === 'COMPLETED' || i.status === 'DONE';\n const liveItems = items.filter((i) => i.status !== 'CANCELLED');\n const completedItems = liveItems.filter(isDone);\n const progress =\n liveItems.length > 0 ? Math.round((completedItems.length / liveItems.length) * 100) : 0;\n\n // Items by status\n const itemsByStatus: Record<string, number> = {};\n for (const item of items) {\n itemsByStatus[item.status] = (itemsByStatus[item.status] || 0) + 1;\n }\n\n // Feature request status across the WORKSPACE, from server counts (it was the 100 newest, counted in the browser).\n const featuresByStatus: Record<string, number> = featureCounts\n ? Object.fromEntries(Object.entries(featureCounts.byStatus).filter(([, n]) => n > 0))\n : {};\n\n // Items by swimlane\n const itemsBySwimlane: Record<string, number> = {};\n for (const item of items) {\n const lane = item.swimlane ?? 'Uncategorized';\n itemsBySwimlane[lane] = (itemsBySwimlane[lane] || 0) + 1;\n }\n\n // Items with overdue end dates\n const now = new Date();\n const overdueItems = liveItems.filter(\n (i) => i.endDate && new Date(i.endDate) < now && !isDone(i)\n );\n\n // \"On track\" replaces an undefined health score (0.7 x progress + 30 x not-overdue share, so an empty roadmap\n // scored 30/100): the share of OPEN items that are neither overdue nor flagged at risk or delayed.\n const openItems = liveItems.filter((i) => !isDone(i));\n const onTrack =\n openItems.length > 0\n ? Math.round(\n (openItems.filter(\n (i) => !overdueItems.includes(i) && i.status !== 'AT_RISK' && i.status !== 'DELAYED'\n ).length /\n openItems.length) *\n 100\n )\n : null;\n\n // Items due per month, keyed by YYYY-MM and sorted by DATE, the latest six kept (they were in storage order,\n // and the last six inserted were kept — out of order, recent months dropped).\n const velocityMap: Record<string, { planned: number; completed: number }> = {};\n for (const item of liveItems) {\n if (item.endDate) {\n const key = item.endDate.slice(0, 7);\n if (!velocityMap[key]) velocityMap[key] = { planned: 0, completed: 0 };\n velocityMap[key].planned += 1;\n if (isDone(item)) velocityMap[key].completed += 1;\n }\n }\n const velocityData = Object.entries(velocityMap)\n .sort(([a], [b]) => a.localeCompare(b))\n .slice(-6)\n .map(([key, vals]) => ({\n month: new Date(`${key}-01T00:00:00Z`).toLocaleDateString('en-US', {\n month: 'short',\n year: '2-digit',\n timeZone: 'UTC',\n }),\n ...vals,\n }));\n const maxVal = Math.max(...velocityData.map((d) => Math.max(d.planned, d.completed)), 1);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Roadmap Analytics</h1>\n <p className=\"text-sm text-muted-foreground\">\n {roadmap.name} - {planningFocusLabel(roadmap.planningFocus)}\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 Open items on track\n </div>\n <p className=\"text-2xl font-bold mt-1\">{onTrack === null ? '—' : `${onTrack}%`}</p>\n <div className=\"w-full h-2 rounded-full bg-muted mt-2 overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-success\"\n style={{ width: `${onTrack ?? 0}%` }}\n />\n </div>\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 Progress\n </div>\n <p className=\"text-2xl font-bold mt-1\">{progress}%</p>\n <p className=\"text-xs text-muted-foreground mt-1\">{items.length} items total</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 <CheckCircle className=\"h-4 w-4\" />\n Completed\n </div>\n <p className=\"text-2xl font-bold mt-1\">{completedItems.length}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">items completed</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 <AlertTriangle className=\"h-4 w-4\" />\n Overdue\n </div>\n <p className=\"text-2xl font-bold mt-1\">{overdueItems.length}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">items past end date</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 Item Velocity\n </CardTitle>\n </CardHeader>\n <CardContent>\n {velocityData.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-8\">\n No timeline data available\n </p>\n ) : (\n <div className=\"flex items-end gap-2 h-48\">\n {velocityData.map((d) => (\n <div key={d.month} className=\"flex-1 flex flex-col items-center gap-1\">\n <div className=\"flex gap-0.5 items-end w-full justify-center h-40\">\n <div\n className=\"w-3 bg-primary/30 rounded-t\"\n style={{ height: `${(d.planned / maxVal) * 100}%` }}\n title={`Planned: ${d.planned}`}\n />\n <div\n className=\"w-3 bg-primary rounded-t\"\n style={{ height: `${(d.completed / maxVal) * 100}%` }}\n title={`Completed: ${d.completed}`}\n />\n </div>\n <span className=\"text-xs text-muted-foreground\">{d.month}</span>\n </div>\n ))}\n </div>\n )}\n <div className=\"flex gap-4 justify-center mt-3 text-xs\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded bg-primary/30\" />\n Planned\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded bg-primary\" />\n Completed\n </span>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Item Status Breakdown</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(itemsByStatus).map(([status, count]) => {\n const total = items.length;\n const pct = total > 0 ? Math.round((count / total) * 100) : 0;\n const colors: Record<string, string> = {\n TODO: 'bg-gray-400',\n PLANNED: 'bg-yellow-400',\n IN_PROGRESS: 'bg-blue-400',\n BLOCKED: 'bg-red-400',\n DONE: 'bg-green-400',\n COMPLETED: 'bg-green-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 </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle>Feature request status (workspace)</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(featuresByStatus).length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No feature request data\n </p>\n ) : (\n Object.entries(featuresByStatus).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 };\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 )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Swimlane Distribution</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(itemsBySwimlane).length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">No swimlane data</p>\n ) : (\n Object.entries(itemsBySwimlane).map(([lane, count]) => {\n const total = items.length;\n const pct = total > 0 ? Math.round((count / total) * 100) : 0;\n return (\n <div key={lane}>\n <div className=\"flex justify-between text-sm mb-1\">\n <span>{lane}</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 bg-primary\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n })\n )}\n </CardContent>\n </Card>\n </div>\n\n {overdueItems.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Overdue Items</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {overdueItems.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between border rounded-md p-3\"\n >\n <div>\n <p className=\"text-sm font-medium\">{item.title}</p>\n <p className=\"text-xs text-muted-foreground\">\n Due: {item.endDate ? new Date(item.endDate).toLocaleDateString() : 'N/A'}\n </p>\n </div>\n <span className=\"text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800 capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </span>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProductsRoadmapAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { TrendingUp, Target, CheckCircle, AlertTriangle, BarChart3, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { useFeatureCounts } from '../hooks/useCounts';\nimport { planningFocusLabel } from '../roadmaps/planningFocus';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { useLiveRefresh, type LiveEvent } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\n\n// PRM-11: this page's one query — re-read on any roadmap- or item-level event, same shape as the detail page's\n// and the Now/Next/Later board's own `useLiveRefresh` calls (each page/component is its own self-contained\n// subscriber, over its own named query).\nconst ROADMAP_ANALYTICS_QUERIES = ['GetRoadmapAnalytics'];\n\nconst GET_ROADMAP_ANALYTICS = gql`\n query GetRoadmapAnalytics($id: ID!) {\n planmagnetRoadmap(id: $id) {\n id\n name\n description\n type\n planningFocus\n isPublic\n ownerId\n createdAt\n updatedAt\n items {\n id\n title\n status\n startDate\n endDate\n swimlane\n }\n }\n }\n`;\n\ninterface RoadmapItemNode {\n id: string;\n title: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n swimlane: string | null;\n}\n\nexport function ProductsRoadmapAnalyticsPagePage() {\n const params = useParams<{ id: string }>();\n const roadmapId = params.id ?? '';\n const navigate = usePlanMagnetNavigate();\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_ROADMAP_ANALYTICS, {\n variables: { id: roadmapId },\n skip: !roadmapId,\n });\n\n const featureCounts = useFeatureCounts();\n\n // PRM-11: once a DELETED event names the roadmap itself, show the clear \"This roadmap was deleted\" state below\n // instead of leaving the charts on stale data — see the same hook/comment on `ProductsRoadmapDetailPagePage`.\n const [roadmapDeleted, setRoadmapDeleted] = useState(false);\n const roadmapLive = useLiveRefresh({\n kind: 'roadmap',\n roadmapId,\n queries: ROADMAP_ANALYTICS_QUERIES,\n onEvent: (event: LiveEvent) => {\n if (event.kind === 'DELETED' && event.id === roadmapId) setRoadmapDeleted(true);\n },\n });\n\n if (roadmapDeleted) {\n return (\n <div className=\"space-y-3 p-6\" data-testid=\"roadmap-deleted\">\n <p className=\"text-sm text-muted-foreground\">This roadmap was deleted.</p>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => navigate('/products/roadmaps-page')}>\n Back to roadmaps\n </Button>\n </div>\n );\n }\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 roadmap = data?.planmagnetRoadmap;\n if (!roadmap) {\n return (\n <div className=\"space-y-3 p-6\" data-testid=\"roadmap-not-found\">\n <p className=\"text-sm text-muted-foreground\">\n This roadmap does not exist in this workspace, or it was deleted.\n </p>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => navigate('/products/roadmaps-page')}>\n Back to roadmaps\n </Button>\n </div>\n );\n }\n\n const items: RoadmapItemNode[] = roadmap.items ?? [];\n\n // BOFF-7994: cancelled items no longer need doing — out of the progress denominator and out of \"overdue\".\n const isDone = (i: RoadmapItemNode) => i.status === 'COMPLETED' || i.status === 'DONE';\n const liveItems = items.filter((i) => i.status !== 'CANCELLED');\n const completedItems = liveItems.filter(isDone);\n const progress =\n liveItems.length > 0 ? Math.round((completedItems.length / liveItems.length) * 100) : 0;\n\n // Items by status\n const itemsByStatus: Record<string, number> = {};\n for (const item of items) {\n itemsByStatus[item.status] = (itemsByStatus[item.status] || 0) + 1;\n }\n\n // Feature request status across the WORKSPACE, from server counts (it was the 100 newest, counted in the browser).\n const featuresByStatus: Record<string, number> = featureCounts\n ? Object.fromEntries(Object.entries(featureCounts.byStatus).filter(([, n]) => n > 0))\n : {};\n\n // Items by swimlane\n const itemsBySwimlane: Record<string, number> = {};\n for (const item of items) {\n const lane = item.swimlane ?? 'Uncategorized';\n itemsBySwimlane[lane] = (itemsBySwimlane[lane] || 0) + 1;\n }\n\n // Items with overdue end dates\n const now = new Date();\n const overdueItems = liveItems.filter(\n (i) => i.endDate && new Date(i.endDate) < now && !isDone(i)\n );\n\n // \"On track\" replaces an undefined health score (0.7 x progress + 30 x not-overdue share, so an empty roadmap\n // scored 30/100): the share of OPEN items that are neither overdue nor flagged at risk or delayed.\n const openItems = liveItems.filter((i) => !isDone(i));\n const onTrack =\n openItems.length > 0\n ? Math.round(\n (openItems.filter(\n (i) => !overdueItems.includes(i) && i.status !== 'AT_RISK' && i.status !== 'DELAYED'\n ).length /\n openItems.length) *\n 100\n )\n : null;\n\n // Items due per month, keyed by YYYY-MM and sorted by DATE, the latest six kept (they were in storage order,\n // and the last six inserted were kept — out of order, recent months dropped).\n const velocityMap: Record<string, { planned: number; completed: number }> = {};\n for (const item of liveItems) {\n if (item.endDate) {\n const key = item.endDate.slice(0, 7);\n if (!velocityMap[key]) velocityMap[key] = { planned: 0, completed: 0 };\n velocityMap[key].planned += 1;\n if (isDone(item)) velocityMap[key].completed += 1;\n }\n }\n const velocityData = Object.entries(velocityMap)\n .sort(([a], [b]) => a.localeCompare(b))\n .slice(-6)\n .map(([key, vals]) => ({\n month: new Date(`${key}-01T00:00:00Z`).toLocaleDateString('en-US', {\n month: 'short',\n year: '2-digit',\n timeZone: 'UTC',\n }),\n ...vals,\n }));\n const maxVal = Math.max(...velocityData.map((d) => Math.max(d.planned, d.completed)), 1);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h1 className=\"text-2xl font-semibold\">Roadmap Analytics</h1>\n <LiveStatus mode={roadmapLive.mode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {roadmap.name} - {planningFocusLabel(roadmap.planningFocus)}\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 Open items on track\n </div>\n <p className=\"text-2xl font-bold mt-1\">{onTrack === null ? '—' : `${onTrack}%`}</p>\n <div className=\"w-full h-2 rounded-full bg-muted mt-2 overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-success\"\n style={{ width: `${onTrack ?? 0}%` }}\n />\n </div>\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 Progress\n </div>\n <p className=\"text-2xl font-bold mt-1\">{progress}%</p>\n <p className=\"text-xs text-muted-foreground mt-1\">{items.length} items total</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 <CheckCircle className=\"h-4 w-4\" />\n Completed\n </div>\n <p className=\"text-2xl font-bold mt-1\">{completedItems.length}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">items completed</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 <AlertTriangle className=\"h-4 w-4\" />\n Overdue\n </div>\n <p className=\"text-2xl font-bold mt-1\">{overdueItems.length}</p>\n <p className=\"text-xs text-muted-foreground mt-1\">items past end date</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 Item Velocity\n </CardTitle>\n </CardHeader>\n <CardContent>\n {velocityData.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-8\">\n No timeline data available\n </p>\n ) : (\n <div className=\"flex items-end gap-2 h-48\">\n {velocityData.map((d) => (\n <div key={d.month} className=\"flex-1 flex flex-col items-center gap-1\">\n <div className=\"flex gap-0.5 items-end w-full justify-center h-40\">\n <div\n className=\"w-3 bg-primary/30 rounded-t\"\n style={{ height: `${(d.planned / maxVal) * 100}%` }}\n title={`Planned: ${d.planned}`}\n />\n <div\n className=\"w-3 bg-primary rounded-t\"\n style={{ height: `${(d.completed / maxVal) * 100}%` }}\n title={`Completed: ${d.completed}`}\n />\n </div>\n <span className=\"text-xs text-muted-foreground\">{d.month}</span>\n </div>\n ))}\n </div>\n )}\n <div className=\"flex gap-4 justify-center mt-3 text-xs\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded bg-primary/30\" />\n Planned\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded bg-primary\" />\n Completed\n </span>\n </div>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Item Status Breakdown</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(itemsByStatus).map(([status, count]) => {\n const total = items.length;\n const pct = total > 0 ? Math.round((count / total) * 100) : 0;\n const colors: Record<string, string> = {\n TODO: 'bg-gray-400',\n PLANNED: 'bg-yellow-400',\n IN_PROGRESS: 'bg-blue-400',\n BLOCKED: 'bg-red-400',\n DONE: 'bg-green-400',\n COMPLETED: 'bg-green-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 </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle>Feature request status (workspace)</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(featuresByStatus).length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No feature request data\n </p>\n ) : (\n Object.entries(featuresByStatus).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 };\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 )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Swimlane Distribution</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(itemsBySwimlane).length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-4\">No swimlane data</p>\n ) : (\n Object.entries(itemsBySwimlane).map(([lane, count]) => {\n const total = items.length;\n const pct = total > 0 ? Math.round((count / total) * 100) : 0;\n return (\n <div key={lane}>\n <div className=\"flex justify-between text-sm mb-1\">\n <span>{lane}</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 bg-primary\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n })\n )}\n </CardContent>\n </Card>\n </div>\n\n {overdueItems.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Overdue Items</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {overdueItems.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between border rounded-md p-3\"\n >\n <div>\n <p className=\"text-sm font-medium\">{item.title}</p>\n <p className=\"text-xs text-muted-foreground\">\n Due: {item.endDate ? new Date(item.endDate).toLocaleDateString() : 'N/A'}\n </p>\n </div>\n <span className=\"text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800 capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </span>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsRoadmapDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsRoadmapDetailPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ProductsRoadmapDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsRoadmapDetailPagePage.tsx"],"names":[],"mappings":"AAkHA,wBAAgB,6BAA6B,4CAsU5C"}
|