@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.
Files changed (141) hide show
  1. package/dist/generated/global-types.d.ts +9 -1
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.d.ts +0 -12
  4. package/dist/generated/wspace-operations.d.ts.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +14 -10
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
  9. package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +149 -127
  10. package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
  11. package/dist/planmagnet/board/ProjectBoard.d.ts +2 -0
  12. package/dist/planmagnet/board/ProjectBoard.d.ts.map +1 -1
  13. package/dist/planmagnet/board/ProjectBoard.js +492 -225
  14. package/dist/planmagnet/board/ProjectBoard.js.map +1 -1
  15. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  16. package/dist/planmagnet/controls/FeatureControls.js +142 -139
  17. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  18. package/dist/planmagnet/controls/FeatureTriage.d.ts +21 -0
  19. package/dist/planmagnet/controls/FeatureTriage.d.ts.map +1 -0
  20. package/dist/planmagnet/controls/FeatureTriage.js +398 -0
  21. package/dist/planmagnet/controls/FeatureTriage.js.map +1 -0
  22. package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -1
  23. package/dist/planmagnet/controls/MilestoneControls.js +76 -73
  24. package/dist/planmagnet/controls/MilestoneControls.js.map +1 -1
  25. package/dist/planmagnet/controls/ProjectControls.d.ts.map +1 -1
  26. package/dist/planmagnet/controls/ProjectControls.js +47 -44
  27. package/dist/planmagnet/controls/ProjectControls.js.map +1 -1
  28. package/dist/planmagnet/controls/ReleaseControls.d.ts.map +1 -1
  29. package/dist/planmagnet/controls/ReleaseControls.js +47 -44
  30. package/dist/planmagnet/controls/ReleaseControls.js.map +1 -1
  31. package/dist/planmagnet/controls/WorkItemControls.d.ts.map +1 -1
  32. package/dist/planmagnet/controls/WorkItemControls.js +68 -65
  33. package/dist/planmagnet/controls/WorkItemControls.js.map +1 -1
  34. package/dist/planmagnet/hooks/useCounts.d.ts +39 -0
  35. package/dist/planmagnet/hooks/useCounts.d.ts.map +1 -0
  36. package/dist/planmagnet/hooks/useCounts.js +209 -0
  37. package/dist/planmagnet/hooks/useCounts.js.map +1 -0
  38. package/dist/planmagnet/import/exportColumns.d.ts +52 -0
  39. package/dist/planmagnet/import/exportColumns.d.ts.map +1 -0
  40. package/dist/planmagnet/import/exportColumns.js +81 -0
  41. package/dist/planmagnet/import/exportColumns.js.map +1 -0
  42. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  43. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +255 -242
  44. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  45. package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  46. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +199 -160
  47. package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
  48. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  49. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +147 -144
  50. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  51. package/dist/planmagnet/pages/GoalsGoalsPagePage.d.ts.map +1 -1
  52. package/dist/planmagnet/pages/GoalsGoalsPagePage.js +2 -7
  53. package/dist/planmagnet/pages/GoalsGoalsPagePage.js.map +1 -1
  54. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
  55. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +173 -173
  56. package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
  57. package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
  58. package/dist/planmagnet/pages/ProductsEditProductPagePage.js +120 -117
  59. package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
  60. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.d.ts.map +1 -1
  61. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +97 -117
  62. package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
  63. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  64. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +168 -103
  65. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  66. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  67. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +117 -103
  68. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  69. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  70. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +4 -8
  71. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  72. package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
  73. package/dist/planmagnet/pages/ProductsProductsPagePage.js +85 -90
  74. package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
  75. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  76. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +12 -2
  77. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  78. package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
  79. package/dist/planmagnet/pages/ProductsReleasesPagePage.js +2 -2
  80. package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
  81. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  82. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +48 -71
  83. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  84. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
  85. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +17 -13
  86. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
  87. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.d.ts.map +1 -1
  88. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js +1 -1
  89. package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
  90. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +6 -6
  91. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  92. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +11 -8
  93. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  94. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +4 -4
  95. package/dist/planmagnet/pages/ProjectsPlanningPagePage.d.ts.map +1 -1
  96. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +1 -1
  97. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js.map +1 -1
  98. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  99. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +152 -139
  100. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  101. package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
  102. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +75 -74
  103. package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
  104. package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts +7 -0
  105. package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
  106. package/dist/planmagnet/pages/ReportsReportsPagePage.js +39 -201
  107. package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
  108. package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
  109. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +118 -99
  110. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
  111. package/dist/planmagnet/pages/TasksImportPagePage.d.ts +2 -0
  112. package/dist/planmagnet/pages/TasksImportPagePage.d.ts.map +1 -0
  113. package/dist/planmagnet/pages/TasksImportPagePage.js +309 -0
  114. package/dist/planmagnet/pages/TasksImportPagePage.js.map +1 -0
  115. package/dist/planmagnet/pages/TasksTasksPagePage.d.ts.map +1 -1
  116. package/dist/planmagnet/pages/TasksTasksPagePage.js +110 -104
  117. package/dist/planmagnet/pages/TasksTasksPagePage.js.map +1 -1
  118. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  119. package/dist/planmagnet/pages/TeamTeamPagePage.js +99 -177
  120. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  121. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -1
  122. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -4
  123. package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -1
  124. package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -1
  125. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +10 -4
  126. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -1
  127. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  128. package/dist/planmagnet/roadmaps/RoadmapControls.js +151 -138
  129. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  130. package/dist/planmagnet/sprints/SprintControls.d.ts.map +1 -1
  131. package/dist/planmagnet/sprints/SprintControls.js +32 -30
  132. package/dist/planmagnet/sprints/SprintControls.js.map +1 -1
  133. package/dist/planmagnet/utils/mutationResult.d.ts +10 -0
  134. package/dist/planmagnet/utils/mutationResult.d.ts.map +1 -1
  135. package/dist/planmagnet/utils/mutationResult.js +2 -1
  136. package/dist/planmagnet/utils/mutationResult.js.map +1 -1
  137. package/dist/planmagnet/utils/rowNavigation.d.ts +2 -0
  138. package/dist/planmagnet/utils/rowNavigation.d.ts.map +1 -1
  139. package/dist/planmagnet/utils/rowNavigation.js +9 -8
  140. package/dist/planmagnet/utils/rowNavigation.js.map +1 -1
  141. 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 { UserPlus as i } from "lucide-react";
