@burdenoff/microfe-planmagnet 2026.1001.4 → 2026.1001.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 (62) hide show
  1. package/dist/planmagnet/controls/CompetitorControls.d.ts +71 -0
  2. package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
  3. package/dist/planmagnet/controls/CompetitorControls.js +346 -12
  4. package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
  5. package/dist/planmagnet/controls/FeatureControls.d.ts +5 -0
  6. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  7. package/dist/planmagnet/controls/FeatureControls.js +363 -218
  8. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  9. package/dist/planmagnet/controls/Pickers.d.ts +15 -0
  10. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  11. package/dist/planmagnet/controls/Pickers.js +61 -1
  12. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  13. package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
  14. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
  15. package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
  16. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
  17. package/dist/planmagnet/dashboards/DashboardUI.js +1 -1
  18. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
  19. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
  20. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
  21. package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts.map +1 -1
  22. package/dist/planmagnet/meetings/ScheduleMeetingDialog.js +1 -1
  23. package/dist/planmagnet/meetings/ScheduleMeetingDialog.js.map +1 -1
  24. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  25. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +289 -282
  26. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  27. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
  28. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
  29. package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
  30. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
  31. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
  32. package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
  33. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
  34. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
  35. package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
  36. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
  37. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
  38. package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
  39. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
  40. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
  41. package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
  42. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  43. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +162 -122
  44. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  45. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  46. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +135 -109
  47. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  48. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  49. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
  50. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  51. package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
  52. package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
  53. package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
  54. package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
  55. package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
  56. package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
  57. package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
  58. package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
  59. package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
  60. package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
  61. package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
  62. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectsPlanningDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsPlanningDetailPagePage.tsx"],"names":[],"mappings":"AA8FA,wBAAgB,8BAA8B,4CA+V7C"}
