@burdenoff/microfe-planmagnet 2026.930.23 → 2026.930.24

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.
Files changed (54) hide show
  1. package/dist/index.js +6 -5
  2. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  3. package/dist/planmagnet/PlanMagnetRoot.js +154 -147
  4. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +66 -62
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/controls/FeatureControls.d.ts +11 -0
  9. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/FeatureControls.js +305 -135
  11. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  12. package/dist/planmagnet/controls/Pickers.d.ts +18 -0
  13. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  14. package/dist/planmagnet/controls/Pickers.js +78 -1
  15. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  16. package/dist/planmagnet/controls/PrioritizationControls.d.ts +72 -0
  17. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -0
  18. package/dist/planmagnet/controls/PrioritizationControls.js +246 -0
  19. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -0
  20. package/dist/planmagnet/index.d.ts +1 -0
  21. package/dist/planmagnet/index.d.ts.map +1 -1
  22. package/dist/planmagnet/index.js +1 -0
  23. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  24. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +131 -125
  25. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  26. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  27. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +183 -99
  28. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  29. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +113 -97
  31. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  32. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts +2 -0
  33. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts.map +1 -0
  34. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js +363 -0
  35. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js.map +1 -0
  36. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  37. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +96 -79
  38. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  39. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  40. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +133 -123
  41. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  42. package/dist/planmagnet/product/ProductControls.d.ts +33 -0
  43. package/dist/planmagnet/product/ProductControls.d.ts.map +1 -1
  44. package/dist/planmagnet/product/ProductControls.js +593 -268
  45. package/dist/planmagnet/product/ProductControls.js.map +1 -1
  46. package/dist/planmagnet/product/routes.d.ts +10 -0
  47. package/dist/planmagnet/product/routes.d.ts.map +1 -0
  48. package/dist/planmagnet/product/routes.js +6 -0
  49. package/dist/planmagnet/product/routes.js.map +1 -0
  50. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts +10 -0
  51. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  52. package/dist/planmagnet/roadmaps/RoadmapControls.js +235 -184
  53. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,363 @@