4
- import { gql as a } from "@apollo/client";
5
- import { useQuery as o } from "@apollo/client/react";
6
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
7
- import { Badge as l, Button as u, Card as d, CardContent as f, CardHeader as p, CardTitle as m } from "@burdenoff/fe-libs/ui";
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 h = a`
10
- query GetTeamData(
11
- $workspaceId: ID!
12
- $projectPagination: PlanmagnetPaginationInput
13
- $workItemPagination: PlanmagnetPaginationInput
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
- ownerId
22
- methodology
17
+ count
18
+ points
23
19
  }
24
- totalCount
25
20
  }
26
- planmagnetWorkItems(workspaceId: $workspaceId, pagination: $workItemPagination) {
27
- items {
28
- id
29
- status
30
- assigneeId
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
- `, g = {
37
- active: "bg-green-400",
38
- away: "bg-yellow-400",
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
- projectPagination: { first: 100 },
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
- if (y && !v) return /* @__PURE__ */ s("div", {
52
- className: "flex items-center justify-center p-12",
53
- children: /* @__PURE__ */ s("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
54
- });
55
- if (b) return /* @__PURE__ */ c("div", {
56
- className: "p-6 text-red-500",
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
- let x = v?.planmagnetProjects?.items ?? [], S = v?.planmagnetWorkItems?.items ?? [], C = new Set(S.filter((e) => e.assigneeId).map((e) => e.assigneeId)), w = new Set(x.filter((e) => e.ownerId).map((e) => e.ownerId)), T = new Set([...C, ...w]), E = Array.from(T).map((e) => {
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__ */ c("div", {
96
+ /* @__PURE__ */ l("div", {
87
97
  className: "flex items-start justify-between gap-4",
88
- children: [/* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("h1", {
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
- E.length,
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: () => _("/team/invite-member-page"),
107
+ onClick: () => o("/team/invite-member-page"),
103
108
  "data-testid": "team-invite",
104
- children: [/* @__PURE__ */ s(i, { className: "mr-1 h-4 w-4" }), "Invite member"]
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: "space-y-4",
109
- children: [/* @__PURE__ */ s("h2", {
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("div", {
139
- className: "space-y-4",
140
- children: [/* @__PURE__ */ s("h2", {
141
- className: "text-lg font-medium",
142
- children: "Members"
143
- }), /* @__PURE__ */ c("div", {
144
- className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
145
- children: [E.map((e) => /* @__PURE__ */ s(d, { children: /* @__PURE__ */ c(f, {
146
- className: "pt-4 space-y-3",
147
- children: [
148
- /* @__PURE__ */ c("div", {
149
- className: "flex items-center gap-3",
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 { _ as TeamTeamPagePage };
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":"AAyFA,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKvE;AAYD,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB,2CAkGA;AAED,KAAK,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEzE,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAM7D;AA6CD,wBAAgB,qBAAqB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,2CAiHrF"}
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.subtitle", "Spent against plan, per budget, across the workspace.")
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}%`} ${h[n]}`,
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 ? h[n] : `${i}% · ${h[n]}`]
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":"AAkDA,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,2CAiKA"}
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 = new Set(C.map((e) => e.id)), B = (N.data?.planmagnetWorkItems.items ?? []).filter((e) => !z.has(e.id)), V = async (e, t) => {
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 && B.length === 0 && /* @__PURE__ */ u("p", {
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: B.map((e) => /* @__PURE__ */ d("li", {
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",