1
+ {"version":3,"file":"ProjectsPlanningDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProjectsPlanningDetailPagePage.tsx"],"names":[],"mappings":"AA8FA,wBAAgB,8BAA8B,4CA0W7C"}
@@ -67,7 +67,7 @@ var k = ["GetSprintDetail", "GetSprintCapacity"], A = y`
67
67
  planmagnetSprintCapacity(sprintId: $sprintId) {
68
68
  sprintId
69
69
  crewfoundryUnavailable
70
- unmatchedAssigneeIds
70
+ unmatchedCount
71
71
  members {
72
72
  actorId
73
73
  employeeName
@@ -286,9 +286,21 @@ function M() {
286
286
  className: "text-xs text-muted-foreground mt-3 border-t pt-3",
287
287
  children: "Workforce allocation from CrewFoundry is unavailable for this workspace — no CrewFoundry module provisioned yet."
288
288
  }),
289
- !U?.crewfoundryUnavailable && (U?.unmatchedAssigneeIds?.length ?? 0) > 0 && /* @__PURE__ */ S("p", {
289
+ !U?.crewfoundryUnavailable && (U?.unmatchedCount ?? 0) > 0 && /* @__PURE__ */ S("p", {
290
290
  className: "text-xs text-muted-foreground mt-3 border-t pt-3",
291
- children: [U.unmatchedAssigneeIds.length, " assignee(s) have no matching CrewFoundry employee record — allocation hours shown only for matched employees."]
291
+ "data-testid": "sprint-capacity-unmatched",
292
+ children: [
293
+ U.unmatchedCount,
294
+ " assignee(s) have no matching CrewFoundry employee record — allocation hours shown only for matched employees.",
295
+ " ",
296
+ /* @__PURE__ */ x("button", {
297
+ type: "button",
298
+ className: "text-primary underline-offset-2 hover:underline",
299
+ "data-testid": "sprint-capacity-unmatched-link",
300
+ onClick: () => M("/team/team-page?focus=unmatched-crew-identities"),
301
+ children: "Fix unmatched identities"
302
+ })
303
+ ]
292
304
  })
293
305
  ] })] })
294
306
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectsPlanningDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsPlanningDetailPagePage.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 { Users, BarChart3, FolderKanban, Pencil } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonAvatar, PersonName, initialsOf } from '../people/People';\nimport { SprintLifecycleActions, SprintWorkItemsPanel } from '../sprints/SprintControls';\nimport { SprintDates } from '../sprints/SprintDates';\nimport { useLiveRefresh, combineLiveRefreshModes } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\nimport { MeetingsCard } from '../meetings/MeetingsCard';\nimport { ScheduleCeremoniesDialog } from '../meetings/ScheduleCeremoniesDialog';\n\n/** Every query this page's own numbers depend on. */\nconst SPRINT_DETAIL_QUERIES = ['GetSprintDetail', 'GetSprintCapacity'];\n\nconst GET_SPRINT_DETAIL = gql`\n query GetSprintDetail($id: ID!) {\n planmagnetSprint(id: $id) {\n id\n name\n projectId\n project {\n id\n name\n }\n teamId\n team {\n id\n name\n projects {\n id\n name\n }\n }\n status\n startDate\n endDate\n startedAt\n completedAt\n baselineStartDate\n baselineEndDate\n planChanges {\n field\n fromValue\n toValue\n sprintStatus\n changedBy\n changedAt\n }\n goal\n capacity\n velocity\n workItems {\n id\n key\n title\n status\n storyPoints\n assigneeId\n priority\n }\n }\n }\n`;\n\n// Real cross-layer CrewFoundry workforce capacity (BOFF-4444). Fails open on\n// the backend — an unreachable/unprovisioned CrewFoundry never blocks this\n// page, it just yields crewfoundryUnavailable: true, handled below.\nconst GET_SPRINT_CAPACITY = gql`\n query GetSprintCapacity($sprintId: ID!) {\n planmagnetSprintCapacity(sprintId: $sprintId) {\n sprintId\n crewfoundryUnavailable\n unmatchedAssigneeIds\n members {\n actorId\n employeeName\n department\n position\n allocatedHours\n billableHours\n utilizationPercent\n assignedStoryPoints\n assignedWorkItemCount\n }\n }\n }\n`;\n\nexport function ProjectsPlanningDetailPagePage() {\n const sprintId = useEntityId('sprintId');\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_SPRINT_DETAIL, {\n variables: { id: sprintId },\n skip: !sprintId,\n });\n\n // Real cross-layer CrewFoundry capacity (BOFF-4444) — called unconditionally\n // (with `skip`) to keep hook order stable across the loading/error/loaded\n // renders above.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: capacityData } = useQuery<Record<string, any>>(GET_SPRINT_CAPACITY, {\n variables: { sprintId },\n skip: !sprintId,\n });\n\n // Live updates (PDM-21/PDM-11): the sprint itself (status, dates, goal, capacity, its work item list) and —\n // once the sprint's project is known — that project's work items (a card's status/points changing without\n // ever being added or removed from the sprint). A team-scoped sprint has no `projectId` (BOFF-8044); the\n // work item stream simply stays off rather than falling back to every project's events.\n const sprintProjectId = data?.planmagnetSprint?.projectId ?? null;\n const sprintLive = useLiveRefresh({\n kind: 'sprint',\n sprintId,\n projectId: sprintProjectId,\n queries: SPRINT_DETAIL_QUERIES,\n });\n const workItemLive = useLiveRefresh({\n kind: 'workItem',\n projectId: sprintProjectId,\n enabled: Boolean(sprintProjectId),\n queries: SPRINT_DETAIL_QUERIES,\n });\n const liveMode = combineLiveRefreshModes(sprintLive.mode, workItemLive.mode);\n\n if (!sprintId)\n return (\n <div className=\"p-6 text-muted-foreground\">\n No planning session selected. Open one from the list.\n </div>\n );\n // Only before the first load: a refetch after a sprint action must not blank\n // the page (it unmounted the controls mid-action).\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 if (error)\n return <div className=\"p-6 text-red-500\">Error loading planning session: {error.message}</div>;\n\n const sprint = data?.planmagnetSprint;\n if (!sprint) return <div className=\"p-6 text-muted-foreground\">Planning session not found.</div>;\n\n const capacity = capacityData?.planmagnetSprintCapacity;\n const allocationByAssignee: Record<\n string,\n {\n employeeName: string;\n allocatedHours: number;\n billableHours: number;\n utilizationPercent: number | null;\n }\n > = {};\n for (const member of capacity?.members ?? []) {\n allocationByAssignee[member.actorId] = {\n employeeName: member.employeeName,\n allocatedHours: member.allocatedHours,\n billableHours: member.billableHours,\n utilizationPercent: member.utilizationPercent,\n };\n }\n\n const workItems = sprint.workItems ?? [];\n const totalPoints = workItems.reduce(\n (sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0),\n 0\n );\n const completedPoints = workItems\n .filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce((sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0), 0);\n // No capacity set means \"not planned\", not \"full\": this used to take the allocated points as the capacity and show\n // 100% utilisation in red, while the list showed no capacity for the same sprint (BOFF-7974).\n const sprintCapacity: number | null = sprint.capacity ?? null;\n const capPct =\n sprintCapacity && sprintCapacity > 0 ? Math.round((totalPoints / sprintCapacity) * 100) : null;\n const remaining = sprintCapacity == null ? null : Math.max(0, sprintCapacity - totalPoints);\n\n // Derive unique assignees from work items\n const uniqueAssignees = [\n ...new Set(\n workItems\n .map((wi: { assigneeId: string | null }) => wi.assigneeId)\n .filter(Boolean) as string[]\n ),\n ];\n\n // A sprint's scope is PROJECT xor TEAM (BOFF-8044). A team sprint has no `project` — showing\n // \"Unknown project\" for it (BOFF-8044 review) was wrong on the one page a user lands on right\n // after creating a team sprint. Its eligible work items are the team's own live member\n // projects, never the whole workspace (see SprintWorkItemsPanel/AddWorkItemsDialog).\n const teamProjects: { id: string; name: string }[] = sprint.team?.projects ?? [];\n const eligibleProjectIds = sprint.team\n ? teamProjects.map((p) => p.id)\n : sprint.projectId\n ? [sprint.projectId]\n : [];\n const projectNames = sprint.team\n ? Object.fromEntries(teamProjects.map((p) => [p.id, p.name]))\n : undefined;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-3\">\n <h1 className=\"text-2xl font-semibold\">{sprint.name}</h1>\n <LiveStatus mode={liveMode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {sprint.team\n ? `Team: ${sprint.team.name}`\n : (sprint.project?.name ?? 'Unknown project')}\n </p>\n <div className=\"mt-1\">\n <SprintDates sprint={sprint} />\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <Badge className=\"capitalize\">{sprint.status.toLowerCase().replace('_', ' ')}</Badge>\n {/* The edit page existed but nothing linked to it, so a sprint's\n name, planned dates, goal and capacity could not be changed from\n the UI (BOFF-7954). Finished sprints are not edited here. */}\n {sprint.status !== 'COMPLETED' && sprint.status !== 'CANCELLED' && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/projects/planning-edit-page', sprint.id))}\n data-testid=\"sprint-edit\"\n >\n <Pencil className=\"mr-1 h-4 w-4\" /> Edit\n </Button>\n )}\n {/* CWF-23: only a project-scoped sprint can propose ceremonies — meetings are project-scoped\n (PlanmagnetMeeting.projectId is required), and a team sprint has no project of its own\n (BOFF-8044). */}\n {!sprint.team && (\n <ScheduleCeremoniesDialog\n projectId={sprint.projectId}\n sprintId={sprint.id}\n startDate={sprint.startDate}\n endDate={sprint.endDate}\n />\n )}\n <SprintLifecycleActions\n sprintId={sprint.id}\n sprintName={sprint.name}\n status={sprint.status}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Capacity Allocation</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div className=\"p-4 bg-muted rounded-md\">\n <p className=\"text-2xl font-bold\">{sprintCapacity ?? '—'}</p>\n <p className=\"text-xs text-muted-foreground\">\n {sprintCapacity == null ? 'No capacity set' : 'Capacity'}\n </p>\n </div>\n <div className=\"p-4 bg-primary/10 rounded-md\">\n <p className=\"text-2xl font-bold text-primary\">{totalPoints}</p>\n <p className=\"text-xs text-muted-foreground\">Allocated</p>\n </div>\n <div className=\"p-4 bg-muted rounded-md\">\n <p className=\"text-2xl font-bold\">{remaining ?? '—'}</p>\n <p className=\"text-xs text-muted-foreground\">Remaining</p>\n </div>\n </div>\n {capPct !== null ? (\n <div>\n <div className=\"flex justify-between text-sm mb-2\">\n <span className=\"text-muted-foreground\">Utilization</span>\n <span className=\"font-medium\">{capPct}%</span>\n </div>\n <div className=\"w-full h-4 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${capPct > 90 ? 'bg-destructive' : 'bg-success'}`}\n style={{ width: `${Math.min(capPct, 100)}%` }}\n />\n </div>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n Set a capacity on the sprint to see its utilisation.\n </p>\n )}\n </CardContent>\n </Card>\n <Card>\n <CardHeader>\n <CardTitle>Work Items</CardTitle>\n </CardHeader>\n <CardContent>\n <SprintWorkItemsPanel\n sprintId={sprint.id}\n eligibleProjectIds={eligibleProjectIds}\n projectNames={projectNames}\n status={sprint.status}\n items={workItems}\n />\n </CardContent>\n </Card>\n <Card>\n <CardHeader>\n <CardTitle>Team Members ({uniqueAssignees.length})</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-2 gap-3\">\n {uniqueAssignees.map((assigneeId: string) => {\n const workforce = allocationByAssignee[assigneeId];\n return (\n <div\n key={assigneeId}\n className=\"flex items-center gap-2 border rounded-md p-2.5\"\n >\n {/* Initials from the name shown next to them, not from the UUID (BOFF-8000). */}\n {workforce?.employeeName ? (\n <span\n className=\"h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary\"\n title={workforce.employeeName}\n >\n {initialsOf(workforce.employeeName) || '?'}\n </span>\n ) : (\n <PersonAvatar\n id={assigneeId}\n className=\"h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary\"\n />\n )}\n <div className=\"min-w-0\">\n <span className=\"text-sm truncate block\">\n {workforce?.employeeName ?? <PersonName id={assigneeId} />}\n </span>\n {workforce && (\n <span className=\"text-xs text-muted-foreground\">\n {workforce.allocatedHours}h allocated ({workforce.billableHours}h\n billable)\n {workforce.utilizationPercent != null &&\n ` · ${workforce.utilizationPercent}% util`}\n </span>\n )}\n </div>\n </div>\n );\n })}\n {uniqueAssignees.length === 0 && (\n <p className=\"text-sm text-muted-foreground col-span-2 text-center py-4\">\n No team members assigned.\n </p>\n )}\n </div>\n {/* Real cross-layer CrewFoundry workforce allocation (BOFF-4444). */}\n {capacity?.crewfoundryUnavailable && uniqueAssignees.length > 0 && (\n <p className=\"text-xs text-muted-foreground mt-3 border-t pt-3\">\n Workforce allocation from CrewFoundry is unavailable for this workspace — no\n CrewFoundry module provisioned yet.\n </p>\n )}\n {!capacity?.crewfoundryUnavailable &&\n (capacity?.unmatchedAssigneeIds?.length ?? 0) > 0 && (\n <p className=\"text-xs text-muted-foreground mt-3 border-t pt-3\">\n {capacity.unmatchedAssigneeIds.length} assignee(s) have no matching CrewFoundry\n employee record — allocation hours shown only for matched employees.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Session Info</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-center gap-2\">\n {sprint.team ? (\n <Users className=\"h-4 w-4 text-muted-foreground\" />\n ) : (\n <FolderKanban className=\"h-4 w-4 text-muted-foreground\" />\n )}\n <span className=\"text-muted-foreground\">{sprint.team ? 'Team:' : 'Project:'}</span>\n <span className=\"font-medium\">\n {sprint.team ? sprint.team.name : (sprint.project?.name ?? 'Unknown project')}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <Users className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">Assignees:</span>\n <span className=\"font-medium\">{uniqueAssignees.length}</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">Capacity:</span>\n <span className=\"font-medium\">\n {sprintCapacity == null ? 'not set' : `${sprintCapacity} pts`}\n </span>\n </div>\n {sprint.velocity != null && (\n <div className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">Velocity:</span>\n <span className=\"font-medium\">{sprint.velocity} pts</span>\n </div>\n )}\n {sprint.goal && (\n <div className=\"border-t pt-3 mt-3\">\n <p className=\"text-muted-foreground text-xs mb-1\">Goal</p>\n <p className=\"text-sm\">{sprint.goal}</p>\n </div>\n )}\n </CardContent>\n </Card>\n {/* GAP-06, CWF-22/23: only a project-scoped sprint has meetings of its own — see the guard above\n ScheduleCeremoniesDialog. */}\n {!sprint.team && sprint.projectId && (\n <MeetingsCard\n workspaceId={workspaceId}\n projectId={sprint.projectId}\n sprintId={sprint.id}\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProjectsPlanningDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsPlanningDetailPagePage.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 { Users, BarChart3, FolderKanban, Pencil } from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonAvatar, PersonName, initialsOf } from '../people/People';\nimport { SprintLifecycleActions, SprintWorkItemsPanel } from '../sprints/SprintControls';\nimport { SprintDates } from '../sprints/SprintDates';\nimport { useLiveRefresh, combineLiveRefreshModes } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\nimport { MeetingsCard } from '../meetings/MeetingsCard';\nimport { ScheduleCeremoniesDialog } from '../meetings/ScheduleCeremoniesDialog';\n\n/** Every query this page's own numbers depend on. */\nconst SPRINT_DETAIL_QUERIES = ['GetSprintDetail', 'GetSprintCapacity'];\n\nconst GET_SPRINT_DETAIL = gql`\n query GetSprintDetail($id: ID!) {\n planmagnetSprint(id: $id) {\n id\n name\n projectId\n project {\n id\n name\n }\n teamId\n team {\n id\n name\n projects {\n id\n name\n }\n }\n status\n startDate\n endDate\n startedAt\n completedAt\n baselineStartDate\n baselineEndDate\n planChanges {\n field\n fromValue\n toValue\n sprintStatus\n changedBy\n changedAt\n }\n goal\n capacity\n velocity\n workItems {\n id\n key\n title\n status\n storyPoints\n assigneeId\n priority\n }\n }\n }\n`;\n\n// Real cross-layer CrewFoundry workforce capacity (BOFF-4444). Fails open on\n// the backend — an unreachable/unprovisioned CrewFoundry never blocks this\n// page, it just yields crewfoundryUnavailable: true, handled below.\nconst GET_SPRINT_CAPACITY = gql`\n query GetSprintCapacity($sprintId: ID!) {\n planmagnetSprintCapacity(sprintId: $sprintId) {\n sprintId\n crewfoundryUnavailable\n unmatchedCount\n members {\n actorId\n employeeName\n department\n position\n allocatedHours\n billableHours\n utilizationPercent\n assignedStoryPoints\n assignedWorkItemCount\n }\n }\n }\n`;\n\nexport function ProjectsPlanningDetailPagePage() {\n const sprintId = useEntityId('sprintId');\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_SPRINT_DETAIL, {\n variables: { id: sprintId },\n skip: !sprintId,\n });\n\n // Real cross-layer CrewFoundry capacity (BOFF-4444) — called unconditionally\n // (with `skip`) to keep hook order stable across the loading/error/loaded\n // renders above.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { data: capacityData } = useQuery<Record<string, any>>(GET_SPRINT_CAPACITY, {\n variables: { sprintId },\n skip: !sprintId,\n });\n\n // Live updates (PDM-21/PDM-11): the sprint itself (status, dates, goal, capacity, its work item list) and —\n // once the sprint's project is known — that project's work items (a card's status/points changing without\n // ever being added or removed from the sprint). A team-scoped sprint has no `projectId` (BOFF-8044); the\n // work item stream simply stays off rather than falling back to every project's events.\n const sprintProjectId = data?.planmagnetSprint?.projectId ?? null;\n const sprintLive = useLiveRefresh({\n kind: 'sprint',\n sprintId,\n projectId: sprintProjectId,\n queries: SPRINT_DETAIL_QUERIES,\n });\n const workItemLive = useLiveRefresh({\n kind: 'workItem',\n projectId: sprintProjectId,\n enabled: Boolean(sprintProjectId),\n queries: SPRINT_DETAIL_QUERIES,\n });\n const liveMode = combineLiveRefreshModes(sprintLive.mode, workItemLive.mode);\n\n if (!sprintId)\n return (\n <div className=\"p-6 text-muted-foreground\">\n No planning session selected. Open one from the list.\n </div>\n );\n // Only before the first load: a refetch after a sprint action must not blank\n // the page (it unmounted the controls mid-action).\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 if (error)\n return <div className=\"p-6 text-red-500\">Error loading planning session: {error.message}</div>;\n\n const sprint = data?.planmagnetSprint;\n if (!sprint) return <div className=\"p-6 text-muted-foreground\">Planning session not found.</div>;\n\n const capacity = capacityData?.planmagnetSprintCapacity;\n const allocationByAssignee: Record<\n string,\n {\n employeeName: string;\n allocatedHours: number;\n billableHours: number;\n utilizationPercent: number | null;\n }\n > = {};\n for (const member of capacity?.members ?? []) {\n allocationByAssignee[member.actorId] = {\n employeeName: member.employeeName,\n allocatedHours: member.allocatedHours,\n billableHours: member.billableHours,\n utilizationPercent: member.utilizationPercent,\n };\n }\n\n const workItems = sprint.workItems ?? [];\n const totalPoints = workItems.reduce(\n (sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0),\n 0\n );\n const completedPoints = workItems\n .filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce((sum: number, wi: { storyPoints: number | null }) => sum + (wi.storyPoints ?? 0), 0);\n // No capacity set means \"not planned\", not \"full\": this used to take the allocated points as the capacity and show\n // 100% utilisation in red, while the list showed no capacity for the same sprint (BOFF-7974).\n const sprintCapacity: number | null = sprint.capacity ?? null;\n const capPct =\n sprintCapacity && sprintCapacity > 0 ? Math.round((totalPoints / sprintCapacity) * 100) : null;\n const remaining = sprintCapacity == null ? null : Math.max(0, sprintCapacity - totalPoints);\n\n // Derive unique assignees from work items\n const uniqueAssignees = [\n ...new Set(\n workItems\n .map((wi: { assigneeId: string | null }) => wi.assigneeId)\n .filter(Boolean) as string[]\n ),\n ];\n\n // A sprint's scope is PROJECT xor TEAM (BOFF-8044). A team sprint has no `project` — showing\n // \"Unknown project\" for it (BOFF-8044 review) was wrong on the one page a user lands on right\n // after creating a team sprint. Its eligible work items are the team's own live member\n // projects, never the whole workspace (see SprintWorkItemsPanel/AddWorkItemsDialog).\n const teamProjects: { id: string; name: string }[] = sprint.team?.projects ?? [];\n const eligibleProjectIds = sprint.team\n ? teamProjects.map((p) => p.id)\n : sprint.projectId\n ? [sprint.projectId]\n : [];\n const projectNames = sprint.team\n ? Object.fromEntries(teamProjects.map((p) => [p.id, p.name]))\n : undefined;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-3\">\n <h1 className=\"text-2xl font-semibold\">{sprint.name}</h1>\n <LiveStatus mode={liveMode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {sprint.team\n ? `Team: ${sprint.team.name}`\n : (sprint.project?.name ?? 'Unknown project')}\n </p>\n <div className=\"mt-1\">\n <SprintDates sprint={sprint} />\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <Badge className=\"capitalize\">{sprint.status.toLowerCase().replace('_', ' ')}</Badge>\n {/* The edit page existed but nothing linked to it, so a sprint's\n name, planned dates, goal and capacity could not be changed from\n the UI (BOFF-7954). Finished sprints are not edited here. */}\n {sprint.status !== 'COMPLETED' && sprint.status !== 'CANCELLED' && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/projects/planning-edit-page', sprint.id))}\n data-testid=\"sprint-edit\"\n >\n <Pencil className=\"mr-1 h-4 w-4\" /> Edit\n </Button>\n )}\n {/* CWF-23: only a project-scoped sprint can propose ceremonies — meetings are project-scoped\n (PlanmagnetMeeting.projectId is required), and a team sprint has no project of its own\n (BOFF-8044). */}\n {!sprint.team && (\n <ScheduleCeremoniesDialog\n projectId={sprint.projectId}\n sprintId={sprint.id}\n startDate={sprint.startDate}\n endDate={sprint.endDate}\n />\n )}\n <SprintLifecycleActions\n sprintId={sprint.id}\n sprintName={sprint.name}\n status={sprint.status}\n />\n </div>\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Capacity Allocation</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div className=\"p-4 bg-muted rounded-md\">\n <p className=\"text-2xl font-bold\">{sprintCapacity ?? '—'}</p>\n <p className=\"text-xs text-muted-foreground\">\n {sprintCapacity == null ? 'No capacity set' : 'Capacity'}\n </p>\n </div>\n <div className=\"p-4 bg-primary/10 rounded-md\">\n <p className=\"text-2xl font-bold text-primary\">{totalPoints}</p>\n <p className=\"text-xs text-muted-foreground\">Allocated</p>\n </div>\n <div className=\"p-4 bg-muted rounded-md\">\n <p className=\"text-2xl font-bold\">{remaining ?? '—'}</p>\n <p className=\"text-xs text-muted-foreground\">Remaining</p>\n </div>\n </div>\n {capPct !== null ? (\n <div>\n <div className=\"flex justify-between text-sm mb-2\">\n <span className=\"text-muted-foreground\">Utilization</span>\n <span className=\"font-medium\">{capPct}%</span>\n </div>\n <div className=\"w-full h-4 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${capPct > 90 ? 'bg-destructive' : 'bg-success'}`}\n style={{ width: `${Math.min(capPct, 100)}%` }}\n />\n </div>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n Set a capacity on the sprint to see its utilisation.\n </p>\n )}\n </CardContent>\n </Card>\n <Card>\n <CardHeader>\n <CardTitle>Work Items</CardTitle>\n </CardHeader>\n <CardContent>\n <SprintWorkItemsPanel\n sprintId={sprint.id}\n eligibleProjectIds={eligibleProjectIds}\n projectNames={projectNames}\n status={sprint.status}\n items={workItems}\n />\n </CardContent>\n </Card>\n <Card>\n <CardHeader>\n <CardTitle>Team Members ({uniqueAssignees.length})</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid grid-cols-2 gap-3\">\n {uniqueAssignees.map((assigneeId: string) => {\n const workforce = allocationByAssignee[assigneeId];\n return (\n <div\n key={assigneeId}\n className=\"flex items-center gap-2 border rounded-md p-2.5\"\n >\n {/* Initials from the name shown next to them, not from the UUID (BOFF-8000). */}\n {workforce?.employeeName ? (\n <span\n className=\"h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary\"\n title={workforce.employeeName}\n >\n {initialsOf(workforce.employeeName) || '?'}\n </span>\n ) : (\n <PersonAvatar\n id={assigneeId}\n className=\"h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary\"\n />\n )}\n <div className=\"min-w-0\">\n <span className=\"text-sm truncate block\">\n {workforce?.employeeName ?? <PersonName id={assigneeId} />}\n </span>\n {workforce && (\n <span className=\"text-xs text-muted-foreground\">\n {workforce.allocatedHours}h allocated ({workforce.billableHours}h\n billable)\n {workforce.utilizationPercent != null &&\n ` · ${workforce.utilizationPercent}% util`}\n </span>\n )}\n </div>\n </div>\n );\n })}\n {uniqueAssignees.length === 0 && (\n <p className=\"text-sm text-muted-foreground col-span-2 text-center py-4\">\n No team members assigned.\n </p>\n )}\n </div>\n {/* Real cross-layer CrewFoundry workforce allocation (BOFF-4444). */}\n {capacity?.crewfoundryUnavailable && uniqueAssignees.length > 0 && (\n <p className=\"text-xs text-muted-foreground mt-3 border-t pt-3\">\n Workforce allocation from CrewFoundry is unavailable for this workspace — no\n CrewFoundry module provisioned yet.\n </p>\n )}\n {!capacity?.crewfoundryUnavailable && (capacity?.unmatchedCount ?? 0) > 0 && (\n <p\n className=\"text-xs text-muted-foreground mt-3 border-t pt-3\"\n data-testid=\"sprint-capacity-unmatched\"\n >\n {capacity.unmatchedCount} assignee(s) have no matching CrewFoundry employee record\n — allocation hours shown only for matched employees.{' '}\n {/* CWF-30: the admin remediation panel on the Members page. */}\n <button\n type=\"button\"\n className=\"text-primary underline-offset-2 hover:underline\"\n data-testid=\"sprint-capacity-unmatched-link\"\n onClick={() => navigate('/team/team-page?focus=unmatched-crew-identities')}\n >\n Fix unmatched identities\n </button>\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Session Info</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-center gap-2\">\n {sprint.team ? (\n <Users className=\"h-4 w-4 text-muted-foreground\" />\n ) : (\n <FolderKanban className=\"h-4 w-4 text-muted-foreground\" />\n )}\n <span className=\"text-muted-foreground\">{sprint.team ? 'Team:' : 'Project:'}</span>\n <span className=\"font-medium\">\n {sprint.team ? sprint.team.name : (sprint.project?.name ?? 'Unknown project')}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <Users className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">Assignees:</span>\n <span className=\"font-medium\">{uniqueAssignees.length}</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">Capacity:</span>\n <span className=\"font-medium\">\n {sprintCapacity == null ? 'not set' : `${sprintCapacity} pts`}\n </span>\n </div>\n {sprint.velocity != null && (\n <div className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">Velocity:</span>\n <span className=\"font-medium\">{sprint.velocity} pts</span>\n </div>\n )}\n {sprint.goal && (\n <div className=\"border-t pt-3 mt-3\">\n <p className=\"text-muted-foreground text-xs mb-1\">Goal</p>\n <p className=\"text-sm\">{sprint.goal}</p>\n </div>\n )}\n </CardContent>\n </Card>\n {/* GAP-06, CWF-22/23: only a project-scoped sprint has meetings of its own — see the guard above\n ScheduleCeremoniesDialog. */}\n {!sprint.team && sprint.projectId && (\n <MeetingsCard\n workspaceId={workspaceId}\n projectId={sprint.projectId}\n sprintId={sprint.id}\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TeamTeamPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamTeamPagePage.tsx"],"names":[],"mappings":"AAyIA,wBAAgB,gBAAgB,4CAkD/B"}
1
+ {"version":3,"file":"TeamTeamPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamTeamPagePage.tsx"],"names":[],"mappings":"AA4IA,wBAAgB,gBAAgB,4CA6D/B"}
@@ -1,14 +1,17 @@
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
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 { Badge as u, Button as d, Card as f, CardContent as p } from "@burdenoff/fe-libs/ui";
9
- import { useWorkspaceDirectory as m } from "@burdenoff/fe-libs/sharing";
4
+ import { readUrlParam as a } from "../utils/urlParam.js";
5
+ import { UnmatchedCrewIdentitiesPanel as o } from "../team/UnmatchedCrewIdentities.js";
6
+ import { useEffect as s } from "react";
7
+ import { UserPlus as c } from "lucide-react";
8
+ import { gql as l } from "@apollo/client";
9
+ import { useQuery as u } from "@apollo/client/react";
10
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
11
+ import { Badge as p, Button as m, Card as h, CardContent as g } from "@burdenoff/fe-libs/ui";
12
+ import { useWorkspaceDirectory as _ } from "@burdenoff/fe-libs/sharing";
10
13
  //#region src/planmagnet/pages/TeamTeamPagePage.tsx