1
+ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
+ import { enumLabel as n } from "../controls/WorkItemControls.js";
3
+ import { detailPath as r } from "../utils/rowNavigation.js";
4
+ import { ProductSelect as i, RoadmapSelect as a } from "../controls/Pickers.js";
5
+ import { readUrlParam as o, writeUrlParam as s } from "../utils/urlParam.js";
6
+ import { KANO_META as c, KANO_VALUES as l, MOSCOW_META as u, MOSCOW_VALUES as d, QuadrantBadge as f } from "../controls/PrioritizationControls.js";
7
+ import { useState as p } from "react";
8
+ import { Gauge as m, Loader2 as h } from "lucide-react";
9
+ import { gql as g } from "@apollo/client";
10
+ import { useQuery as _ } from "@apollo/client/react";
11
+ import { jsx as v, jsxs as y } from "react/jsx-runtime";
12
+ import { Badge as b, Card as x, CardContent as S, CardHeader as C, CardTitle as w, Tabs as T, TabsContent as E, TabsList as D, TabsTrigger as O } from "@burdenoff/fe-libs/ui";
13
+ //#region src/planmagnet/pages/ProductsPrioritizationPagePage.tsx
14
+ var k = "\n bucket\n count\n truncated\n", A = g`
15
+ query FeaturePrioritizationMatrix($workspaceId: ID!, $productId: ID, $roadmapId: ID) {
16
+ planmagnetFeaturePrioritizationMatrix(
17
+ workspaceId: $workspaceId
18
+ productId: $productId
19
+ roadmapId: $roadmapId
20
+ ) {
21
+ source
22
+ moscow {
23
+ method
24
+ unscored
25
+ buckets {
26
+ ${k}
27
+ featureRequests {
28
+ id
29
+ title
30
+ status
31
+ }
32
+ }
33
+ }
34
+ kano {
35
+ method
36
+ unscored
37
+ buckets {
38
+ ${k}
39
+ featureRequests {
40
+ id
41
+ title
42
+ status
43
+ }
44
+ }
45
+ }
46
+ valueEffort {
47
+ method
48
+ unscored
49
+ buckets {
50
+ ${k}
51
+ featureRequests {
52
+ id
53
+ title
54
+ status
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ `, j = g`
61
+ query RoadmapPrioritizationMatrix($workspaceId: ID!, $productId: ID, $roadmapId: ID) {
62
+ planmagnetRoadmapPrioritizationMatrix(
63
+ workspaceId: $workspaceId
64
+ productId: $productId
65
+ roadmapId: $roadmapId
66
+ ) {
67
+ source
68
+ moscow {
69
+ method
70
+ unscored
71
+ buckets {
72
+ ${k}
73
+ roadmapItems {
74
+ id
75
+ title
76
+ status
77
+ roadmapId
78
+ }
79
+ }
80
+ }
81
+ kano {
82
+ method
83
+ unscored
84
+ buckets {
85
+ ${k}
86
+ roadmapItems {
87
+ id
88
+ title
89
+ status
90
+ roadmapId
91
+ }
92
+ }
93
+ }
94
+ valueEffort {
95
+ method
96
+ unscored
97
+ buckets {
98
+ ${k}
99
+ roadmapItems {
100
+ id
101
+ title
102
+ status
103
+ roadmapId
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ `, M = [["QUICK_WIN", "MAJOR_PROJECT"], ["FILL_IN", "TIME_SINK"]];
110
+ function N(e, t) {
111
+ return e?.find((e) => e.bucket === t) ?? {
112
+ bucket: t,
113
+ count: 0,
114
+ truncated: !1
115
+ };
116
+ }
117
+ function P({ header: e, bucket: t, source: i, navigate: a }) {
118
+ let o = t.featureRequests ?? t.roadmapItems ?? [], s = (e) => i === "FEATURE_REQUEST" ? r("/products/feature-detail-page", e.id) : r("/products/roadmap-detail-page", e.roadmapId);
119
+ return /* @__PURE__ */ y(x, {
120
+ "data-testid": "prio-bucket",
121
+ className: "flex-1 min-w-[220px]",
122
+ children: [/* @__PURE__ */ v(C, {
123
+ className: "pb-2",
124
+ children: /* @__PURE__ */ y("div", {
125
+ className: "flex items-center justify-between gap-2",
126
+ children: [e, /* @__PURE__ */ v(b, {
127
+ variant: "outline",
128
+ children: t.count
129
+ })]
130
+ })
131
+ }), /* @__PURE__ */ y(S, {
132
+ className: "space-y-1",
133
+ children: [
134
+ o.length === 0 && /* @__PURE__ */ v("p", {
135
+ className: "text-xs text-muted-foreground",
136
+ children: "No items"
137
+ }),
138
+ /* @__PURE__ */ v("ul", {
139
+ className: "space-y-1",
140
+ children: o.map((e) => /* @__PURE__ */ v("li", { children: /* @__PURE__ */ y("button", {
141
+ type: "button",
142
+ className: "w-full rounded-md border p-2 text-left text-xs hover:bg-muted",
143
+ onClick: () => a(s(e)),
144
+ children: [/* @__PURE__ */ v("span", {
145
+ className: "block truncate font-medium",
146
+ children: e.title
147
+ }), /* @__PURE__ */ v("span", {
148
+ className: "text-[10px] text-muted-foreground capitalize",
149
+ children: n(e.status)
150
+ })]
151
+ }) }, e.id))
152
+ }),
153
+ t.truncated && /* @__PURE__ */ y("p", {
154
+ className: "pt-1 text-xs text-muted-foreground",
155
+ children: [
156
+ "+",
157
+ Math.max(t.count - o.length, 0),
158
+ " more"
159
+ ]
160
+ })
161
+ ]
162
+ })]
163
+ });
164
+ }
165
+ function F({ count: e, method: t }) {
166
+ return e ? /* @__PURE__ */ y("p", {
167
+ className: "rounded-md border border-dashed p-2 text-xs text-muted-foreground",
168
+ children: [
169
+ e,
170
+ " item",
171
+ e === 1 ? "" : "s",
172
+ " not yet scored for ",
173
+ t,
174
+ " — set it from the item's edit form to see it grouped here."
175
+ ]
176
+ }) : null;
177
+ }
178
+ function I() {
179
+ let { workspaceId: n } = e(), r = t(), [g, b] = p(() => o("source") === "ROADMAP_ITEM" ? "ROADMAP_ITEM" : "FEATURE_REQUEST"), x = (e) => {
180
+ b(e), s("source", e === "FEATURE_REQUEST" ? null : e);
181
+ }, [S, C] = p(() => {
182
+ let e = o("method");
183
+ return e === "kano" || e === "valueEffort" ? e : "moscow";
184
+ }), k = (e) => {
185
+ C(e), s("method", e === "moscow" ? null : e);
186
+ }, [I, L] = p(""), [R, z] = p(""), B = {
187
+ workspaceId: n ?? "",
188
+ productId: I || void 0,
189
+ roadmapId: R || void 0
190
+ }, V = _(A, {
191
+ variables: B,
192
+ skip: !n || g !== "FEATURE_REQUEST"
193
+ }), H = _(j, {
194
+ variables: B,
195
+ skip: !n || g !== "ROADMAP_ITEM"
196
+ }), U = g === "FEATURE_REQUEST" ? V.loading : H.loading, W = g === "FEATURE_REQUEST" ? V.error : H.error, G = g === "FEATURE_REQUEST" ? V.data : H.data;
197
+ if (U && !G) return /* @__PURE__ */ v("div", {
198
+ className: "flex items-center justify-center p-12",
199
+ children: /* @__PURE__ */ v(h, { className: "h-8 w-8 animate-spin text-muted-foreground" })
200
+ });
201
+ if (W) return /* @__PURE__ */ y("div", {
202
+ className: "p-6 text-destructive",
203
+ children: ["Failed to load the prioritization matrix: ", W.message]
204
+ });
205
+ let K = g === "FEATURE_REQUEST" ? V.data?.planmagnetFeaturePrioritizationMatrix : H.data?.planmagnetRoadmapPrioritizationMatrix;
206
+ return /* @__PURE__ */ y("div", {
207
+ className: "space-y-6 p-6",
208
+ "data-testid": "prio-page",
209
+ children: [
210
+ /* @__PURE__ */ y("div", {
211
+ className: "flex items-center gap-2",
212
+ children: [/* @__PURE__ */ v(m, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h1", {
213
+ className: "text-2xl font-semibold",
214
+ children: "Prioritization"
215
+ }), /* @__PURE__ */ v("p", {
216
+ className: "text-sm text-muted-foreground",
217
+ children: "MoSCoW, Kano and Value-vs-Effort, over feature requests or roadmap initiatives."
218
+ })] })]
219
+ }),
220
+ /* @__PURE__ */ y("div", {
221
+ className: "flex flex-wrap items-center justify-between gap-3",
222
+ children: [/* @__PURE__ */ v("div", {
223
+ className: "flex items-center gap-2",
224
+ role: "group",
225
+ "aria-label": "Prioritization source",
226
+ "data-testid": "prio-source",
227
+ children: ["FEATURE_REQUEST", "ROADMAP_ITEM"].map((e) => /* @__PURE__ */ v("button", {
228
+ type: "button",
229
+ "aria-pressed": g === e,
230
+ onClick: () => x(e),
231
+ className: `px-3 py-1.5 text-sm rounded-md ${g === e ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`,
232
+ children: e === "FEATURE_REQUEST" ? "Feature requests" : "Roadmap initiatives"
233
+ }, e))
234
+ }), /* @__PURE__ */ y("div", {
235
+ className: "flex flex-wrap items-end gap-3",
236
+ children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
237
+ htmlFor: "prio-product",
238
+ className: "text-xs text-muted-foreground",
239
+ children: "Product"
240
+ }), /* @__PURE__ */ v("div", {
241
+ className: "mt-1",
242
+ children: /* @__PURE__ */ v(i, {
243
+ id: "prio-product",
244
+ value: I,
245
+ noneLabel: "All products",
246
+ onChange: (e) => {
247
+ L(e), z("");
248
+ }
249
+ })
250
+ })] }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
251
+ htmlFor: "prio-roadmap",
252
+ className: "text-xs text-muted-foreground",
253
+ children: "Roadmap"
254
+ }), /* @__PURE__ */ v("div", {
255
+ className: "mt-1",
256
+ children: /* @__PURE__ */ v(a, {
257
+ id: "prio-roadmap",
258
+ value: R,
259
+ noneLabel: "All roadmaps",
260
+ productId: I || void 0,
261
+ onChange: z
262
+ })
263
+ })] })]
264
+ })]
265
+ }),
266
+ /* @__PURE__ */ y(T, {
267
+ value: S,
268
+ onValueChange: (e) => k(e),
269
+ children: [
270
+ /* @__PURE__ */ y(D, { children: [
271
+ /* @__PURE__ */ v(O, {
272
+ value: "moscow",
273
+ "data-testid": "prio-method-moscow",
274
+ children: "MoSCoW"
275
+ }),
276
+ /* @__PURE__ */ v(O, {
277
+ value: "kano",
278
+ "data-testid": "prio-method-kano",
279
+ children: "Kano"
280
+ }),
281
+ /* @__PURE__ */ v(O, {
282
+ value: "valueEffort",
283
+ "data-testid": "prio-value-effort",
284
+ children: "Value vs Effort"
285
+ })
286
+ ] }),
287
+ /* @__PURE__ */ y(E, {
288
+ value: "moscow",
289
+ className: "mt-4 space-y-3",
290
+ children: [/* @__PURE__ */ v(F, {
291
+ count: K?.moscow.unscored ?? 0,
292
+ method: "MoSCoW"
293
+ }), /* @__PURE__ */ v("div", {
294
+ className: "flex flex-wrap gap-4",
295
+ children: d.map((e) => /* @__PURE__ */ v(P, {
296
+ header: /* @__PURE__ */ v(w, {
297
+ className: "text-sm",
298
+ children: u[e].label
299
+ }),
300
+ bucket: N(K?.moscow.buckets, e),
301
+ source: g,
302
+ navigate: r
303
+ }, e))
304
+ })]
305
+ }),
306
+ /* @__PURE__ */ y(E, {
307
+ value: "kano",
308
+ className: "mt-4 space-y-3",
309
+ children: [/* @__PURE__ */ v(F, {
310
+ count: K?.kano.unscored ?? 0,
311
+ method: "Kano"
312
+ }), /* @__PURE__ */ v("div", {
313
+ className: "flex flex-wrap gap-4",
314
+ children: l.map((e) => /* @__PURE__ */ v(P, {
315
+ header: /* @__PURE__ */ v(w, {
316
+ className: "text-sm",
317
+ children: c[e].label
318
+ }),
319
+ bucket: N(K?.kano.buckets, e),
320
+ source: g,
321
+ navigate: r
322
+ }, e))
323
+ })]
324
+ }),
325
+ /* @__PURE__ */ y(E, {
326
+ value: "valueEffort",
327
+ className: "mt-4 space-y-3",
328
+ children: [
329
+ /* @__PURE__ */ v(F, {
330
+ count: K?.valueEffort.unscored ?? 0,
331
+ method: "Value vs Effort"
332
+ }),
333
+ /* @__PURE__ */ y("div", {
334
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-2 text-center text-xs text-muted-foreground",
335
+ children: [/* @__PURE__ */ v("span", { children: "High value" }), /* @__PURE__ */ v("span", {})]
336
+ }),
337
+ /* @__PURE__ */ v("div", {
338
+ className: "space-y-4",
339
+ children: M.map((e, t) => /* @__PURE__ */ v("div", {
340
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-4",
341
+ children: e.map((e) => /* @__PURE__ */ v(P, {
342
+ header: /* @__PURE__ */ v(f, { quadrant: e }),
343
+ bucket: N(K?.valueEffort.buckets, e),
344
+ source: g,
345
+ navigate: r
346
+ }, e))
347
+ }, t))
348
+ }),
349
+ /* @__PURE__ */ y("div", {
350
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-2 text-center text-xs text-muted-foreground",
351
+ children: [/* @__PURE__ */ v("span", { children: "Low effort" }), /* @__PURE__ */ v("span", { children: "High effort" })]
352
+ })
353
+ ]
354
+ })
355
+ ]
356
+ })
357
+ ]
358
+ });
359
+ }
360
+ //#endregion
361
+ export { I as ProductsPrioritizationPagePage };
362
+
363
+ //# sourceMappingURL=ProductsPrioritizationPagePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductsPrioritizationPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProductsPrioritizationPagePage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useState, type ReactNode } from 'react';\nimport {\n Badge,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { Gauge, Loader2 } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { readUrlParam, writeUrlParam } from '../utils/urlParam';\nimport { ProductSelect, RoadmapSelect } from '../controls/Pickers';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport {\n KANO_META,\n KANO_VALUES,\n MOSCOW_META,\n MOSCOW_VALUES,\n QuadrantBadge,\n} from '../controls/PrioritizationControls';\n\n/**\n * Prioritization (PRM-27): MoSCoW, Kano and Value-vs-Effort views over either feature requests or roadmap\n * initiatives, each scoped by an optional product/roadmap. Two distinct queries back this page —\n * `planmagnetFeaturePrioritizationMatrix` / `planmagnetRoadmapPrioritizationMatrix` — because the server gates\n * them on different RBAC resources (`planmagnet-feature` vs `planmagnet-roadmap`); the page fetches whichever\n * one the source toggle selects and skips the other.\n *\n * Every bucket the server returns is already the FULL set for its method (even empty ones, all zero) — the\n * column/cell order below is still driven by this file's own MOSCOW_VALUES/KANO_VALUES/QUADRANT_VALUES so a\n * bucket always renders in the same place even if the server ever changed its own array order.\n */\n\nconst BUCKET_FIELDS = `\n bucket\n count\n truncated\n`;\n\nconst FEATURE_MATRIX = gql`\n query FeaturePrioritizationMatrix($workspaceId: ID!, $productId: ID, $roadmapId: ID) {\n planmagnetFeaturePrioritizationMatrix(\n workspaceId: $workspaceId\n productId: $productId\n roadmapId: $roadmapId\n ) {\n source\n moscow {\n method\n unscored\n buckets {\n ${BUCKET_FIELDS}\n featureRequests {\n id\n title\n status\n }\n }\n }\n kano {\n method\n unscored\n buckets {\n ${BUCKET_FIELDS}\n featureRequests {\n id\n title\n status\n }\n }\n }\n valueEffort {\n method\n unscored\n buckets {\n ${BUCKET_FIELDS}\n featureRequests {\n id\n title\n status\n }\n }\n }\n }\n }\n`;\n\nconst ROADMAP_MATRIX = gql`\n query RoadmapPrioritizationMatrix($workspaceId: ID!, $productId: ID, $roadmapId: ID) {\n planmagnetRoadmapPrioritizationMatrix(\n workspaceId: $workspaceId\n productId: $productId\n roadmapId: $roadmapId\n ) {\n source\n moscow {\n method\n unscored\n buckets {\n ${BUCKET_FIELDS}\n roadmapItems {\n id\n title\n status\n roadmapId\n }\n }\n }\n kano {\n method\n unscored\n buckets {\n ${BUCKET_FIELDS}\n roadmapItems {\n id\n title\n status\n roadmapId\n }\n }\n }\n valueEffort {\n method\n unscored\n buckets {\n ${BUCKET_FIELDS}\n roadmapItems {\n id\n title\n status\n roadmapId\n }\n }\n }\n }\n }\n`;\n\ntype Source = 'FEATURE_REQUEST' | 'ROADMAP_ITEM';\ntype Method = 'moscow' | 'kano' | 'valueEffort';\n\nconst VALUE_EFFORT_GRID: readonly (readonly [string, string])[] = [\n ['QUICK_WIN', 'MAJOR_PROJECT'],\n ['FILL_IN', 'TIME_SINK'],\n];\n\n/**\n * One item in a bucket. The two queries below select only one of `featureRequests`/`roadmapItems` each (never\n * both), but both shapes are folded into this single interface — same \"populated only for one branch\" contract\n * the server's own `PlanmagnetPrioritizationBucket` documents — so `BucketCard`/`findBucket` need only one type.\n */\ninterface PrioritizationItem {\n id: string;\n title: string;\n status: string;\n roadmapId?: string;\n}\n\ninterface PrioritizationBucket {\n bucket: string;\n count: number;\n truncated: boolean;\n featureRequests?: PrioritizationItem[] | null;\n roadmapItems?: PrioritizationItem[] | null;\n}\n\ninterface PrioritizationMethodResult {\n method: string;\n unscored: number;\n buckets: PrioritizationBucket[];\n}\n\ninterface PrioritizationMatrix {\n source: string;\n moscow: PrioritizationMethodResult;\n kano: PrioritizationMethodResult;\n valueEffort: PrioritizationMethodResult;\n}\n\ninterface FeatureMatrixData {\n planmagnetFeaturePrioritizationMatrix: PrioritizationMatrix;\n}\n\ninterface RoadmapMatrixData {\n planmagnetRoadmapPrioritizationMatrix: PrioritizationMatrix;\n}\n\nfunction findBucket(\n buckets: PrioritizationBucket[] | undefined,\n value: string\n): PrioritizationBucket {\n return buckets?.find((b) => b.bucket === value) ?? { bucket: value, count: 0, truncated: false };\n}\n\n/** One bucket's items, capped by the server and marked truncated — link each to its own detail page. */\nfunction BucketCard({\n header,\n bucket,\n source,\n navigate,\n}: {\n header: ReactNode;\n bucket: PrioritizationBucket;\n source: Source;\n navigate: (path: string) => void;\n}) {\n const items = bucket.featureRequests ?? bucket.roadmapItems ?? [];\n const linkFor = (item: { id: string; roadmapId?: string }) =>\n source === 'FEATURE_REQUEST'\n ? detailPath('/products/feature-detail-page', item.id)\n : detailPath('/products/roadmap-detail-page', item.roadmapId);\n\n return (\n <Card data-testid=\"prio-bucket\" className=\"flex-1 min-w-[220px]\">\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between gap-2\">\n {header}\n <Badge variant=\"outline\">{bucket.count}</Badge>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-1\">\n {items.length === 0 && <p className=\"text-xs text-muted-foreground\">No items</p>}\n <ul className=\"space-y-1\">\n {items.map((item) => (\n <li key={item.id}>\n <button\n type=\"button\"\n className=\"w-full rounded-md border p-2 text-left text-xs hover:bg-muted\"\n onClick={() => navigate(linkFor(item))}\n >\n <span className=\"block truncate font-medium\">{item.title}</span>\n <span className=\"text-[10px] text-muted-foreground capitalize\">\n {enumLabel(item.status)}\n </span>\n </button>\n </li>\n ))}\n </ul>\n {bucket.truncated && (\n <p className=\"pt-1 text-xs text-muted-foreground\">\n +{Math.max(bucket.count - items.length, 0)} more\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n\nfunction UnscoredHint({ count, method }: { count: number; method: string }) {\n if (!count) return null;\n return (\n <p className=\"rounded-md border border-dashed p-2 text-xs text-muted-foreground\">\n {count} item{count === 1 ? '' : 's'} not yet scored for {method} — set it from the item&apos;s\n edit form to see it grouped here.\n </p>\n );\n}\n\nexport function ProductsPrioritizationPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n\n const [source, setSourceState] = useState<Source>(() =>\n readUrlParam('source') === 'ROADMAP_ITEM' ? 'ROADMAP_ITEM' : 'FEATURE_REQUEST'\n );\n const setSource = (next: Source) => {\n setSourceState(next);\n writeUrlParam('source', next === 'FEATURE_REQUEST' ? null : next);\n };\n\n const [method, setMethodState] = useState<Method>(() => {\n const fromUrl = readUrlParam('method');\n return fromUrl === 'kano' || fromUrl === 'valueEffort' ? fromUrl : 'moscow';\n });\n const setMethod = (next: Method) => {\n setMethodState(next);\n writeUrlParam('method', next === 'moscow' ? null : next);\n };\n\n const [productId, setProductId] = useState('');\n const [roadmapId, setRoadmapId] = useState('');\n\n const variables = {\n workspaceId: workspaceId ?? '',\n productId: productId || undefined,\n roadmapId: roadmapId || undefined,\n };\n const featureResult = useQuery<FeatureMatrixData>(FEATURE_MATRIX, {\n variables,\n skip: !workspaceId || source !== 'FEATURE_REQUEST',\n });\n const roadmapResult = useQuery<RoadmapMatrixData>(ROADMAP_MATRIX, {\n variables,\n skip: !workspaceId || source !== 'ROADMAP_ITEM',\n });\n const loading = source === 'FEATURE_REQUEST' ? featureResult.loading : roadmapResult.loading;\n const error = source === 'FEATURE_REQUEST' ? featureResult.error : roadmapResult.error;\n const data = source === 'FEATURE_REQUEST' ? featureResult.data : roadmapResult.data;\n\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return (\n <div className=\"p-6 text-destructive\">\n Failed to load the prioritization matrix: {error.message}\n </div>\n );\n }\n\n const matrix: PrioritizationMatrix | undefined =\n source === 'FEATURE_REQUEST'\n ? featureResult.data?.planmagnetFeaturePrioritizationMatrix\n : roadmapResult.data?.planmagnetRoadmapPrioritizationMatrix;\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"prio-page\">\n <div className=\"flex items-center gap-2\">\n <Gauge className=\"h-5 w-5 text-primary\" />\n <div>\n <h1 className=\"text-2xl font-semibold\">Prioritization</h1>\n <p className=\"text-sm text-muted-foreground\">\n MoSCoW, Kano and Value-vs-Effort, over feature requests or roadmap initiatives.\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div\n className=\"flex items-center gap-2\"\n role=\"group\"\n aria-label=\"Prioritization source\"\n data-testid=\"prio-source\"\n >\n {(['FEATURE_REQUEST', 'ROADMAP_ITEM'] as const).map((s) => (\n <button\n key={s}\n type=\"button\"\n aria-pressed={source === s}\n onClick={() => setSource(s)}\n className={`px-3 py-1.5 text-sm rounded-md ${source === s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}\n >\n {s === 'FEATURE_REQUEST' ? 'Feature requests' : 'Roadmap initiatives'}\n </button>\n ))}\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div>\n <label htmlFor=\"prio-product\" className=\"text-xs text-muted-foreground\">\n Product\n </label>\n <div className=\"mt-1\">\n <ProductSelect\n id=\"prio-product\"\n value={productId}\n noneLabel=\"All products\"\n onChange={(next) => {\n setProductId(next);\n setRoadmapId('');\n }}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"prio-roadmap\" className=\"text-xs text-muted-foreground\">\n Roadmap\n </label>\n <div className=\"mt-1\">\n <RoadmapSelect\n id=\"prio-roadmap\"\n value={roadmapId}\n noneLabel=\"All roadmaps\"\n productId={productId || undefined}\n onChange={setRoadmapId}\n />\n </div>\n </div>\n </div>\n </div>\n\n <Tabs value={method} onValueChange={(next) => setMethod(next as Method)}>\n <TabsList>\n <TabsTrigger value=\"moscow\" data-testid=\"prio-method-moscow\">\n MoSCoW\n </TabsTrigger>\n <TabsTrigger value=\"kano\" data-testid=\"prio-method-kano\">\n Kano\n </TabsTrigger>\n <TabsTrigger value=\"valueEffort\" data-testid=\"prio-value-effort\">\n Value vs Effort\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"moscow\" className=\"mt-4 space-y-3\">\n <UnscoredHint count={matrix?.moscow.unscored ?? 0} method=\"MoSCoW\" />\n <div className=\"flex flex-wrap gap-4\">\n {MOSCOW_VALUES.map((v) => (\n <BucketCard\n key={v}\n header={<CardTitle className=\"text-sm\">{MOSCOW_META[v].label}</CardTitle>}\n bucket={findBucket(matrix?.moscow.buckets, v)}\n source={source}\n navigate={navigate}\n />\n ))}\n </div>\n </TabsContent>\n\n <TabsContent value=\"kano\" className=\"mt-4 space-y-3\">\n <UnscoredHint count={matrix?.kano.unscored ?? 0} method=\"Kano\" />\n <div className=\"flex flex-wrap gap-4\">\n {KANO_VALUES.map((v) => (\n <BucketCard\n key={v}\n header={<CardTitle className=\"text-sm\">{KANO_META[v].label}</CardTitle>}\n bucket={findBucket(matrix?.kano.buckets, v)}\n source={source}\n navigate={navigate}\n />\n ))}\n </div>\n </TabsContent>\n\n <TabsContent value=\"valueEffort\" className=\"mt-4 space-y-3\">\n <UnscoredHint count={matrix?.valueEffort.unscored ?? 0} method=\"Value vs Effort\" />\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2 text-center text-xs text-muted-foreground\">\n <span>High value</span>\n <span />\n </div>\n <div className=\"space-y-4\">\n {VALUE_EFFORT_GRID.map((row, rowIndex) => (\n <div key={rowIndex} className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n {row.map((v) => (\n <BucketCard\n key={v}\n header={<QuadrantBadge quadrant={v} />}\n bucket={findBucket(matrix?.valueEffort.buckets, v)}\n source={source}\n navigate={navigate}\n />\n ))}\n </div>\n ))}\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2 text-center text-xs text-muted-foreground\">\n <span>Low effort</span>\n <span>High effort</span>\n </div>\n </TabsContent>\n </Tabs>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsProductDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsProductDetailPagePage.tsx"],"names":[],"mappings":"AAuGA,wBAAgB,6BAA6B,4CAwK5C"}
1
+ {"version":3,"file":"ProductsProductDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsProductDetailPagePage.tsx"],"names":[],"mappings":"AAyHA,wBAAgB,6BAA6B,4CAqL5C"}