@burdenoff/microfe-planmagnet 2026.924.3 → 2026.924.5
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/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +180 -172
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts +15 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +302 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AnalyticsAnalyticsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"names":[],"mappings":"AAsQA,wBAAgB,0BAA0B,4CAsczC"}
|
|
@@ -6,21 +6,22 @@ import { Bar as i } from "../../node_modules/recharts/es6/cartesian/Bar.js";
|
|
|
6
6
|
import { ReferenceLine as ee } from "../../node_modules/recharts/es6/cartesian/ReferenceLine.js";
|
|
7
7
|
import { CartesianGrid as a } from "../../node_modules/recharts/es6/cartesian/CartesianGrid.js";
|
|
8
8
|
import { Line as o } from "../../node_modules/recharts/es6/cartesian/Line.js";
|
|
9
|
-
import { Area as
|
|
10
|
-
import { XAxis as
|
|
11
|
-
import { YAxis as
|
|
12
|
-
import { LineChart as
|
|
13
|
-
import { BarChart as
|
|
14
|
-
import { AreaChart as
|
|
9
|
+
import { Area as te } from "../../node_modules/recharts/es6/cartesian/Area.js";
|
|
10
|
+
import { XAxis as s } from "../../node_modules/recharts/es6/cartesian/XAxis.js";
|
|
11
|
+
import { YAxis as c } from "../../node_modules/recharts/es6/cartesian/YAxis.js";
|
|
12
|
+
import { LineChart as ne } from "../../node_modules/recharts/es6/chart/LineChart.js";
|
|
13
|
+
import { BarChart as re } from "../../node_modules/recharts/es6/chart/BarChart.js";
|
|
14
|
+
import { AreaChart as ie } from "../../node_modules/recharts/es6/chart/AreaChart.js";
|
|
15
|
+
import { BudgetPerformanceCard as l, CycleTimeCard as u } from "./analytics/DeliveryHealthCards.js";
|
|
15
16
|
import { useState as d } from "react";
|
|
16
|
-
import { BarChart3 as f, FolderKanban as
|
|
17
|
-
import { gql as
|
|
18
|
-
import { useQuery as
|
|
19
|
-
import { Fragment as
|
|
17
|
+
import { BarChart3 as f, FolderKanban as p, Target as m, TrendingUp as h, Zap as ae } from "lucide-react";
|
|
18
|
+
import { gql as g } from "@apollo/client";
|
|
19
|
+
import { useQuery as _ } from "@apollo/client/react";
|
|
20
|
+
import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
20
21
|
import { useI18n as oe } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
21
|
-
import { Badge as se, Card as
|
|
22
|
+
import { Badge as se, Card as x, CardContent as S, CardHeader as C, CardTitle as w, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k } from "@burdenoff/fe-libs/ui";
|
|
22
23
|
//#region src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx
|
|
23
|
-
var
|
|
24
|
+
var A = g`
|
|
24
25
|
query GetAnalyticsData(
|
|
25
26
|
$workspaceId: ID!
|
|
26
27
|
$projectPagination: PlanmagnetPaginationInput
|
|
@@ -73,7 +74,7 @@ var ce = h`
|
|
|
73
74
|
totalCount
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
|
-
`,
|
|
77
|
+
`, j = g`
|
|
77
78
|
query GetSprintBurndown($workspaceId: ID!, $sprintId: ID!) {
|
|
78
79
|
planmagnetSprintBurndown(workspaceId: $workspaceId, sprintId: $sprintId) {
|
|
79
80
|
sprintId
|
|
@@ -90,7 +91,7 @@ var ce = h`
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
`,
|
|
94
|
+
`, ce = g`
|
|
94
95
|
query GetVelocity($workspaceId: ID!, $projectId: ID!, $trailingSprintCount: Int) {
|
|
95
96
|
planmagnetVelocity(
|
|
96
97
|
workspaceId: $workspaceId
|
|
@@ -110,7 +111,7 @@ var ce = h`
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
`,
|
|
114
|
+
`, le = g`
|
|
114
115
|
query GetCumulativeFlow($workspaceId: ID!, $projectId: ID!) {
|
|
115
116
|
planmagnetCumulativeFlow(workspaceId: $workspaceId, projectId: $projectId) {
|
|
116
117
|
projectId
|
|
@@ -125,7 +126,7 @@ var ce = h`
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
|
-
`,
|
|
129
|
+
`, M = {
|
|
129
130
|
BACKLOG: "#9ca3af",
|
|
130
131
|
TODO: "#60a5fa",
|
|
131
132
|
IN_PROGRESS: "#fbbf24",
|
|
@@ -134,396 +135,403 @@ var ce = h`
|
|
|
134
135
|
DONE: "#4ade80",
|
|
135
136
|
CANCELLED: "#f87171"
|
|
136
137
|
};
|
|
137
|
-
function
|
|
138
|
+
function N(e) {
|
|
138
139
|
let t = new Date(e);
|
|
139
140
|
return `${t.getUTCMonth() + 1}/${t.getUTCDate()}`;
|
|
140
141
|
}
|
|
141
|
-
function
|
|
142
|
-
let { workspaceId:
|
|
142
|
+
function P() {
|
|
143
|
+
let { workspaceId: g } = e(), { t: P } = oe(), [ue, de] = d(""), [fe, F] = d(""), { data: I, loading: L, error: R } = _(A, {
|
|
143
144
|
variables: {
|
|
144
|
-
workspaceId:
|
|
145
|
+
workspaceId: g,
|
|
145
146
|
projectPagination: { first: 100 },
|
|
146
147
|
workItemPagination: { first: 500 },
|
|
147
148
|
sprintPagination: { first: 100 },
|
|
148
149
|
milestonePagination: { first: 100 }
|
|
149
150
|
},
|
|
150
|
-
skip: !
|
|
151
|
-
}),
|
|
151
|
+
skip: !g
|
|
152
|
+
}), z = I?.planmagnetProjects?.items ?? [], B = I?.planmagnetWorkItems?.items ?? [], V = I?.planmagnetSprints?.items ?? [], pe = I?.planmagnetMilestones?.items ?? [], H = ue || z[0]?.id || "", U = V.filter((e) => e.projectId === H), W = fe || U[0]?.id || "", { data: me, loading: he } = _(j, {
|
|
152
153
|
variables: {
|
|
153
|
-
workspaceId:
|
|
154
|
-
sprintId:
|
|
154
|
+
workspaceId: g,
|
|
155
|
+
sprintId: W
|
|
155
156
|
},
|
|
156
|
-
skip: !
|
|
157
|
-
}), { data:
|
|
157
|
+
skip: !g || !W
|
|
158
|
+
}), { data: ge, loading: _e } = _(ce, {
|
|
158
159
|
variables: {
|
|
159
|
-
workspaceId:
|
|
160
|
-
projectId:
|
|
160
|
+
workspaceId: g,
|
|
161
|
+
projectId: H,
|
|
161
162
|
trailingSprintCount: 6
|
|
162
163
|
},
|
|
163
|
-
skip: !
|
|
164
|
-
}), { data:
|
|
164
|
+
skip: !g || !H
|
|
165
|
+
}), { data: ve, loading: ye } = _(le, {
|
|
165
166
|
variables: {
|
|
166
|
-
workspaceId:
|
|
167
|
-
projectId:
|
|
167
|
+
workspaceId: g,
|
|
168
|
+
projectId: H
|
|
168
169
|
},
|
|
169
|
-
skip: !
|
|
170
|
+
skip: !g || !H
|
|
170
171
|
});
|
|
171
|
-
if (
|
|
172
|
+
if (L) return /* @__PURE__ */ y("div", {
|
|
172
173
|
className: "flex items-center justify-center p-12",
|
|
173
|
-
children: /* @__PURE__ */
|
|
174
|
+
children: /* @__PURE__ */ y("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
174
175
|
});
|
|
175
|
-
if (
|
|
176
|
+
if (R) return /* @__PURE__ */ b("div", {
|
|
176
177
|
className: "p-6 text-red-500",
|
|
177
178
|
children: [
|
|
178
|
-
|
|
179
|
+
P("planmagnet.analytics.errorLoading", "Error loading analytics"),
|
|
179
180
|
": ",
|
|
180
|
-
|
|
181
|
+
R.message
|
|
181
182
|
]
|
|
182
183
|
});
|
|
183
|
-
let
|
|
184
|
-
day:
|
|
184
|
+
let G = B.length, be = B.filter((e) => e.status === "DONE").length, K = B.filter((e) => e.status === "BLOCKED").length, xe = B.reduce((e, t) => e + (t.storyPoints ?? 0), 0), Se = B.filter((e) => e.status === "DONE").reduce((e, t) => e + (t.storyPoints ?? 0), 0), Ce = V.filter((e) => e.status === "ACTIVE").length, we = pe.filter((e) => e.status === "AT_RISK" || e.status === "OVERDUE").length, q = z.filter((e) => e.status === "ACTIVE").length, Te = G > 0 ? Math.round(be / G * 100) : 0, J = (me?.planmagnetSprintBurndown)?.points.map((e) => ({
|
|
185
|
+
day: N(e.date),
|
|
185
186
|
remaining: e.remainingPoints,
|
|
186
187
|
completed: e.completedPoints
|
|
187
|
-
})) ?? [], Y =
|
|
188
|
+
})) ?? [], Y = ge?.planmagnetVelocity, X = Y?.sprints.map((e) => ({
|
|
188
189
|
name: e.sprintName,
|
|
189
190
|
completed: e.completedPoints,
|
|
190
191
|
planned: e.plannedPoints ?? void 0
|
|
191
|
-
})) ?? [], Z = Y ? Math.round(Y.averageVelocity) : 0,
|
|
192
|
-
let t = { day:
|
|
192
|
+
})) ?? [], Z = Y ? Math.round(Y.averageVelocity) : 0, Ee = Y ? Math.round(Y.forecastNextSprintPoints) : 0, Q = ve?.planmagnetCumulativeFlow, De = Array.from(new Set(Q?.points.flatMap((e) => e.statuses.map((e) => e.status)) ?? [])), $ = Q?.points.map((e) => {
|
|
193
|
+
let t = { day: N(e.date) };
|
|
193
194
|
for (let n of e.statuses) t[n.status] = n.count;
|
|
194
195
|
return t;
|
|
195
|
-
}) ?? [],
|
|
196
|
+
}) ?? [], Oe = [
|
|
196
197
|
{
|
|
197
|
-
label:
|
|
198
|
-
value: String(
|
|
199
|
-
trend: `${
|
|
198
|
+
label: P("planmagnet.analytics.metrics.totalWorkItems.label", "Total Work Items"),
|
|
199
|
+
value: String(G),
|
|
200
|
+
trend: `${Te}% ${P("planmagnet.analytics.metrics.totalWorkItems.trend", "completed")}`
|
|
200
201
|
},
|
|
201
202
|
{
|
|
202
|
-
label:
|
|
203
|
-
value: String(
|
|
204
|
-
trend: `${
|
|
203
|
+
label: P("planmagnet.analytics.metrics.storyPoints.label", "Story Points"),
|
|
204
|
+
value: String(xe),
|
|
205
|
+
trend: `${Se} ${P("planmagnet.analytics.metrics.storyPoints.trend", "completed")}`
|
|
205
206
|
},
|
|
206
207
|
{
|
|
207
|
-
label:
|
|
208
|
+
label: P("planmagnet.analytics.metrics.activeProjects.label", "Active Projects"),
|
|
208
209
|
value: String(q),
|
|
209
|
-
trend: `${
|
|
210
|
+
trend: `${z.length} ${P("planmagnet.analytics.metrics.activeProjects.trend", "total")}`
|
|
210
211
|
},
|
|
211
212
|
{
|
|
212
|
-
label:
|
|
213
|
-
value: String(
|
|
214
|
-
trend:
|
|
213
|
+
label: P("planmagnet.analytics.metrics.blockedItems.label", "Blocked Items"),
|
|
214
|
+
value: String(K),
|
|
215
|
+
trend: K > 0 ? P("planmagnet.analytics.metrics.blockedItems.attention", "Needs attention") : P("planmagnet.analytics.metrics.blockedItems.clear", "All clear")
|
|
215
216
|
}
|
|
216
217
|
];
|
|
217
|
-
return /* @__PURE__ */
|
|
218
|
+
return /* @__PURE__ */ b("div", {
|
|
218
219
|
className: "space-y-6 p-6",
|
|
219
220
|
children: [
|
|
220
|
-
/* @__PURE__ */
|
|
221
|
+
/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h1", {
|
|
221
222
|
className: "text-2xl font-semibold",
|
|
222
|
-
children:
|
|
223
|
-
}), /* @__PURE__ */
|
|
223
|
+
children: P("planmagnet.analytics.title", "Analytics Dashboard")
|
|
224
|
+
}), /* @__PURE__ */ y("p", {
|
|
224
225
|
className: "text-sm text-muted-foreground",
|
|
225
|
-
children:
|
|
226
|
+
children: P("planmagnet.analytics.subtitle", "Overview of project health and team performance")
|
|
226
227
|
})] }),
|
|
227
|
-
/* @__PURE__ */
|
|
228
|
+
/* @__PURE__ */ y("div", {
|
|
228
229
|
className: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
|
229
|
-
children:
|
|
230
|
+
children: Oe.map((e) => /* @__PURE__ */ y(x, { children: /* @__PURE__ */ b(S, {
|
|
230
231
|
className: "pt-4",
|
|
231
232
|
children: [
|
|
232
|
-
/* @__PURE__ */
|
|
233
|
+
/* @__PURE__ */ y("p", {
|
|
233
234
|
className: "text-xs text-muted-foreground",
|
|
234
235
|
children: e.label
|
|
235
236
|
}),
|
|
236
|
-
/* @__PURE__ */
|
|
237
|
+
/* @__PURE__ */ y("p", {
|
|
237
238
|
className: "text-xl font-bold mt-1",
|
|
238
239
|
children: e.value
|
|
239
240
|
}),
|
|
240
|
-
/* @__PURE__ */
|
|
241
|
+
/* @__PURE__ */ y("p", {
|
|
241
242
|
className: "text-xs text-green-600 mt-1",
|
|
242
243
|
children: e.trend
|
|
243
244
|
})
|
|
244
245
|
]
|
|
245
246
|
}) }, e.label))
|
|
246
247
|
}),
|
|
247
|
-
/* @__PURE__ */
|
|
248
|
+
/* @__PURE__ */ b("div", {
|
|
248
249
|
className: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
|
249
250
|
children: [
|
|
250
|
-
/* @__PURE__ */
|
|
251
|
+
/* @__PURE__ */ y(x, { children: /* @__PURE__ */ b(S, {
|
|
251
252
|
className: "pt-4",
|
|
252
|
-
children: [/* @__PURE__ */
|
|
253
|
+
children: [/* @__PURE__ */ b("div", {
|
|
253
254
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
254
|
-
children: [/* @__PURE__ */
|
|
255
|
-
}), /* @__PURE__ */
|
|
255
|
+
children: [/* @__PURE__ */ y(p, { className: "h-4 w-4" }), P("planmagnet.analytics.summary.activeProjects", "Active Projects")]
|
|
256
|
+
}), /* @__PURE__ */ y("p", {
|
|
256
257
|
className: "text-xl font-bold mt-1",
|
|
257
258
|
children: q
|
|
258
259
|
})]
|
|
259
260
|
}) }),
|
|
260
|
-
/* @__PURE__ */
|
|
261
|
+
/* @__PURE__ */ y(x, { children: /* @__PURE__ */ b(S, {
|
|
261
262
|
className: "pt-4",
|
|
262
|
-
children: [/* @__PURE__ */
|
|
263
|
+
children: [/* @__PURE__ */ b("div", {
|
|
263
264
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
264
|
-
children: [/* @__PURE__ */
|
|
265
|
-
}), /* @__PURE__ */
|
|
265
|
+
children: [/* @__PURE__ */ y(ae, { className: "h-4 w-4" }), P("planmagnet.analytics.summary.activeSprints", "Active Sprints")]
|
|
266
|
+
}), /* @__PURE__ */ y("p", {
|
|
266
267
|
className: "text-xl font-bold mt-1",
|
|
267
|
-
children:
|
|
268
|
+
children: Ce
|
|
268
269
|
})]
|
|
269
270
|
}) }),
|
|
270
|
-
/* @__PURE__ */
|
|
271
|
+
/* @__PURE__ */ y(x, { children: /* @__PURE__ */ b(S, {
|
|
271
272
|
className: "pt-4",
|
|
272
|
-
children: [/* @__PURE__ */
|
|
273
|
+
children: [/* @__PURE__ */ b("div", {
|
|
273
274
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
274
|
-
children: [/* @__PURE__ */
|
|
275
|
-
}), /* @__PURE__ */
|
|
275
|
+
children: [/* @__PURE__ */ y(h, { className: "h-4 w-4" }), P("planmagnet.analytics.summary.avgVelocity", "Avg Velocity")]
|
|
276
|
+
}), /* @__PURE__ */ b("p", {
|
|
276
277
|
className: "text-xl font-bold mt-1",
|
|
277
278
|
children: [
|
|
278
279
|
Z,
|
|
279
280
|
" ",
|
|
280
|
-
|
|
281
|
+
P("planmagnet.analytics.summary.pointsShort", "pts")
|
|
281
282
|
]
|
|
282
283
|
})]
|
|
283
284
|
}) }),
|
|
284
|
-
/* @__PURE__ */
|
|
285
|
+
/* @__PURE__ */ y(x, { children: /* @__PURE__ */ b(S, {
|
|
285
286
|
className: "pt-4",
|
|
286
|
-
children: [/* @__PURE__ */
|
|
287
|
+
children: [/* @__PURE__ */ b("div", {
|
|
287
288
|
className: "flex items-center gap-2 text-muted-foreground text-xs",
|
|
288
|
-
children: [/* @__PURE__ */
|
|
289
|
-
}), /* @__PURE__ */
|
|
289
|
+
children: [/* @__PURE__ */ y(m, { className: "h-4 w-4" }), P("planmagnet.analytics.summary.atRisk", "At Risk")]
|
|
290
|
+
}), /* @__PURE__ */ b("p", {
|
|
290
291
|
className: "text-xl font-bold mt-1",
|
|
291
292
|
children: [
|
|
292
|
-
|
|
293
|
+
we,
|
|
293
294
|
" ",
|
|
294
|
-
|
|
295
|
+
P("planmagnet.analytics.summary.milestones", "milestones")
|
|
295
296
|
]
|
|
296
297
|
})]
|
|
297
298
|
}) })
|
|
298
299
|
]
|
|
299
300
|
}),
|
|
300
|
-
/* @__PURE__ */
|
|
301
|
+
/* @__PURE__ */ b("div", {
|
|
301
302
|
className: "flex flex-col sm:flex-row gap-3 items-start sm:items-center",
|
|
302
|
-
children: [/* @__PURE__ */
|
|
303
|
+
children: [/* @__PURE__ */ y("h2", {
|
|
303
304
|
className: "text-lg font-semibold",
|
|
304
|
-
children:
|
|
305
|
-
}), /* @__PURE__ */
|
|
305
|
+
children: P("planmagnet.analytics.delivery.title", "Sprint Delivery Analytics")
|
|
306
|
+
}), /* @__PURE__ */ b("div", {
|
|
306
307
|
className: "flex gap-2 sm:ml-auto",
|
|
307
|
-
children: [/* @__PURE__ */
|
|
308
|
-
value:
|
|
308
|
+
children: [/* @__PURE__ */ b(T, {
|
|
309
|
+
value: H,
|
|
309
310
|
onValueChange: (e) => {
|
|
310
|
-
de(e),
|
|
311
|
+
de(e), F("");
|
|
311
312
|
},
|
|
312
|
-
children: [/* @__PURE__ */
|
|
313
|
+
children: [/* @__PURE__ */ y(O, {
|
|
313
314
|
className: "h-8 w-48",
|
|
314
|
-
children: /* @__PURE__ */
|
|
315
|
-
}), /* @__PURE__ */
|
|
315
|
+
children: /* @__PURE__ */ y(k, { placeholder: P("planmagnet.analytics.delivery.projectPlaceholder", "Select project") })
|
|
316
|
+
}), /* @__PURE__ */ y(E, { children: z.map((e) => /* @__PURE__ */ y(D, {
|
|
316
317
|
value: e.id,
|
|
317
318
|
children: e.name
|
|
318
319
|
}, e.id)) })]
|
|
319
|
-
}), /* @__PURE__ */
|
|
320
|
-
value:
|
|
321
|
-
onValueChange:
|
|
322
|
-
children: [/* @__PURE__ */
|
|
320
|
+
}), /* @__PURE__ */ b(T, {
|
|
321
|
+
value: W,
|
|
322
|
+
onValueChange: F,
|
|
323
|
+
children: [/* @__PURE__ */ y(O, {
|
|
323
324
|
className: "h-8 w-48",
|
|
324
|
-
children: /* @__PURE__ */
|
|
325
|
-
}), /* @__PURE__ */
|
|
325
|
+
children: /* @__PURE__ */ y(k, { placeholder: P("planmagnet.analytics.delivery.sprintPlaceholder", "Select sprint") })
|
|
326
|
+
}), /* @__PURE__ */ y(E, { children: U.map((e) => /* @__PURE__ */ y(D, {
|
|
326
327
|
value: e.id,
|
|
327
328
|
children: e.name
|
|
328
329
|
}, e.id)) })]
|
|
329
330
|
})]
|
|
330
331
|
})]
|
|
331
332
|
}),
|
|
332
|
-
/* @__PURE__ */
|
|
333
|
+
/* @__PURE__ */ b("div", {
|
|
333
334
|
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
334
|
-
children: [/* @__PURE__ */
|
|
335
|
+
children: [/* @__PURE__ */ b(x, { children: [/* @__PURE__ */ y(C, { children: /* @__PURE__ */ b(w, {
|
|
335
336
|
className: "flex items-center gap-2",
|
|
336
|
-
children: [/* @__PURE__ */
|
|
337
|
-
}) }), /* @__PURE__ */
|
|
337
|
+
children: [/* @__PURE__ */ y(f, { className: "h-4 w-4" }), P("planmagnet.analytics.burndown.title", "Sprint Burndown")]
|
|
338
|
+
}) }), /* @__PURE__ */ y(S, { children: he ? /* @__PURE__ */ y("div", {
|
|
338
339
|
className: "flex justify-center py-12",
|
|
339
|
-
children: /* @__PURE__ */
|
|
340
|
-
}) : J.length > 0 ? /* @__PURE__ */
|
|
340
|
+
children: /* @__PURE__ */ y("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
341
|
+
}) : J.length > 0 ? /* @__PURE__ */ y(r, {
|
|
341
342
|
width: "100%",
|
|
342
343
|
height: 220,
|
|
343
|
-
children: /* @__PURE__ */
|
|
344
|
+
children: /* @__PURE__ */ b(ne, {
|
|
344
345
|
data: J,
|
|
345
346
|
children: [
|
|
346
|
-
/* @__PURE__ */
|
|
347
|
+
/* @__PURE__ */ y(a, {
|
|
347
348
|
strokeDasharray: "3 3",
|
|
348
349
|
opacity: .2
|
|
349
350
|
}),
|
|
350
|
-
/* @__PURE__ */
|
|
351
|
+
/* @__PURE__ */ y(s, {
|
|
351
352
|
dataKey: "day",
|
|
352
353
|
tick: { fontSize: 11 }
|
|
353
354
|
}),
|
|
354
|
-
/* @__PURE__ */
|
|
355
|
+
/* @__PURE__ */ y(c, {
|
|
355
356
|
tick: { fontSize: 11 },
|
|
356
357
|
allowDecimals: !1
|
|
357
358
|
}),
|
|
358
|
-
/* @__PURE__ */
|
|
359
|
-
/* @__PURE__ */
|
|
360
|
-
/* @__PURE__ */
|
|
359
|
+
/* @__PURE__ */ y(n, {}),
|
|
360
|
+
/* @__PURE__ */ y(t, { wrapperStyle: { fontSize: 12 } }),
|
|
361
|
+
/* @__PURE__ */ y(o, {
|
|
361
362
|
type: "monotone",
|
|
362
363
|
dataKey: "remaining",
|
|
363
|
-
name:
|
|
364
|
+
name: P("planmagnet.analytics.burndown.remaining", "Remaining points"),
|
|
364
365
|
stroke: "#f59e0b",
|
|
365
366
|
strokeWidth: 2,
|
|
366
367
|
dot: { r: 2 }
|
|
367
368
|
}),
|
|
368
|
-
/* @__PURE__ */
|
|
369
|
+
/* @__PURE__ */ y(o, {
|
|
369
370
|
type: "monotone",
|
|
370
371
|
dataKey: "completed",
|
|
371
|
-
name:
|
|
372
|
+
name: P("planmagnet.analytics.burndown.completed", "Completed points"),
|
|
372
373
|
stroke: "#22c55e",
|
|
373
374
|
strokeWidth: 2,
|
|
374
375
|
dot: { r: 2 }
|
|
375
376
|
})
|
|
376
377
|
]
|
|
377
378
|
})
|
|
378
|
-
}) : /* @__PURE__ */
|
|
379
|
+
}) : /* @__PURE__ */ y("p", {
|
|
379
380
|
className: "text-sm text-muted-foreground py-8 text-center",
|
|
380
|
-
children:
|
|
381
|
-
}) })] }), /* @__PURE__ */
|
|
381
|
+
children: P("planmagnet.analytics.burndown.empty", "No sprint selected, or the sprint has no work items yet")
|
|
382
|
+
}) })] }), /* @__PURE__ */ b(x, { children: [/* @__PURE__ */ y(C, { children: /* @__PURE__ */ b(w, {
|
|
382
383
|
className: "flex items-center gap-2",
|
|
383
384
|
children: [
|
|
384
|
-
/* @__PURE__ */
|
|
385
|
-
|
|
385
|
+
/* @__PURE__ */ y(h, { className: "h-4 w-4" }),
|
|
386
|
+
P("planmagnet.analytics.velocity.title", "Sprint Velocity"),
|
|
386
387
|
" (",
|
|
387
388
|
X.length,
|
|
388
389
|
" ",
|
|
389
|
-
|
|
390
|
+
P("planmagnet.analytics.velocity.sprints", "Sprints"),
|
|
390
391
|
")"
|
|
391
392
|
]
|
|
392
|
-
}) }), /* @__PURE__ */
|
|
393
|
+
}) }), /* @__PURE__ */ y(S, { children: _e ? /* @__PURE__ */ y("div", {
|
|
393
394
|
className: "flex justify-center py-12",
|
|
394
|
-
children: /* @__PURE__ */
|
|
395
|
-
}) : X.length > 0 ? /* @__PURE__ */ v
|
|
395
|
+
children: /* @__PURE__ */ y("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
396
|
+
}) : X.length > 0 ? /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ y(r, {
|
|
396
397
|
width: "100%",
|
|
397
398
|
height: 190,
|
|
398
|
-
children: /* @__PURE__ */
|
|
399
|
+
children: /* @__PURE__ */ b(re, {
|
|
399
400
|
data: X,
|
|
400
401
|
children: [
|
|
401
|
-
/* @__PURE__ */
|
|
402
|
+
/* @__PURE__ */ y(a, {
|
|
402
403
|
strokeDasharray: "3 3",
|
|
403
404
|
opacity: .2
|
|
404
405
|
}),
|
|
405
|
-
/* @__PURE__ */
|
|
406
|
+
/* @__PURE__ */ y(s, {
|
|
406
407
|
dataKey: "name",
|
|
407
408
|
tick: { fontSize: 11 }
|
|
408
409
|
}),
|
|
409
|
-
/* @__PURE__ */
|
|
410
|
+
/* @__PURE__ */ y(c, {
|
|
410
411
|
tick: { fontSize: 11 },
|
|
411
412
|
allowDecimals: !1
|
|
412
413
|
}),
|
|
413
|
-
/* @__PURE__ */
|
|
414
|
-
/* @__PURE__ */
|
|
415
|
-
/* @__PURE__ */
|
|
414
|
+
/* @__PURE__ */ y(n, {}),
|
|
415
|
+
/* @__PURE__ */ y(t, { wrapperStyle: { fontSize: 12 } }),
|
|
416
|
+
/* @__PURE__ */ y(i, {
|
|
416
417
|
dataKey: "planned",
|
|
417
|
-
name:
|
|
418
|
+
name: P("planmagnet.analytics.velocity.planned", "Planned"),
|
|
418
419
|
fill: "#cbd5e1"
|
|
419
420
|
}),
|
|
420
|
-
/* @__PURE__ */
|
|
421
|
+
/* @__PURE__ */ y(i, {
|
|
421
422
|
dataKey: "completed",
|
|
422
|
-
name:
|
|
423
|
+
name: P("planmagnet.analytics.velocity.completed", "Completed"),
|
|
423
424
|
fill: "#6366f1"
|
|
424
425
|
}),
|
|
425
|
-
/* @__PURE__ */
|
|
426
|
+
/* @__PURE__ */ y(ee, {
|
|
426
427
|
y: Z,
|
|
427
428
|
stroke: "#22c55e",
|
|
428
429
|
strokeDasharray: "4 4",
|
|
429
430
|
label: {
|
|
430
|
-
value:
|
|
431
|
+
value: P("planmagnet.analytics.velocity.avgLabel", "avg"),
|
|
431
432
|
fontSize: 10,
|
|
432
433
|
position: "right"
|
|
433
434
|
}
|
|
434
435
|
})
|
|
435
436
|
]
|
|
436
437
|
})
|
|
437
|
-
}), /* @__PURE__ */
|
|
438
|
+
}), /* @__PURE__ */ y("p", {
|
|
438
439
|
className: "text-xs text-muted-foreground text-center mt-2",
|
|
439
|
-
children:
|
|
440
|
-
})] }) : /* @__PURE__ */
|
|
440
|
+
children: P("planmagnet.analytics.velocity.forecast", "Forecast for next sprint: {{points}} pts (trailing average)", { points: Ee })
|
|
441
|
+
})] }) : /* @__PURE__ */ y("p", {
|
|
441
442
|
className: "text-sm text-muted-foreground py-8 text-center",
|
|
442
|
-
children:
|
|
443
|
+
children: P("planmagnet.analytics.velocity.empty", "No completed sprints yet for this project")
|
|
443
444
|
}) })] })]
|
|
444
445
|
}),
|
|
445
|
-
/* @__PURE__ */
|
|
446
|
+
/* @__PURE__ */ b(x, { children: [/* @__PURE__ */ y(C, { children: /* @__PURE__ */ b(w, {
|
|
446
447
|
className: "flex items-center gap-2",
|
|
447
|
-
children: [/* @__PURE__ */
|
|
448
|
-
}) }), /* @__PURE__ */
|
|
448
|
+
children: [/* @__PURE__ */ y(f, { className: "h-4 w-4" }), P("planmagnet.analytics.cfd.title", "Cumulative Flow Diagram")]
|
|
449
|
+
}) }), /* @__PURE__ */ y(S, { children: ye ? /* @__PURE__ */ y("div", {
|
|
449
450
|
className: "flex justify-center py-12",
|
|
450
|
-
children: /* @__PURE__ */
|
|
451
|
-
}) : $.length > 0 ? /* @__PURE__ */
|
|
451
|
+
children: /* @__PURE__ */ y("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
452
|
+
}) : $.length > 0 ? /* @__PURE__ */ y(r, {
|
|
452
453
|
width: "100%",
|
|
453
454
|
height: 260,
|
|
454
|
-
children: /* @__PURE__ */
|
|
455
|
+
children: /* @__PURE__ */ b(ie, {
|
|
455
456
|
data: $,
|
|
456
457
|
children: [
|
|
457
|
-
/* @__PURE__ */
|
|
458
|
+
/* @__PURE__ */ y(a, {
|
|
458
459
|
strokeDasharray: "3 3",
|
|
459
460
|
opacity: .2
|
|
460
461
|
}),
|
|
461
|
-
/* @__PURE__ */
|
|
462
|
+
/* @__PURE__ */ y(s, {
|
|
462
463
|
dataKey: "day",
|
|
463
464
|
tick: { fontSize: 11 }
|
|
464
465
|
}),
|
|
465
|
-
/* @__PURE__ */
|
|
466
|
+
/* @__PURE__ */ y(c, {
|
|
466
467
|
tick: { fontSize: 11 },
|
|
467
468
|
allowDecimals: !1
|
|
468
469
|
}),
|
|
469
|
-
/* @__PURE__ */
|
|
470
|
-
/* @__PURE__ */
|
|
471
|
-
|
|
470
|
+
/* @__PURE__ */ y(n, {}),
|
|
471
|
+
/* @__PURE__ */ y(t, { wrapperStyle: { fontSize: 12 } }),
|
|
472
|
+
De.map((e) => /* @__PURE__ */ y(te, {
|
|
472
473
|
type: "monotone",
|
|
473
474
|
dataKey: e,
|
|
474
475
|
name: e.toLowerCase().replace("_", " "),
|
|
475
476
|
stackId: "cfd",
|
|
476
|
-
stroke:
|
|
477
|
-
fill:
|
|
477
|
+
stroke: M[e] ?? "#a3a3a3",
|
|
478
|
+
fill: M[e] ?? "#a3a3a3",
|
|
478
479
|
fillOpacity: .7
|
|
479
480
|
}, e))
|
|
480
481
|
]
|
|
481
482
|
})
|
|
482
|
-
}) : /* @__PURE__ */
|
|
483
|
+
}) : /* @__PURE__ */ y("p", {
|
|
483
484
|
className: "text-sm text-muted-foreground py-8 text-center",
|
|
484
|
-
children:
|
|
485
|
+
children: P("planmagnet.analytics.cfd.empty", "No work items found for this project")
|
|
485
486
|
}) })] }),
|
|
486
|
-
/* @__PURE__ */
|
|
487
|
+
/* @__PURE__ */ b("div", {
|
|
488
|
+
className: "grid grid-cols-1 lg:grid-cols-2 gap-6",
|
|
489
|
+
children: [/* @__PURE__ */ y(u, {
|
|
490
|
+
workspaceId: g,
|
|
491
|
+
projectId: H
|
|
492
|
+
}), /* @__PURE__ */ y(l, { workspaceId: g })]
|
|
493
|
+
}),
|
|
494
|
+
/* @__PURE__ */ b(x, { children: [/* @__PURE__ */ y(C, { children: /* @__PURE__ */ y(w, { children: P("planmagnet.analytics.projectHealth.title", "Project Health") }) }), /* @__PURE__ */ y(S, { children: /* @__PURE__ */ b("div", {
|
|
487
495
|
className: "space-y-3",
|
|
488
|
-
children: [
|
|
489
|
-
let t =
|
|
490
|
-
return /* @__PURE__ */
|
|
496
|
+
children: [z.filter((e) => e.status === "ACTIVE").map((e) => {
|
|
497
|
+
let t = B.filter((e) => e.assigneeId != null), n = t.filter((e) => e.status === "DONE").length, r = t.length > 0 ? Math.round(n / t.length * 100) : 0, i = e.endDate && new Date(e.endDate) < /* @__PURE__ */ new Date() && r < 100 ? "at_risk" : r >= 75 ? "on_track" : "in_progress";
|
|
498
|
+
return /* @__PURE__ */ b("div", {
|
|
491
499
|
className: "flex items-center gap-4",
|
|
492
500
|
children: [
|
|
493
|
-
/* @__PURE__ */
|
|
501
|
+
/* @__PURE__ */ y("span", {
|
|
494
502
|
className: "text-sm font-medium w-40 truncate",
|
|
495
503
|
children: e.name
|
|
496
504
|
}),
|
|
497
|
-
/* @__PURE__ */
|
|
505
|
+
/* @__PURE__ */ y("div", {
|
|
498
506
|
className: "flex-1",
|
|
499
|
-
children: /* @__PURE__ */
|
|
507
|
+
children: /* @__PURE__ */ y("div", {
|
|
500
508
|
className: "w-full h-2 rounded-full bg-muted overflow-hidden",
|
|
501
|
-
children: /* @__PURE__ */
|
|
509
|
+
children: /* @__PURE__ */ y("div", {
|
|
502
510
|
className: `h-full rounded-full ${i === "at_risk" ? "bg-orange-400" : i === "on_track" ? "bg-green-400" : "bg-blue-400"}`,
|
|
503
511
|
style: { width: `${r}%` }
|
|
504
512
|
})
|
|
505
513
|
})
|
|
506
514
|
}),
|
|
507
|
-
/* @__PURE__ */
|
|
515
|
+
/* @__PURE__ */ b("span", {
|
|
508
516
|
className: "text-sm w-12 text-right",
|
|
509
517
|
children: [r, "%"]
|
|
510
518
|
}),
|
|
511
|
-
/* @__PURE__ */
|
|
519
|
+
/* @__PURE__ */ y(se, {
|
|
512
520
|
variant: "outline",
|
|
513
521
|
className: "capitalize text-xs w-20 justify-center",
|
|
514
522
|
children: i.replace("_", " ")
|
|
515
523
|
})
|
|
516
524
|
]
|
|
517
525
|
}, e.id);
|
|
518
|
-
}),
|
|
526
|
+
}), z.filter((e) => e.status === "ACTIVE").length === 0 && /* @__PURE__ */ y("p", {
|
|
519
527
|
className: "text-sm text-muted-foreground text-center py-4",
|
|
520
|
-
children:
|
|
528
|
+
children: P("planmagnet.analytics.projectHealth.empty", "No active projects found")
|
|
521
529
|
})]
|
|
522
530
|
}) })] })
|
|
523
531
|
]
|
|
524
532
|
});
|
|
525
533
|
}
|
|
526
534
|
//#endregion
|
|
527
|
-
export {
|
|
535
|
+
export { P as AnalyticsAnalyticsPagePage };
|
|
528
536
|
|
|
529
537
|
//# sourceMappingURL=AnalyticsAnalyticsPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { TrendingUp, Zap, FolderKanban, BarChart3, Target } from 'lucide-react';\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Legend,\n Line,\n LineChart,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\nconst GET_ANALYTICS_DATA = gql`\n query GetAnalyticsData(\n $workspaceId: ID!\n $projectPagination: PlanmagnetPaginationInput\n $workItemPagination: PlanmagnetPaginationInput\n $sprintPagination: PlanmagnetPaginationInput\n $milestonePagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $projectPagination) {\n items {\n id\n name\n key\n status\n startDate\n endDate\n tags\n }\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: $workItemPagination) {\n items {\n id\n status\n priority\n storyPoints\n assigneeId\n }\n totalCount\n }\n planmagnetSprints(workspaceId: $workspaceId, pagination: $sprintPagination) {\n items {\n id\n name\n status\n velocity\n capacity\n projectId\n startDate\n endDate\n }\n totalCount\n }\n planmagnetMilestones(workspaceId: $workspaceId, pagination: $milestonePagination) {\n items {\n id\n name\n status\n dueDate\n }\n totalCount\n }\n }\n`;\n\n// BOFF-4447: real server-computed sprint delivery analytics — burndown,\n// velocity, and cumulative flow are all aggregated in wspace-planmagnet-svc\n// (src/modules/analytics) from the Sprint/WorkItem data model plus the\n// WorkItemStatusTransition audit trail, not derived client-side from raw\n// lists like the summary cards above.\nconst GET_SPRINT_BURNDOWN = gql`\n query GetSprintBurndown($workspaceId: ID!, $sprintId: ID!) {\n planmagnetSprintBurndown(workspaceId: $workspaceId, sprintId: $sprintId) {\n sprintId\n startDate\n endDate\n totalPoints\n totalCount\n points {\n date\n remainingPoints\n remainingCount\n completedPoints\n completedCount\n }\n }\n }\n`;\n\nconst GET_VELOCITY = gql`\n query GetVelocity($workspaceId: ID!, $projectId: ID!, $trailingSprintCount: Int) {\n planmagnetVelocity(\n workspaceId: $workspaceId\n projectId: $projectId\n trailingSprintCount: $trailingSprintCount\n ) {\n projectId\n averageVelocity\n forecastNextSprintPoints\n sprints {\n sprintId\n sprintName\n sequenceNumber\n completedPoints\n completedCount\n plannedPoints\n }\n }\n }\n`;\n\nconst GET_CUMULATIVE_FLOW = gql`\n query GetCumulativeFlow($workspaceId: ID!, $projectId: ID!) {\n planmagnetCumulativeFlow(workspaceId: $workspaceId, projectId: $projectId) {\n projectId\n startDate\n endDate\n points {\n date\n statuses {\n status\n count\n }\n }\n }\n }\n`;\n\ninterface ProjectNode {\n id: string;\n name: string;\n key: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n tags: string[];\n}\n\ninterface WorkItemNode {\n id: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n}\n\ninterface SprintNode {\n id: string;\n name: string;\n status: string;\n velocity: number | null;\n capacity: number | null;\n projectId: string;\n startDate: string | null;\n endDate: string | null;\n}\n\ninterface MilestoneNode {\n id: string;\n name: string;\n status: string;\n dueDate: string | null;\n}\n\ninterface AnalyticsData {\n planmagnetProjects: { items: ProjectNode[]; totalCount: number };\n planmagnetWorkItems: { items: WorkItemNode[]; totalCount: number };\n planmagnetSprints: { items: SprintNode[]; totalCount: number };\n planmagnetMilestones: { items: MilestoneNode[]; totalCount: number };\n}\n\ninterface BurndownData {\n planmagnetSprintBurndown: {\n sprintId: string;\n startDate: string | null;\n endDate: string | null;\n totalPoints: number;\n totalCount: number;\n points: {\n date: string;\n remainingPoints: number;\n remainingCount: number;\n completedPoints: number;\n completedCount: number;\n }[];\n } | null;\n}\n\ninterface VelocityData {\n planmagnetVelocity: {\n projectId: string;\n averageVelocity: number;\n forecastNextSprintPoints: number;\n sprints: {\n sprintId: string;\n sprintName: string;\n sequenceNumber: number;\n completedPoints: number;\n completedCount: number;\n plannedPoints: number | null;\n }[];\n };\n}\n\ninterface CumulativeFlowData {\n planmagnetCumulativeFlow: {\n projectId: string;\n startDate: string;\n endDate: string;\n points: {\n date: string;\n statuses: { status: string; count: number }[];\n }[];\n };\n}\n\n// recharts' `stroke`/`fill` props render as literal SVG paint attributes, not\n// Tailwind classNames — they cannot consume the semantic `bg-*`/`text-*`\n// utility tokens the lint rule expects everywhere else on this page. This is\n// the one place on the page raw color values are load-bearing rather than\n// stylistic, matching the same constraint every other MFE chart primitive in\n// the fleet (e.g. microfe-assethandler's `ChartCard`/`DonutChart`) hits.\nconst CFD_STATUS_COLORS: Record<string, string> = {\n BACKLOG: '#9ca3af',\n TODO: '#60a5fa',\n IN_PROGRESS: '#fbbf24',\n IN_REVIEW: '#a78bfa',\n TESTING: '#f472b6',\n DONE: '#4ade80',\n CANCELLED: '#f87171',\n};\n\nfunction formatDay(iso: string): string {\n const d = new Date(iso);\n return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;\n}\n\nexport function AnalyticsAnalyticsPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n const [selectedProjectId, setSelectedProjectId] = useState<string>('');\n const [selectedSprintId, setSelectedSprintId] = useState<string>('');\n\n const { data, loading, error } = useQuery<AnalyticsData>(GET_ANALYTICS_DATA, {\n variables: {\n workspaceId,\n projectPagination: { first: 100 },\n workItemPagination: { first: 500 },\n sprintPagination: { first: 100 },\n milestonePagination: { first: 100 },\n },\n skip: !workspaceId,\n });\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const tasks = data?.planmagnetWorkItems?.items ?? [];\n const sprints = data?.planmagnetSprints?.items ?? [];\n const milestones = data?.planmagnetMilestones?.items ?? [];\n\n // Default the project/sprint selectors to the first available option once\n // the summary data loads, so the delivery-analytics charts render without\n // requiring a manual pick every time the page opens. Filtering a list of\n // at most a few hundred sprints is cheap enough to redo on every render —\n // memoizing it would only need its own dependency-array workaround since\n // the `?? []` fallback above hands back a fresh array identity per render\n // whenever the query hasn't resolved yet.\n const effectiveProjectId = selectedProjectId || projects[0]?.id || '';\n const sprintsForProject = sprints.filter((s) => s.projectId === effectiveProjectId);\n const effectiveSprintId = selectedSprintId || sprintsForProject[0]?.id || '';\n\n const { data: burndownData, loading: burndownLoading } = useQuery<BurndownData>(\n GET_SPRINT_BURNDOWN,\n {\n variables: { workspaceId, sprintId: effectiveSprintId },\n skip: !workspaceId || !effectiveSprintId,\n }\n );\n\n const { data: velocityData, loading: velocityLoading } = useQuery<VelocityData>(GET_VELOCITY, {\n variables: { workspaceId, projectId: effectiveProjectId, trailingSprintCount: 6 },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n const { data: cfdData, loading: cfdLoading } = useQuery<CumulativeFlowData>(GET_CUMULATIVE_FLOW, {\n variables: { workspaceId, projectId: effectiveProjectId },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.analytics.errorLoading', 'Error loading analytics')}: {error.message}\n </div>\n );\n }\n\n const totalTasks = tasks.length;\n const doneTasks = tasks.filter((t) => t.status === 'DONE').length;\n const blockedTasks = tasks.filter((t) => t.status === 'BLOCKED').length;\n const totalPoints = tasks.reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n const completedPoints = tasks\n .filter((t) => t.status === 'DONE')\n .reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n\n const activeSprints = sprints.filter((s) => s.status === 'ACTIVE').length;\n const atRiskMs = milestones.filter(\n (m) => m.status === 'AT_RISK' || m.status === 'OVERDUE'\n ).length;\n const activeProjects = projects.filter((p) => p.status === 'ACTIVE').length;\n const completionRate = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;\n\n const burndown = burndownData?.planmagnetSprintBurndown;\n const burndownChartData =\n burndown?.points.map((p) => ({\n day: formatDay(p.date),\n remaining: p.remainingPoints,\n completed: p.completedPoints,\n })) ?? [];\n\n const velocity = velocityData?.planmagnetVelocity;\n const velocityChartData =\n velocity?.sprints.map((s) => ({\n name: s.sprintName,\n completed: s.completedPoints,\n planned: s.plannedPoints ?? undefined,\n })) ?? [];\n const avgVelocity = velocity ? Math.round(velocity.averageVelocity) : 0;\n const forecastNextSprint = velocity ? Math.round(velocity.forecastNextSprintPoints) : 0;\n\n const cfd = cfdData?.planmagnetCumulativeFlow;\n const cfdStatusKeys = Array.from(\n new Set(cfd?.points.flatMap((p) => p.statuses.map((s) => s.status)) ?? [])\n );\n const cfdChartData =\n cfd?.points.map((p) => {\n const row: Record<string, string | number> = { day: formatDay(p.date) };\n for (const s of p.statuses) row[s.status] = s.count;\n return row;\n }) ?? [];\n\n const metrics = [\n {\n label: t('planmagnet.analytics.metrics.totalWorkItems.label', 'Total Work Items'),\n value: String(totalTasks),\n trend: `${completionRate}% ${t('planmagnet.analytics.metrics.totalWorkItems.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.storyPoints.label', 'Story Points'),\n value: String(totalPoints),\n trend: `${completedPoints} ${t('planmagnet.analytics.metrics.storyPoints.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.activeProjects.label', 'Active Projects'),\n value: String(activeProjects),\n trend: `${projects.length} ${t('planmagnet.analytics.metrics.activeProjects.trend', 'total')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.blockedItems.label', 'Blocked Items'),\n value: String(blockedTasks),\n trend:\n blockedTasks > 0\n ? t('planmagnet.analytics.metrics.blockedItems.attention', 'Needs attention')\n : t('planmagnet.analytics.metrics.blockedItems.clear', 'All clear'),\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n {t('planmagnet.analytics.title', 'Analytics Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.subtitle', 'Overview of project health and team performance')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">{m.label}</p>\n <p className=\"text-xl font-bold mt-1\">{m.value}</p>\n <p className=\"text-xs text-green-600 mt-1\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\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 <FolderKanban className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.activeProjects', 'Active Projects')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{activeProjects}</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 <Zap className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.activeSprints', 'Active Sprints')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{activeSprints}</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 {t('planmagnet.analytics.summary.avgVelocity', 'Avg Velocity')}\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {avgVelocity} {t('planmagnet.analytics.summary.pointsShort', 'pts')}\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 <Target className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.atRisk', 'At Risk')}\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {atRiskMs} {t('planmagnet.analytics.summary.milestones', 'milestones')}\n </p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-start sm:items-center\">\n <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.delivery.title', 'Sprint Delivery Analytics')}\n </h2>\n <div className=\"flex gap-2 sm:ml-auto\">\n <Select\n value={effectiveProjectId}\n onValueChange={(v) => {\n setSelectedProjectId(v);\n setSelectedSprintId('');\n }}\n >\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t(\n 'planmagnet.analytics.delivery.projectPlaceholder',\n 'Select project'\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {projects.map((p) => (\n <SelectItem key={p.id} value={p.id}>\n {p.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select value={effectiveSprintId} onValueChange={setSelectedSprintId}>\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t('planmagnet.analytics.delivery.sprintPlaceholder', 'Select sprint')}\n />\n </SelectTrigger>\n <SelectContent>\n {sprintsForProject.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\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 {t('planmagnet.analytics.burndown.title', 'Sprint Burndown')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {burndownLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : burndownChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={220}>\n <LineChart data={burndownChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n <Line\n type=\"monotone\"\n dataKey=\"remaining\"\n name={t('planmagnet.analytics.burndown.remaining', 'Remaining points')}\n stroke=\"#f59e0b\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"completed\"\n name={t('planmagnet.analytics.burndown.completed', 'Completed points')}\n stroke=\"#22c55e\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n </LineChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.burndown.empty',\n 'No sprint selected, or the sprint has no work items yet'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <TrendingUp className=\"h-4 w-4\" />\n {t('planmagnet.analytics.velocity.title', 'Sprint Velocity')} (\n {velocityChartData.length} {t('planmagnet.analytics.velocity.sprints', 'Sprints')})\n </CardTitle>\n </CardHeader>\n <CardContent>\n {velocityLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : velocityChartData.length > 0 ? (\n <>\n <ResponsiveContainer width=\"100%\" height={190}>\n <BarChart data={velocityChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"name\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n <Bar\n dataKey=\"planned\"\n name={t('planmagnet.analytics.velocity.planned', 'Planned')}\n fill=\"#cbd5e1\"\n />\n <Bar\n dataKey=\"completed\"\n name={t('planmagnet.analytics.velocity.completed', 'Completed')}\n fill=\"#6366f1\"\n />\n <ReferenceLine\n y={avgVelocity}\n stroke=\"#22c55e\"\n strokeDasharray=\"4 4\"\n label={{\n value: t('planmagnet.analytics.velocity.avgLabel', 'avg'),\n fontSize: 10,\n position: 'right',\n }}\n />\n </BarChart>\n </ResponsiveContainer>\n <p className=\"text-xs text-muted-foreground text-center mt-2\">\n {t(\n 'planmagnet.analytics.velocity.forecast',\n 'Forecast for next sprint: {{points}} pts (trailing average)',\n { points: forecastNextSprint }\n )}\n </p>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.velocity.empty',\n 'No completed sprints yet for this project'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n {t('planmagnet.analytics.cfd.title', 'Cumulative Flow Diagram')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {cfdLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : cfdChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={260}>\n <AreaChart data={cfdChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n {cfdStatusKeys.map((status) => (\n <Area\n key={status}\n type=\"monotone\"\n dataKey={status}\n name={status.toLowerCase().replace('_', ' ')}\n stackId=\"cfd\"\n stroke={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fill={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fillOpacity={0.7}\n />\n ))}\n </AreaChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t('planmagnet.analytics.cfd.empty', 'No work items found for this project')}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('planmagnet.analytics.projectHealth.title', 'Project Health')}</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {projects\n .filter((p) => p.status === 'ACTIVE')\n .map((p) => {\n const projectTasks = tasks.filter((t) => t.assigneeId != null);\n const projectDone = projectTasks.filter((t) => t.status === 'DONE').length;\n const progress =\n projectTasks.length > 0\n ? Math.round((projectDone / projectTasks.length) * 100)\n : 0;\n const hasOverdue = p.endDate && new Date(p.endDate) < new Date() && progress < 100;\n const health = hasOverdue ? 'at_risk' : progress >= 75 ? 'on_track' : 'in_progress';\n return (\n <div key={p.id} className=\"flex items-center gap-4\">\n <span className=\"text-sm font-medium w-40 truncate\">{p.name}</span>\n <div className=\"flex-1\">\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${health === 'at_risk' ? 'bg-orange-400' : health === 'on_track' ? 'bg-green-400' : 'bg-blue-400'}`}\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n <span className=\"text-sm w-12 text-right\">{progress}%</span>\n <Badge variant=\"outline\" className=\"capitalize text-xs w-20 justify-center\">\n {health.replace('_', ' ')}\n </Badge>\n </div>\n );\n })}\n {projects.filter((p) => p.status === 'ACTIVE').length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.analytics.projectHealth.empty', 'No active projects found')}\n </p>\n )}\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { TrendingUp, Zap, FolderKanban, BarChart3, Target } from 'lucide-react';\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Legend,\n Line,\n LineChart,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { BudgetPerformanceCard, CycleTimeCard } from './analytics/DeliveryHealthCards';\n\nconst GET_ANALYTICS_DATA = gql`\n query GetAnalyticsData(\n $workspaceId: ID!\n $projectPagination: PlanmagnetPaginationInput\n $workItemPagination: PlanmagnetPaginationInput\n $sprintPagination: PlanmagnetPaginationInput\n $milestonePagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $projectPagination) {\n items {\n id\n name\n key\n status\n startDate\n endDate\n tags\n }\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: $workItemPagination) {\n items {\n id\n status\n priority\n storyPoints\n assigneeId\n }\n totalCount\n }\n planmagnetSprints(workspaceId: $workspaceId, pagination: $sprintPagination) {\n items {\n id\n name\n status\n velocity\n capacity\n projectId\n startDate\n endDate\n }\n totalCount\n }\n planmagnetMilestones(workspaceId: $workspaceId, pagination: $milestonePagination) {\n items {\n id\n name\n status\n dueDate\n }\n totalCount\n }\n }\n`;\n\n// BOFF-4447: real server-computed sprint delivery analytics — burndown,\n// velocity, and cumulative flow are all aggregated in wspace-planmagnet-svc\n// (src/modules/analytics) from the Sprint/WorkItem data model plus the\n// WorkItemStatusTransition audit trail, not derived client-side from raw\n// lists like the summary cards above.\nconst GET_SPRINT_BURNDOWN = gql`\n query GetSprintBurndown($workspaceId: ID!, $sprintId: ID!) {\n planmagnetSprintBurndown(workspaceId: $workspaceId, sprintId: $sprintId) {\n sprintId\n startDate\n endDate\n totalPoints\n totalCount\n points {\n date\n remainingPoints\n remainingCount\n completedPoints\n completedCount\n }\n }\n }\n`;\n\nconst GET_VELOCITY = gql`\n query GetVelocity($workspaceId: ID!, $projectId: ID!, $trailingSprintCount: Int) {\n planmagnetVelocity(\n workspaceId: $workspaceId\n projectId: $projectId\n trailingSprintCount: $trailingSprintCount\n ) {\n projectId\n averageVelocity\n forecastNextSprintPoints\n sprints {\n sprintId\n sprintName\n sequenceNumber\n completedPoints\n completedCount\n plannedPoints\n }\n }\n }\n`;\n\nconst GET_CUMULATIVE_FLOW = gql`\n query GetCumulativeFlow($workspaceId: ID!, $projectId: ID!) {\n planmagnetCumulativeFlow(workspaceId: $workspaceId, projectId: $projectId) {\n projectId\n startDate\n endDate\n points {\n date\n statuses {\n status\n count\n }\n }\n }\n }\n`;\n\ninterface ProjectNode {\n id: string;\n name: string;\n key: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n tags: string[];\n}\n\ninterface WorkItemNode {\n id: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n}\n\ninterface SprintNode {\n id: string;\n name: string;\n status: string;\n velocity: number | null;\n capacity: number | null;\n projectId: string;\n startDate: string | null;\n endDate: string | null;\n}\n\ninterface MilestoneNode {\n id: string;\n name: string;\n status: string;\n dueDate: string | null;\n}\n\ninterface AnalyticsData {\n planmagnetProjects: { items: ProjectNode[]; totalCount: number };\n planmagnetWorkItems: { items: WorkItemNode[]; totalCount: number };\n planmagnetSprints: { items: SprintNode[]; totalCount: number };\n planmagnetMilestones: { items: MilestoneNode[]; totalCount: number };\n}\n\ninterface BurndownData {\n planmagnetSprintBurndown: {\n sprintId: string;\n startDate: string | null;\n endDate: string | null;\n totalPoints: number;\n totalCount: number;\n points: {\n date: string;\n remainingPoints: number;\n remainingCount: number;\n completedPoints: number;\n completedCount: number;\n }[];\n } | null;\n}\n\ninterface VelocityData {\n planmagnetVelocity: {\n projectId: string;\n averageVelocity: number;\n forecastNextSprintPoints: number;\n sprints: {\n sprintId: string;\n sprintName: string;\n sequenceNumber: number;\n completedPoints: number;\n completedCount: number;\n plannedPoints: number | null;\n }[];\n };\n}\n\ninterface CumulativeFlowData {\n planmagnetCumulativeFlow: {\n projectId: string;\n startDate: string;\n endDate: string;\n points: {\n date: string;\n statuses: { status: string; count: number }[];\n }[];\n };\n}\n\n// recharts' `stroke`/`fill` props render as literal SVG paint attributes, not\n// Tailwind classNames — they cannot consume the semantic `bg-*`/`text-*`\n// utility tokens the lint rule expects everywhere else on this page. This is\n// the one place on the page raw color values are load-bearing rather than\n// stylistic, matching the same constraint every other MFE chart primitive in\n// the fleet (e.g. microfe-assethandler's `ChartCard`/`DonutChart`) hits.\nconst CFD_STATUS_COLORS: Record<string, string> = {\n BACKLOG: '#9ca3af',\n TODO: '#60a5fa',\n IN_PROGRESS: '#fbbf24',\n IN_REVIEW: '#a78bfa',\n TESTING: '#f472b6',\n DONE: '#4ade80',\n CANCELLED: '#f87171',\n};\n\nfunction formatDay(iso: string): string {\n const d = new Date(iso);\n return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;\n}\n\nexport function AnalyticsAnalyticsPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n const [selectedProjectId, setSelectedProjectId] = useState<string>('');\n const [selectedSprintId, setSelectedSprintId] = useState<string>('');\n\n const { data, loading, error } = useQuery<AnalyticsData>(GET_ANALYTICS_DATA, {\n variables: {\n workspaceId,\n projectPagination: { first: 100 },\n workItemPagination: { first: 500 },\n sprintPagination: { first: 100 },\n milestonePagination: { first: 100 },\n },\n skip: !workspaceId,\n });\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const tasks = data?.planmagnetWorkItems?.items ?? [];\n const sprints = data?.planmagnetSprints?.items ?? [];\n const milestones = data?.planmagnetMilestones?.items ?? [];\n\n // Default the project/sprint selectors to the first available option once\n // the summary data loads, so the delivery-analytics charts render without\n // requiring a manual pick every time the page opens. Filtering a list of\n // at most a few hundred sprints is cheap enough to redo on every render —\n // memoizing it would only need its own dependency-array workaround since\n // the `?? []` fallback above hands back a fresh array identity per render\n // whenever the query hasn't resolved yet.\n const effectiveProjectId = selectedProjectId || projects[0]?.id || '';\n const sprintsForProject = sprints.filter((s) => s.projectId === effectiveProjectId);\n const effectiveSprintId = selectedSprintId || sprintsForProject[0]?.id || '';\n\n const { data: burndownData, loading: burndownLoading } = useQuery<BurndownData>(\n GET_SPRINT_BURNDOWN,\n {\n variables: { workspaceId, sprintId: effectiveSprintId },\n skip: !workspaceId || !effectiveSprintId,\n }\n );\n\n const { data: velocityData, loading: velocityLoading } = useQuery<VelocityData>(GET_VELOCITY, {\n variables: { workspaceId, projectId: effectiveProjectId, trailingSprintCount: 6 },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n const { data: cfdData, loading: cfdLoading } = useQuery<CumulativeFlowData>(GET_CUMULATIVE_FLOW, {\n variables: { workspaceId, projectId: effectiveProjectId },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.analytics.errorLoading', 'Error loading analytics')}: {error.message}\n </div>\n );\n }\n\n const totalTasks = tasks.length;\n const doneTasks = tasks.filter((t) => t.status === 'DONE').length;\n const blockedTasks = tasks.filter((t) => t.status === 'BLOCKED').length;\n const totalPoints = tasks.reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n const completedPoints = tasks\n .filter((t) => t.status === 'DONE')\n .reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n\n const activeSprints = sprints.filter((s) => s.status === 'ACTIVE').length;\n const atRiskMs = milestones.filter(\n (m) => m.status === 'AT_RISK' || m.status === 'OVERDUE'\n ).length;\n const activeProjects = projects.filter((p) => p.status === 'ACTIVE').length;\n const completionRate = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;\n\n const burndown = burndownData?.planmagnetSprintBurndown;\n const burndownChartData =\n burndown?.points.map((p) => ({\n day: formatDay(p.date),\n remaining: p.remainingPoints,\n completed: p.completedPoints,\n })) ?? [];\n\n const velocity = velocityData?.planmagnetVelocity;\n const velocityChartData =\n velocity?.sprints.map((s) => ({\n name: s.sprintName,\n completed: s.completedPoints,\n planned: s.plannedPoints ?? undefined,\n })) ?? [];\n const avgVelocity = velocity ? Math.round(velocity.averageVelocity) : 0;\n const forecastNextSprint = velocity ? Math.round(velocity.forecastNextSprintPoints) : 0;\n\n const cfd = cfdData?.planmagnetCumulativeFlow;\n const cfdStatusKeys = Array.from(\n new Set(cfd?.points.flatMap((p) => p.statuses.map((s) => s.status)) ?? [])\n );\n const cfdChartData =\n cfd?.points.map((p) => {\n const row: Record<string, string | number> = { day: formatDay(p.date) };\n for (const s of p.statuses) row[s.status] = s.count;\n return row;\n }) ?? [];\n\n const metrics = [\n {\n label: t('planmagnet.analytics.metrics.totalWorkItems.label', 'Total Work Items'),\n value: String(totalTasks),\n trend: `${completionRate}% ${t('planmagnet.analytics.metrics.totalWorkItems.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.storyPoints.label', 'Story Points'),\n value: String(totalPoints),\n trend: `${completedPoints} ${t('planmagnet.analytics.metrics.storyPoints.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.activeProjects.label', 'Active Projects'),\n value: String(activeProjects),\n trend: `${projects.length} ${t('planmagnet.analytics.metrics.activeProjects.trend', 'total')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.blockedItems.label', 'Blocked Items'),\n value: String(blockedTasks),\n trend:\n blockedTasks > 0\n ? t('planmagnet.analytics.metrics.blockedItems.attention', 'Needs attention')\n : t('planmagnet.analytics.metrics.blockedItems.clear', 'All clear'),\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n {t('planmagnet.analytics.title', 'Analytics Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.subtitle', 'Overview of project health and team performance')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">{m.label}</p>\n <p className=\"text-xl font-bold mt-1\">{m.value}</p>\n <p className=\"text-xs text-green-600 mt-1\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\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 <FolderKanban className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.activeProjects', 'Active Projects')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{activeProjects}</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 <Zap className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.activeSprints', 'Active Sprints')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{activeSprints}</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 {t('planmagnet.analytics.summary.avgVelocity', 'Avg Velocity')}\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {avgVelocity} {t('planmagnet.analytics.summary.pointsShort', 'pts')}\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 <Target className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.atRisk', 'At Risk')}\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {atRiskMs} {t('planmagnet.analytics.summary.milestones', 'milestones')}\n </p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-start sm:items-center\">\n <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.delivery.title', 'Sprint Delivery Analytics')}\n </h2>\n <div className=\"flex gap-2 sm:ml-auto\">\n <Select\n value={effectiveProjectId}\n onValueChange={(v) => {\n setSelectedProjectId(v);\n setSelectedSprintId('');\n }}\n >\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t(\n 'planmagnet.analytics.delivery.projectPlaceholder',\n 'Select project'\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {projects.map((p) => (\n <SelectItem key={p.id} value={p.id}>\n {p.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select value={effectiveSprintId} onValueChange={setSelectedSprintId}>\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t('planmagnet.analytics.delivery.sprintPlaceholder', 'Select sprint')}\n />\n </SelectTrigger>\n <SelectContent>\n {sprintsForProject.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\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 {t('planmagnet.analytics.burndown.title', 'Sprint Burndown')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {burndownLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : burndownChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={220}>\n <LineChart data={burndownChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n <Line\n type=\"monotone\"\n dataKey=\"remaining\"\n name={t('planmagnet.analytics.burndown.remaining', 'Remaining points')}\n stroke=\"#f59e0b\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"completed\"\n name={t('planmagnet.analytics.burndown.completed', 'Completed points')}\n stroke=\"#22c55e\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n </LineChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.burndown.empty',\n 'No sprint selected, or the sprint has no work items yet'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <TrendingUp className=\"h-4 w-4\" />\n {t('planmagnet.analytics.velocity.title', 'Sprint Velocity')} (\n {velocityChartData.length} {t('planmagnet.analytics.velocity.sprints', 'Sprints')})\n </CardTitle>\n </CardHeader>\n <CardContent>\n {velocityLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : velocityChartData.length > 0 ? (\n <>\n <ResponsiveContainer width=\"100%\" height={190}>\n <BarChart data={velocityChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"name\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n <Bar\n dataKey=\"planned\"\n name={t('planmagnet.analytics.velocity.planned', 'Planned')}\n fill=\"#cbd5e1\"\n />\n <Bar\n dataKey=\"completed\"\n name={t('planmagnet.analytics.velocity.completed', 'Completed')}\n fill=\"#6366f1\"\n />\n <ReferenceLine\n y={avgVelocity}\n stroke=\"#22c55e\"\n strokeDasharray=\"4 4\"\n label={{\n value: t('planmagnet.analytics.velocity.avgLabel', 'avg'),\n fontSize: 10,\n position: 'right',\n }}\n />\n </BarChart>\n </ResponsiveContainer>\n <p className=\"text-xs text-muted-foreground text-center mt-2\">\n {t(\n 'planmagnet.analytics.velocity.forecast',\n 'Forecast for next sprint: {{points}} pts (trailing average)',\n { points: forecastNextSprint }\n )}\n </p>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.velocity.empty',\n 'No completed sprints yet for this project'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n {t('planmagnet.analytics.cfd.title', 'Cumulative Flow Diagram')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {cfdLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : cfdChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={260}>\n <AreaChart data={cfdChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n {cfdStatusKeys.map((status) => (\n <Area\n key={status}\n type=\"monotone\"\n dataKey={status}\n name={status.toLowerCase().replace('_', ' ')}\n stackId=\"cfd\"\n stroke={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fill={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fillOpacity={0.7}\n />\n ))}\n </AreaChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t('planmagnet.analytics.cfd.empty', 'No work items found for this project')}\n </p>\n )}\n </CardContent>\n </Card>\n {/* Cycle time follows the delivery selector above; budgets are workspace-wide. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <CycleTimeCard workspaceId={workspaceId} projectId={effectiveProjectId} />\n <BudgetPerformanceCard workspaceId={workspaceId} />\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('planmagnet.analytics.projectHealth.title', 'Project Health')}</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {projects\n .filter((p) => p.status === 'ACTIVE')\n .map((p) => {\n const projectTasks = tasks.filter((t) => t.assigneeId != null);\n const projectDone = projectTasks.filter((t) => t.status === 'DONE').length;\n const progress =\n projectTasks.length > 0\n ? Math.round((projectDone / projectTasks.length) * 100)\n : 0;\n const hasOverdue = p.endDate && new Date(p.endDate) < new Date() && progress < 100;\n const health = hasOverdue ? 'at_risk' : progress >= 75 ? 'on_track' : 'in_progress';\n return (\n <div key={p.id} className=\"flex items-center gap-4\">\n <span className=\"text-sm font-medium w-40 truncate\">{p.name}</span>\n <div className=\"flex-1\">\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${health === 'at_risk' ? 'bg-orange-400' : health === 'on_track' ? 'bg-green-400' : 'bg-blue-400'}`}\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n <span className=\"text-sm w-12 text-right\">{progress}%</span>\n <Badge variant=\"outline\" className=\"capitalize text-xs w-20 justify-center\">\n {health.replace('_', ' ')}\n </Badge>\n </div>\n );\n })}\n {projects.filter((p) => p.status === 'ACTIVE').length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.analytics.projectHealth.empty', 'No active projects found')}\n </p>\n )}\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Hours read naturally up to two days; beyond that, days. */
|
|
2
|
+
export declare function formatDuration(hours: number | null | undefined): string;
|
|
3
|
+
export declare function CycleTimeCard({ workspaceId, projectId, }: {
|
|
4
|
+
workspaceId?: string | null;
|
|
5
|
+
projectId: string;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
type BudgetState = 'no-plan' | 'on-track' | 'near-limit' | 'over-budget';
|
|
8
|
+
/** Share of the plan spent; `null` when there is no plan to measure against. */
|
|
9
|
+
export declare function budgetUsage(planned: number, spent: number): number | null;
|
|
10
|
+
export declare function budgetState(usage: number | null): BudgetState;
|
|
11
|
+
export declare function BudgetPerformanceCard({ workspaceId }: {
|
|
12
|
+
workspaceId?: string | null;
|
|
13
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=DeliveryHealthCards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeliveryHealthCards.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/pages/analytics/DeliveryHealthCards.tsx"],"names":[],"mappings":"AAyFA,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKvE;AAYD,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB,2CAkGA;AAED,KAAK,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEzE,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAM7D;AA6CD,wBAAgB,qBAAqB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,2CAiHrF"}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { AlertOctagon as e, AlertTriangle as t, CheckCircle2 as n, CircleDashed as r, Timer as i, Wallet as a } from "lucide-react";
|
|
2
|
+
import { gql as o } from "@apollo/client";
|
|
3
|
+
import { useQuery as s } from "@apollo/client/react";
|
|
4
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
5
|
+
import { useI18n as u } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
6
|
+
import { Card as d, CardContent as f, CardHeader as p, CardTitle as m } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/planmagnet/pages/analytics/DeliveryHealthCards.tsx
|
|
8
|
+
var h = o`
|
|
9
|
+
query GetProjectCycleTime($workspaceId: ID!, $projectId: ID!) {
|
|
10
|
+
planmagnetCycleTime(workspaceId: $workspaceId, projectId: $projectId) {
|
|
11
|
+
averageCycleTimeHours
|
|
12
|
+
medianCycleTimeHours
|
|
13
|
+
sampleSize
|
|
14
|
+
items {
|
|
15
|
+
workItemId
|
|
16
|
+
key
|
|
17
|
+
title
|
|
18
|
+
cycleTimeHours
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`, g = o`
|
|
23
|
+
query GetBudgetPerformance($workspaceId: ID!) {
|
|
24
|
+
planmagnetBudgets(workspaceId: $workspaceId, pagination: { first: 50 }) {
|
|
25
|
+
items {
|
|
26
|
+
id
|
|
27
|
+
name
|
|
28
|
+
currency
|
|
29
|
+
plannedAmount
|
|
30
|
+
spentAmount
|
|
31
|
+
remainingAmount
|
|
32
|
+
project {
|
|
33
|
+
id
|
|
34
|
+
name
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
function _(e) {
|
|
41
|
+
if (e == null || !Number.isFinite(e)) return "—";
|
|
42
|
+
if (e < 48) return `${e < 10 ? e.toFixed(1) : Math.round(e)} h`;
|
|
43
|
+
let t = e / 24;
|
|
44
|
+
return `${t < 10 ? t.toFixed(1) : Math.round(t)} d`;
|
|
45
|
+
}
|
|
46
|
+
function v({ label: e, value: t, hint: n }) {
|
|
47
|
+
return /* @__PURE__ */ l("div", {
|
|
48
|
+
className: "rounded-md border p-3",
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ c("p", {
|
|
51
|
+
className: "text-xs text-muted-foreground",
|
|
52
|
+
children: e
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ c("p", {
|
|
55
|
+
className: "mt-1 text-2xl font-semibold",
|
|
56
|
+
children: t
|
|
57
|
+
}),
|
|
58
|
+
n ? /* @__PURE__ */ c("p", {
|
|
59
|
+
className: "mt-0.5 text-xs text-muted-foreground",
|
|
60
|
+
children: n
|
|
61
|
+
}) : null
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function y({ workspaceId: e, projectId: t }) {
|
|
66
|
+
let { t: n } = u(), { data: r, loading: a, error: o } = s(h, {
|
|
67
|
+
variables: {
|
|
68
|
+
workspaceId: e,
|
|
69
|
+
projectId: t
|
|
70
|
+
},
|
|
71
|
+
skip: !e || !t
|
|
72
|
+
}), g = r?.planmagnetCycleTime, y = [...g?.items ?? []].sort((e, t) => t.cycleTimeHours - e.cycleTimeHours).slice(0, 5);
|
|
73
|
+
return /* @__PURE__ */ l(d, { children: [/* @__PURE__ */ l(p, { children: [/* @__PURE__ */ l(m, {
|
|
74
|
+
className: "flex items-center gap-2",
|
|
75
|
+
children: [/* @__PURE__ */ c(i, { className: "h-4 w-4" }), n("planmagnet.analytics.cycleTime.title", "Cycle time")]
|
|
76
|
+
}), /* @__PURE__ */ c("p", {
|
|
77
|
+
className: "text-xs text-muted-foreground",
|
|
78
|
+
children: n("planmagnet.analytics.cycleTime.subtitle", "From first moving to In Progress (or creation) to Done, for the selected project.")
|
|
79
|
+
})] }), /* @__PURE__ */ c(f, { children: t ? a ? /* @__PURE__ */ c("p", {
|
|
80
|
+
className: "text-sm text-muted-foreground",
|
|
81
|
+
children: n("planmagnet.analytics.loading", "Loading…")
|
|
82
|
+
}) : o ? /* @__PURE__ */ l("p", {
|
|
83
|
+
className: "text-sm text-status-error-text",
|
|
84
|
+
children: [
|
|
85
|
+
n("planmagnet.analytics.cycleTime.error", "Cycle time could not be loaded"),
|
|
86
|
+
":",
|
|
87
|
+
" ",
|
|
88
|
+
o.message
|
|
89
|
+
]
|
|
90
|
+
}) : !g || g.sampleSize === 0 ? /* @__PURE__ */ c("p", {
|
|
91
|
+
className: "text-sm text-muted-foreground",
|
|
92
|
+
children: n("planmagnet.analytics.cycleTime.empty", "No completed work items in this project yet — cycle time appears once items reach Done.")
|
|
93
|
+
}) : /* @__PURE__ */ l("div", {
|
|
94
|
+
className: "space-y-4",
|
|
95
|
+
children: [/* @__PURE__ */ l("div", {
|
|
96
|
+
className: "grid grid-cols-3 gap-3",
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ c(v, {
|
|
99
|
+
label: n("planmagnet.analytics.cycleTime.median", "Median"),
|
|
100
|
+
value: _(g.medianCycleTimeHours)
|
|
101
|
+
}),
|
|
102
|
+
/* @__PURE__ */ c(v, {
|
|
103
|
+
label: n("planmagnet.analytics.cycleTime.average", "Average"),
|
|
104
|
+
value: _(g.averageCycleTimeHours)
|
|
105
|
+
}),
|
|
106
|
+
/* @__PURE__ */ c(v, {
|
|
107
|
+
label: n("planmagnet.analytics.cycleTime.sample", "Items measured"),
|
|
108
|
+
value: String(g.sampleSize)
|
|
109
|
+
})
|
|
110
|
+
]
|
|
111
|
+
}), /* @__PURE__ */ l("table", {
|
|
112
|
+
className: "w-full text-sm",
|
|
113
|
+
children: [
|
|
114
|
+
/* @__PURE__ */ c("caption", {
|
|
115
|
+
className: "mb-2 text-left text-xs text-muted-foreground",
|
|
116
|
+
children: n("planmagnet.analytics.cycleTime.slowest", "Slowest completed items")
|
|
117
|
+
}),
|
|
118
|
+
/* @__PURE__ */ c("thead", { children: /* @__PURE__ */ l("tr", {
|
|
119
|
+
className: "text-left text-xs text-muted-foreground",
|
|
120
|
+
children: [/* @__PURE__ */ c("th", {
|
|
121
|
+
className: "py-1 pr-2 font-normal",
|
|
122
|
+
children: n("planmagnet.analytics.cycleTime.item", "Item")
|
|
123
|
+
}), /* @__PURE__ */ c("th", {
|
|
124
|
+
className: "py-1 text-right font-normal",
|
|
125
|
+
children: n("planmagnet.analytics.cycleTime.duration", "Cycle time")
|
|
126
|
+
})]
|
|
127
|
+
}) }),
|
|
128
|
+
/* @__PURE__ */ c("tbody", { children: y.map((e) => /* @__PURE__ */ l("tr", {
|
|
129
|
+
className: "border-t",
|
|
130
|
+
children: [/* @__PURE__ */ l("td", {
|
|
131
|
+
className: "py-1.5 pr-2",
|
|
132
|
+
children: [
|
|
133
|
+
/* @__PURE__ */ c("span", {
|
|
134
|
+
className: "font-medium",
|
|
135
|
+
children: e.key
|
|
136
|
+
}),
|
|
137
|
+
" ",
|
|
138
|
+
/* @__PURE__ */ c("span", {
|
|
139
|
+
className: "text-muted-foreground",
|
|
140
|
+
children: e.title
|
|
141
|
+
})
|
|
142
|
+
]
|
|
143
|
+
}), /* @__PURE__ */ c("td", {
|
|
144
|
+
className: "py-1.5 text-right tabular-nums",
|
|
145
|
+
children: _(e.cycleTimeHours)
|
|
146
|
+
})]
|
|
147
|
+
}, e.workItemId)) })
|
|
148
|
+
]
|
|
149
|
+
})]
|
|
150
|
+
}) : /* @__PURE__ */ c("p", {
|
|
151
|
+
className: "text-sm text-muted-foreground",
|
|
152
|
+
children: n("planmagnet.analytics.cycleTime.noProject", "Select a project to see its cycle time.")
|
|
153
|
+
}) })] });
|
|
154
|
+
}
|
|
155
|
+
function b(e, t) {
|
|
156
|
+
return !Number.isFinite(e) || e <= 0 ? null : t / e;
|
|
157
|
+
}
|
|
158
|
+
function x(e) {
|
|
159
|
+
return e === null ? "no-plan" : e <= .85 ? "on-track" : e <= 1 ? "near-limit" : "over-budget";
|
|
160
|
+
}
|
|
161
|
+
var S = {
|
|
162
|
+
"no-plan": {
|
|
163
|
+
track: "bg-muted",
|
|
164
|
+
fill: "bg-muted",
|
|
165
|
+
Icon: r,
|
|
166
|
+
text: "text-muted-foreground"
|
|
167
|
+
},
|
|
168
|
+
"on-track": {
|
|
169
|
+
track: "bg-primary/15",
|
|
170
|
+
fill: "bg-primary",
|
|
171
|
+
Icon: n,
|
|
172
|
+
text: "text-muted-foreground"
|
|
173
|
+
},
|
|
174
|
+
"near-limit": {
|
|
175
|
+
track: "bg-status-warning-subtle",
|
|
176
|
+
fill: "bg-status-warning",
|
|
177
|
+
Icon: t,
|
|
178
|
+
text: "text-status-warning-text"
|
|
179
|
+
},
|
|
180
|
+
"over-budget": {
|
|
181
|
+
track: "bg-status-error-subtle",
|
|
182
|
+
fill: "bg-status-error",
|
|
183
|
+
Icon: e,
|
|
184
|
+
text: "text-status-error-text"
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function C(e, t) {
|
|
188
|
+
try {
|
|
189
|
+
return new Intl.NumberFormat(void 0, {
|
|
190
|
+
style: "currency",
|
|
191
|
+
currency: t,
|
|
192
|
+
maximumFractionDigits: 0
|
|
193
|
+
}).format(e);
|
|
194
|
+
} catch {
|
|
195
|
+
return `${Math.round(e).toLocaleString()} ${t}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function w({ workspaceId: e }) {
|
|
199
|
+
let { t } = u(), { data: n, loading: r, error: i } = s(g, {
|
|
200
|
+
variables: { workspaceId: e },
|
|
201
|
+
skip: !e
|
|
202
|
+
}), o = n?.planmagnetBudgets?.items ?? [], h = {
|
|
203
|
+
"no-plan": t("planmagnet.analytics.budget.noPlan", "No plan set"),
|
|
204
|
+
"on-track": t("planmagnet.analytics.budget.onTrack", "On track"),
|
|
205
|
+
"near-limit": t("planmagnet.analytics.budget.nearLimit", "Near limit"),
|
|
206
|
+
"over-budget": t("planmagnet.analytics.budget.over", "Over budget")
|
|
207
|
+
};
|
|
208
|
+
return /* @__PURE__ */ l(d, { children: [/* @__PURE__ */ l(p, { children: [/* @__PURE__ */ l(m, {
|
|
209
|
+
className: "flex items-center gap-2",
|
|
210
|
+
children: [/* @__PURE__ */ c(a, { className: "h-4 w-4" }), t("planmagnet.analytics.budget.title", "Budget performance")]
|
|
211
|
+
}), /* @__PURE__ */ c("p", {
|
|
212
|
+
className: "text-xs text-muted-foreground",
|
|
213
|
+
children: t("planmagnet.analytics.budget.subtitle", "Spent against plan, per budget, across the workspace.")
|
|
214
|
+
})] }), /* @__PURE__ */ c(f, { children: r ? /* @__PURE__ */ c("p", {
|
|
215
|
+
className: "text-sm text-muted-foreground",
|
|
216
|
+
children: t("planmagnet.analytics.loading", "Loading…")
|
|
217
|
+
}) : i ? /* @__PURE__ */ l("p", {
|
|
218
|
+
className: "text-sm text-status-error-text",
|
|
219
|
+
children: [
|
|
220
|
+
t("planmagnet.analytics.budget.error", "Budgets could not be loaded"),
|
|
221
|
+
": ",
|
|
222
|
+
i.message
|
|
223
|
+
]
|
|
224
|
+
}) : o.length === 0 ? /* @__PURE__ */ c("p", {
|
|
225
|
+
className: "text-sm text-muted-foreground",
|
|
226
|
+
children: t("planmagnet.analytics.budget.empty", "No budgets in this workspace yet.")
|
|
227
|
+
}) : /* @__PURE__ */ l("table", {
|
|
228
|
+
className: "w-full text-sm",
|
|
229
|
+
children: [/* @__PURE__ */ c("thead", { children: /* @__PURE__ */ l("tr", {
|
|
230
|
+
className: "text-left text-xs text-muted-foreground",
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ c("th", {
|
|
233
|
+
className: "py-1 pr-2 font-normal",
|
|
234
|
+
children: t("planmagnet.analytics.budget.name", "Budget")
|
|
235
|
+
}),
|
|
236
|
+
/* @__PURE__ */ c("th", {
|
|
237
|
+
className: "py-1 pr-2 text-right font-normal",
|
|
238
|
+
children: t("planmagnet.analytics.budget.spent", "Spent")
|
|
239
|
+
}),
|
|
240
|
+
/* @__PURE__ */ c("th", {
|
|
241
|
+
className: "py-1 pr-2 text-right font-normal",
|
|
242
|
+
children: t("planmagnet.analytics.budget.planned", "Planned")
|
|
243
|
+
}),
|
|
244
|
+
/* @__PURE__ */ c("th", {
|
|
245
|
+
className: "w-2/5 py-1 font-normal",
|
|
246
|
+
children: t("planmagnet.analytics.budget.usage", "Used")
|
|
247
|
+
})
|
|
248
|
+
]
|
|
249
|
+
}) }), /* @__PURE__ */ c("tbody", { children: o.map((e) => {
|
|
250
|
+
let t = b(e.plannedAmount, e.spentAmount), n = x(t), r = S[n], i = t === null ? null : Math.round(t * 100);
|
|
251
|
+
return /* @__PURE__ */ l("tr", {
|
|
252
|
+
className: "border-t align-middle",
|
|
253
|
+
children: [
|
|
254
|
+
/* @__PURE__ */ l("td", {
|
|
255
|
+
className: "py-2 pr-2",
|
|
256
|
+
children: [/* @__PURE__ */ c("span", {
|
|
257
|
+
className: "font-medium",
|
|
258
|
+
children: e.name
|
|
259
|
+
}), e.project ? /* @__PURE__ */ c("span", {
|
|
260
|
+
className: "block text-xs text-muted-foreground",
|
|
261
|
+
children: e.project.name
|
|
262
|
+
}) : null]
|
|
263
|
+
}),
|
|
264
|
+
/* @__PURE__ */ c("td", {
|
|
265
|
+
className: "py-2 pr-2 text-right tabular-nums",
|
|
266
|
+
children: C(e.spentAmount, e.currency)
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ c("td", {
|
|
269
|
+
className: "py-2 pr-2 text-right tabular-nums",
|
|
270
|
+
children: C(e.plannedAmount, e.currency)
|
|
271
|
+
}),
|
|
272
|
+
/* @__PURE__ */ l("td", {
|
|
273
|
+
className: "py-2",
|
|
274
|
+
children: [/* @__PURE__ */ c("div", {
|
|
275
|
+
role: "meter",
|
|
276
|
+
"aria-valuemin": 0,
|
|
277
|
+
"aria-valuemax": Math.max(100, i ?? 0),
|
|
278
|
+
"aria-valuenow": i ?? 0,
|
|
279
|
+
"aria-label": `${e.name}: ${i === null ? "no plan" : `${i}%`} ${h[n]}`,
|
|
280
|
+
title: `${C(e.spentAmount, e.currency)} of ${C(e.plannedAmount, e.currency)}`,
|
|
281
|
+
className: `h-2 w-full overflow-hidden rounded-full ${r.track}`,
|
|
282
|
+
children: /* @__PURE__ */ c("div", {
|
|
283
|
+
className: `h-full rounded-full ${r.fill}`,
|
|
284
|
+
style: { width: `${Math.min(i ?? 0, 100)}%` }
|
|
285
|
+
})
|
|
286
|
+
}), /* @__PURE__ */ l("span", {
|
|
287
|
+
className: `mt-1 flex items-center gap-1 text-xs ${r.text}`,
|
|
288
|
+
children: [/* @__PURE__ */ c(r.Icon, {
|
|
289
|
+
className: "h-3 w-3",
|
|
290
|
+
"aria-hidden": "true"
|
|
291
|
+
}), i === null ? h[n] : `${i}% · ${h[n]}`]
|
|
292
|
+
})]
|
|
293
|
+
})
|
|
294
|
+
]
|
|
295
|
+
}, e.id);
|
|
296
|
+
}) })]
|
|
297
|
+
}) })] });
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
export { w as BudgetPerformanceCard, y as CycleTimeCard };
|
|
301
|
+
|
|
302
|
+
//# sourceMappingURL=DeliveryHealthCards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeliveryHealthCards.js","names":[],"sources":["../../../../src/planmagnet/pages/analytics/DeliveryHealthCards.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n AlertOctagon,\n AlertTriangle,\n CheckCircle2,\n CircleDashed,\n Timer,\n Wallet,\n} from 'lucide-react';\n\n/**\n * Two analytics the service already computed and no page showed (P1.1).\n *\n * - Cycle time: `planmagnetCycleTime` returned average, median and per-item\n * hours with zero UI consumers.\n * - Budget performance: budgets carry planned / spent / remaining, but the\n * analytics page never surfaced them.\n *\n * Forms follow the dataviz method: headline numbers are stat tiles, per-item\n * detail is a table, and \"spent against a limit\" is a meter whose fill carries\n * severity while its track is a lighter step of the same ramp. Over-budget is a\n * STATUS, so it always ships with an icon and a label — never colour alone.\n * Every colour is a semantic token.\n *\n * Each card owns its query at the top of the component, so neither can land a\n * hook after an early return in the host page.\n */\n\nconst GET_CYCLE_TIME = gql`\n query GetProjectCycleTime($workspaceId: ID!, $projectId: ID!) {\n planmagnetCycleTime(workspaceId: $workspaceId, projectId: $projectId) {\n averageCycleTimeHours\n medianCycleTimeHours\n sampleSize\n items {\n workItemId\n key\n title\n cycleTimeHours\n }\n }\n }\n`;\n\nconst GET_BUDGET_PERFORMANCE = gql`\n query GetBudgetPerformance($workspaceId: ID!) {\n planmagnetBudgets(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n currency\n plannedAmount\n spentAmount\n remainingAmount\n project {\n id\n name\n }\n }\n }\n }\n`;\n\ninterface CycleTimeData {\n planmagnetCycleTime: {\n averageCycleTimeHours: number | null;\n medianCycleTimeHours: number | null;\n sampleSize: number;\n items: Array<{ workItemId: string; key: string; title: string; cycleTimeHours: number }>;\n };\n}\n\ninterface BudgetData {\n planmagnetBudgets: {\n items: Array<{\n id: string;\n name: string;\n currency: string;\n plannedAmount: number;\n spentAmount: number;\n remainingAmount: number;\n project: { id: string; name: string } | null;\n }>;\n };\n}\n\n/** Hours read naturally up to two days; beyond that, days. */\nexport function formatDuration(hours: number | null | undefined): string {\n if (hours === null || hours === undefined || !Number.isFinite(hours)) return '—';\n if (hours < 48) return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)} h`;\n const days = hours / 24;\n return `${days < 10 ? days.toFixed(1) : Math.round(days)} d`;\n}\n\nfunction StatTile({ label, value, hint }: { label: string; value: string; hint?: string }) {\n return (\n <div className=\"rounded-md border p-3\">\n <p className=\"text-xs text-muted-foreground\">{label}</p>\n <p className=\"mt-1 text-2xl font-semibold\">{value}</p>\n {hint ? <p className=\"mt-0.5 text-xs text-muted-foreground\">{hint}</p> : null}\n </div>\n );\n}\n\nexport function CycleTimeCard({\n workspaceId,\n projectId,\n}: {\n workspaceId?: string | null;\n projectId: string;\n}) {\n const { t } = useI18n();\n const { data, loading, error } = useQuery<CycleTimeData>(GET_CYCLE_TIME, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n const cycle = data?.planmagnetCycleTime;\n const slowest = [...(cycle?.items ?? [])]\n .sort((a, b) => b.cycleTimeHours - a.cycleTimeHours)\n .slice(0, 5);\n\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Timer className=\"h-4 w-4\" />\n {t('planmagnet.analytics.cycleTime.title', 'Cycle time')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.cycleTime.subtitle',\n 'From first moving to In Progress (or creation) to Done, for the selected project.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n {!projectId ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.analytics.cycleTime.noProject',\n 'Select a project to see its cycle time.'\n )}\n </p>\n ) : loading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {t('planmagnet.analytics.cycleTime.error', 'Cycle time could not be loaded')}:{' '}\n {error.message}\n </p>\n ) : !cycle || cycle.sampleSize === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.analytics.cycleTime.empty',\n 'No completed work items in this project yet — cycle time appears once items reach Done.'\n )}\n </p>\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-3 gap-3\">\n <StatTile\n label={t('planmagnet.analytics.cycleTime.median', 'Median')}\n value={formatDuration(cycle.medianCycleTimeHours)}\n />\n <StatTile\n label={t('planmagnet.analytics.cycleTime.average', 'Average')}\n value={formatDuration(cycle.averageCycleTimeHours)}\n />\n <StatTile\n label={t('planmagnet.analytics.cycleTime.sample', 'Items measured')}\n value={String(cycle.sampleSize)}\n />\n </div>\n <table className=\"w-full text-sm\">\n <caption className=\"mb-2 text-left text-xs text-muted-foreground\">\n {t('planmagnet.analytics.cycleTime.slowest', 'Slowest completed items')}\n </caption>\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.cycleTime.item', 'Item')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.analytics.cycleTime.duration', 'Cycle time')}\n </th>\n </tr>\n </thead>\n <tbody>\n {slowest.map((item) => (\n <tr key={item.workItemId} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">\n <span className=\"font-medium\">{item.key}</span>{' '}\n <span className=\"text-muted-foreground\">{item.title}</span>\n </td>\n <td className=\"py-1.5 text-right tabular-nums\">\n {formatDuration(item.cycleTimeHours)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n\ntype BudgetState = 'no-plan' | 'on-track' | 'near-limit' | 'over-budget';\n\n/** Share of the plan spent; `null` when there is no plan to measure against. */\nexport function budgetUsage(planned: number, spent: number): number | null {\n if (!Number.isFinite(planned) || planned <= 0) return null;\n return spent / planned;\n}\n\nexport function budgetState(usage: number | null): BudgetState {\n // Without a plan there is nothing to be on track AGAINST — say so, rather\n // than reporting a comforting \"On track\" the numbers cannot support.\n if (usage === null) return 'no-plan';\n if (usage <= 0.85) return 'on-track';\n return usage <= 1 ? 'near-limit' : 'over-budget';\n}\n\nconst STATE_STYLE: Record<\n BudgetState,\n { track: string; fill: string; Icon: typeof CheckCircle2; text: string }\n> = {\n 'no-plan': {\n track: 'bg-muted',\n fill: 'bg-muted',\n Icon: CircleDashed,\n text: 'text-muted-foreground',\n },\n 'on-track': {\n track: 'bg-primary/15',\n fill: 'bg-primary',\n Icon: CheckCircle2,\n text: 'text-muted-foreground',\n },\n 'near-limit': {\n track: 'bg-status-warning-subtle',\n fill: 'bg-status-warning',\n Icon: AlertTriangle,\n text: 'text-status-warning-text',\n },\n 'over-budget': {\n track: 'bg-status-error-subtle',\n fill: 'bg-status-error',\n Icon: AlertOctagon,\n text: 'text-status-error-text',\n },\n};\n\nfunction money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(amount);\n } catch {\n // An unrecognised currency code must not blank the whole table.\n return `${Math.round(amount).toLocaleString()} ${currency}`;\n }\n}\n\nexport function BudgetPerformanceCard({ workspaceId }: { workspaceId?: string | null }) {\n const { t } = useI18n();\n const { data, loading, error } = useQuery<BudgetData>(GET_BUDGET_PERFORMANCE, {\n variables: { workspaceId },\n skip: !workspaceId,\n });\n const budgets = data?.planmagnetBudgets?.items ?? [];\n const label: Record<BudgetState, string> = {\n 'no-plan': t('planmagnet.analytics.budget.noPlan', 'No plan set'),\n 'on-track': t('planmagnet.analytics.budget.onTrack', 'On track'),\n 'near-limit': t('planmagnet.analytics.budget.nearLimit', 'Near limit'),\n 'over-budget': t('planmagnet.analytics.budget.over', 'Over budget'),\n };\n\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Wallet className=\"h-4 w-4\" />\n {t('planmagnet.analytics.budget.title', 'Budget performance')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.budget.subtitle',\n 'Spent against plan, per budget, across the workspace.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n {loading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {t('planmagnet.analytics.budget.error', 'Budgets could not be loaded')}: {error.message}\n </p>\n ) : budgets.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.budget.empty', 'No budgets in this workspace yet.')}\n </p>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.budget.name', 'Budget')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.analytics.budget.spent', 'Spent')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.analytics.budget.planned', 'Planned')}\n </th>\n <th className=\"w-2/5 py-1 font-normal\">\n {t('planmagnet.analytics.budget.usage', 'Used')}\n </th>\n </tr>\n </thead>\n <tbody>\n {budgets.map((budget) => {\n const usage = budgetUsage(budget.plannedAmount, budget.spentAmount);\n const state = budgetState(usage);\n const style = STATE_STYLE[state];\n const percent = usage === null ? null : Math.round(usage * 100);\n return (\n <tr key={budget.id} className=\"border-t align-middle\">\n <td className=\"py-2 pr-2\">\n <span className=\"font-medium\">{budget.name}</span>\n {budget.project ? (\n <span className=\"block text-xs text-muted-foreground\">\n {budget.project.name}\n </span>\n ) : null}\n </td>\n <td className=\"py-2 pr-2 text-right tabular-nums\">\n {money(budget.spentAmount, budget.currency)}\n </td>\n <td className=\"py-2 pr-2 text-right tabular-nums\">\n {money(budget.plannedAmount, budget.currency)}\n </td>\n <td className=\"py-2\">\n <div\n role=\"meter\"\n aria-valuemin={0}\n // ARIA requires valuenow to sit inside [min, max]. An\n // over-budget 130% with a max of 100 is invalid, and\n // assistive tech may clamp it and announce \"100%\".\n aria-valuemax={Math.max(100, percent ?? 0)}\n aria-valuenow={percent ?? 0}\n aria-label={`${budget.name}: ${percent === null ? 'no plan' : `${percent}%`} ${label[state]}`}\n title={`${money(budget.spentAmount, budget.currency)} of ${money(budget.plannedAmount, budget.currency)}`}\n className={`h-2 w-full overflow-hidden rounded-full ${style.track}`}\n >\n <div\n className={`h-full rounded-full ${style.fill}`}\n style={{ width: `${Math.min(percent ?? 0, 100)}%` }}\n />\n </div>\n <span className={`mt-1 flex items-center gap-1 text-xs ${style.text}`}>\n <style.Icon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {percent === null ? label[state] : `${percent}% · ${label[state]}`}\n </span>\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|