11
- var h = o`
14
+ var v = l`
12
15
  query TeamMemberCounts($workspaceId: ID!, $userId: String!) {
13
16
  planmagnetWorkItemCounts(workspaceId: $workspaceId, filter: { assigneeId: $userId }) {
14
17
  total
@@ -26,7 +29,7 @@ var h = o`
26
29
  totalCount
27
30
  }
28
31
  }
29
- `, g = o`
32
+ `, y = l`
30
33
  query TeamMemberRoles($actorId: String!, $scopeId: String!) {
31
34
  actorRoleAssignments(actorId: $actorId, scopeId: $scopeId, pagination: { limit: 10 }) {
32
35
  id
@@ -38,8 +41,8 @@ var h = o`
38
41
  }
39
42
  }
40
43
  `;
41
- function _({ userId: t }) {
42
- let { workspaceId: n } = e(), { data: r, error: i } = s(g, {
44
+ function b({ userId: t }) {
45
+ let { workspaceId: n } = e(), { data: r, error: i } = u(y, {
43
46
  variables: {
44
47
  actorId: t,
45
48
  scopeId: n
@@ -48,68 +51,68 @@ function _({ userId: t }) {
48
51
  });
49
52
  if (i) return null;
50
53
  let a = [...new Set((r?.actorRoleAssignments ?? []).filter((e) => e.isActive && e.role?.name).map((e) => e.role?.name))];
51
- return a.length === 0 ? null : /* @__PURE__ */ c("div", {
54
+ return a.length === 0 ? null : /* @__PURE__ */ d("div", {
52
55
  className: "flex flex-wrap gap-1",
53
56
  "data-testid": "team-member-roles",
54
- children: a.map((e) => /* @__PURE__ */ c(u, {
57
+ children: a.map((e) => /* @__PURE__ */ d(p, {
55
58
  variant: "secondary",
56
59
  className: "text-xs",
57
60
  children: e
58
61
  }, e))
59
62
  });
60
63
  }
61
- function v({ userId: t }) {
62
- let { workspaceId: a } = e(), { data: o, error: u } = s(h, {
64
+ function x({ userId: t }) {
65
+ let { workspaceId: a } = e(), { data: o, error: s } = u(v, {
63
66
  variables: {
64
67
  workspaceId: a,
65
68
  userId: t
66
69
  },
67
70
  skip: !a
68
- }), d = u ? void 0 : o?.planmagnetWorkItemCounts, m = d?.byStatus.find((e) => e.status === "DONE");
69
- return /* @__PURE__ */ c(f, {
71
+ }), c = s ? void 0 : o?.planmagnetWorkItemCounts, l = c?.byStatus.find((e) => e.status === "DONE");
72
+ return /* @__PURE__ */ d(h, {
70
73
  "data-testid": "team-member",
71
- children: /* @__PURE__ */ l(p, {
74
+ children: /* @__PURE__ */ f(g, {
72
75
  className: "pt-4 space-y-3",
73
- children: [/* @__PURE__ */ l("div", {
76
+ children: [/* @__PURE__ */ f("div", {
74
77
  className: "flex items-center gap-3",
75
- children: [/* @__PURE__ */ c(n, {
78
+ children: [/* @__PURE__ */ d(n, {
76
79
  id: t,
77
80
  className: "h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium text-primary"
78
- }), /* @__PURE__ */ l("div", {
81
+ }), /* @__PURE__ */ f("div", {
79
82
  className: "min-w-0 space-y-1",
80
- children: [/* @__PURE__ */ c("p", {
83
+ children: [/* @__PURE__ */ d("p", {
81
84
  className: "text-sm font-medium",
82
- children: /* @__PURE__ */ c(r, { id: t })
83
- }), /* @__PURE__ */ c(_, { userId: t })]
85
+ children: /* @__PURE__ */ d(r, { id: t })
86
+ }), /* @__PURE__ */ d(b, { userId: t })]
84
87
  })]
85
- }), /* @__PURE__ */ l("div", {
88
+ }), /* @__PURE__ */ f("div", {
86
89
  className: "grid grid-cols-4 gap-2 text-center text-xs",
87
90
  children: [
88
- /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
91
+ /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("p", {
89
92
  className: "font-semibold",
90
- children: i(d?.total)
91
- }), /* @__PURE__ */ c("p", {
93
+ children: i(c?.total)
94
+ }), /* @__PURE__ */ d("p", {
92
95
  className: "text-muted-foreground",
93
96
  children: "Assigned"
94
97
  })] }),
95
- /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
98
+ /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("p", {
96
99
  className: "font-semibold",
97
- children: i(d ? m?.count ?? 0 : null)
98
- }), /* @__PURE__ */ c("p", {
100
+ children: i(c ? l?.count ?? 0 : null)
101
+ }), /* @__PURE__ */ d("p", {
99
102
  className: "text-muted-foreground",
100
103
  children: "Done"
101
104
  })] }),
102
- /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
105
+ /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("p", {
103
106
  className: "font-semibold",
104
- children: i(d ? m?.points ?? 0 : null)
105
- }), /* @__PURE__ */ c("p", {
107
+ children: i(c ? l?.points ?? 0 : null)
108
+ }), /* @__PURE__ */ d("p", {
106
109
  className: "text-muted-foreground",
107
110
  children: "Points done"
108
111
  })] }),
109
- /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
112
+ /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("p", {
110
113
  className: "font-semibold",
111
- children: i(u ? null : o?.planmagnetProjects.totalCount)
112
- }), /* @__PURE__ */ c("p", {
114
+ children: i(s ? null : o?.planmagnetProjects.totalCount)
115
+ }), /* @__PURE__ */ d("p", {
113
116
  className: "text-muted-foreground",
114
117
  children: "Projects owned"
115
118
  })] })
@@ -118,45 +121,48 @@ function v({ userId: t }) {
118
121
  })
119
122
  });
120
123
  }
121
- function y() {
122
- let { workspaceId: n, authToken: r, apiGatewayUrl: i } = e(), o = t(), { members: s, loading: u, hasMore: f, fetchMore: p } = m({
124
+ function S() {
125
+ let { workspaceId: n, authToken: r, apiGatewayUrl: i } = e(), l = t(), { members: u, loading: p, hasMore: h, fetchMore: g } = _({
123
126
  workspaceId: n ?? "",
124
127
  authToken: r ?? void 0,
125
128
  apiGatewayUrl: i ?? void 0
126
129
  });
127
- return /* @__PURE__ */ l("div", {
130
+ return s(() => {
131
+ a("focus") === "unmatched-crew-identities" && document.getElementById("unmatched-crew-identities")?.scrollIntoView({ block: "start" });
132
+ }, []), /* @__PURE__ */ f("div", {
128
133
  className: "space-y-6 p-6",
129
134
  children: [
130
- /* @__PURE__ */ l("div", {
135
+ /* @__PURE__ */ f("div", {
131
136
  className: "flex items-start justify-between gap-4",
132
- children: [/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("h1", {
137
+ children: [/* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("h1", {
133
138
  className: "text-2xl font-semibold",
134
139
  children: "Team Overview"
135
- }), /* @__PURE__ */ c("p", {
140
+ }), /* @__PURE__ */ d("p", {
136
141
  className: "text-sm text-muted-foreground",
137
- children: u && s.length === 0 ? "Loading members…" : `${s.length}${f ? "+" : ""} workspace member${s.length === 1 ? "" : "s"}`
138
- })] }), /* @__PURE__ */ l(d, {
142
+ children: p && u.length === 0 ? "Loading members…" : `${u.length}${h ? "+" : ""} workspace member${u.length === 1 ? "" : "s"}`
143
+ })] }), /* @__PURE__ */ f(m, {
139
144
  size: "sm",
140
145
  variant: "outline",
141
- onClick: () => o("/team/invite-member-page"),
146
+ onClick: () => l("/team/invite-member-page"),
142
147
  "data-testid": "team-invite",
143
- children: [/* @__PURE__ */ c(a, { className: "mr-1 h-4 w-4" }), "Invite member"]
148
+ children: [/* @__PURE__ */ d(c, { className: "mr-1 h-4 w-4" }), "Invite member"]
144
149
  })]
145
150
  }),
146
- /* @__PURE__ */ c("div", {
151
+ /* @__PURE__ */ d(o, {}),
152
+ /* @__PURE__ */ d("div", {
147
153
  className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
148
- children: s.map((e) => /* @__PURE__ */ c(v, { userId: e.id }, e.id))
154
+ children: u.map((e) => /* @__PURE__ */ d(x, { userId: e.id }, e.id))
149
155
  }),
150
- !u && s.length === 0 && /* @__PURE__ */ c("p", {
156
+ !p && u.length === 0 && /* @__PURE__ */ d("p", {
151
157
  className: "text-sm text-muted-foreground text-center py-4",
152
158
  children: "No members found. Invite someone to the workspace to work together."
153
159
  }),
154
- f && /* @__PURE__ */ c("div", {
160
+ h && /* @__PURE__ */ d("div", {
155
161
  className: "text-center",
156
- children: /* @__PURE__ */ c(d, {
162
+ children: /* @__PURE__ */ d(m, {
157
163
  size: "sm",
158
164
  variant: "outline",
159
- onClick: p,
165
+ onClick: g,
160
166
  children: "Show more members"
161
167
  })
162
168
  })
@@ -164,6 +170,6 @@ function y() {
164
170
  });
165
171
  }
166
172
  //#endregion
167
- export { y as TeamTeamPagePage };
173
+ export { S as TeamTeamPagePage };
168
174
 
169
175
  //# sourceMappingURL=TeamTeamPagePage.js.map
@@ -1 +1 @@
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 { Badge, 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\n/**\n * The member's roles in this workspace (CWF-37: the roster shows each person's role). Needs rbac:read, which not every\n * viewer has: a refusal just hides the chips instead of breaking the card.\n */\nconst MEMBER_ROLES = gql`\n query TeamMemberRoles($actorId: String!, $scopeId: String!) {\n actorRoleAssignments(actorId: $actorId, scopeId: $scopeId, pagination: { limit: 10 }) {\n id\n isActive\n role {\n id\n name\n }\n }\n }\n`;\n\nfunction MemberRoles({ userId }: { userId: string }) {\n const { workspaceId } = usePlanMagnet();\n const { data, error } = useQuery<{\n actorRoleAssignments: {\n id: string;\n isActive: boolean;\n role: { id: string; name: string } | null;\n }[];\n }>(MEMBER_ROLES, {\n variables: { actorId: userId, scopeId: workspaceId },\n skip: !workspaceId,\n });\n if (error) return null;\n const names = [\n ...new Set(\n (data?.actorRoleAssignments ?? [])\n .filter((a) => a.isActive && a.role?.name)\n .map((a) => a.role?.name as string)\n ),\n ];\n if (names.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\" data-testid=\"team-member-roles\">\n {names.map((name) => (\n <Badge key={name} variant=\"secondary\" className=\"text-xs\">\n {name}\n </Badge>\n ))}\n </div>\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 <div className=\"min-w-0 space-y-1\">\n <p className=\"text-sm font-medium\">\n <PersonName id={userId} />\n </p>\n <MemberRoles userId={userId} />\n </div>\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":";;;;;;;;;;AAgBA,IAAM,IAAgB,CAAG;;;;;;;;;;;;;;;;;;GAwBnB,IAAe,CAAG;;;;;;;;;;;;AAaxB,SAAS,EAAY,EAAE,aAA8B;CACnD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EAMrB,GAAc;EACf,WAAW;GAAE,SAAS;GAAQ,SAAS;GAAa;EACpD,MAAM,CAAC;EACR,CAAC;AACF,KAAI,EAAO,QAAO;CAClB,IAAM,IAAQ,CACZ,GAAG,IAAI,KACJ,GAAM,wBAAwB,EAAE,EAC9B,QAAQ,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,CACzC,KAAK,MAAM,EAAE,MAAM,KAAe,CACtC,CACF;AAED,QADI,EAAM,WAAW,IAAU,OAE7B,kBAAC,OAAD;EAAK,WAAU;EAAuB,eAAY;YAC/C,EAAM,KAAK,MACV,kBAAC,GAAD;GAAkB,SAAQ;GAAY,WAAU;aAC7C;GACK,EAFI,EAEJ,CACR;EACE,CAAA;;AAIV,SAAS,EAAW,EAAE,aAA8B;CAClD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EAMrB,GAAe;EAAE,WAAW;GAAE;GAAa;GAAQ;EAAE,MAAM,CAAC;EAAa,CAAC,EACvE,IAAS,IAAQ,KAAA,IAAY,GAAM,0BACnC,IAAO,GAAQ,SAAS,MAAM,MAAM,EAAE,WAAW,OAAO;AAC9D,QACE,kBAAC,GAAD;EAAM,eAAY;YAChB,kBAAC,GAAD;GAAa,WAAU;aAAvB,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,IAAI;KACJ,WAAU;KACV,CAAA,EACF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBACX,kBAAC,GAAD,EAAY,IAAI,GAAU,CAAA;MACxB,CAAA,EACJ,kBAAC,GAAD,EAAqB,WAAU,CAAA,CAC3B;OACF;OACN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiB,EAAW,GAAQ,MAAM;MAAK,CAAA,EAC5D,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAY,CAAA,CAC7C,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiB,EAAW,IAAU,GAAM,SAAS,IAAK,KAAK;MAAK,CAAA,EACjF,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAQ,CAAA,CACzC,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiB,EAAW,IAAU,GAAM,UAAU,IAAK,KAAK;MAAK,CAAA,EAElF,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAe,CAAA,CAChD,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAW,IAAQ,OAAO,GAAM,mBAAmB,WAAW;MAC7D,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAkB,CAAA,CACnD,EAAA,CAAA;KACF;MACM;;EACT,CAAA;;AAIX,SAAgB,IAAmB;CACjC,IAAM,EAAE,gBAAa,cAAW,qBAAkB,GAAe,EAC3D,IAAW,GAAuB,EAClC,EAAE,YAAS,YAAS,YAAS,iBAAc,EAAsB;EACrE,aAAa,KAAe;EAC5B,WAAW,KAAa,KAAA;EACxB,eAAe,KAAiB,KAAA;EACjC,CAAC;AAEF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAyB;KAAkB,CAAA,EACzD,kBAAC,KAAD;KAAG,WAAU;eACV,KAAW,EAAQ,WAAW,IAC3B,qBACA,GAAG,EAAQ,SAAS,IAAU,MAAM,GAAG,mBAAmB,EAAQ,WAAW,IAAI,KAAK;KACxF,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAe,EAAS,2BAA2B;KACnD,eAAY;eAJd,CAME,kBAAC,GAAD,EAAU,WAAU,gBAAiB,CAAA,EAAA,gBAE9B;OACL;;GAEN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MACZ,kBAAC,GAAD,EAA4B,QAAQ,EAAO,IAAM,EAAhC,EAAO,GAAyB,CACjD;IACE,CAAA;GACL,CAAC,KAAW,EAAQ,WAAW,KAC9B,kBAAC,KAAD;IAAG,WAAU;cAAiD;IAE1D,CAAA;GAEL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAQ,MAAK;KAAK,SAAQ;KAAU,SAAS;eAAW;KAE/C,CAAA;IACL,CAAA;GAEJ"}
1
+ {"version":3,"file":"TeamTeamPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TeamTeamPagePage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useWorkspaceDirectory } from '@burdenoff/fe-libs/sharing';\nimport { Badge, 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';\nimport { readUrlParam } from '../utils/urlParam';\nimport { UnmatchedCrewIdentitiesPanel } from '../team/UnmatchedCrewIdentities';\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\n/**\n * The member's roles in this workspace (CWF-37: the roster shows each person's role). Needs rbac:read, which not every\n * viewer has: a refusal just hides the chips instead of breaking the card.\n */\nconst MEMBER_ROLES = gql`\n query TeamMemberRoles($actorId: String!, $scopeId: String!) {\n actorRoleAssignments(actorId: $actorId, scopeId: $scopeId, pagination: { limit: 10 }) {\n id\n isActive\n role {\n id\n name\n }\n }\n }\n`;\n\nfunction MemberRoles({ userId }: { userId: string }) {\n const { workspaceId } = usePlanMagnet();\n const { data, error } = useQuery<{\n actorRoleAssignments: {\n id: string;\n isActive: boolean;\n role: { id: string; name: string } | null;\n }[];\n }>(MEMBER_ROLES, {\n variables: { actorId: userId, scopeId: workspaceId },\n skip: !workspaceId,\n });\n if (error) return null;\n const names = [\n ...new Set(\n (data?.actorRoleAssignments ?? [])\n .filter((a) => a.isActive && a.role?.name)\n .map((a) => a.role?.name as string)\n ),\n ];\n if (names.length === 0) return null;\n return (\n <div className=\"flex flex-wrap gap-1\" data-testid=\"team-member-roles\">\n {names.map((name) => (\n <Badge key={name} variant=\"secondary\" className=\"text-xs\">\n {name}\n </Badge>\n ))}\n </div>\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 <div className=\"min-w-0 space-y-1\">\n <p className=\"text-sm font-medium\">\n <PersonName id={userId} />\n </p>\n <MemberRoles userId={userId} />\n </div>\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 // CWF-30: a deep link from the sprint capacity view (`?focus=unmatched-crew-identities`) scrolls straight to\n // the panel below instead of leaving the admin to hunt for it on a page that can run to hundreds of members.\n useEffect(() => {\n if (readUrlParam('focus') !== 'unmatched-crew-identities') return;\n document.getElementById('unmatched-crew-identities')?.scrollIntoView({ block: 'start' });\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 {/* CWF-30: workspace-wide, not per-team — a member can be unmatched regardless of which Team (BOFF-8044)\n they sit on, so this lives on the member roster rather than any one team's own page. */}\n <UnmatchedCrewIdentitiesPanel />\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":";;;;;;;;;;;;;AAmBA,IAAM,IAAgB,CAAG;;;;;;;;;;;;;;;;;;GAwBnB,IAAe,CAAG;;;;;;;;;;;;AAaxB,SAAS,EAAY,EAAE,aAA8B;CACnD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EAMrB,GAAc;EACf,WAAW;GAAE,SAAS;GAAQ,SAAS;GAAa;EACpD,MAAM,CAAC;EACR,CAAC;AACF,KAAI,EAAO,QAAO;CAClB,IAAM,IAAQ,CACZ,GAAG,IAAI,KACJ,GAAM,wBAAwB,EAAE,EAC9B,QAAQ,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,CACzC,KAAK,MAAM,EAAE,MAAM,KAAe,CACtC,CACF;AAED,QADI,EAAM,WAAW,IAAU,OAE7B,kBAAC,OAAD;EAAK,WAAU;EAAuB,eAAY;YAC/C,EAAM,KAAK,MACV,kBAAC,GAAD;GAAkB,SAAQ;GAAY,WAAU;aAC7C;GACK,EAFI,EAEJ,CACR;EACE,CAAA;;AAIV,SAAS,EAAW,EAAE,aAA8B;CAClD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,aAAU,EAMrB,GAAe;EAAE,WAAW;GAAE;GAAa;GAAQ;EAAE,MAAM,CAAC;EAAa,CAAC,EACvE,IAAS,IAAQ,KAAA,IAAY,GAAM,0BACnC,IAAO,GAAQ,SAAS,MAAM,MAAM,EAAE,WAAW,OAAO;AAC9D,QACE,kBAAC,GAAD;EAAM,eAAY;YAChB,kBAAC,GAAD;GAAa,WAAU;aAAvB,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,IAAI;KACJ,WAAU;KACV,CAAA,EACF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBACX,kBAAC,GAAD,EAAY,IAAI,GAAU,CAAA;MACxB,CAAA,EACJ,kBAAC,GAAD,EAAqB,WAAU,CAAA,CAC3B;OACF;OACN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiB,EAAW,GAAQ,MAAM;MAAK,CAAA,EAC5D,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAY,CAAA,CAC7C,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiB,EAAW,IAAU,GAAM,SAAS,IAAK,KAAK;MAAK,CAAA,EACjF,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAQ,CAAA,CACzC,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAiB,EAAW,IAAU,GAAM,UAAU,IAAK,KAAK;MAAK,CAAA,EAElF,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAe,CAAA,CAChD,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAW,IAAQ,OAAO,GAAM,mBAAmB,WAAW;MAC7D,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBAAwB;MAAkB,CAAA,CACnD,EAAA,CAAA;KACF;MACM;;EACT,CAAA;;AAIX,SAAgB,IAAmB;CACjC,IAAM,EAAE,gBAAa,cAAW,qBAAkB,GAAe,EAC3D,IAAW,GAAuB,EAClC,EAAE,YAAS,YAAS,YAAS,iBAAc,EAAsB;EACrE,aAAa,KAAe;EAC5B,WAAW,KAAa,KAAA;EACxB,eAAe,KAAiB,KAAA;EACjC,CAAC;AASF,QALA,QAAgB;AACV,IAAa,QAAQ,KAAK,+BAC9B,SAAS,eAAe,4BAA4B,EAAE,eAAe,EAAE,OAAO,SAAS,CAAC;IACvF,EAAE,CAAC,EAGJ,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAAyB;KAAkB,CAAA,EACzD,kBAAC,KAAD;KAAG,WAAU;eACV,KAAW,EAAQ,WAAW,IAC3B,qBACA,GAAG,EAAQ,SAAS,IAAU,MAAM,GAAG,mBAAmB,EAAQ,WAAW,IAAI,KAAK;KACxF,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAe,EAAS,2BAA2B;KACnD,eAAY;eAJd,CAME,kBAAC,GAAD,EAAU,WAAU,gBAAiB,CAAA,EAAA,gBAE9B;OACL;;GAIN,kBAAC,GAAD,EAAgC,CAAA;GAEhC,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MACZ,kBAAC,GAAD,EAA4B,QAAQ,EAAO,IAAM,EAAhC,EAAO,GAAyB,CACjD;IACE,CAAA;GACL,CAAC,KAAW,EAAQ,WAAW,KAC9B,kBAAC,KAAD;IAAG,WAAU;cAAiD;IAE1D,CAAA;GAEL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAQ,MAAK;KAAK,SAAQ;KAAU,SAAS;eAAW;KAE/C,CAAA;IACL,CAAA;GAEJ"}
@@ -0,0 +1,8 @@
1
+ export declare function MetricsSection({ workspaceId, projectId, startDate, endDate, }: {
2
+ workspaceId?: string | null;
3
+ /** Omitted = no project selected (ANR-09 scope set to "All projects") — nothing project-scoped to show. */
4
+ projectId?: string;
5
+ startDate: string;
6
+ endDate: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=MetricsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MetricsCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/pages/analytics/MetricsCard.tsx"],"names":[],"mappings":"AA0HA,wBAAgB,cAAc,CAAC,EAC7B,WAAW,EACX,SAAS,EACT,SAAS,EACT,OAAO,GACR,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,2GAA2G;IAC3G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,2CA2HA"}
@@ -0,0 +1,164 @@
1
+ import { StatTile as e, formatDuration as t } from "./DeliveryHealthCards.js";
2
+ import { Activity as n, Info as r } from "lucide-react";
3
+ import { gql as i } from "@apollo/client";
4
+ import { useQuery as a } from "@apollo/client/react";
5
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
6
+ import { useI18n as c } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
7
+ import { Card as l, CardContent as u, CardHeader as d, CardTitle as f } from "@burdenoff/fe-libs/ui";
8
+ //#region src/planmagnet/pages/analytics/MetricsCard.tsx
9
+ var p = i`
10
+ query GetWorkspaceMetrics($workspaceId: ID!, $timeRange: PlanmagnetTimeRangeInput) {
11
+ planmagnetMetrics(workspaceId: $workspaceId, timeRange: $timeRange) {
12
+ category
13
+ value
14
+ unit
15
+ trend
16
+ target
17
+ calculatedAt
18
+ }
19
+ }
20
+ `, m = i`
21
+ query GetProjectAnalyticsMetrics(
22
+ $workspaceId: ID!
23
+ $projectId: ID
24
+ $timeRange: PlanmagnetTimeRangeInput
25
+ ) {
26
+ planmagnetAnalytics(workspaceId: $workspaceId, projectId: $projectId, timeRange: $timeRange) {
27
+ projectMetrics {
28
+ category
29
+ value
30
+ unit
31
+ trend
32
+ target
33
+ calculatedAt
34
+ }
35
+ teamMetrics {
36
+ category
37
+ value
38
+ unit
39
+ trend
40
+ target
41
+ calculatedAt
42
+ }
43
+ }
44
+ }
45
+ `, h = {
46
+ sprintOnTimeRate: "Sprint on-time rate",
47
+ releaseOnTimeRate: "Release on-time rate",
48
+ milestoneOnTimeRate: "Milestone on-time rate",
49
+ scopeAdherenceRate: "Scope adherence rate",
50
+ velocity: "Velocity",
51
+ throughput: "Throughput",
52
+ cycleTime: "Cycle time",
53
+ wip: "Work in progress",
54
+ sprintRemainingPoints: "Sprint remaining points",
55
+ teamVelocity: "Team velocity"
56
+ };
57
+ function g(e) {
58
+ return h[e] ?? e;
59
+ }
60
+ function _(e) {
61
+ return e.unit === "%" ? `${Math.round(e.value)}%` : e.unit === "hours" ? t(e.value) : `${Math.round(e.value * 100) / 100} ${e.unit}`;
62
+ }
63
+ function v({ metric: t }) {
64
+ let n = [];
65
+ return t.trend != null && n.push(`Trend ${t.trend > 0 ? "+" : ""}${t.trend}`), t.target != null && n.push(`Target ${t.target}${t.unit === "%" ? "%" : ` ${t.unit}`}`), /* @__PURE__ */ o(e, {
66
+ label: g(t.category),
67
+ value: _(t),
68
+ hint: n.length > 0 ? n.join(" · ") : void 0
69
+ });
70
+ }
71
+ function y({ workspaceId: e, projectId: t, startDate: i, endDate: h }) {
72
+ let { t: g } = c(), _ = {
73
+ startDate: i,
74
+ endDate: h
75
+ }, { data: y, loading: b, error: x } = a(p, {
76
+ variables: {
77
+ workspaceId: e,
78
+ timeRange: _
79
+ },
80
+ skip: !e
81
+ }), { data: S, loading: C, error: w } = a(m, {
82
+ variables: {
83
+ workspaceId: e,
84
+ projectId: t,
85
+ timeRange: _
86
+ },
87
+ skip: !e || !t
88
+ }), T = y?.planmagnetMetrics ?? [], E = S?.planmagnetAnalytics.projectMetrics ?? [], D = S?.planmagnetAnalytics.teamMetrics ?? [], O = [...E, ...D];
89
+ return /* @__PURE__ */ s(l, {
90
+ "data-testid": "metrics-card",
91
+ children: [/* @__PURE__ */ s(d, { children: [/* @__PURE__ */ s(f, {
92
+ className: "flex items-center gap-2",
93
+ children: [/* @__PURE__ */ o(n, { className: "h-4 w-4" }), g("planmagnet.analytics.metrics.title", "Metrics")]
94
+ }), /* @__PURE__ */ o("p", {
95
+ className: "text-xs text-muted-foreground",
96
+ children: g("planmagnet.analytics.metrics.subtitle", "Workspace-wide KPIs, plus the selected project's own metrics, composed from the queries above into one generic shape.")
97
+ })] }), /* @__PURE__ */ s(u, {
98
+ className: "space-y-4",
99
+ children: [
100
+ /* @__PURE__ */ s("div", { children: [/* @__PURE__ */ o("h3", {
101
+ className: "text-sm font-medium mb-1",
102
+ children: g("planmagnet.analytics.metrics.workspaceTitle", "Workspace")
103
+ }), b ? /* @__PURE__ */ o("p", {
104
+ className: "text-sm text-muted-foreground",
105
+ children: g("planmagnet.analytics.loading", "Loading…")
106
+ }) : x ? /* @__PURE__ */ s("p", {
107
+ className: "text-sm text-status-error-text",
108
+ children: [
109
+ g("planmagnet.analytics.metrics.error", "Metrics could not be loaded"),
110
+ ":",
111
+ " ",
112
+ x.message
113
+ ]
114
+ }) : T.length === 0 ? /* @__PURE__ */ o("p", {
115
+ className: "text-sm text-muted-foreground py-2",
116
+ children: g("planmagnet.analytics.metrics.workspaceEmpty", "Nothing to show in this range yet.")
117
+ }) : /* @__PURE__ */ o("div", {
118
+ className: "grid grid-cols-2 gap-3 sm:grid-cols-4",
119
+ "data-testid": "metrics-workspace-tiles",
120
+ children: T.map((e) => /* @__PURE__ */ o(v, { metric: e }, e.category))
121
+ })] }),
122
+ /* @__PURE__ */ s("div", {
123
+ className: "border-t pt-3",
124
+ children: [/* @__PURE__ */ o("h3", {
125
+ className: "text-sm font-medium mb-1",
126
+ children: g("planmagnet.analytics.metrics.projectTitle", "Selected project")
127
+ }), t ? C ? /* @__PURE__ */ o("p", {
128
+ className: "text-sm text-muted-foreground",
129
+ children: g("planmagnet.analytics.loading", "Loading…")
130
+ }) : w ? /* @__PURE__ */ s("p", {
131
+ className: "text-sm text-status-error-text",
132
+ children: [
133
+ g("planmagnet.analytics.metrics.projectError", "Project metrics could not be loaded"),
134
+ ": ",
135
+ w.message
136
+ ]
137
+ }) : O.length === 0 ? /* @__PURE__ */ o("p", {
138
+ className: "text-sm text-muted-foreground py-2",
139
+ children: g("planmagnet.analytics.metrics.projectEmpty", "Nothing to show for this project in this range yet.")
140
+ }) : /* @__PURE__ */ o("div", {
141
+ className: "grid grid-cols-2 gap-3 sm:grid-cols-4",
142
+ "data-testid": "metrics-project-tiles",
143
+ children: O.map((e) => /* @__PURE__ */ o(v, { metric: e }, e.category))
144
+ }) : /* @__PURE__ */ o("p", {
145
+ className: "text-sm text-muted-foreground py-2",
146
+ children: g("planmagnet.analytics.metrics.noProject", "Pick a project above to see its own metrics.")
147
+ })]
148
+ }),
149
+ /* @__PURE__ */ s("p", {
150
+ className: "flex items-start gap-1.5 text-xs text-muted-foreground border-t pt-2",
151
+ "data-testid": "metrics-cache-note",
152
+ children: [/* @__PURE__ */ o(r, {
153
+ className: "h-3.5 w-3.5 shrink-0 mt-0.5",
154
+ "aria-hidden": !0
155
+ }), g("planmagnet.analytics.metrics.cacheNote", "Updated within the last minute — these metrics are cached for 60 seconds.")]
156
+ })
157
+ ]
158
+ })]
159
+ });
160
+ }
161
+ //#endregion
162
+ export { y as MetricsSection };
163
+
164
+ //# sourceMappingURL=MetricsCard.js.map