@burdenoff/microfe-planmagnet 2026.927.4 → 2026.927.6
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/generated/global-types.d.ts +9 -1
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-operations.d.ts +0 -12
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +14 -10
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +149 -127
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
- package/dist/planmagnet/board/ProjectBoard.d.ts +2 -0
- package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
- package/dist/planmagnet/board/ProjectBoard.js +492 -225
- package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +142 -139
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/FeatureTriage.d.ts +21 -0
- package/dist/planmagnet/controls/FeatureTriage.d.ts.map +1 -0
- package/dist/planmagnet/controls/FeatureTriage.js +398 -0
- package/dist/planmagnet/controls/FeatureTriage.js.map +1 -0
- package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/MilestoneControls.js +76 -73
- package/dist/planmagnet/controls/MilestoneControls.js.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/ProjectControls.js +47 -44
- package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
- package/dist/planmagnet/controls/ReleaseControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/ReleaseControls.js +47 -44
- package/dist/planmagnet/controls/ReleaseControls.js.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/WorkItemControls.js +68 -65
- package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
- package/dist/planmagnet/hooks/useCounts.d.ts +39 -0
- package/dist/planmagnet/hooks/useCounts.d.ts.map +1 -0
- package/dist/planmagnet/hooks/useCounts.js +209 -0
- package/dist/planmagnet/hooks/useCounts.js.map +1 -0
- package/dist/planmagnet/import/exportColumns.d.ts +52 -0
- package/dist/planmagnet/import/exportColumns.d.ts.map +1 -0
- package/dist/planmagnet/import/exportColumns.js +81 -0
- package/dist/planmagnet/import/exportColumns.js.map +1 -0
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +255 -242
- 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 +199 -160
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +147 -144
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.js +2 -7
- 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 +173 -173
- 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 +120 -117
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +97 -117
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +168 -103
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +117 -103
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +4 -8
- 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 +85 -90
- 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 +12 -2
- 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 +2 -2
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +48 -71
- package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +17 -13
- 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 +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +6 -6
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +11 -8
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +4 -4
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +152 -139
- 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 +75 -74
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts +7 -0
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +39 -201
- package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +118 -99
- package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksImportPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/TasksImportPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/TasksImportPagePage.js +309 -0
- package/dist/planmagnet/pages/TasksImportPagePage.js.map +1 -0
- package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTasksPagePage.js +110 -104
- package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +99 -177
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -4
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -1
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +10 -4
- package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +151 -138
- package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
- package/dist/planmagnet/sprints/SprintControls.js +32 -30
- package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
- package/dist/planmagnet/utils/mutationResult.d.ts +10 -0
- package/dist/planmagnet/utils/mutationResult.d.ts.map +1 -1
- package/dist/planmagnet/utils/mutationResult.js +2 -1
- package/dist/planmagnet/utils/mutationResult.js.map +1 -1
- package/dist/planmagnet/utils/rowNavigation.d.ts +2 -0
- package/dist/planmagnet/utils/rowNavigation.d.ts.map +1 -1
- package/dist/planmagnet/utils/rowNavigation.js +9 -8
- package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,213 +1,135 @@
|
|
|
1
1
|
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { PersonAvatar as n, PersonName as r } from "../people/People.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { countLabel as i } from "../hooks/useCounts.js";
|
|
4
|
+
import { UserPlus as a } from "lucide-react";
|
|
5
|
+
import { gql as o } from "@apollo/client";
|
|
6
|
+
import { useQuery as s } from "@apollo/client/react";
|
|
7
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
8
|
+
import { Button as u, Card as d, CardContent as f } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
import { useWorkspaceDirectory as p } from "@burdenoff/fe-libs/sharing";
|
|
8
10
|
//#region src/planmagnet/pages/TeamTeamPagePage.tsx
|
|
9
|
-
var
|
|
10
|
-
query
|
|
11
|
-
$workspaceId:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
) {
|
|
15
|
-
planmagnetProjects(workspaceId: $workspaceId, pagination: $projectPagination) {
|
|
16
|
-
items {
|
|
17
|
-
id
|
|
18
|
-
name
|
|
19
|
-
key
|
|
11
|
+
var m = o`
|
|
12
|
+
query TeamMemberCounts($workspaceId: ID!, $userId: String!) {
|
|
13
|
+
planmagnetWorkItemCounts(workspaceId: $workspaceId, filter: { assigneeId: $userId }) {
|
|
14
|
+
total
|
|
15
|
+
byStatus {
|
|
20
16
|
status
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
count
|
|
18
|
+
points
|
|
23
19
|
}
|
|
24
|
-
totalCount
|
|
25
20
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
storyPoints
|
|
32
|
-
}
|
|
21
|
+
planmagnetProjects(
|
|
22
|
+
workspaceId: $workspaceId
|
|
23
|
+
filter: { ownerId: $userId }
|
|
24
|
+
pagination: { first: 1 }
|
|
25
|
+
) {
|
|
33
26
|
totalCount
|
|
34
27
|
}
|
|
35
28
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
busy: "bg-red-400",
|
|
40
|
-
offline: "bg-gray-300"
|
|
41
|
-
};
|
|
42
|
-
function _() {
|
|
43
|
-
let { workspaceId: a } = e(), _ = t(), { data: v, loading: y, error: b } = o(h, {
|
|
29
|
+
`;
|
|
30
|
+
function h({ userId: t }) {
|
|
31
|
+
let { workspaceId: a } = e(), { data: o, error: u } = s(m, {
|
|
44
32
|
variables: {
|
|
45
33
|
workspaceId: a,
|
|
46
|
-
|
|
47
|
-
workItemPagination: { first: 500 }
|
|
34
|
+
userId: t
|
|
48
35
|
},
|
|
49
36
|
skip: !a
|
|
37
|
+
}), p = u ? void 0 : o?.planmagnetWorkItemCounts, h = p?.byStatus.find((e) => e.status === "DONE");
|
|
38
|
+
return /* @__PURE__ */ c(d, {
|
|
39
|
+
"data-testid": "team-member",
|
|
40
|
+
children: /* @__PURE__ */ l(f, {
|
|
41
|
+
className: "pt-4 space-y-3",
|
|
42
|
+
children: [/* @__PURE__ */ l("div", {
|
|
43
|
+
className: "flex items-center gap-3",
|
|
44
|
+
children: [/* @__PURE__ */ c(n, {
|
|
45
|
+
id: t,
|
|
46
|
+
className: "h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium text-primary"
|
|
47
|
+
}), /* @__PURE__ */ c("p", {
|
|
48
|
+
className: "text-sm font-medium",
|
|
49
|
+
children: /* @__PURE__ */ c(r, { id: t })
|
|
50
|
+
})]
|
|
51
|
+
}), /* @__PURE__ */ l("div", {
|
|
52
|
+
className: "grid grid-cols-4 gap-2 text-center text-xs",
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
|
|
55
|
+
className: "font-semibold",
|
|
56
|
+
children: i(p?.total)
|
|
57
|
+
}), /* @__PURE__ */ c("p", {
|
|
58
|
+
className: "text-muted-foreground",
|
|
59
|
+
children: "Assigned"
|
|
60
|
+
})] }),
|
|
61
|
+
/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
|
|
62
|
+
className: "font-semibold",
|
|
63
|
+
children: i(p ? h?.count ?? 0 : null)
|
|
64
|
+
}), /* @__PURE__ */ c("p", {
|
|
65
|
+
className: "text-muted-foreground",
|
|
66
|
+
children: "Done"
|
|
67
|
+
})] }),
|
|
68
|
+
/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
|
|
69
|
+
className: "font-semibold",
|
|
70
|
+
children: i(p ? h?.points ?? 0 : null)
|
|
71
|
+
}), /* @__PURE__ */ c("p", {
|
|
72
|
+
className: "text-muted-foreground",
|
|
73
|
+
children: "Points done"
|
|
74
|
+
})] }),
|
|
75
|
+
/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
|
|
76
|
+
className: "font-semibold",
|
|
77
|
+
children: i(u ? null : o?.planmagnetProjects.totalCount)
|
|
78
|
+
}), /* @__PURE__ */ c("p", {
|
|
79
|
+
className: "text-muted-foreground",
|
|
80
|
+
children: "Projects owned"
|
|
81
|
+
})] })
|
|
82
|
+
]
|
|
83
|
+
})]
|
|
84
|
+
})
|
|
50
85
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
children: ["Error: ", b.message]
|
|
86
|
+
}
|
|
87
|
+
function g() {
|
|
88
|
+
let { workspaceId: n, authToken: r, apiGatewayUrl: i } = e(), o = t(), { members: s, loading: d, hasMore: f, fetchMore: m } = p({
|
|
89
|
+
workspaceId: n ?? "",
|
|
90
|
+
authToken: r ?? void 0,
|
|
91
|
+
apiGatewayUrl: i ?? void 0
|
|
58
92
|
});
|
|
59
|
-
|
|
60
|
-
let t = S.filter((t) => t.assigneeId === e), n = t.length, r = t.filter((e) => e.status === "DONE").length, i = t.filter((e) => e.status === "DONE").reduce((e, t) => e + (t.storyPoints ?? 0), 0);
|
|
61
|
-
return {
|
|
62
|
-
id: e,
|
|
63
|
-
role: x.filter((t) => t.ownerId === e).length > 0 ? "Project Owner" : "Contributor",
|
|
64
|
-
department: "Engineering",
|
|
65
|
-
tasksAssigned: n,
|
|
66
|
-
tasksCompleted: r,
|
|
67
|
-
velocity: i,
|
|
68
|
-
status: n > 0 ? "active" : "offline"
|
|
69
|
-
};
|
|
70
|
-
}), D = x.reduce((e, t) => {
|
|
71
|
-
let n = t.methodology ?? "unassigned";
|
|
72
|
-
return e[n] || (e[n] = {
|
|
73
|
-
projects: [],
|
|
74
|
-
ownerIds: /* @__PURE__ */ new Set()
|
|
75
|
-
}), e[n].projects.push(t), t.ownerId && e[n].ownerIds.add(t.ownerId), e;
|
|
76
|
-
}, {}), O = Object.entries(D).map(([e, t]) => ({
|
|
77
|
-
id: e,
|
|
78
|
-
name: `${e.charAt(0).toUpperCase()}${e.slice(1)} Team`,
|
|
79
|
-
description: `Projects using ${e} methodology`,
|
|
80
|
-
projectsCount: t.projects.length,
|
|
81
|
-
leadId: Array.from(t.ownerIds)[0] ?? null
|
|
82
|
-
}));
|
|
83
|
-
return /* @__PURE__ */ c("div", {
|
|
93
|
+
return /* @__PURE__ */ l("div", {
|
|
84
94
|
className: "space-y-6 p-6",
|
|
85
95
|
children: [
|
|
86
|
-
/* @__PURE__ */
|
|
96
|
+
/* @__PURE__ */ l("div", {
|
|
87
97
|
className: "flex items-start justify-between gap-4",
|
|
88
|
-
children: [/* @__PURE__ */
|
|
98
|
+
children: [/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("h1", {
|
|
89
99
|
className: "text-2xl font-semibold",
|
|
90
100
|
children: "Team Overview"
|
|
91
101
|
}), /* @__PURE__ */ c("p", {
|
|
92
102
|
className: "text-sm text-muted-foreground",
|
|
93
|
-
children:
|
|
94
|
-
|
|
95
|
-
" contributors across ",
|
|
96
|
-
O.length,
|
|
97
|
-
" teams"
|
|
98
|
-
]
|
|
99
|
-
})] }), /* @__PURE__ */ c(u, {
|
|
103
|
+
children: d && s.length === 0 ? "Loading members…" : `${s.length}${f ? "+" : ""} workspace member${s.length === 1 ? "" : "s"}`
|
|
104
|
+
})] }), /* @__PURE__ */ l(u, {
|
|
100
105
|
size: "sm",
|
|
101
106
|
variant: "outline",
|
|
102
|
-
onClick: () =>
|
|
107
|
+
onClick: () => o("/team/invite-member-page"),
|
|
103
108
|
"data-testid": "team-invite",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
109
|
+
children: [/* @__PURE__ */ c(a, { className: "mr-1 h-4 w-4" }), "Invite member"]
|
|
105
110
|
})]
|
|
106
111
|
}),
|
|
107
112
|
/* @__PURE__ */ c("div", {
|
|
108
|
-
className: "
|
|
109
|
-
children:
|
|
110
|
-
className: "text-lg font-medium",
|
|
111
|
-
children: "Teams"
|
|
112
|
-
}), /* @__PURE__ */ c("div", {
|
|
113
|
-
className: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
|
114
|
-
children: [O.map((e) => /* @__PURE__ */ c(d, { children: [/* @__PURE__ */ s(p, {
|
|
115
|
-
className: "pb-2",
|
|
116
|
-
children: /* @__PURE__ */ c("div", {
|
|
117
|
-
className: "flex items-center justify-between",
|
|
118
|
-
children: [/* @__PURE__ */ s(m, {
|
|
119
|
-
className: "text-base",
|
|
120
|
-
children: e.name
|
|
121
|
-
}), /* @__PURE__ */ c(l, {
|
|
122
|
-
variant: "outline",
|
|
123
|
-
children: [e.projectsCount, " projects"]
|
|
124
|
-
})]
|
|
125
|
-
})
|
|
126
|
-
}), /* @__PURE__ */ c(f, { children: [/* @__PURE__ */ s("p", {
|
|
127
|
-
className: "text-sm text-muted-foreground",
|
|
128
|
-
children: e.description
|
|
129
|
-
}), /* @__PURE__ */ c("p", {
|
|
130
|
-
className: "text-xs text-muted-foreground mt-2",
|
|
131
|
-
children: ["Lead: ", e.leadId ? /* @__PURE__ */ s(r, { id: e.leadId }) : "No lead"]
|
|
132
|
-
})] })] }, e.id)), O.length === 0 && /* @__PURE__ */ s("p", {
|
|
133
|
-
className: "text-sm text-muted-foreground col-span-2 text-center py-4",
|
|
134
|
-
children: "No teams found. Create projects to see team groupings."
|
|
135
|
-
})]
|
|
136
|
-
})]
|
|
113
|
+
className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
|
|
114
|
+
children: s.map((e) => /* @__PURE__ */ c(h, { userId: e.id }, e.id))
|
|
137
115
|
}),
|
|
138
|
-
/* @__PURE__ */ c("
|
|
139
|
-
className: "
|
|
140
|
-
children:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
children: [/* @__PURE__ */ c("div", {
|
|
151
|
-
className: "relative",
|
|
152
|
-
children: [/* @__PURE__ */ s(n, {
|
|
153
|
-
id: e.id,
|
|
154
|
-
className: "h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium text-primary"
|
|
155
|
-
}), /* @__PURE__ */ s("div", { className: `absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-background ${g[e.status]}` })]
|
|
156
|
-
}), /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
|
|
157
|
-
className: "text-sm font-medium",
|
|
158
|
-
children: /* @__PURE__ */ s(r, { id: e.id })
|
|
159
|
-
}), /* @__PURE__ */ s("p", {
|
|
160
|
-
className: "text-xs text-muted-foreground",
|
|
161
|
-
children: e.role
|
|
162
|
-
})] })]
|
|
163
|
-
}),
|
|
164
|
-
/* @__PURE__ */ c("div", {
|
|
165
|
-
className: "grid grid-cols-3 gap-2 text-center text-xs",
|
|
166
|
-
children: [
|
|
167
|
-
/* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
|
|
168
|
-
className: "font-semibold",
|
|
169
|
-
children: e.tasksAssigned
|
|
170
|
-
}), /* @__PURE__ */ s("p", {
|
|
171
|
-
className: "text-muted-foreground",
|
|
172
|
-
children: "Assigned"
|
|
173
|
-
})] }),
|
|
174
|
-
/* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
|
|
175
|
-
className: "font-semibold",
|
|
176
|
-
children: e.tasksCompleted
|
|
177
|
-
}), /* @__PURE__ */ s("p", {
|
|
178
|
-
className: "text-muted-foreground",
|
|
179
|
-
children: "Done"
|
|
180
|
-
})] }),
|
|
181
|
-
/* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("p", {
|
|
182
|
-
className: "font-semibold",
|
|
183
|
-
children: e.velocity
|
|
184
|
-
}), /* @__PURE__ */ s("p", {
|
|
185
|
-
className: "text-muted-foreground",
|
|
186
|
-
children: "Points done"
|
|
187
|
-
})] })
|
|
188
|
-
]
|
|
189
|
-
}),
|
|
190
|
-
/* @__PURE__ */ c("div", {
|
|
191
|
-
className: "flex justify-between text-xs",
|
|
192
|
-
children: [/* @__PURE__ */ s("span", {
|
|
193
|
-
className: "text-muted-foreground",
|
|
194
|
-
children: e.department
|
|
195
|
-
}), /* @__PURE__ */ s("span", {
|
|
196
|
-
className: "capitalize text-muted-foreground",
|
|
197
|
-
children: e.status
|
|
198
|
-
})]
|
|
199
|
-
})
|
|
200
|
-
]
|
|
201
|
-
}) }, e.id)), E.length === 0 && /* @__PURE__ */ s("p", {
|
|
202
|
-
className: "text-sm text-muted-foreground col-span-3 text-center py-4",
|
|
203
|
-
children: "No team members found. Assign work items to see members here."
|
|
204
|
-
})]
|
|
205
|
-
})]
|
|
116
|
+
!d && s.length === 0 && /* @__PURE__ */ c("p", {
|
|
117
|
+
className: "text-sm text-muted-foreground text-center py-4",
|
|
118
|
+
children: "No members found. Invite someone to the workspace to work together."
|
|
119
|
+
}),
|
|
120
|
+
f && /* @__PURE__ */ c("div", {
|
|
121
|
+
className: "text-center",
|
|
122
|
+
children: /* @__PURE__ */ c(u, {
|
|
123
|
+
size: "sm",
|
|
124
|
+
variant: "outline",
|
|
125
|
+
onClick: m,
|
|
126
|
+
children: "Show more members"
|
|
127
|
+
})
|
|
206
128
|
})
|
|
207
129
|
]
|
|
208
130
|
});
|
|
209
131
|
}
|
|
210
132
|
//#endregion
|
|
211
|
-
export {
|
|
133
|
+
export { g as TeamTeamPagePage };
|
|
212
134
|
|
|
213
135
|
//# sourceMappingURL=TeamTeamPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamTeamPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TeamTeamPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge, Button } from '@burdenoff/fe-libs/ui';\nimport { UserPlus, Users } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonAvatar, PersonName } from '../people/People';\n\nconst GET_TEAM_DATA = gql`\n query GetTeamData(\n $workspaceId: ID!\n $projectPagination: PlanmagnetPaginationInput\n $workItemPagination: PlanmagnetPaginationInput\n ) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $projectPagination) {\n items {\n id\n name\n key\n status\n ownerId\n methodology\n }\n totalCount\n }\n planmagnetWorkItems(workspaceId: $workspaceId, pagination: $workItemPagination) {\n items {\n id\n status\n assigneeId\n storyPoints\n }\n totalCount\n }\n }\n`;\n\ninterface ProjectNode {\n id: string;\n name: string;\n key: string;\n status: string;\n ownerId: string | null;\n methodology: string | null;\n}\n\ninterface WorkItemNode {\n id: string;\n status: string;\n assigneeId: string | null;\n storyPoints: number | null;\n}\n\ninterface TeamData {\n projects: { items: ProjectNode[]; totalCount: number };\n workItems: { items: WorkItemNode[]; totalCount: number };\n}\n\nconst statusColor: Record<string, string> = {\n active: 'bg-green-400',\n away: 'bg-yellow-400',\n busy: 'bg-red-400',\n offline: 'bg-gray-300',\n};\n\nexport function TeamTeamPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n\n const { data, loading, error } = useQuery<TeamData>(GET_TEAM_DATA, {\n variables: {\n workspaceId,\n projectPagination: { first: 100 },\n workItemPagination: { first: 500 },\n },\n skip: !workspaceId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const workItems = data?.planmagnetWorkItems?.items ?? [];\n\n const assigneeIds = new Set(\n workItems.filter((wi) => wi.assigneeId).map((wi) => wi.assigneeId as string)\n );\n const ownerIds = new Set(projects.filter((p) => p.ownerId).map((p) => p.ownerId as string));\n const allMemberIds = new Set([...assigneeIds, ...ownerIds]);\n\n const members = Array.from(allMemberIds).map((memberId) => {\n const memberWorkItems = workItems.filter((wi) => wi.assigneeId === memberId);\n const assigned = memberWorkItems.length;\n const completed = memberWorkItems.filter((wi) => wi.status === 'DONE').length;\n const velocity = memberWorkItems\n .filter((wi) => wi.status === 'DONE')\n .reduce((s, wi) => s + (wi.storyPoints ?? 0), 0);\n const memberProjects = projects.filter((p) => p.ownerId === memberId);\n\n return {\n id: memberId,\n role: memberProjects.length > 0 ? 'Project Owner' : 'Contributor',\n department: 'Engineering',\n tasksAssigned: assigned,\n tasksCompleted: completed,\n velocity,\n status: assigned > 0 ? 'active' : 'offline',\n };\n });\n\n const teamsByMethodology = projects.reduce(\n (acc, p) => {\n const method = p.methodology ?? 'unassigned';\n if (!acc[method]) {\n acc[method] = { projects: [], ownerIds: new Set<string>() };\n }\n acc[method].projects.push(p);\n if (p.ownerId) acc[method].ownerIds.add(p.ownerId);\n return acc;\n },\n {} as Record<string, { projects: ProjectNode[]; ownerIds: Set<string> }>\n );\n\n const teams = Object.entries(teamsByMethodology).map(([methodology, group]) => ({\n id: methodology,\n name: `${methodology.charAt(0).toUpperCase()}${methodology.slice(1)} Team`,\n description: `Projects using ${methodology} methodology`,\n projectsCount: group.projects.length,\n // null, not a sentinel string: PersonName would try to resolve \"Unassigned\" as a user id (\"User Unassigned\").\n leadId: Array.from(group.ownerIds)[0] ?? null,\n }));\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Team Overview</h1>\n <p className=\"text-sm text-muted-foreground\">\n {members.length} contributors across {teams.length} teams\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate('/team/invite-member-page')}\n data-testid=\"team-invite\"\n >\n <UserPlus className=\"mr-1 h-4 w-4\" />\n Invite member\n </Button>\n </div>\n\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-medium\">Teams</h2>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n {teams.map((team) => (\n <Card key={team.id}>\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"text-base\">{team.name}</CardTitle>\n <Badge variant=\"outline\">{team.projectsCount} projects</Badge>\n </div>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm text-muted-foreground\">{team.description}</p>\n <p className=\"text-xs text-muted-foreground mt-2\">\n Lead: {team.leadId ? <PersonName id={team.leadId} /> : 'No lead'}\n </p>\n </CardContent>\n </Card>\n ))}\n {teams.length === 0 && (\n <p className=\"text-sm text-muted-foreground col-span-2 text-center py-4\">\n No teams found. Create projects to see team groupings.\n </p>\n )}\n </div>\n </div>\n\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-medium\">Members</h2>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n {members.map((member) => (\n <Card key={member.id}>\n <CardContent className=\"pt-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"relative\">\n {/* Name and initials come from the resolved identity: this card used to print the raw user\n UUID as the name, with initials cut from the UUID (BOFF-7945). */}\n <PersonAvatar\n id={member.id}\n className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium text-primary\"\n />\n <div\n className={`absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-background ${statusColor[member.status]}`}\n />\n </div>\n <div>\n <p className=\"text-sm font-medium\">\n <PersonName id={member.id} />\n </p>\n <p className=\"text-xs text-muted-foreground\">{member.role}</p>\n </div>\n </div>\n <div className=\"grid grid-cols-3 gap-2 text-center text-xs\">\n <div>\n <p className=\"font-semibold\">{member.tasksAssigned}</p>\n <p className=\"text-muted-foreground\">Assigned</p>\n </div>\n <div>\n <p className=\"font-semibold\">{member.tasksCompleted}</p>\n <p className=\"text-muted-foreground\">Done</p>\n </div>\n <div>\n <p className=\"font-semibold\">{member.velocity}</p>\n {/* All-time DONE points of this person's items: not a sprint velocity (BOFF-7944). */}\n <p className=\"text-muted-foreground\">Points done</p>\n </div>\n </div>\n <div className=\"flex justify-between text-xs\">\n <span className=\"text-muted-foreground\">{member.department}</span>\n <span className=\"capitalize text-muted-foreground\">{member.status}</span>\n </div>\n </CardContent>\n </Card>\n ))}\n {members.length === 0 && (\n <p className=\"text-sm text-muted-foreground col-span-3 text-center py-4\">\n No team members found. Assign work items to see members here.\n </p>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamTeamPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TeamTeamPagePage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Card, CardContent, Button } from '@burdenoff/fe-libs/ui';\nimport { UserPlus } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { PersonAvatar, PersonName } from '../people/People';\nimport { countLabel } from '../hooks/useCounts';\n\n/**\n * Team overview (BOFF-7972). Members are the workspace's real members (the workspace directory), and each member's\n * numbers are server counts over ALL of their work. The page used to build members from the newest 100 work items\n * and 100 projects (so anyone whose work was older undercounted or vanished), invent \"teams\" by grouping projects\n * by methodology, and make up an \"Engineering\" department and online/offline presence.\n */\n\nconst MEMBER_COUNTS = gql`\n query TeamMemberCounts($workspaceId: ID!, $userId: String!) {\n planmagnetWorkItemCounts(workspaceId: $workspaceId, filter: { assigneeId: $userId }) {\n total\n byStatus {\n status\n count\n points\n }\n }\n planmagnetProjects(\n workspaceId: $workspaceId\n filter: { ownerId: $userId }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n }\n`;\n\nfunction MemberCard({ userId }: { userId: string }) {\n const { workspaceId } = usePlanMagnet();\n const { data, error } = useQuery<{\n planmagnetWorkItemCounts: {\n total: number;\n byStatus: { status: string; count: number; points: number }[];\n };\n planmagnetProjects: { totalCount: number };\n }>(MEMBER_COUNTS, { variables: { workspaceId, userId }, skip: !workspaceId });\n const counts = error ? undefined : data?.planmagnetWorkItemCounts;\n const done = counts?.byStatus.find((s) => s.status === 'DONE');\n return (\n <Card data-testid=\"team-member\">\n <CardContent className=\"pt-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <PersonAvatar\n id={userId}\n className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium text-primary\"\n />\n <p className=\"text-sm font-medium\">\n <PersonName id={userId} />\n </p>\n </div>\n <div className=\"grid grid-cols-4 gap-2 text-center text-xs\">\n <div>\n <p className=\"font-semibold\">{countLabel(counts?.total)}</p>\n <p className=\"text-muted-foreground\">Assigned</p>\n </div>\n <div>\n <p className=\"font-semibold\">{countLabel(counts ? (done?.count ?? 0) : null)}</p>\n <p className=\"text-muted-foreground\">Done</p>\n </div>\n <div>\n <p className=\"font-semibold\">{countLabel(counts ? (done?.points ?? 0) : null)}</p>\n {/* All-time DONE points of this person's items: not a sprint velocity (BOFF-7944). */}\n <p className=\"text-muted-foreground\">Points done</p>\n </div>\n <div>\n <p className=\"font-semibold\">\n {countLabel(error ? null : data?.planmagnetProjects.totalCount)}\n </p>\n <p className=\"text-muted-foreground\">Projects owned</p>\n </div>\n </div>\n </CardContent>\n </Card>\n );\n}\n\nexport function TeamTeamPagePage() {\n const { workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { members, loading, hasMore, fetchMore } = useWorkspaceDirectory({\n workspaceId: workspaceId ?? '',\n authToken: authToken ?? undefined,\n apiGatewayUrl: apiGatewayUrl ?? undefined,\n });\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">Team Overview</h1>\n <p className=\"text-sm text-muted-foreground\">\n {loading && members.length === 0\n ? 'Loading members…'\n : `${members.length}${hasMore ? '+' : ''} workspace member${members.length === 1 ? '' : 's'}`}\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate('/team/invite-member-page')}\n data-testid=\"team-invite\"\n >\n <UserPlus className=\"mr-1 h-4 w-4\" />\n Invite member\n </Button>\n </div>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n {members.map((member) => (\n <MemberCard key={member.id} userId={member.id} />\n ))}\n </div>\n {!loading && members.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n No members found. Invite someone to the workspace to work together.\n </p>\n )}\n {hasMore && (\n <div className=\"text-center\">\n <Button size=\"sm\" variant=\"outline\" onClick={fetchMore}>\n Show more members\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeliveryHealthCards.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/pages/analytics/DeliveryHealthCards.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DeliveryHealthCards.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/pages/analytics/DeliveryHealthCards.tsx"],"names":[],"mappings":"AA2FA,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKvE;AAYD,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB,2CAkGA;AAED,KAAK,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEzE,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAM7D;AA6CD,wBAAgB,qBAAqB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,2CA8HrF"}
|
|
@@ -34,6 +34,7 @@ var h = o`
|
|
|
34
34
|
name
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
totalCount
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
`;
|
|
@@ -199,7 +200,7 @@ function w({ workspaceId: e }) {
|
|
|
199
200
|
let { t } = u(), { data: n, loading: r, error: i } = s(g, {
|
|
200
201
|
variables: { workspaceId: e },
|
|
201
202
|
skip: !e
|
|
202
|
-
}), o = n?.planmagnetBudgets?.items ?? [], h = {
|
|
203
|
+
}), o = [...n?.planmagnetBudgets?.items ?? []].sort((e, t) => (b(t.plannedAmount, t.spentAmount) ?? -1) - (b(e.plannedAmount, e.spentAmount) ?? -1)), h = n?.planmagnetBudgets?.totalCount ?? o.length, _ = {
|
|
203
204
|
"no-plan": t("planmagnet.analytics.budget.noPlan", "No plan set"),
|
|
204
205
|
"on-track": t("planmagnet.analytics.budget.onTrack", "On track"),
|
|
205
206
|
"near-limit": t("planmagnet.analytics.budget.nearLimit", "Near limit"),
|
|
@@ -210,7 +211,10 @@ function w({ workspaceId: e }) {
|
|
|
210
211
|
children: [/* @__PURE__ */ c(a, { className: "h-4 w-4" }), t("planmagnet.analytics.budget.title", "Budget performance")]
|
|
211
212
|
}), /* @__PURE__ */ c("p", {
|
|
212
213
|
className: "text-xs text-muted-foreground",
|
|
213
|
-
children: t("planmagnet.analytics.budget.
|
|
214
|
+
children: h > o.length ? t("planmagnet.analytics.budget.subtitlePartial", "Spent against plan, per budget — the newest {{shown}} of {{total}}, riskiest first.", {
|
|
215
|
+
shown: o.length,
|
|
216
|
+
total: h
|
|
217
|
+
}) : t("planmagnet.analytics.budget.subtitle", "Spent against plan, per budget, across the workspace.")
|
|
214
218
|
})] }), /* @__PURE__ */ c(f, { children: r ? /* @__PURE__ */ c("p", {
|
|
215
219
|
className: "text-sm text-muted-foreground",
|
|
216
220
|
children: t("planmagnet.analytics.loading", "Loading…")
|
|
@@ -276,7 +280,7 @@ function w({ workspaceId: e }) {
|
|
|
276
280
|
"aria-valuemin": 0,
|
|
277
281
|
"aria-valuemax": Math.max(100, i ?? 0),
|
|
278
282
|
"aria-valuenow": i ?? 0,
|
|
279
|
-
"aria-label": `${e.name}: ${i === null ? "no plan" : `${i}%`} ${
|
|
283
|
+
"aria-label": `${e.name}: ${i === null ? "no plan" : `${i}%`} ${_[n]}`,
|
|
280
284
|
title: `${C(e.spentAmount, e.currency)} of ${C(e.plannedAmount, e.currency)}`,
|
|
281
285
|
className: `h-2 w-full overflow-hidden rounded-full ${r.track}`,
|
|
282
286
|
children: /* @__PURE__ */ c("div", {
|
|
@@ -288,7 +292,7 @@ function w({ workspaceId: e }) {
|
|
|
288
292
|
children: [/* @__PURE__ */ c(r.Icon, {
|
|
289
293
|
className: "h-3 w-3",
|
|
290
294
|
"aria-hidden": "true"
|
|
291
|
-
}), i === null ?
|
|
295
|
+
}), i === null ? _[n] : `${i}% · ${_[n]}`]
|
|
292
296
|
})]
|
|
293
297
|
})
|
|
294
298
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeliveryHealthCards.js","names":[],"sources":["../../../../src/planmagnet/pages/analytics/DeliveryHealthCards.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n AlertOctagon,\n AlertTriangle,\n CheckCircle2,\n CircleDashed,\n Timer,\n Wallet,\n} from 'lucide-react';\n\n/**\n * Two analytics the service already computed and no page showed (P1.1).\n *\n * - Cycle time: `planmagnetCycleTime` returned average, median and per-item\n * hours with zero UI consumers.\n * - Budget performance: budgets carry planned / spent / remaining, but the\n * analytics page never surfaced them.\n *\n * Forms follow the dataviz method: headline numbers are stat tiles, per-item\n * detail is a table, and \"spent against a limit\" is a meter whose fill carries\n * severity while its track is a lighter step of the same ramp. Over-budget is a\n * STATUS, so it always ships with an icon and a label — never colour alone.\n * Every colour is a semantic token.\n *\n * Each card owns its query at the top of the component, so neither can land a\n * hook after an early return in the host page.\n */\n\nconst GET_CYCLE_TIME = gql`\n query GetProjectCycleTime($workspaceId: ID!, $projectId: ID!) {\n planmagnetCycleTime(workspaceId: $workspaceId, projectId: $projectId) {\n averageCycleTimeHours\n medianCycleTimeHours\n sampleSize\n items {\n workItemId\n key\n title\n cycleTimeHours\n }\n }\n }\n`;\n\nconst GET_BUDGET_PERFORMANCE = gql`\n query GetBudgetPerformance($workspaceId: ID!) {\n planmagnetBudgets(workspaceId: $workspaceId, pagination: { first: 50 }) {\n items {\n id\n name\n currency\n plannedAmount\n spentAmount\n remainingAmount\n project {\n id\n name\n }\n }\n }\n }\n`;\n\ninterface CycleTimeData {\n planmagnetCycleTime: {\n averageCycleTimeHours: number | null;\n medianCycleTimeHours: number | null;\n sampleSize: number;\n items: Array<{ workItemId: string; key: string; title: string; cycleTimeHours: number }>;\n };\n}\n\ninterface BudgetData {\n planmagnetBudgets: {\n items: Array<{\n id: string;\n name: string;\n currency: string;\n plannedAmount: number;\n spentAmount: number;\n remainingAmount: number;\n project: { id: string; name: string } | null;\n }>;\n };\n}\n\n/** Hours read naturally up to two days; beyond that, days. */\nexport function formatDuration(hours: number | null | undefined): string {\n if (hours === null || hours === undefined || !Number.isFinite(hours)) return '—';\n if (hours < 48) return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)} h`;\n const days = hours / 24;\n return `${days < 10 ? days.toFixed(1) : Math.round(days)} d`;\n}\n\nfunction StatTile({ label, value, hint }: { label: string; value: string; hint?: string }) {\n return (\n <div className=\"rounded-md border p-3\">\n <p className=\"text-xs text-muted-foreground\">{label}</p>\n <p className=\"mt-1 text-2xl font-semibold\">{value}</p>\n {hint ? <p className=\"mt-0.5 text-xs text-muted-foreground\">{hint}</p> : null}\n </div>\n );\n}\n\nexport function CycleTimeCard({\n workspaceId,\n projectId,\n}: {\n workspaceId?: string | null;\n projectId: string;\n}) {\n const { t } = useI18n();\n const { data, loading, error } = useQuery<CycleTimeData>(GET_CYCLE_TIME, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n const cycle = data?.planmagnetCycleTime;\n const slowest = [...(cycle?.items ?? [])]\n .sort((a, b) => b.cycleTimeHours - a.cycleTimeHours)\n .slice(0, 5);\n\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Timer className=\"h-4 w-4\" />\n {t('planmagnet.analytics.cycleTime.title', 'Cycle time')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.cycleTime.subtitle',\n 'From first moving to In Progress (or creation) to Done, for the selected project.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n {!projectId ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.analytics.cycleTime.noProject',\n 'Select a project to see its cycle time.'\n )}\n </p>\n ) : loading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {t('planmagnet.analytics.cycleTime.error', 'Cycle time could not be loaded')}:{' '}\n {error.message}\n </p>\n ) : !cycle || cycle.sampleSize === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.analytics.cycleTime.empty',\n 'No completed work items in this project yet — cycle time appears once items reach Done.'\n )}\n </p>\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-3 gap-3\">\n <StatTile\n label={t('planmagnet.analytics.cycleTime.median', 'Median')}\n value={formatDuration(cycle.medianCycleTimeHours)}\n />\n <StatTile\n label={t('planmagnet.analytics.cycleTime.average', 'Average')}\n value={formatDuration(cycle.averageCycleTimeHours)}\n />\n <StatTile\n label={t('planmagnet.analytics.cycleTime.sample', 'Items measured')}\n value={String(cycle.sampleSize)}\n />\n </div>\n <table className=\"w-full text-sm\">\n <caption className=\"mb-2 text-left text-xs text-muted-foreground\">\n {t('planmagnet.analytics.cycleTime.slowest', 'Slowest completed items')}\n </caption>\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.cycleTime.item', 'Item')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.analytics.cycleTime.duration', 'Cycle time')}\n </th>\n </tr>\n </thead>\n <tbody>\n {slowest.map((item) => (\n <tr key={item.workItemId} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">\n <span className=\"font-medium\">{item.key}</span>{' '}\n <span className=\"text-muted-foreground\">{item.title}</span>\n </td>\n <td className=\"py-1.5 text-right tabular-nums\">\n {formatDuration(item.cycleTimeHours)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n\ntype BudgetState = 'no-plan' | 'on-track' | 'near-limit' | 'over-budget';\n\n/** Share of the plan spent; `null` when there is no plan to measure against. */\nexport function budgetUsage(planned: number, spent: number): number | null {\n if (!Number.isFinite(planned) || planned <= 0) return null;\n return spent / planned;\n}\n\nexport function budgetState(usage: number | null): BudgetState {\n // Without a plan there is nothing to be on track AGAINST — say so, rather\n // than reporting a comforting \"On track\" the numbers cannot support.\n if (usage === null) return 'no-plan';\n if (usage <= 0.85) return 'on-track';\n return usage <= 1 ? 'near-limit' : 'over-budget';\n}\n\nconst STATE_STYLE: Record<\n BudgetState,\n { track: string; fill: string; Icon: typeof CheckCircle2; text: string }\n> = {\n 'no-plan': {\n track: 'bg-muted',\n fill: 'bg-muted',\n Icon: CircleDashed,\n text: 'text-muted-foreground',\n },\n 'on-track': {\n track: 'bg-primary/15',\n fill: 'bg-primary',\n Icon: CheckCircle2,\n text: 'text-muted-foreground',\n },\n 'near-limit': {\n track: 'bg-status-warning-subtle',\n fill: 'bg-status-warning',\n Icon: AlertTriangle,\n text: 'text-status-warning-text',\n },\n 'over-budget': {\n track: 'bg-status-error-subtle',\n fill: 'bg-status-error',\n Icon: AlertOctagon,\n text: 'text-status-error-text',\n },\n};\n\nfunction money(amount: number, currency: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(amount);\n } catch {\n // An unrecognised currency code must not blank the whole table.\n return `${Math.round(amount).toLocaleString()} ${currency}`;\n }\n}\n\nexport function BudgetPerformanceCard({ workspaceId }: { workspaceId?: string | null }) {\n const { t } = useI18n();\n const { data, loading, error } = useQuery<BudgetData>(GET_BUDGET_PERFORMANCE, {\n variables: { workspaceId },\n skip: !workspaceId,\n });\n const budgets = data?.planmagnetBudgets?.items ?? [];\n const label: Record<BudgetState, string> = {\n 'no-plan': t('planmagnet.analytics.budget.noPlan', 'No plan set'),\n 'on-track': t('planmagnet.analytics.budget.onTrack', 'On track'),\n 'near-limit': t('planmagnet.analytics.budget.nearLimit', 'Near limit'),\n 'over-budget': t('planmagnet.analytics.budget.over', 'Over budget'),\n };\n\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Wallet className=\"h-4 w-4\" />\n {t('planmagnet.analytics.budget.title', 'Budget performance')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.budget.subtitle',\n 'Spent against plan, per budget, across the workspace.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n {loading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {t('planmagnet.analytics.budget.error', 'Budgets could not be loaded')}: {error.message}\n </p>\n ) : budgets.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.budget.empty', 'No budgets in this workspace yet.')}\n </p>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.budget.name', 'Budget')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.analytics.budget.spent', 'Spent')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.analytics.budget.planned', 'Planned')}\n </th>\n <th className=\"w-2/5 py-1 font-normal\">\n {t('planmagnet.analytics.budget.usage', 'Used')}\n </th>\n </tr>\n </thead>\n <tbody>\n {budgets.map((budget) => {\n const usage = budgetUsage(budget.plannedAmount, budget.spentAmount);\n const state = budgetState(usage);\n const style = STATE_STYLE[state];\n const percent = usage === null ? null : Math.round(usage * 100);\n return (\n <tr key={budget.id} className=\"border-t align-middle\">\n <td className=\"py-2 pr-2\">\n <span className=\"font-medium\">{budget.name}</span>\n {budget.project ? (\n <span className=\"block text-xs text-muted-foreground\">\n {budget.project.name}\n </span>\n ) : null}\n </td>\n <td className=\"py-2 pr-2 text-right tabular-nums\">\n {money(budget.spentAmount, budget.currency)}\n </td>\n <td className=\"py-2 pr-2 text-right tabular-nums\">\n {money(budget.plannedAmount, budget.currency)}\n </td>\n <td className=\"py-2\">\n <div\n role=\"meter\"\n aria-valuemin={0}\n // ARIA requires valuenow to sit inside [min, max]. An\n // over-budget 130% with a max of 100 is invalid, and\n // assistive tech may clamp it and announce \"100%\".\n aria-valuemax={Math.max(100, percent ?? 0)}\n aria-valuenow={percent ?? 0}\n aria-label={`${budget.name}: ${percent === null ? 'no plan' : `${percent}%`} ${label[state]}`}\n title={`${money(budget.spentAmount, budget.currency)} of ${money(budget.plannedAmount, budget.currency)}`}\n className={`h-2 w-full overflow-hidden rounded-full ${style.track}`}\n >\n <div\n className={`h-full rounded-full ${style.fill}`}\n style={{ width: `${Math.min(percent ?? 0, 100)}%` }}\n />\n </div>\n <span className={`mt-1 flex items-center gap-1 text-xs ${style.text}`}>\n <style.Icon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {percent === null ? label[state] : `${percent}% · ${label[state]}`}\n </span>\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
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 totalCount\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 totalCount: number;\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 // Riskiest first: over budget, then by share spent (BOFF-7996). The page holds the newest 50; when there are more,\n // the subtitle says so instead of claiming the whole workspace.\n const budgets = [...(data?.planmagnetBudgets?.items ?? [])].sort(\n (a, b) =>\n (budgetUsage(b.plannedAmount, b.spentAmount) ?? -1) -\n (budgetUsage(a.plannedAmount, a.spentAmount) ?? -1)\n );\n const totalBudgets = data?.planmagnetBudgets?.totalCount ?? budgets.length;\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 {totalBudgets > budgets.length\n ? t(\n 'planmagnet.analytics.budget.subtitlePartial',\n 'Spent against plan, per budget — the newest {{shown}} of {{total}}, riskiest first.',\n { shown: budgets.length, total: totalBudgets }\n )\n : t(\n 'planmagnet.analytics.budget.subtitle',\n 'Spent against plan, per budget, across the workspace.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n {loading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {t('planmagnet.analytics.budget.error', 'Budgets could not be loaded')}: {error.message}\n </p>\n ) : budgets.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.budget.empty', 'No budgets in this workspace yet.')}\n </p>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.budget.name', 'Budget')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.analytics.budget.spent', 'Spent')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.analytics.budget.planned', 'Planned')}\n </th>\n <th className=\"w-2/5 py-1 font-normal\">\n {t('planmagnet.analytics.budget.usage', 'Used')}\n </th>\n </tr>\n </thead>\n <tbody>\n {budgets.map((budget) => {\n const usage = budgetUsage(budget.plannedAmount, budget.spentAmount);\n const state = budgetState(usage);\n const style = STATE_STYLE[state];\n const percent = usage === null ? null : Math.round(usage * 100);\n return (\n <tr key={budget.id} className=\"border-t align-middle\">\n <td className=\"py-2 pr-2\">\n <span className=\"font-medium\">{budget.name}</span>\n {budget.project ? (\n <span className=\"block text-xs text-muted-foreground\">\n {budget.project.name}\n </span>\n ) : null}\n </td>\n <td className=\"py-2 pr-2 text-right tabular-nums\">\n {money(budget.spentAmount, budget.currency)}\n </td>\n <td className=\"py-2 pr-2 text-right tabular-nums\">\n {money(budget.plannedAmount, budget.currency)}\n </td>\n <td className=\"py-2\">\n <div\n role=\"meter\"\n aria-valuemin={0}\n // ARIA requires valuenow to sit inside [min, max]. An\n // over-budget 130% with a max of 100 is invalid, and\n // assistive tech may clamp it and announce \"100%\".\n aria-valuemax={Math.max(100, percent ?? 0)}\n aria-valuenow={percent ?? 0}\n aria-label={`${budget.name}: ${percent === null ? 'no plan' : `${percent}%`} ${label[state]}`}\n title={`${money(budget.spentAmount, budget.currency)} of ${money(budget.plannedAmount, budget.currency)}`}\n className={`h-2 w-full overflow-hidden rounded-full ${style.track}`}\n >\n <div\n className={`h-full rounded-full ${style.fill}`}\n style={{ width: `${Math.min(percent ?? 0, 100)}%` }}\n />\n </div>\n <span className={`mt-1 flex items-center gap-1 text-xs ${style.text}`}>\n <style.Icon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {percent === null ? label[state] : `${percent}% · ${label[state]}`}\n </span>\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReleaseWorkItemsCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseWorkItemsCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReleaseWorkItemsCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseWorkItemsCard.tsx"],"names":[],"mappings":"AAmDA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,KAAK,GACN,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B,2CAgLA"}
|
|
@@ -16,6 +16,7 @@ var y = s`
|
|
|
16
16
|
title
|
|
17
17
|
status
|
|
18
18
|
}
|
|
19
|
+
totalCount
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
`, b = s`
|
|
@@ -37,6 +38,7 @@ function S({ releaseId: s, projectId: S, items: C }) {
|
|
|
37
38
|
workspaceId: w,
|
|
38
39
|
filter: {
|
|
39
40
|
projectId: S,
|
|
41
|
+
excludeIds: C.map((e) => e.id),
|
|
40
42
|
...M ? { search: M } : {}
|
|
41
43
|
}
|
|
42
44
|
},
|
|
@@ -44,7 +46,7 @@ function S({ releaseId: s, projectId: S, items: C }) {
|
|
|
44
46
|
}), P = {
|
|
45
47
|
refetchQueries: ["GetRelease", "ReleaseReadiness"],
|
|
46
48
|
awaitRefetchQueries: !0
|
|
47
|
-
}, [F, I] = c(b, P), [L, R] = c(x, P), z =
|
|
49
|
+
}, [F, I] = c(b, P), [L, R] = c(x, P), z = N.data?.planmagnetWorkItems.items ?? [], B = (N.data?.planmagnetWorkItems.totalCount ?? 0) - z.length, V = async (e, t) => {
|
|
48
50
|
j(null);
|
|
49
51
|
try {
|
|
50
52
|
await n(e());
|
|
@@ -145,13 +147,17 @@ function S({ releaseId: s, projectId: S, items: C }) {
|
|
|
145
147
|
className: "text-sm text-destructive",
|
|
146
148
|
children: ["Work items could not be loaded: ", N.error.message]
|
|
147
149
|
}),
|
|
148
|
-
N.data &&
|
|
150
|
+
N.data && z.length === 0 && /* @__PURE__ */ u("p", {
|
|
149
151
|
className: "text-sm text-muted-foreground",
|
|
150
|
-
children: "No other work items match."
|
|
152
|
+
children: M ? "No other work items match." : "Every work item in this project is already in the release."
|
|
153
|
+
}),
|
|
154
|
+
B > 0 && /* @__PURE__ */ d("p", {
|
|
155
|
+
className: "text-xs text-muted-foreground",
|
|
156
|
+
children: [B, " more — type a key or title to narrow the list."]
|
|
151
157
|
}),
|
|
152
158
|
/* @__PURE__ */ u("ul", {
|
|
153
159
|
className: "space-y-1",
|
|
154
|
-
children:
|
|
160
|
+
children: z.map((e) => /* @__PURE__ */ d("li", {
|
|
155
161
|
className: "flex items-center justify-between gap-2 text-sm",
|
|
156
162
|
children: [/* @__PURE__ */ d("span", {
|
|
157
163
|
className: "min-w-0 truncate",
|