@burdenoff/microfe-planmagnet 2026.924.2 → 2026.924.4
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/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +93 -142
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +150 -149
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
- 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/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +113 -116
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.js +59 -58
- package/dist/planmagnet/pages/GoalsGoalsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +150 -149
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js +108 -107
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +93 -92
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +89 -87
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +94 -93
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js +57 -56
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +82 -81
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +72 -71
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js +38 -37
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +72 -71
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js +47 -46
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +120 -119
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +131 -130
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +77 -75
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +71 -70
- package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +58 -57
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.js +81 -72
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.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/dist/planmagnet/people/People.d.ts +20 -0
- package/dist/planmagnet/people/People.d.ts.map +1 -0
- package/dist/planmagnet/people/People.js +42 -0
- package/dist/planmagnet/people/People.js.map +1 -0
- package/package.json +1 -1
|
@@ -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": 100,
|
|
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-valuemax={100}\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":";;;;;;;AA+BA,IAAM,IAAiB,CAAG;;;;;;;;;;;;;;GAgBpB,IAAyB,CAAG;;;;;;;;;;;;;;;;;;AA2ClC,SAAgB,EAAe,GAA0C;AACvE,KAAI,KAAU,QAA+B,CAAC,OAAO,SAAS,EAAM,CAAE,QAAO;AAC7E,KAAI,IAAQ,GAAI,QAAO,GAAG,IAAQ,KAAK,EAAM,QAAQ,EAAE,GAAG,KAAK,MAAM,EAAM,CAAC;CAC5E,IAAM,IAAO,IAAQ;AACrB,QAAO,GAAG,IAAO,KAAK,EAAK,QAAQ,EAAE,GAAG,KAAK,MAAM,EAAK,CAAC;;AAG3D,SAAS,EAAS,EAAE,UAAO,UAAO,WAAyD;AACzF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,KAAD;IAAG,WAAU;cAAiC;IAAU,CAAA;GACxD,kBAAC,KAAD;IAAG,WAAU;cAA+B;IAAU,CAAA;GACrD,IAAO,kBAAC,KAAD;IAAG,WAAU;cAAwC;IAAS,CAAA,GAAG;GACrE;;;AAIV,SAAgB,EAAc,EAC5B,gBACA,gBAIC;CACD,IAAM,EAAE,SAAM,GAAS,EACjB,EAAE,SAAM,YAAS,aAAU,EAAwB,GAAgB;EACvE,WAAW;GAAE;GAAa;GAAW;EACrC,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EACI,IAAQ,GAAM,qBACd,IAAU,CAAC,GAAI,GAAO,SAAS,EAAE,CAAE,CACtC,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,eAAe,CACnD,MAAM,GAAG,EAAE;AAEd,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAW,WAAU;YAArB,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAC5B,EAAE,wCAAwC,aAAa,CAC9C;KACZ,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,2CACA,oFACD;EACC,CAAA,CACO,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UACI,IAOE,IACF,kBAAC,KAAD;EAAG,WAAU;YACV,EAAE,gCAAgC,WAAW;EAC5C,CAAA,GACF,IACF,kBAAC,KAAD;EAAG,WAAU;YAAb;GACG,EAAE,wCAAwC,iCAAiC;GAAC;GAAE;GAC9E,EAAM;GACL;MACF,CAAC,KAAS,EAAM,eAAe,IACjC,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,wCACA,0FACD;EACC,CAAA,GAEJ,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,OAAO,EAAE,yCAAyC,SAAS;KAC3D,OAAO,EAAe,EAAM,qBAAqB;KACjD,CAAA;IACF,kBAAC,GAAD;KACE,OAAO,EAAE,0CAA0C,UAAU;KAC7D,OAAO,EAAe,EAAM,sBAAsB;KAClD,CAAA;IACF,kBAAC,GAAD;KACE,OAAO,EAAE,yCAAyC,iBAAiB;KACnE,OAAO,OAAO,EAAM,WAAW;KAC/B,CAAA;IACE;MACN,kBAAC,SAAD;GAAO,WAAU;aAAjB;IACE,kBAAC,WAAD;KAAS,WAAU;eAChB,EAAE,0CAA0C,0BAA0B;KAC/D,CAAA;IACV,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,uCAAuC,OAAO;MAC9C,CAAA,EACL,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,2CAA2C,aAAa;MACxD,CAAA,CACF;QACC,CAAA;IACR,kBAAC,SAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,MAAD;KAA0B,WAAU;eAApC,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAK;QAAW,CAAA;OAAC;OAChD,kBAAC,QAAD;QAAM,WAAU;kBAAyB,EAAK;QAAa,CAAA;OACxD;SACL,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAe,EAAK,eAAe;MACjC,CAAA,CACF;OARI,EAAK,WAQT,CACL,EACI,CAAA;IACF;KACJ;MAlEN,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,4CACA,0CACD;EACC,CAAA,EA+DM,CAAA,CACT,EAAA,CAAA;;AAOX,SAAgB,EAAY,GAAiB,GAA8B;AAEzE,QADI,CAAC,OAAO,SAAS,EAAQ,IAAI,KAAW,IAAU,OAC/C,IAAQ;;AAGjB,SAAgB,EAAY,GAAmC;AAK7D,QAFI,MAAU,OAAa,YACvB,KAAS,MAAa,aACnB,KAAS,IAAI,eAAe;;AAGrC,IAAM,IAGF;CACF,WAAW;EACT,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACD,YAAY;EACV,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACD,cAAc;EACZ,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACD,eAAe;EACb,OAAO;EACP,MAAM;EACN,MAAM;EACN,MAAM;EACP;CACF;AAED,SAAS,EAAM,GAAgB,GAA0B;AACvD,KAAI;AACF,SAAO,IAAI,KAAK,aAAa,KAAA,GAAW;GACtC,OAAO;GACP;GACA,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAO;SACX;AAEN,SAAO,GAAG,KAAK,MAAM,EAAO,CAAC,gBAAgB,CAAC,GAAG;;;AAIrD,SAAgB,EAAsB,EAAE,kBAAgD;CACtF,IAAM,EAAE,MAAM,GAAS,EACjB,EAAE,SAAM,YAAS,aAAU,EAAqB,GAAwB;EAC5E,WAAW,EAAE,gBAAa;EAC1B,MAAM,CAAC;EACR,CAAC,EACI,IAAU,GAAM,mBAAmB,SAAS,EAAE,EAC9C,IAAqC;EACzC,WAAW,EAAE,sCAAsC,cAAc;EACjE,YAAY,EAAE,uCAAuC,WAAW;EAChE,cAAc,EAAE,yCAAyC,aAAa;EACtE,eAAe,EAAE,oCAAoC,cAAc;EACpE;AAED,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAW,WAAU;YAArB,CACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAC7B,EAAE,qCAAqC,qBAAqB,CACnD;KACZ,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,wCACA,wDACD;EACC,CAAA,CACO,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UACG,IACC,kBAAC,KAAD;EAAG,WAAU;YACV,EAAE,gCAAgC,WAAW;EAC5C,CAAA,GACF,IACF,kBAAC,KAAD;EAAG,WAAU;YAAb;GACG,EAAE,qCAAqC,8BAA8B;GAAC;GAAG,EAAM;GAC9E;MACF,EAAQ,WAAW,IACrB,kBAAC,KAAD;EAAG,WAAU;YACV,EAAE,qCAAqC,oCAAoC;EAC1E,CAAA,GAEJ,kBAAC,SAAD;EAAO,WAAU;YAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;GAAI,WAAU;aAAd;IACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,oCAAoC,SAAS;KAC7C,CAAA;IACL,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,qCAAqC,QAAQ;KAC7C,CAAA;IACL,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,uCAAuC,UAAU;KACjD,CAAA;IACL,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,qCAAqC,OAAO;KAC5C,CAAA;IACF;MACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAQ,EAAY,EAAO,eAAe,EAAO,YAAY,EAC7D,IAAQ,EAAY,EAAM,EAC1B,IAAQ,EAAY,IACpB,IAAU,MAAU,OAAO,OAAO,KAAK,MAAM,IAAQ,IAAI;AAC/D,UACE,kBAAC,MAAD;IAAoB,WAAU;cAA9B;KACE,kBAAC,MAAD;MAAI,WAAU;gBAAd,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAe,EAAO;OAAY,CAAA,EACjD,EAAO,UACN,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAO,QAAQ;OACX,CAAA,GACL,KACD;;KACL,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAM,EAAO,aAAa,EAAO,SAAS;MACxC,CAAA;KACL,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAM,EAAO,eAAe,EAAO,SAAS;MAC1C,CAAA;KACL,kBAAC,MAAD;MAAI,WAAU;gBAAd,CACE,kBAAC,OAAD;OACE,MAAK;OACL,iBAAe;OACf,iBAAe;OACf,iBAAe,KAAW;OAC1B,cAAY,GAAG,EAAO,KAAK,IAAI,MAAY,OAAO,YAAY,GAAG,EAAQ,GAAG,GAAG,EAAM;OACrF,OAAO,GAAG,EAAM,EAAO,aAAa,EAAO,SAAS,CAAC,MAAM,EAAM,EAAO,eAAe,EAAO,SAAS;OACvG,WAAW,2CAA2C,EAAM;iBAE5D,kBAAC,OAAD;QACE,WAAW,uBAAuB,EAAM;QACxC,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,KAAW,GAAG,IAAI,CAAC,IAAI;QACnD,CAAA;OACE,CAAA,EACN,kBAAC,QAAD;OAAM,WAAW,wCAAwC,EAAM;iBAA/D,CACE,kBAAC,EAAM,MAAP;QAAY,WAAU;QAAU,eAAY;QAAS,CAAA,EACpD,MAAY,OAAO,EAAM,KAAS,GAAG,EAAQ,MAAM,EAAM,KACrD;SACJ;;KACF;MAnCI,EAAO,GAmCX;IAEP,EACI,CAAA,CACF;KAEE,CAAA,CACT,EAAA,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export declare function PeopleProvider({ workspaceId, children, }: {
|
|
4
|
+
workspaceId?: string;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export interface PersonNameProps {
|
|
8
|
+
/** A user id. Empty or missing renders an em dash. */
|
|
9
|
+
id: string | null | undefined;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Inline text on purpose. fe-libs' `ActorIdentity` is a block (avatar plus two
|
|
14
|
+
* stacked paragraphs); every place this replaces is an inline label —
|
|
15
|
+
* "Owner: …", a table cell, a <span> inside a <p> — where a block would break
|
|
16
|
+
* the layout and nest a <div> inside a <p>. The email, when known, is the
|
|
17
|
+
* hover title, so two people with the same name stay distinguishable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function PersonName({ id, className }: PersonNameProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=People.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"People.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/people/People.tsx"],"names":[],"mappings":"AAMA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AA8Bf,wBAAgB,cAAc,CAAC,EAC7B,WAAW,EACX,QAAQ,GACT,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAkBA;AAED,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,eAAe,2CAgB5D"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createContext as e, useCallback as t, useContext as n, useEffect as r, useMemo as i, useState as a } from "react";
|
|
2
|
+
import { jsx as o } from "react/jsx-runtime";
|
|
3
|
+
import { buildActorFallback as s, useActorProfiles as c } from "@burdenoff/fe-libs/shared";
|
|
4
|
+
//#region src/planmagnet/people/People.tsx
|
|
5
|
+
var l = e(null);
|
|
6
|
+
function u({ workspaceId: e, children: n }) {
|
|
7
|
+
let [r, s] = a([]), u = t((e) => {
|
|
8
|
+
s((t) => t.includes(e) ? t : [...t, e]);
|
|
9
|
+
}, []), d = c(i(() => r.map((e) => ({
|
|
10
|
+
actorId: e,
|
|
11
|
+
actorType: "user"
|
|
12
|
+
})), [r]), { workspaceId: e }), f = i(() => ({
|
|
13
|
+
request: u,
|
|
14
|
+
profiles: d
|
|
15
|
+
}), [u, d]);
|
|
16
|
+
return /* @__PURE__ */ o(l.Provider, {
|
|
17
|
+
value: f,
|
|
18
|
+
children: n
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function d({ id: e, className: t }) {
|
|
22
|
+
let i = n(l), a = i?.request;
|
|
23
|
+
if (r(() => {
|
|
24
|
+
e && a && a(e);
|
|
25
|
+
}, [e, a]), !e) return /* @__PURE__ */ o("span", {
|
|
26
|
+
className: t ?? "text-muted-foreground",
|
|
27
|
+
children: "—"
|
|
28
|
+
});
|
|
29
|
+
let c = i?.profiles[e] ?? s({
|
|
30
|
+
actorId: e,
|
|
31
|
+
actorType: "user"
|
|
32
|
+
});
|
|
33
|
+
return /* @__PURE__ */ o("span", {
|
|
34
|
+
className: t,
|
|
35
|
+
title: c.email ?? void 0,
|
|
36
|
+
children: c.displayName
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { u as PeopleProvider, d as PersonName };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=People.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"People.js","names":[],"sources":["../../../src/planmagnet/people/People.tsx"],"sourcesContent":["import {\n buildActorFallback,\n useActorProfiles,\n type ActorInput,\n type ActorProfileMap,\n} from '@burdenoff/fe-libs/shared';\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\n/**\n * Show people by name, not by UUID.\n *\n * Until 2026-09-24, 21 places in this MFE rendered a raw user id where a\n * person belonged — project owners, roadmap owners, assignees, the team lead.\n * fe-libs already resolves ids to workspace-member identities\n * (`useActorProfiles`, with a 5-minute cache), but it batches per CALL and\n * does not dedupe across calls, so calling it from every row would fire a\n * burst of lookups on the first render of each list.\n *\n * So there is exactly one call, here. Every `<PersonName id>` on screen\n * registers its id with the provider; ids registered during the same render\n * land in one state update, and the provider resolves them together. A\n * growing list costs one request for the NEW ids only — already-resolved ids\n * come from the fe-libs cache.\n *\n * An id that cannot be resolved (not a member of this workspace, lookup\n * failed) renders fe-libs' short fallback, \"User 1a2b3c\", rather than the full\n * UUID: still identifiable, never a wall of hex.\n */\n\ninterface PeopleContextValue {\n request: (id: string) => void;\n profiles: ActorProfileMap;\n}\n\nconst PeopleContext = createContext<PeopleContextValue | null>(null);\n\nexport function PeopleProvider({\n workspaceId,\n children,\n}: {\n workspaceId?: string;\n children: ReactNode;\n}) {\n const [ids, setIds] = useState<readonly string[]>([]);\n\n const request = useCallback((id: string) => {\n setIds((previous) => (previous.includes(id) ? previous : [...previous, id]));\n }, []);\n\n const actors = useMemo<ActorInput[]>(\n () => ids.map((actorId) => ({ actorId, actorType: 'user' })),\n [ids]\n );\n\n // No apiGatewayUrl on purpose: this MFE is handed the WORKSPACE gateway, and\n // the hook resolves the right gateways from the hostname itself.\n const profiles = useActorProfiles(actors, { workspaceId });\n\n const value = useMemo(() => ({ request, profiles }), [request, profiles]);\n return <PeopleContext.Provider value={value}>{children}</PeopleContext.Provider>;\n}\n\nexport interface PersonNameProps {\n /** A user id. Empty or missing renders an em dash. */\n id: string | null | undefined;\n className?: string;\n}\n\n/**\n * Inline text on purpose. fe-libs' `ActorIdentity` is a block (avatar plus two\n * stacked paragraphs); every place this replaces is an inline label —\n * \"Owner: …\", a table cell, a <span> inside a <p> — where a block would break\n * the layout and nest a <div> inside a <p>. The email, when known, is the\n * hover title, so two people with the same name stay distinguishable.\n */\nexport function PersonName({ id, className }: PersonNameProps) {\n const context = useContext(PeopleContext);\n const request = context?.request;\n\n useEffect(() => {\n if (id && request) request(id);\n }, [id, request]);\n\n if (!id) return <span className={className ?? 'text-muted-foreground'}>—</span>;\n\n const profile = context?.profiles[id] ?? buildActorFallback({ actorId: id, actorType: 'user' });\n return (\n <span className={className} title={profile.email ?? undefined}>\n {profile.displayName}\n </span>\n );\n}\n"],"mappings":";;;;AA0CA,IAAM,IAAgB,EAAyC,KAAK;AAEpE,SAAgB,EAAe,EAC7B,gBACA,eAIC;CACD,IAAM,CAAC,GAAK,KAAU,EAA4B,EAAE,CAAC,EAE/C,IAAU,GAAa,MAAe;AAC1C,KAAQ,MAAc,EAAS,SAAS,EAAG,GAAG,IAAW,CAAC,GAAG,GAAU,EAAG,CAAE;IAC3E,EAAE,CAAC,EASA,IAAW,EAPF,QACP,EAAI,KAAK,OAAa;EAAE;EAAS,WAAW;EAAQ,EAAE,EAC5D,CAAC,EAAI,CACN,EAIyC,EAAE,gBAAa,CAAC,EAEpD,IAAQ,SAAe;EAAE;EAAS;EAAU,GAAG,CAAC,GAAS,EAAS,CAAC;AACzE,QAAO,kBAAC,EAAc,UAAf;EAA+B;EAAQ;EAAkC,CAAA;;AAgBlF,SAAgB,EAAW,EAAE,OAAI,gBAA8B;CAC7D,IAAM,IAAU,EAAW,EAAc,EACnC,IAAU,GAAS;AAMzB,KAJA,QAAgB;AACd,EAAI,KAAM,KAAS,EAAQ,EAAG;IAC7B,CAAC,GAAI,EAAQ,CAAC,EAEb,CAAC,EAAI,QAAO,kBAAC,QAAD;EAAM,WAAW,KAAa;YAAyB;EAAQ,CAAA;CAE/E,IAAM,IAAU,GAAS,SAAS,MAAO,EAAmB;EAAE,SAAS;EAAI,WAAW;EAAQ,CAAC;AAC/F,QACE,kBAAC,QAAD;EAAiB;EAAW,OAAO,EAAQ,SAAS,KAAA;YACjD,EAAQ;EACJ,CAAA"}
|