@burdenoff/microfe-planmagnet 2026.930.21 → 2026.930.23
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/node_modules/lodash/_arrayEach.js +14 -0
- package/dist/node_modules/lodash/_arrayEach.js.map +1 -0
- package/dist/node_modules/lodash/_assignValue.js +17 -0
- package/dist/node_modules/lodash/_assignValue.js.map +1 -0
- package/dist/node_modules/lodash/_baseAssign.js +16 -0
- package/dist/node_modules/lodash/_baseAssign.js.map +1 -0
- package/dist/node_modules/lodash/_baseAssignIn.js +16 -0
- package/dist/node_modules/lodash/_baseAssignIn.js.map +1 -0
- package/dist/node_modules/lodash/_baseClone.js +64 -0
- package/dist/node_modules/lodash/_baseClone.js.map +1 -0
- package/dist/node_modules/lodash/_baseCreate.js +21 -0
- package/dist/node_modules/lodash/_baseCreate.js.map +1 -0
- package/dist/node_modules/lodash/_baseIsMap.js +16 -0
- package/dist/node_modules/lodash/_baseIsMap.js.map +1 -0
- package/dist/node_modules/lodash/_baseIsSet.js +16 -0
- package/dist/node_modules/lodash/_baseIsSet.js.map +1 -0
- package/dist/node_modules/lodash/_baseKeysIn.js +20 -0
- package/dist/node_modules/lodash/_baseKeysIn.js.map +1 -0
- package/dist/node_modules/lodash/_baseSum.js +17 -0
- package/dist/node_modules/lodash/_baseSum.js.map +1 -0
- package/dist/node_modules/lodash/_baseUnset.js +26 -0
- package/dist/node_modules/lodash/_baseUnset.js.map +1 -0
- package/dist/node_modules/lodash/_cloneArrayBuffer.js +16 -0
- package/dist/node_modules/lodash/_cloneArrayBuffer.js.map +1 -0
- package/dist/node_modules/lodash/_cloneBuffer.js +17 -0
- package/dist/node_modules/lodash/_cloneBuffer.js.map +1 -0
- package/dist/node_modules/lodash/_cloneDataView.js +16 -0
- package/dist/node_modules/lodash/_cloneDataView.js.map +1 -0
- package/dist/node_modules/lodash/_cloneRegExp.js +15 -0
- package/dist/node_modules/lodash/_cloneRegExp.js.map +1 -0
- package/dist/node_modules/lodash/_cloneSymbol.js +15 -0
- package/dist/node_modules/lodash/_cloneSymbol.js.map +1 -0
- package/dist/node_modules/lodash/_cloneTypedArray.js +16 -0
- package/dist/node_modules/lodash/_cloneTypedArray.js.map +1 -0
- package/dist/node_modules/lodash/_copyArray.js +15 -0
- package/dist/node_modules/lodash/_copyArray.js.map +1 -0
- package/dist/node_modules/lodash/_copyObject.js +22 -0
- package/dist/node_modules/lodash/_copyObject.js.map +1 -0
- package/dist/node_modules/lodash/_copySymbols.js +16 -0
- package/dist/node_modules/lodash/_copySymbols.js.map +1 -0
- package/dist/node_modules/lodash/_copySymbolsIn.js +16 -0
- package/dist/node_modules/lodash/_copySymbolsIn.js.map +1 -0
- package/dist/node_modules/lodash/_customOmitClone.js +15 -0
- package/dist/node_modules/lodash/_customOmitClone.js.map +1 -0
- package/dist/node_modules/lodash/_flatRest.js +17 -0
- package/dist/node_modules/lodash/_flatRest.js.map +1 -0
- package/dist/node_modules/lodash/_getAllKeysIn.js +17 -0
- package/dist/node_modules/lodash/_getAllKeysIn.js.map +1 -0
- package/dist/node_modules/lodash/_getSymbolsIn.js +18 -0
- package/dist/node_modules/lodash/_getSymbolsIn.js.map +1 -0
- package/dist/node_modules/lodash/_initCloneArray.js +15 -0
- package/dist/node_modules/lodash/_initCloneArray.js.map +1 -0
- package/dist/node_modules/lodash/_initCloneByTag.js +40 -0
- package/dist/node_modules/lodash/_initCloneByTag.js.map +1 -0
- package/dist/node_modules/lodash/_initCloneObject.js +17 -0
- package/dist/node_modules/lodash/_initCloneObject.js.map +1 -0
- package/dist/node_modules/lodash/_nativeKeysIn.js +15 -0
- package/dist/node_modules/lodash/_nativeKeysIn.js.map +1 -0
- package/dist/node_modules/lodash/_parent.js +16 -0
- package/dist/node_modules/lodash/_parent.js.map +1 -0
- package/dist/node_modules/lodash/flatten.js +15 -0
- package/dist/node_modules/lodash/flatten.js.map +1 -0
- package/dist/node_modules/lodash/isMap.js +14 -0
- package/dist/node_modules/lodash/isMap.js.map +1 -0
- package/dist/node_modules/lodash/isSet.js +14 -0
- package/dist/node_modules/lodash/isSet.js.map +1 -0
- package/dist/node_modules/lodash/keysIn.js +17 -0
- package/dist/node_modules/lodash/keysIn.js.map +1 -0
- package/dist/node_modules/lodash/maxBy.js +17 -0
- package/dist/node_modules/lodash/maxBy.js.map +1 -0
- package/dist/node_modules/lodash/omit.js +28 -0
- package/dist/node_modules/lodash/omit.js.map +1 -0
- package/dist/node_modules/lodash/sumBy.js +16 -0
- package/dist/node_modules/lodash/sumBy.js.map +1 -0
- package/dist/node_modules/recharts/es6/chart/Sankey.js +556 -0
- package/dist/node_modules/recharts/es6/chart/Sankey.js.map +1 -0
- package/dist/node_modules/recharts/es6/chart/Treemap.js +545 -0
- package/dist/node_modules/recharts/es6/chart/Treemap.js.map +1 -0
- package/dist/node_modules/recharts/es6/shape/Polygon.js +112 -0
- package/dist/node_modules/recharts/es6/shape/Polygon.js.map +1 -0
- package/dist/node_modules/recharts/es6/util/Constants.js +31 -0
- package/dist/node_modules/recharts/es6/util/Constants.js.map +1 -0
- package/dist/node_modules/recharts/es6/util/ReactUtils.js +15 -10
- package/dist/node_modules/recharts/es6/util/ReactUtils.js.map +1 -1
- package/dist/planmagnet/controls/AutomationRuleControls.js +10 -10
- package/dist/planmagnet/controls/CompetitorControls.js +7 -7
- package/dist/planmagnet/controls/FeatureControls.js +38 -38
- package/dist/planmagnet/controls/ProgramControls.js +8 -8
- package/dist/planmagnet/dependency/DependencyGraph.d.ts.map +1 -1
- package/dist/planmagnet/dependency/DependencyGraph.js +13 -10
- package/dist/planmagnet/dependency/DependencyGraph.js.map +1 -1
- package/dist/planmagnet/dependency/DependencyPanel.js +13 -13
- package/dist/planmagnet/dependency/GanttChart.d.ts.map +1 -1
- package/dist/planmagnet/dependency/GanttChart.js +11 -5
- package/dist/planmagnet/dependency/GanttChart.js.map +1 -1
- package/dist/planmagnet/pages/ActivityActivityPagePage.js +16 -16
- package/dist/planmagnet/pages/ActivityActivityPagePage.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +91 -73
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js +6 -6
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +4 -4
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +11 -11
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +5 -5
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +5 -5
- package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js +6 -6
- package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +8 -8
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +10 -10
- package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +6 -6
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +5 -5
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +8 -8
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +4 -4
- package/dist/planmagnet/pages/analytics/StatusFlowCard.d.ts +8 -0
- package/dist/planmagnet/pages/analytics/StatusFlowCard.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/StatusFlowCard.js +234 -0
- package/dist/planmagnet/pages/analytics/StatusFlowCard.js.map +1 -0
- package/dist/planmagnet/pages/analytics/WorkBreakdownCard.d.ts +6 -0
- package/dist/planmagnet/pages/analytics/WorkBreakdownCard.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +367 -0
- package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js.map +1 -0
- package/dist/planmagnet/roadmaps/RoadmapControls.js +17 -17
- package/dist/planmagnet/utils/statusColor.d.ts +8 -9
- package/dist/planmagnet/utils/statusColor.d.ts.map +1 -1
- package/dist/planmagnet/utils/statusColor.js +8 -40
- package/dist/planmagnet/utils/statusColor.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Tooltip as e } from "../../../node_modules/recharts/es6/component/Tooltip.js";
|
|
2
|
+
import { ResponsiveContainer as t } from "../../../node_modules/recharts/es6/component/ResponsiveContainer.js";
|
|
3
|
+
import { Sankey as n } from "../../../node_modules/recharts/es6/chart/Sankey.js";
|
|
4
|
+
import { WORK_ITEM_STATUSES as r, enumLabel as i } from "../../controls/WorkItemControls.js";
|
|
5
|
+
import { statusVisual as a } from "../../utils/statusColor.js";
|
|
6
|
+
import { isForbidden as o } from "../../utils/graphqlErrors.js";
|
|
7
|
+
import { useMemo as s } from "react";
|
|
8
|
+
import { Workflow as c } from "lucide-react";
|
|
9
|
+
import { gql as l } from "@apollo/client";
|
|
10
|
+
import { useQuery as u } from "@apollo/client/react";
|
|
11
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
12
|
+
import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { Card as m, CardContent as h, CardHeader as g, CardTitle as _ } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/planmagnet/pages/analytics/StatusFlowCard.tsx
|
|
15
|
+
var v = l`
|
|
16
|
+
query GetAnalyticsStatusFlow(
|
|
17
|
+
$workspaceId: ID!
|
|
18
|
+
$projectId: ID!
|
|
19
|
+
$from: DateTime!
|
|
20
|
+
$to: DateTime!
|
|
21
|
+
) {
|
|
22
|
+
planmagnetStatusFlow(workspaceId: $workspaceId, projectId: $projectId, from: $from, to: $to) {
|
|
23
|
+
fromStatus
|
|
24
|
+
toStatus
|
|
25
|
+
count
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`, y = 366, b = y * 864e5;
|
|
29
|
+
function x(e, t) {
|
|
30
|
+
let n = Date.parse(e), r = Date.parse(t);
|
|
31
|
+
return !Number.isFinite(n) || !Number.isFinite(r) || r - n <= b ? {
|
|
32
|
+
from: e,
|
|
33
|
+
to: t,
|
|
34
|
+
clamped: !1
|
|
35
|
+
} : {
|
|
36
|
+
from: new Date(r - b).toISOString(),
|
|
37
|
+
to: t,
|
|
38
|
+
clamped: !0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function S(e) {
|
|
42
|
+
let t = r.filter((t) => e.some((e) => e.fromStatus === t)), n = r.filter((t) => e.some((e) => e.toStatus === t)), a = new Map(t.map((e, t) => [e, t])), o = new Map(n.map((e, n) => [e, t.length + n]));
|
|
43
|
+
return {
|
|
44
|
+
nodes: [...t.map((e) => ({
|
|
45
|
+
name: i(e),
|
|
46
|
+
status: e,
|
|
47
|
+
side: "from"
|
|
48
|
+
})), ...n.map((e) => ({
|
|
49
|
+
name: i(e),
|
|
50
|
+
status: e,
|
|
51
|
+
side: "to"
|
|
52
|
+
}))],
|
|
53
|
+
links: e.flatMap((e) => {
|
|
54
|
+
let t = a.get(e.fromStatus), n = o.get(e.toStatus);
|
|
55
|
+
return t === void 0 || n === void 0 ? [] : [{
|
|
56
|
+
source: t,
|
|
57
|
+
target: n,
|
|
58
|
+
value: e.count
|
|
59
|
+
}];
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function C({ x: e, y: t, width: n, height: r, payload: i }) {
|
|
64
|
+
let o = a(i.status), s = i.side === "from";
|
|
65
|
+
return /* @__PURE__ */ f("g", { children: [/* @__PURE__ */ d("rect", {
|
|
66
|
+
x: e,
|
|
67
|
+
y: t,
|
|
68
|
+
width: n,
|
|
69
|
+
height: r,
|
|
70
|
+
rx: 2,
|
|
71
|
+
style: {
|
|
72
|
+
fill: o.color,
|
|
73
|
+
stroke: o.color
|
|
74
|
+
},
|
|
75
|
+
strokeWidth: 1
|
|
76
|
+
}), /* @__PURE__ */ d("text", {
|
|
77
|
+
x: s ? e - 6 : e + n + 6,
|
|
78
|
+
y: t + r / 2,
|
|
79
|
+
dy: "0.32em",
|
|
80
|
+
textAnchor: s ? "end" : "start",
|
|
81
|
+
className: "fill-foreground text-[11px]",
|
|
82
|
+
children: i.name
|
|
83
|
+
})] });
|
|
84
|
+
}
|
|
85
|
+
function w({ sourceX: e, sourceY: t, targetX: n, targetY: r, sourceControlX: i, targetControlX: o, linkWidth: s, payload: c }) {
|
|
86
|
+
let l = a(c.source.status);
|
|
87
|
+
return /* @__PURE__ */ d("path", {
|
|
88
|
+
d: `M${e},${t} C${i},${t} ${o},${r} ${n},${r}`,
|
|
89
|
+
style: { stroke: l.color },
|
|
90
|
+
strokeWidth: Math.max(s, 1),
|
|
91
|
+
strokeOpacity: .45,
|
|
92
|
+
fill: "none"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function T({ workspaceId: r, projectId: a, startDate: l, endDate: b }) {
|
|
96
|
+
let { t: T } = p(), E = s(() => x(l, b), [l, b]), { data: D, loading: O, error: k } = u(v, {
|
|
97
|
+
variables: {
|
|
98
|
+
workspaceId: r,
|
|
99
|
+
projectId: a,
|
|
100
|
+
from: E.from,
|
|
101
|
+
to: E.to
|
|
102
|
+
},
|
|
103
|
+
skip: !r || !a
|
|
104
|
+
}), A = D?.planmagnetStatusFlow ?? [], { nodes: j, links: M } = S(A), N = [...A].sort((e, t) => t.count - e.count);
|
|
105
|
+
return /* @__PURE__ */ f(m, {
|
|
106
|
+
"data-testid": "status-flow-card",
|
|
107
|
+
children: [/* @__PURE__ */ f(g, { children: [
|
|
108
|
+
/* @__PURE__ */ f(_, {
|
|
109
|
+
className: "flex items-center gap-2",
|
|
110
|
+
children: [/* @__PURE__ */ d(c, { className: "h-4 w-4" }), T("planmagnet.analytics.statusFlow.title", "Status flow")]
|
|
111
|
+
}),
|
|
112
|
+
/* @__PURE__ */ d("p", {
|
|
113
|
+
className: "text-xs text-muted-foreground",
|
|
114
|
+
children: T("planmagnet.analytics.statusFlow.subtitle", "How work items moved between statuses in the selected range.")
|
|
115
|
+
}),
|
|
116
|
+
E.clamped && /* @__PURE__ */ d("p", {
|
|
117
|
+
className: "text-xs text-status-warning-text",
|
|
118
|
+
children: T("planmagnet.analytics.statusFlow.clamped", "Status flow is capped at a {{days}}-day window — showing the most recent {{days}} days of the selected range (from {{from}}).", {
|
|
119
|
+
days: y,
|
|
120
|
+
from: E.from.slice(0, 10)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
] }), /* @__PURE__ */ d(h, { children: a ? O && !D ? /* @__PURE__ */ d("div", {
|
|
124
|
+
className: "flex justify-center py-12",
|
|
125
|
+
children: /* @__PURE__ */ d("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
126
|
+
}) : k ? /* @__PURE__ */ d("p", {
|
|
127
|
+
className: "text-sm text-status-error-text",
|
|
128
|
+
children: o(k) ? T("planmagnet.analytics.statusFlow.forbidden", "You do not have access to status flow for this project.") : `${T("planmagnet.analytics.statusFlow.error", "Status flow could not be loaded")}: ${k.message}`
|
|
129
|
+
}) : A.length === 0 ? /* @__PURE__ */ d("p", {
|
|
130
|
+
className: "text-sm text-muted-foreground py-8 text-center",
|
|
131
|
+
children: T("planmagnet.analytics.statusFlow.empty", "No status changes recorded for this project in the selected range.")
|
|
132
|
+
}) : /* @__PURE__ */ f("div", {
|
|
133
|
+
className: "space-y-4",
|
|
134
|
+
children: [/* @__PURE__ */ d("div", {
|
|
135
|
+
"data-testid": "viz-status-flow",
|
|
136
|
+
children: /* @__PURE__ */ d(t, {
|
|
137
|
+
width: "100%",
|
|
138
|
+
height: Math.max(260, j.length * 24),
|
|
139
|
+
children: /* @__PURE__ */ d(n, {
|
|
140
|
+
data: {
|
|
141
|
+
nodes: j,
|
|
142
|
+
links: M
|
|
143
|
+
},
|
|
144
|
+
node: C,
|
|
145
|
+
link: w,
|
|
146
|
+
nodeWidth: 10,
|
|
147
|
+
nodePadding: 16,
|
|
148
|
+
linkCurvature: .5,
|
|
149
|
+
margin: {
|
|
150
|
+
top: 8,
|
|
151
|
+
right: 90,
|
|
152
|
+
bottom: 8,
|
|
153
|
+
left: 90
|
|
154
|
+
},
|
|
155
|
+
children: /* @__PURE__ */ d(e, { content: ({ active: e, payload: t }) => {
|
|
156
|
+
if (!e || !t || t.length === 0) return null;
|
|
157
|
+
let n = t[0]?.payload?.payload;
|
|
158
|
+
return n ? "source" in n ? /* @__PURE__ */ f("div", {
|
|
159
|
+
className: "rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs shadow-md",
|
|
160
|
+
children: [/* @__PURE__ */ f("p", {
|
|
161
|
+
className: "font-medium text-popover-foreground",
|
|
162
|
+
children: [
|
|
163
|
+
n.source.name,
|
|
164
|
+
" → ",
|
|
165
|
+
n.target.name
|
|
166
|
+
]
|
|
167
|
+
}), /* @__PURE__ */ d("p", {
|
|
168
|
+
className: "text-muted-foreground",
|
|
169
|
+
children: T("planmagnet.analytics.statusFlow.tooltipCount", "{{count}} item(s)", { count: n.value })
|
|
170
|
+
})]
|
|
171
|
+
}) : /* @__PURE__ */ d("div", {
|
|
172
|
+
className: "rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs shadow-md",
|
|
173
|
+
children: /* @__PURE__ */ d("p", {
|
|
174
|
+
className: "font-medium text-popover-foreground",
|
|
175
|
+
children: n.name
|
|
176
|
+
})
|
|
177
|
+
}) : null;
|
|
178
|
+
} })
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
}), /* @__PURE__ */ f("table", {
|
|
182
|
+
className: "w-full text-sm",
|
|
183
|
+
"data-testid": "viz-status-flow-table",
|
|
184
|
+
children: [
|
|
185
|
+
/* @__PURE__ */ d("caption", {
|
|
186
|
+
className: "mb-2 text-left text-xs text-muted-foreground",
|
|
187
|
+
children: T("planmagnet.analytics.statusFlow.tableCaption", "Status transitions")
|
|
188
|
+
}),
|
|
189
|
+
/* @__PURE__ */ d("thead", { children: /* @__PURE__ */ f("tr", {
|
|
190
|
+
className: "text-left text-xs text-muted-foreground",
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ d("th", {
|
|
193
|
+
className: "py-1 pr-2 font-normal",
|
|
194
|
+
children: T("planmagnet.analytics.statusFlow.from", "From")
|
|
195
|
+
}),
|
|
196
|
+
/* @__PURE__ */ d("th", {
|
|
197
|
+
className: "py-1 pr-2 font-normal",
|
|
198
|
+
children: T("planmagnet.analytics.statusFlow.to", "To")
|
|
199
|
+
}),
|
|
200
|
+
/* @__PURE__ */ d("th", {
|
|
201
|
+
className: "py-1 text-right font-normal",
|
|
202
|
+
children: T("planmagnet.analytics.statusFlow.count", "Count")
|
|
203
|
+
})
|
|
204
|
+
]
|
|
205
|
+
}) }),
|
|
206
|
+
/* @__PURE__ */ d("tbody", { children: N.map((e) => /* @__PURE__ */ f("tr", {
|
|
207
|
+
className: "border-t",
|
|
208
|
+
children: [
|
|
209
|
+
/* @__PURE__ */ d("td", {
|
|
210
|
+
className: "py-1.5 pr-2",
|
|
211
|
+
children: i(e.fromStatus)
|
|
212
|
+
}),
|
|
213
|
+
/* @__PURE__ */ d("td", {
|
|
214
|
+
className: "py-1.5 pr-2",
|
|
215
|
+
children: i(e.toStatus)
|
|
216
|
+
}),
|
|
217
|
+
/* @__PURE__ */ d("td", {
|
|
218
|
+
className: "py-1.5 text-right tabular-nums",
|
|
219
|
+
children: e.count
|
|
220
|
+
})
|
|
221
|
+
]
|
|
222
|
+
}, `${e.fromStatus}-${e.toStatus}`)) })
|
|
223
|
+
]
|
|
224
|
+
})]
|
|
225
|
+
}) : /* @__PURE__ */ d("p", {
|
|
226
|
+
className: "text-sm text-muted-foreground",
|
|
227
|
+
children: T("planmagnet.analytics.statusFlow.noProject", "Select a project to see its status flow.")
|
|
228
|
+
}) })]
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { T as StatusFlowCard };
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=StatusFlowCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusFlowCard.js","names":[],"sources":["../../../../src/planmagnet/pages/analytics/StatusFlowCard.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { 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 { Workflow } from 'lucide-react';\nimport { ResponsiveContainer, Sankey, Tooltip } from 'recharts';\nimport { WORK_ITEM_STATUSES, enumLabel } from '../../controls/WorkItemControls';\nimport { statusVisual } from '../../utils/statusColor';\nimport { isForbidden } from '../../utils/graphqlErrors';\n\n/**\n * ANR-24: status-flow Sankey (wspace-planmagnet-svc#65 `planmagnetStatusFlow`) — aggregated status-to-status\n * transition counts for one project, over a date window capped server-side at 366 days.\n *\n * recharts' Sankey lays its nodes out by depth, walked outward from every node with no incoming link\n * (`getNodesTree` in recharts' own source), with NO cycle guard — a real transition graph almost always has\n * cycles (e.g. IN_REVIEW -> IN_PROGRESS -> IN_REVIEW, a change request), which would recurse forever. The fix:\n * every status becomes TWO Sankey nodes, a \"from\" one and a \"to\" one, so every link only ever points from the\n * \"from\" column to the \"to\" column — a strict two-level DAG by construction, however tangled the underlying\n * status graph is. This is a display transform only; it does not change what the data means (every link is\n * still exactly one observed fromStatus -> toStatus edge, whichever status is on which side of the column).\n */\n\ntype WorkItemStatusValue = (typeof WORK_ITEM_STATUSES)[number];\n\nconst GET_STATUS_FLOW = gql`\n query GetAnalyticsStatusFlow(\n $workspaceId: ID!\n $projectId: ID!\n $from: DateTime!\n $to: DateTime!\n ) {\n planmagnetStatusFlow(workspaceId: $workspaceId, projectId: $projectId, from: $from, to: $to) {\n fromStatus\n toStatus\n count\n }\n }\n`;\n\ninterface StatusFlowEdge {\n fromStatus: WorkItemStatusValue;\n toStatus: WorkItemStatusValue;\n count: number;\n}\n\ninterface StatusFlowData {\n planmagnetStatusFlow: StatusFlowEdge[];\n}\n\ninterface FlowNodeDatum {\n name: string;\n status: WorkItemStatusValue;\n side: 'from' | 'to';\n}\n\ninterface FlowLinkDatum {\n source: number;\n target: number;\n value: number;\n}\n\ninterface FlowLinkPayload {\n source: FlowNodeDatum;\n target: FlowNodeDatum;\n value: number;\n}\n\n/** The server's own cap (see the field's doc comment in graphql/wspace.graphql) — the UI never sends a wider\n * window, and says so when the page's own range would have asked for one. */\nconst MAX_WINDOW_DAYS = 366;\nconst MAX_WINDOW_MS = MAX_WINDOW_DAYS * 86_400_000;\n\nfunction clampWindow(\n startDate: string,\n endDate: string\n): { from: string; to: string; clamped: boolean } {\n const startMs = Date.parse(startDate);\n const endMs = Date.parse(endDate);\n if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs - startMs <= MAX_WINDOW_MS) {\n return { from: startDate, to: endDate, clamped: false };\n }\n return { from: new Date(endMs - MAX_WINDOW_MS).toISOString(), to: endDate, clamped: true };\n}\n\n/** Every status that actually appears becomes a \"from\" node (left column) and/or a \"to\" node (right column), in\n * the app's canonical status order — never in whatever order the API happened to return edges. */\nfunction buildFlow(edges: StatusFlowEdge[]): { nodes: FlowNodeDatum[]; links: FlowLinkDatum[] } {\n const fromStatuses = WORK_ITEM_STATUSES.filter((s) => edges.some((e) => e.fromStatus === s));\n const toStatuses = WORK_ITEM_STATUSES.filter((s) => edges.some((e) => e.toStatus === s));\n const fromIndex = new Map(fromStatuses.map((s, i) => [s, i]));\n const toIndex = new Map(toStatuses.map((s, i) => [s, fromStatuses.length + i]));\n\n const nodes: FlowNodeDatum[] = [\n ...fromStatuses.map((s) => ({ name: enumLabel(s), status: s, side: 'from' as const })),\n ...toStatuses.map((s) => ({ name: enumLabel(s), status: s, side: 'to' as const })),\n ];\n\n const links: FlowLinkDatum[] = edges.flatMap((e) => {\n const source = fromIndex.get(e.fromStatus);\n const target = toIndex.get(e.toStatus);\n // Every edge's statuses come straight from fromStatuses/toStatuses above, so this can never actually miss —\n // the check just keeps the result type honest without a non-null assertion.\n return source === undefined || target === undefined ? [] : [{ source, target, value: e.count }];\n });\n\n return { nodes, links };\n}\n\ninterface FlowNodeProps {\n x: number;\n y: number;\n width: number;\n height: number;\n payload: FlowNodeDatum;\n}\n\nfunction FlowNode({ x, y, width, height, payload }: FlowNodeProps) {\n const visual = statusVisual(payload.status);\n const isFrom = payload.side === 'from';\n return (\n <g>\n <rect\n x={x}\n y={y}\n width={width}\n height={height}\n rx={2}\n style={{ fill: visual.color, stroke: visual.color }}\n strokeWidth={1}\n />\n <text\n x={isFrom ? x - 6 : x + width + 6}\n y={y + height / 2}\n dy=\"0.32em\"\n textAnchor={isFrom ? 'end' : 'start'}\n className=\"fill-foreground text-[11px]\"\n >\n {payload.name}\n </text>\n </g>\n );\n}\n\ninterface FlowLinkProps {\n sourceX: number;\n sourceY: number;\n targetX: number;\n targetY: number;\n sourceControlX: number;\n targetControlX: number;\n linkWidth: number;\n payload: FlowLinkPayload;\n}\n\nfunction FlowLink({\n sourceX,\n sourceY,\n targetX,\n targetY,\n sourceControlX,\n targetControlX,\n linkWidth,\n payload,\n}: FlowLinkProps) {\n // Colored by the transition's origin status — the same fixed status->color mapping every other visualization\n // in this app (Gantt, dependency graph) uses, read via statusVisual rather than a second color table.\n const visual = statusVisual(payload.source.status);\n return (\n <path\n d={`M${sourceX},${sourceY} C${sourceControlX},${sourceY} ${targetControlX},${targetY} ${targetX},${targetY}`}\n style={{ stroke: visual.color }}\n strokeWidth={Math.max(linkWidth, 1)}\n strokeOpacity={0.45}\n fill=\"none\"\n />\n );\n}\n\nexport function StatusFlowCard({\n workspaceId,\n projectId,\n startDate,\n endDate,\n}: {\n workspaceId?: string | null;\n /** Status flow is always one project (`projectId: ID!` on the server) — same convention as CycleTimeCard. */\n projectId: string;\n startDate: string;\n endDate: string;\n}) {\n const { t } = useI18n();\n const clamp = useMemo(() => clampWindow(startDate, endDate), [startDate, endDate]);\n const { data, loading, error } = useQuery<StatusFlowData>(GET_STATUS_FLOW, {\n variables: { workspaceId, projectId, from: clamp.from, to: clamp.to },\n skip: !workspaceId || !projectId,\n });\n // Recomputed straight from `data` on every render rather than behind its own useMemo: at most a handful of\n // statuses and edges, and `?? []` below would hand memoization a fresh array identity every render anyway —\n // same call this page's own project/sprint default-picking makes for a similarly small list.\n const edges = data?.planmagnetStatusFlow ?? [];\n const { nodes, links } = buildFlow(edges);\n const sortedEdges = [...edges].sort((a, b) => b.count - a.count);\n\n return (\n <Card data-testid=\"status-flow-card\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Workflow className=\"h-4 w-4\" />\n {t('planmagnet.analytics.statusFlow.title', 'Status flow')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.statusFlow.subtitle',\n 'How work items moved between statuses in the selected range.'\n )}\n </p>\n {clamp.clamped && (\n <p className=\"text-xs text-status-warning-text\">\n {t(\n 'planmagnet.analytics.statusFlow.clamped',\n 'Status flow is capped at a {{days}}-day window — showing the most recent {{days}} days of the selected range (from {{from}}).',\n { days: MAX_WINDOW_DAYS, from: clamp.from.slice(0, 10) }\n )}\n </p>\n )}\n </CardHeader>\n <CardContent>\n {!projectId ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.analytics.statusFlow.noProject',\n 'Select a project to see its status flow.'\n )}\n </p>\n ) : loading && !data ? (\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 ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.analytics.statusFlow.forbidden',\n 'You do not have access to status flow for this project.'\n )\n : `${t('planmagnet.analytics.statusFlow.error', 'Status flow could not be loaded')}: ${error.message}`}\n </p>\n ) : edges.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.statusFlow.empty',\n 'No status changes recorded for this project in the selected range.'\n )}\n </p>\n ) : (\n <div className=\"space-y-4\">\n <div data-testid=\"viz-status-flow\">\n <ResponsiveContainer width=\"100%\" height={Math.max(260, nodes.length * 24)}>\n <Sankey\n data={{ nodes, links }}\n node={FlowNode}\n link={FlowLink}\n nodeWidth={10}\n nodePadding={16}\n linkCurvature={0.5}\n margin={{ top: 8, right: 90, bottom: 8, left: 90 }}\n >\n <Tooltip\n content={({ active, payload }) => {\n if (!active || !payload || payload.length === 0) return null;\n // recharts types a Sankey tooltip payload's inner `.payload` as `any` — it nests our own\n // node/link datum one level below recharts' own node/link render-props wrapper. Narrowed\n // here, once, rather than threading `any` through the render below.\n const el = payload[0]?.payload as\n | { payload?: FlowNodeDatum | FlowLinkPayload }\n | undefined;\n const inner = el?.payload;\n if (!inner) return null;\n if ('source' in inner) {\n return (\n <div className=\"rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs shadow-md\">\n <p className=\"font-medium text-popover-foreground\">\n {inner.source.name} → {inner.target.name}\n </p>\n <p className=\"text-muted-foreground\">\n {t(\n 'planmagnet.analytics.statusFlow.tooltipCount',\n '{{count}} item(s)',\n {\n count: inner.value,\n }\n )}\n </p>\n </div>\n );\n }\n return (\n <div className=\"rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs shadow-md\">\n <p className=\"font-medium text-popover-foreground\">{inner.name}</p>\n </div>\n );\n }}\n />\n </Sankey>\n </ResponsiveContainer>\n </div>\n\n {/* Accessible table alternative — every edge the Sankey draws, in the same data. */}\n <table className=\"w-full text-sm\" data-testid=\"viz-status-flow-table\">\n <caption className=\"mb-2 text-left text-xs text-muted-foreground\">\n {t('planmagnet.analytics.statusFlow.tableCaption', 'Status transitions')}\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.statusFlow.from', 'From')}\n </th>\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.statusFlow.to', 'To')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.analytics.statusFlow.count', 'Count')}\n </th>\n </tr>\n </thead>\n <tbody>\n {sortedEdges.map((e) => (\n <tr key={`${e.fromStatus}-${e.toStatus}`} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">{enumLabel(e.fromStatus)}</td>\n <td className=\"py-1.5 pr-2\">{enumLabel(e.toStatus)}</td>\n <td className=\"py-1.5 text-right tabular-nums\">{e.count}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA0BA,IAAM,IAAkB,CAAG;;;;;;;;;;;;;GA6CrB,IAAkB,KAClB,IAAgB,IAAkB;AAExC,SAAS,EACP,GACA,GACgD;CAChD,IAAM,IAAU,KAAK,MAAM,EAAU,EAC/B,IAAQ,KAAK,MAAM,EAAQ;AAIjC,QAHI,CAAC,OAAO,SAAS,EAAQ,IAAI,CAAC,OAAO,SAAS,EAAM,IAAI,IAAQ,KAAW,IACtE;EAAE,MAAM;EAAW,IAAI;EAAS,SAAS;EAAO,GAElD;EAAE,MAAM,IAAI,KAAK,IAAQ,EAAc,CAAC,aAAa;EAAE,IAAI;EAAS,SAAS;EAAM;;AAK5F,SAAS,EAAU,GAA6E;CAC9F,IAAM,IAAe,EAAmB,QAAQ,MAAM,EAAM,MAAM,MAAM,EAAE,eAAe,EAAE,CAAC,EACtF,IAAa,EAAmB,QAAQ,MAAM,EAAM,MAAM,MAAM,EAAE,aAAa,EAAE,CAAC,EAClF,IAAY,IAAI,IAAI,EAAa,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,EACvD,IAAU,IAAI,IAAI,EAAW,KAAK,GAAG,MAAM,CAAC,GAAG,EAAa,SAAS,EAAE,CAAC,CAAC;AAe/E,QAAO;EAAE,OAbsB,CAC7B,GAAG,EAAa,KAAK,OAAO;GAAE,MAAM,EAAU,EAAE;GAAE,QAAQ;GAAG,MAAM;GAAiB,EAAE,EACtF,GAAG,EAAW,KAAK,OAAO;GAAE,MAAM,EAAU,EAAE;GAAE,QAAQ;GAAG,MAAM;GAAe,EAAE,CACnF;EAUe,OARe,EAAM,SAAS,MAAM;GAClD,IAAM,IAAS,EAAU,IAAI,EAAE,WAAW,EACpC,IAAS,EAAQ,IAAI,EAAE,SAAS;AAGtC,UAAO,MAAW,KAAA,KAAa,MAAW,KAAA,IAAY,EAAE,GAAG,CAAC;IAAE;IAAQ;IAAQ,OAAO,EAAE;IAAO,CAAC;IAC/F;EAEqB;;AAWzB,SAAS,EAAS,EAAE,MAAG,MAAG,UAAO,WAAQ,cAA0B;CACjE,IAAM,IAAS,EAAa,EAAQ,OAAO,EACrC,IAAS,EAAQ,SAAS;AAChC,QACE,kBAAC,KAAD,EAAA,UAAA,CACE,kBAAC,QAAD;EACK;EACA;EACI;EACC;EACR,IAAI;EACJ,OAAO;GAAE,MAAM,EAAO;GAAO,QAAQ,EAAO;GAAO;EACnD,aAAa;EACb,CAAA,EACF,kBAAC,QAAD;EACE,GAAG,IAAS,IAAI,IAAI,IAAI,IAAQ;EAChC,GAAG,IAAI,IAAS;EAChB,IAAG;EACH,YAAY,IAAS,QAAQ;EAC7B,WAAU;YAET,EAAQ;EACJ,CAAA,CACL,EAAA,CAAA;;AAeR,SAAS,EAAS,EAChB,YACA,YACA,YACA,YACA,mBACA,mBACA,cACA,cACgB;CAGhB,IAAM,IAAS,EAAa,EAAQ,OAAO,OAAO;AAClD,QACE,kBAAC,QAAD;EACE,GAAG,IAAI,EAAQ,GAAG,EAAQ,IAAI,EAAe,GAAG,EAAQ,GAAG,EAAe,GAAG,EAAQ,GAAG,EAAQ,GAAG;EACnG,OAAO,EAAE,QAAQ,EAAO,OAAO;EAC/B,aAAa,KAAK,IAAI,GAAW,EAAE;EACnC,eAAe;EACf,MAAK;EACL,CAAA;;AAIN,SAAgB,EAAe,EAC7B,gBACA,cACA,cACA,cAOC;CACD,IAAM,EAAE,SAAM,GAAS,EACjB,IAAQ,QAAc,EAAY,GAAW,EAAQ,EAAE,CAAC,GAAW,EAAQ,CAAC,EAC5E,EAAE,SAAM,YAAS,aAAU,EAAyB,GAAiB;EACzE,WAAW;GAAE;GAAa;GAAW,MAAM,EAAM;GAAM,IAAI,EAAM;GAAI;EACrE,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EAII,IAAQ,GAAM,wBAAwB,EAAE,EACxC,EAAE,UAAO,aAAU,EAAU,EAAM,EACnC,IAAc,CAAC,GAAG,EAAM,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AAEhE,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAW,WAAU;cAArB,CACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAC/B,EAAE,yCAAyC,cAAc,CAChD;;GACZ,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,4CACA,+DACD;IACC,CAAA;GACH,EAAM,WACL,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,2CACA,iIACA;KAAE,MAAM;KAAiB,MAAM,EAAM,KAAK,MAAM,GAAG,GAAG;KAAE,CACzD;IACC,CAAA;GAEK,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UACI,IAOE,KAAW,CAAC,IACd,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;GAC9F,CAAA,GACJ,IACF,kBAAC,KAAD;GAAG,WAAU;aACV,EAAY,EAAM,GACf,EACE,6CACA,0DACD,GACD,GAAG,EAAE,yCAAyC,kCAAkC,CAAC,IAAI,EAAM;GAC7F,CAAA,GACF,EAAM,WAAW,IACnB,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,yCACA,qEACD;GACC,CAAA,GAEJ,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,eAAY;cACf,kBAAC,GAAD;KAAqB,OAAM;KAAO,QAAQ,KAAK,IAAI,KAAK,EAAM,SAAS,GAAG;eACxE,kBAAC,GAAD;MACE,MAAM;OAAE;OAAO;OAAO;MACtB,MAAM;MACN,MAAM;MACN,WAAW;MACX,aAAa;MACb,eAAe;MACf,QAAQ;OAAE,KAAK;OAAG,OAAO;OAAI,QAAQ;OAAG,MAAM;OAAI;gBAElD,kBAAC,GAAD,EACE,UAAU,EAAE,WAAQ,iBAAc;AAChC,WAAI,CAAC,KAAU,CAAC,KAAW,EAAQ,WAAW,EAAG,QAAO;OAOxD,IAAM,IAHK,EAAQ,IAAI,SAGL;AAoBlB,cAnBK,IACD,YAAY,IAEZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAb;UACG,EAAM,OAAO;UAAK;UAAI,EAAM,OAAO;UAClC;YACJ,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,gDACA,qBACA,EACE,OAAO,EAAM,OACd,CACF;SACC,CAAA,CACA;YAIR,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,KAAD;SAAG,WAAU;mBAAuC,EAAM;SAAS,CAAA;QAC/D,CAAA,GAtBW;SAyBrB,CAAA;MACK,CAAA;KACW,CAAA;IAClB,CAAA,EAGN,kBAAC,SAAD;IAAO,WAAU;IAAiB,eAAY;cAA9C;KACE,kBAAC,WAAD;MAAS,WAAU;gBAChB,EAAE,gDAAgD,qBAAqB;MAChE,CAAA;KACV,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAE,wCAAwC,OAAO;QAC/C,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAE,sCAAsC,KAAK;QAC3C,CAAA;OACL,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAE,yCAAyC,QAAQ;QACjD,CAAA;OACF;SACC,CAAA;KACR,kBAAC,SAAD,EAAA,UACG,EAAY,KAAK,MAChB,kBAAC,MAAD;MAA0C,WAAU;gBAApD;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAe,EAAU,EAAE,WAAW;QAAM,CAAA;OAC1D,kBAAC,MAAD;QAAI,WAAU;kBAAe,EAAU,EAAE,SAAS;QAAM,CAAA;OACxD,kBAAC,MAAD;QAAI,WAAU;kBAAkC,EAAE;QAAW,CAAA;OAC1D;QAJI,GAAG,EAAE,WAAW,GAAG,EAAE,WAIzB,CACL,EACI,CAAA;KACF;MACJ;OA3GN,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,6CACA,2CACD;GACC,CAAA,EAwGM,CAAA,CACT"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function WorkBreakdownCard({ workspaceId, projectId, }: {
|
|
2
|
+
workspaceId?: string | null;
|
|
3
|
+
/** ANR-09 project scope. Omitted/undefined = every project (up to 50 — see truncatedProjects below). */
|
|
4
|
+
projectId?: string;
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=WorkBreakdownCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkBreakdownCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/pages/analytics/WorkBreakdownCard.tsx"],"names":[],"mappings":"AAoVA,wBAAgB,iBAAiB,CAAC,EAChC,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wGAAwG;IACxG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAwNA"}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { Tooltip as t } from "../../../node_modules/recharts/es6/component/Tooltip.js";
|
|
3
|
+
import { ResponsiveContainer as n } from "../../../node_modules/recharts/es6/component/ResponsiveContainer.js";
|
|
4
|
+
import { Treemap as r } from "../../../node_modules/recharts/es6/chart/Treemap.js";
|
|
5
|
+
import { isForbidden as i } from "../../utils/graphqlErrors.js";
|
|
6
|
+
import { rowLinkProps as a } from "../../utils/rowNavigation.js";
|
|
7
|
+
import { truncate as o } from "../../dependency/DependencyGraph.js";
|
|
8
|
+
import { useState as s } from "react";
|
|
9
|
+
import { LayoutGrid as c } from "lucide-react";
|
|
10
|
+
import { gql as l } from "@apollo/client";
|
|
11
|
+
import { useQuery as u } from "@apollo/client/react";
|
|
12
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
13
|
+
import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
14
|
+
import { Card as m, CardContent as h, CardHeader as g, CardTitle as _ } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
//#region src/planmagnet/pages/analytics/WorkBreakdownCard.tsx
|
|
16
|
+
var v = l`
|
|
17
|
+
query GetAnalyticsWorkBreakdown($workspaceId: ID!, $projectIds: [ID!]) {
|
|
18
|
+
planmagnetWorkBreakdown(workspaceId: $workspaceId, projectIds: $projectIds) {
|
|
19
|
+
truncatedProjects
|
|
20
|
+
projects {
|
|
21
|
+
projectId
|
|
22
|
+
projectName
|
|
23
|
+
truncatedEpics
|
|
24
|
+
epics {
|
|
25
|
+
epicId
|
|
26
|
+
epicTitle
|
|
27
|
+
openCount
|
|
28
|
+
doneCount
|
|
29
|
+
openPoints
|
|
30
|
+
donePoints
|
|
31
|
+
}
|
|
32
|
+
noEpic {
|
|
33
|
+
openCount
|
|
34
|
+
doneCount
|
|
35
|
+
openPoints
|
|
36
|
+
donePoints
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
function y(e, t) {
|
|
43
|
+
return t === "items" ? {
|
|
44
|
+
size: e.openCount + e.doneCount,
|
|
45
|
+
done: e.doneCount
|
|
46
|
+
} : {
|
|
47
|
+
size: e.openPoints + e.donePoints,
|
|
48
|
+
done: e.donePoints
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function b(e, t) {
|
|
52
|
+
let { size: n, done: r } = y(e, t);
|
|
53
|
+
return n > 0 ? r / n : null;
|
|
54
|
+
}
|
|
55
|
+
var x = .14, S = 1;
|
|
56
|
+
function C(e, t, n) {
|
|
57
|
+
let r = [];
|
|
58
|
+
for (let i of e) {
|
|
59
|
+
let e = i.epics.map((e) => ({
|
|
60
|
+
name: e.epicTitle,
|
|
61
|
+
size: y(e, t).size,
|
|
62
|
+
doneRatio: b(e, t),
|
|
63
|
+
epicId: e.epicId,
|
|
64
|
+
isNoEpic: !1,
|
|
65
|
+
isHeader: !1,
|
|
66
|
+
projectId: i.projectId,
|
|
67
|
+
projectName: i.projectName,
|
|
68
|
+
openCount: e.openCount,
|
|
69
|
+
doneCount: e.doneCount,
|
|
70
|
+
openPoints: e.openPoints,
|
|
71
|
+
donePoints: e.donePoints
|
|
72
|
+
})).filter((e) => e.size > 0), a = y(i.noEpic, t).size;
|
|
73
|
+
a > 0 && e.push({
|
|
74
|
+
name: n,
|
|
75
|
+
size: a,
|
|
76
|
+
doneRatio: b(i.noEpic, t),
|
|
77
|
+
epicId: null,
|
|
78
|
+
isNoEpic: !0,
|
|
79
|
+
isHeader: !1,
|
|
80
|
+
projectId: i.projectId,
|
|
81
|
+
projectName: i.projectName,
|
|
82
|
+
openCount: i.noEpic.openCount,
|
|
83
|
+
doneCount: i.noEpic.doneCount,
|
|
84
|
+
openPoints: i.noEpic.openPoints,
|
|
85
|
+
donePoints: i.noEpic.donePoints
|
|
86
|
+
});
|
|
87
|
+
let o = e.reduce((e, t) => e + t.size, 0);
|
|
88
|
+
if (o <= 0) continue;
|
|
89
|
+
let s = {
|
|
90
|
+
name: i.projectName,
|
|
91
|
+
size: Math.max(Math.round(o * x), S),
|
|
92
|
+
doneRatio: null,
|
|
93
|
+
epicId: null,
|
|
94
|
+
isNoEpic: !1,
|
|
95
|
+
isHeader: !0,
|
|
96
|
+
projectId: i.projectId,
|
|
97
|
+
projectName: i.projectName,
|
|
98
|
+
openCount: 0,
|
|
99
|
+
doneCount: 0,
|
|
100
|
+
openPoints: 0,
|
|
101
|
+
donePoints: 0
|
|
102
|
+
};
|
|
103
|
+
r.push({
|
|
104
|
+
name: i.projectName,
|
|
105
|
+
projectId: i.projectId,
|
|
106
|
+
children: [s, ...e]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
111
|
+
function w(e) {
|
|
112
|
+
return e === null ? .12 : .18 + e * .72;
|
|
113
|
+
}
|
|
114
|
+
function T(e) {
|
|
115
|
+
let { x: t = 0, y: n = 0, width: r = 0, height: i = 0, depth: s = 0, name: c = "", isHeader: l = !1, isNoEpic: u = !1, epicId: p = null, doneRatio: m = null, onOpenEpic: h, noEpicLabel: g } = e;
|
|
116
|
+
if (r <= 0 || i <= 0 || s === 0) return null;
|
|
117
|
+
if (s === 1) return /* @__PURE__ */ d("rect", {
|
|
118
|
+
x: t,
|
|
119
|
+
y: n,
|
|
120
|
+
width: r,
|
|
121
|
+
height: i,
|
|
122
|
+
rx: 4,
|
|
123
|
+
className: "fill-none stroke-border",
|
|
124
|
+
strokeWidth: 1
|
|
125
|
+
});
|
|
126
|
+
let _ = u ? g : c, v = r > 28 && i > 14, y = o(_, Math.max(1, Math.floor((r - 8) / 6)));
|
|
127
|
+
if (l) return /* @__PURE__ */ f("g", { children: [/* @__PURE__ */ d("rect", {
|
|
128
|
+
x: t,
|
|
129
|
+
y: n,
|
|
130
|
+
width: r,
|
|
131
|
+
height: i,
|
|
132
|
+
className: "fill-muted"
|
|
133
|
+
}), v && /* @__PURE__ */ d("text", {
|
|
134
|
+
x: t + 4,
|
|
135
|
+
y: n + i / 2,
|
|
136
|
+
dy: "0.32em",
|
|
137
|
+
className: "fill-foreground text-[11px] font-medium",
|
|
138
|
+
children: y
|
|
139
|
+
})] });
|
|
140
|
+
let b = /* @__PURE__ */ d("rect", {
|
|
141
|
+
x: t,
|
|
142
|
+
y: n,
|
|
143
|
+
width: r,
|
|
144
|
+
height: i,
|
|
145
|
+
className: "stroke-background",
|
|
146
|
+
style: {
|
|
147
|
+
fill: "var(--color-chart-2)",
|
|
148
|
+
fillOpacity: w(m)
|
|
149
|
+
},
|
|
150
|
+
strokeWidth: 2
|
|
151
|
+
}), x = v && /* @__PURE__ */ d("text", {
|
|
152
|
+
x: t + 4,
|
|
153
|
+
y: n + i / 2,
|
|
154
|
+
dy: "0.32em",
|
|
155
|
+
className: "fill-foreground text-[11px]",
|
|
156
|
+
children: y
|
|
157
|
+
});
|
|
158
|
+
if (!p) return /* @__PURE__ */ f("g", { children: [b, x] });
|
|
159
|
+
let S = `${c} — ${Math.round((m ?? 0) * 100)}% done`;
|
|
160
|
+
return /* @__PURE__ */ f("g", {
|
|
161
|
+
...a(() => h(p), S),
|
|
162
|
+
className: "cursor-pointer focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2",
|
|
163
|
+
children: [
|
|
164
|
+
/* @__PURE__ */ d("title", { children: S }),
|
|
165
|
+
b,
|
|
166
|
+
x
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function E({ workspaceId: a, projectId: o }) {
|
|
171
|
+
let { t: l } = p(), y = e(), [b, x] = s("items"), { data: S, loading: w, error: E } = u(v, {
|
|
172
|
+
variables: {
|
|
173
|
+
workspaceId: a,
|
|
174
|
+
projectIds: o ? [o] : void 0
|
|
175
|
+
},
|
|
176
|
+
skip: !a
|
|
177
|
+
}), D = l("planmagnet.analytics.workBreakdown.noEpic", "No epic"), O = S?.planmagnetWorkBreakdown, k = O?.projects ?? [], A = C(k, b, D), j = k.filter((e) => e.truncatedEpics).map((e) => e.projectName);
|
|
178
|
+
function M(e) {
|
|
179
|
+
y(`/tasks/task-detail-page?taskId=${encodeURIComponent(e)}`);
|
|
180
|
+
}
|
|
181
|
+
return /* @__PURE__ */ f(m, {
|
|
182
|
+
"data-testid": "work-breakdown-card",
|
|
183
|
+
children: [/* @__PURE__ */ f(g, { children: [/* @__PURE__ */ f("div", {
|
|
184
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
185
|
+
children: [/* @__PURE__ */ f(_, {
|
|
186
|
+
className: "flex items-center gap-2",
|
|
187
|
+
children: [/* @__PURE__ */ d(c, { className: "h-4 w-4" }), l("planmagnet.analytics.workBreakdown.title", "Work breakdown")]
|
|
188
|
+
}), /* @__PURE__ */ d("div", {
|
|
189
|
+
className: "flex items-center gap-1 rounded-md border p-0.5",
|
|
190
|
+
role: "group",
|
|
191
|
+
"aria-label": l("planmagnet.analytics.workBreakdown.metricLabel", "Size by"),
|
|
192
|
+
"data-testid": "viz-breakdown-metric",
|
|
193
|
+
children: ["items", "points"].map((e) => /* @__PURE__ */ d("button", {
|
|
194
|
+
type: "button",
|
|
195
|
+
"aria-pressed": b === e,
|
|
196
|
+
onClick: () => x(e),
|
|
197
|
+
className: `px-2 py-1 text-xs rounded ${b === e ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`,
|
|
198
|
+
children: e === "items" ? l("planmagnet.analytics.workBreakdown.items", "Items") : l("planmagnet.analytics.workBreakdown.points", "Story points")
|
|
199
|
+
}, e))
|
|
200
|
+
})]
|
|
201
|
+
}), /* @__PURE__ */ d("p", {
|
|
202
|
+
className: "text-xs text-muted-foreground",
|
|
203
|
+
children: l("planmagnet.analytics.workBreakdown.subtitle", "Live open/done work, by project and epic — current state, not limited by the date range above.")
|
|
204
|
+
})] }), /* @__PURE__ */ d(h, { children: w && !S ? /* @__PURE__ */ d("div", {
|
|
205
|
+
className: "flex justify-center py-12",
|
|
206
|
+
children: /* @__PURE__ */ d("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
207
|
+
}) : E ? /* @__PURE__ */ d("p", {
|
|
208
|
+
className: "text-sm text-status-error-text",
|
|
209
|
+
children: i(E) ? l("planmagnet.analytics.workBreakdown.forbidden", "You do not have access to the work breakdown for this scope.") : `${l("planmagnet.analytics.workBreakdown.error", "Work breakdown could not be loaded")}: ${E.message}`
|
|
210
|
+
}) : A.length === 0 ? /* @__PURE__ */ d("p", {
|
|
211
|
+
className: "text-sm text-muted-foreground py-8 text-center",
|
|
212
|
+
children: l("planmagnet.analytics.workBreakdown.empty", "No live work items found for this scope.")
|
|
213
|
+
}) : /* @__PURE__ */ f("div", {
|
|
214
|
+
className: "space-y-3",
|
|
215
|
+
children: [
|
|
216
|
+
O?.truncatedProjects && /* @__PURE__ */ d("p", {
|
|
217
|
+
className: "text-xs text-status-warning-text",
|
|
218
|
+
children: l("planmagnet.analytics.workBreakdown.truncatedProjects", "Showing the first 50 projects with live work. Narrow by project above to see a specific one.")
|
|
219
|
+
}),
|
|
220
|
+
j.length > 0 && /* @__PURE__ */ d("p", {
|
|
221
|
+
className: "text-xs text-status-warning-text",
|
|
222
|
+
children: l("planmagnet.analytics.workBreakdown.truncatedEpics", "Showing at most 200 epics for: {{projects}}.", { projects: j.join(", ") })
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ d("div", {
|
|
225
|
+
"data-testid": "viz-work-breakdown",
|
|
226
|
+
children: /* @__PURE__ */ d(n, {
|
|
227
|
+
width: "100%",
|
|
228
|
+
height: 360,
|
|
229
|
+
children: /* @__PURE__ */ d(r, {
|
|
230
|
+
data: A,
|
|
231
|
+
dataKey: "size",
|
|
232
|
+
type: "flat",
|
|
233
|
+
aspectRatio: 4 / 3,
|
|
234
|
+
content: /* @__PURE__ */ d(T, {
|
|
235
|
+
onOpenEpic: M,
|
|
236
|
+
noEpicLabel: D
|
|
237
|
+
}),
|
|
238
|
+
children: /* @__PURE__ */ d(t, { content: ({ active: e, payload: t }) => {
|
|
239
|
+
if (!e || !t || t.length === 0) return null;
|
|
240
|
+
let n = t[0]?.payload;
|
|
241
|
+
return !n || n.isHeader ? null : /* @__PURE__ */ f("div", {
|
|
242
|
+
className: "rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs shadow-md",
|
|
243
|
+
children: [
|
|
244
|
+
/* @__PURE__ */ d("p", {
|
|
245
|
+
className: "font-medium text-popover-foreground",
|
|
246
|
+
children: n.isNoEpic ? D : n.name
|
|
247
|
+
}),
|
|
248
|
+
!n.isNoEpic && /* @__PURE__ */ d("p", {
|
|
249
|
+
className: "text-muted-foreground",
|
|
250
|
+
children: n.projectName
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ f("p", {
|
|
253
|
+
className: "mt-1",
|
|
254
|
+
children: [
|
|
255
|
+
l("planmagnet.analytics.workBreakdown.tooltipOpen", "Open"),
|
|
256
|
+
":",
|
|
257
|
+
" ",
|
|
258
|
+
n.openCount,
|
|
259
|
+
" (",
|
|
260
|
+
n.openPoints,
|
|
261
|
+
" ",
|
|
262
|
+
l("planmagnet.analytics.workBreakdown.pts", "pts"),
|
|
263
|
+
") ·",
|
|
264
|
+
" ",
|
|
265
|
+
l("planmagnet.analytics.workBreakdown.tooltipDone", "Done"),
|
|
266
|
+
":",
|
|
267
|
+
" ",
|
|
268
|
+
n.doneCount,
|
|
269
|
+
" (",
|
|
270
|
+
n.donePoints,
|
|
271
|
+
" ",
|
|
272
|
+
l("planmagnet.analytics.workBreakdown.pts", "pts"),
|
|
273
|
+
")"
|
|
274
|
+
]
|
|
275
|
+
}),
|
|
276
|
+
/* @__PURE__ */ f("p", { children: [
|
|
277
|
+
l("planmagnet.analytics.workBreakdown.doneRatio", "Done ratio"),
|
|
278
|
+
":",
|
|
279
|
+
" ",
|
|
280
|
+
n.doneRatio === null ? "—" : `${Math.round(n.doneRatio * 100)}%`
|
|
281
|
+
] })
|
|
282
|
+
]
|
|
283
|
+
});
|
|
284
|
+
} })
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
}),
|
|
288
|
+
/* @__PURE__ */ d("p", {
|
|
289
|
+
className: "text-xs text-muted-foreground",
|
|
290
|
+
children: l("planmagnet.analytics.workBreakdown.legend", "Shade = share done (lighter → more open, darker → more done). Click an epic to open it.")
|
|
291
|
+
}),
|
|
292
|
+
/* @__PURE__ */ f("table", {
|
|
293
|
+
className: "w-full text-sm",
|
|
294
|
+
"data-testid": "viz-work-breakdown-table",
|
|
295
|
+
children: [
|
|
296
|
+
/* @__PURE__ */ d("caption", {
|
|
297
|
+
className: "mb-2 text-left text-xs text-muted-foreground",
|
|
298
|
+
children: l("planmagnet.analytics.workBreakdown.tableCaption", "Work breakdown by project and epic")
|
|
299
|
+
}),
|
|
300
|
+
/* @__PURE__ */ d("thead", { children: /* @__PURE__ */ f("tr", {
|
|
301
|
+
className: "text-left text-xs text-muted-foreground",
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ d("th", {
|
|
304
|
+
className: "py-1 pr-2 font-normal",
|
|
305
|
+
children: l("planmagnet.analytics.workBreakdown.project", "Project")
|
|
306
|
+
}),
|
|
307
|
+
/* @__PURE__ */ d("th", {
|
|
308
|
+
className: "py-1 pr-2 font-normal",
|
|
309
|
+
children: l("planmagnet.analytics.workBreakdown.epic", "Epic")
|
|
310
|
+
}),
|
|
311
|
+
/* @__PURE__ */ d("th", {
|
|
312
|
+
className: "py-1 text-right font-normal",
|
|
313
|
+
children: l("planmagnet.analytics.workBreakdown.tooltipOpen", "Open")
|
|
314
|
+
}),
|
|
315
|
+
/* @__PURE__ */ d("th", {
|
|
316
|
+
className: "py-1 text-right font-normal",
|
|
317
|
+
children: l("planmagnet.analytics.workBreakdown.tooltipDone", "Done")
|
|
318
|
+
}),
|
|
319
|
+
/* @__PURE__ */ d("th", {
|
|
320
|
+
className: "py-1 text-right font-normal",
|
|
321
|
+
children: l("planmagnet.analytics.workBreakdown.doneRatio", "Done ratio")
|
|
322
|
+
})
|
|
323
|
+
]
|
|
324
|
+
}) }),
|
|
325
|
+
/* @__PURE__ */ d("tbody", { children: A.flatMap((e) => e.children.filter((e) => !e.isHeader).map((t) => {
|
|
326
|
+
let n = t.epicId, r = b === "items" ? t.openCount : t.openPoints, i = b === "items" ? t.doneCount : t.donePoints;
|
|
327
|
+
return /* @__PURE__ */ f("tr", {
|
|
328
|
+
className: "border-t",
|
|
329
|
+
children: [
|
|
330
|
+
/* @__PURE__ */ d("td", {
|
|
331
|
+
className: "py-1.5 pr-2",
|
|
332
|
+
children: e.name
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ d("td", {
|
|
335
|
+
className: "py-1.5 pr-2",
|
|
336
|
+
children: n ? /* @__PURE__ */ d("button", {
|
|
337
|
+
type: "button",
|
|
338
|
+
className: "text-left text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary",
|
|
339
|
+
onClick: () => M(n),
|
|
340
|
+
children: t.name
|
|
341
|
+
}) : D
|
|
342
|
+
}),
|
|
343
|
+
/* @__PURE__ */ d("td", {
|
|
344
|
+
className: "py-1.5 text-right tabular-nums",
|
|
345
|
+
children: r
|
|
346
|
+
}),
|
|
347
|
+
/* @__PURE__ */ d("td", {
|
|
348
|
+
className: "py-1.5 text-right tabular-nums",
|
|
349
|
+
children: i
|
|
350
|
+
}),
|
|
351
|
+
/* @__PURE__ */ d("td", {
|
|
352
|
+
className: "py-1.5 text-right tabular-nums",
|
|
353
|
+
children: t.doneRatio === null ? "—" : `${Math.round(t.doneRatio * 100)}%`
|
|
354
|
+
})
|
|
355
|
+
]
|
|
356
|
+
}, `${e.projectId}-${n ?? "no-epic"}`);
|
|
357
|
+
})) })
|
|
358
|
+
]
|
|
359
|
+
})
|
|
360
|
+
]
|
|
361
|
+
}) })]
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
export { E as WorkBreakdownCard };
|
|
366
|
+
|
|
367
|
+
//# sourceMappingURL=WorkBreakdownCard.js.map
|