@burdenoff/microfe-planmagnet 2026.1002.2 → 2026.1002.3

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 (154) hide show
  1. package/dist/generated/wspace-operations.d.ts +574 -0
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +486 -16
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/index.js +8 -8
  6. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  7. package/dist/planmagnet/PlanMagnetRoot.js +174 -168
  8. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  9. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  10. package/dist/planmagnet/PlanMagnetRoutes.js +63 -59
  11. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  12. package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
  13. package/dist/planmagnet/board/BulkActions.js +106 -55
  14. package/dist/planmagnet/board/BulkActions.js.map +1 -1
  15. package/dist/planmagnet/controls/Pickers.d.ts +3 -3
  16. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  17. package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
  18. package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
  19. package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
  20. package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
  21. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
  22. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
  23. package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
  24. package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
  25. package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
  26. package/dist/planmagnet/index.d.ts +1 -1
  27. package/dist/planmagnet/index.d.ts.map +1 -1
  28. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
  29. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
  31. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
  32. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
  33. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
  34. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
  35. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
  36. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
  37. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
  38. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
  39. package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
  40. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  41. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +94 -92
  42. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  43. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  44. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
  45. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  46. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  47. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
  48. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  49. package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
  50. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
  51. package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
  52. package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
  53. package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
  54. package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
  55. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  56. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +157 -155
  57. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  58. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
  59. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
  60. package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
  61. package/dist/planmagnet/product/FeatureLinkPanels.js +66 -66
  62. package/dist/planmagnet/product/ProductFeatureControls.js +38 -38
  63. package/dist/planmagnet/product/ProductFeatureLinkCard.js +8 -8
  64. package/dist/planmagnet/product/ProductLineControls.js +83 -83
  65. package/dist/planmagnet/product/ProductTeamsPanel.js +6 -6
  66. package/dist/planmagnet/product/routes.d.ts +6 -0
  67. package/dist/planmagnet/product/routes.d.ts.map +1 -1
  68. package/dist/planmagnet/product/routes.js +2 -2
  69. package/dist/planmagnet/product/routes.js.map +1 -1
  70. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
  71. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
  72. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
  73. package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
  74. package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
  75. package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
  76. package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
  77. package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
  78. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
  79. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
  80. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
  81. package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
  82. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
  83. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
  84. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
  85. package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
  86. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
  87. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
  88. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
  89. package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
  90. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
  91. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
  92. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
  93. package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
  94. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
  95. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
  96. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
  97. package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
  98. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
  99. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
  100. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
  101. package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
  102. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
  103. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
  104. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
  105. package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
  106. package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
  107. package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
  108. package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
  109. package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
  110. package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
  111. package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
  112. package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
  113. package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
  114. package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
  115. package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
  116. package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
  117. package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
  118. package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
  119. package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
  120. package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
  121. package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
  122. package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
  123. package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
  124. package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
  125. package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
  126. package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
  127. package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
  128. package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
  129. package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
  130. package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
  131. package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
  132. package/dist/planmagnet/skills/SkillChips.js +57 -0
  133. package/dist/planmagnet/skills/SkillChips.js.map +1 -0
  134. package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
  135. package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
  136. package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
  137. package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
  138. package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
  139. package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
  140. package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
  141. package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
  142. package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
  143. package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
  144. package/dist/planmagnet/skills/skillOperations.js +44 -0
  145. package/dist/planmagnet/skills/skillOperations.js.map +1 -0
  146. package/dist/planmagnet/skills/skillTags.d.ts +103 -0
  147. package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
  148. package/dist/planmagnet/skills/skillTags.js +115 -0
  149. package/dist/planmagnet/skills/skillTags.js.map +1 -0
  150. package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
  151. package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
  152. package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
  153. package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
  154. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"BulkActions.js","names":[],"sources":["../../../src/planmagnet/board/BulkActions.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { X } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { WORK_ITEM_PRIORITIES, enumLabel, useAssigneeOptions } from '../controls/WorkItemControls';\nimport { type ProjectWorkflow, workflowLabel } from '../hooks/useProjectWorkflow';\n\n/**\n * Bulk update on the project board (BOFF-8047, capability register: Bulk update, `api-only`). The resolver and its\n * whitelist (`BULK_UPDATE_ALLOWED_FIELDS`: status, priority, assigneeId, sprintId, type, labels) have been real\n * since before this change; nothing in the UI ever called it. The server checks every selected item against the\n * project's workflow (BOFF-8043) and writes all-or-nothing — a refused move names how many items and why, and\n * nothing is changed. This bar only sends the fields the user actually touched; the rest are left out of the\n * mutation input entirely (not sent as \"no change\" — omitted), which is what a real bulk mutation call needs\n * (a `null` on `assigneeId`/`sprintId` explicitly clears it).\n */\n\nconst NO_CHANGE = '';\nconst UNASSIGN = '__unassign__';\nconst NO_SPRINT = '__no_sprint__';\nconst OPEN_SPRINT_STATUSES = new Set(['PLANNING', 'ACTIVE', 'REVIEW']);\n\nconst BULK_UPDATE = gql`\n mutation ProjectBoardBulkUpdate($input: PlanmagnetBulkUpdateWorkItemsInput!) {\n planmagnetBulkUpdateWorkItems(input: $input) {\n id\n status\n priority\n assigneeId\n sprintId\n }\n }\n`;\n\nconst PROJECT_SPRINTS = gql`\n query BulkActionsProjectSprints($workspaceId: ID!, $projectId: String) {\n planmagnetSprints(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n name\n status\n }\n }\n }\n`;\n\n/** Every query whose numbers a bulk change can move — the same list ProjectBoard's own mutations refetch. */\nexport const BULK_REFETCH = [\n 'ProjectBoard',\n 'ProjectBoards',\n 'GetWorkItems',\n 'GetWorkItemCounts',\n 'GetDashboardData',\n];\n\nexport function BulkActionsBar({\n projectId,\n selectedIds,\n workflow,\n onDone,\n}: {\n projectId: string;\n selectedIds: string[];\n workflow: ProjectWorkflow | null;\n onDone: () => void;\n}) {\n const { workspaceId } = usePlanMagnet();\n const [status, setStatus] = useState(NO_CHANGE);\n const [priority, setPriority] = useState(NO_CHANGE);\n const [assigneeId, setAssigneeId] = useState(NO_CHANGE);\n const [sprintId, setSprintId] = useState(NO_CHANGE);\n const [error, setError] = useState<string | null>(null);\n const [applied, setApplied] = useState<number | null>(null);\n\n const { options: assigneeOptions } = useAssigneeOptions(null);\n const { data: sprintsData } = useQuery<{\n planmagnetSprints?: { items: { id: string; name: string; status: string }[] };\n }>(PROJECT_SPRINTS, {\n variables: { workspaceId: workspaceId ?? '', projectId },\n skip: !workspaceId,\n });\n const sprints = (sprintsData?.planmagnetSprints?.items ?? []).filter((s) =>\n OPEN_SPRINT_STATUSES.has(s.status)\n );\n\n const [bulkUpdate, state] = useMutation<{ planmagnetBulkUpdateWorkItems: { id: string }[] }>(\n BULK_UPDATE,\n { refetchQueries: BULK_REFETCH, awaitRefetchQueries: true }\n );\n\n // Visible statuses this project uses; a hidden status stays off the bulk picker (matches every other status\n // control here). The server still owns the real per-item transition rule — this only offers sane targets.\n const statuses = (workflow?.statuses ?? []).filter((s) => !s.hidden);\n\n const dirty =\n status !== NO_CHANGE ||\n priority !== NO_CHANGE ||\n assigneeId !== NO_CHANGE ||\n sprintId !== NO_CHANGE;\n\n const reset = () => {\n setStatus(NO_CHANGE);\n setPriority(NO_CHANGE);\n setAssigneeId(NO_CHANGE);\n setSprintId(NO_CHANGE);\n };\n\n const apply = async () => {\n setError(null);\n setApplied(null);\n const input: Record<string, unknown> = { ids: selectedIds };\n if (status !== NO_CHANGE) input.status = status;\n if (priority !== NO_CHANGE) input.priority = priority;\n if (assigneeId !== NO_CHANGE) input.assigneeId = assigneeId === UNASSIGN ? null : assigneeId;\n if (sprintId !== NO_CHANGE) input.sprintId = sprintId === NO_SPRINT ? null : sprintId;\n try {\n const result = await throwOnError(bulkUpdate({ variables: { input } }));\n // The selection is left in place on success (rather than cleared): clearing it would unmount this bar\n // before its \"N tasks updated\" message could ever be read — the same class of bug as a refusal message\n // sitting on a row a refetch removes. The user clears it themselves once they have seen the result.\n setApplied(result.data?.planmagnetBulkUpdateWorkItems.length ?? selectedIds.length);\n reset();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The selected tasks could not be updated.');\n }\n };\n\n const onField =\n <T,>(setter: (v: T) => void) =>\n (value: T) => {\n setApplied(null);\n setError(null);\n setter(value);\n };\n\n return (\n <div\n className=\"flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 p-2\"\n data-testid=\"board-bulk-actions\"\n >\n <span className=\"text-sm font-medium\" data-testid=\"board-bulk-count\">\n {selectedIds.length} selected\n </span>\n\n <select\n aria-label=\"Bulk: set status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={status}\n disabled={state.loading}\n onChange={(e) => onField(setStatus)(e.target.value)}\n >\n <option value={NO_CHANGE}>Status: no change</option>\n {statuses.map((s) => (\n <option key={s.status} value={s.status}>\n Status: {workflowLabel(workflow, s.status)}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Bulk: set priority\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={priority}\n disabled={state.loading}\n onChange={(e) => onField(setPriority)(e.target.value)}\n >\n <option value={NO_CHANGE}>Priority: no change</option>\n {WORK_ITEM_PRIORITIES.map((p) => (\n <option key={p} value={p}>\n Priority: {enumLabel(p)}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Bulk: set assignee\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm max-w-[12rem]\"\n value={assigneeId}\n disabled={state.loading}\n onChange={(e) => onField(setAssigneeId)(e.target.value)}\n >\n <option value={NO_CHANGE}>Assignee: no change</option>\n <option value={UNASSIGN}>Assignee: Unassign</option>\n {assigneeOptions.map((o) => (\n <option key={o.id} value={o.id}>\n Assignee: {o.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Bulk: set sprint\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm max-w-[12rem]\"\n value={sprintId}\n disabled={state.loading}\n onChange={(e) => onField(setSprintId)(e.target.value)}\n >\n <option value={NO_CHANGE}>Sprint: no change</option>\n <option value={NO_SPRINT}>Sprint: Remove from sprint</option>\n {sprints.map((s) => (\n <option key={s.id} value={s.id}>\n Sprint: {s.name}\n </option>\n ))}\n </select>\n\n <Button\n size=\"sm\"\n data-testid=\"board-bulk-apply\"\n disabled={!dirty || state.loading || selectedIds.length === 0}\n onClick={() => void apply()}\n >\n {state.loading ? 'Applying…' : `Apply to ${selectedIds.length}`}\n </Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDone} disabled={state.loading}>\n <X className=\"mr-1 h-4 w-4\" />\n Clear selection\n </Button>\n\n {applied !== null && (\n <span role=\"status\" className=\"text-sm text-status-success-text\">\n {applied} task{applied === 1 ? '' : 's'} updated.\n </span>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BulkActions.js","names":[],"sources":["../../../src/planmagnet/board/BulkActions.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { X } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { WORK_ITEM_PRIORITIES, enumLabel, useAssigneeOptions } from '../controls/WorkItemControls';\nimport { type ProjectWorkflow, workflowLabel } from '../hooks/useProjectWorkflow';\nimport { RequiredSkillsEditor } from '../skills/RequiredSkillsEditor';\nimport { requiredSkillsRefusal } from '../skills/skillTags';\nimport { useTr } from '../../shared/hooks/useTr';\n\n/**\n * Bulk update on the project board (BOFF-8047, capability register: Bulk update, `api-only`). The resolver and its\n * whitelist (`BULK_UPDATE_ALLOWED_FIELDS`: status, priority, assigneeId, sprintId, type, labels) have been real\n * since before this change; nothing in the UI ever called it. The server checks every selected item against the\n * project's workflow (BOFF-8043) and writes all-or-nothing — a refused move names how many items and why, and\n * nothing is changed. This bar only sends the fields the user actually touched; the rest are left out of the\n * mutation input entirely (not sent as \"no change\" — omitted), which is what a real bulk mutation call needs\n * (a `null` on `assigneeId`/`sprintId` explicitly clears it).\n *\n * Required skills (ANR-07, ANR-28): `requiredSkills` REPLACES every selected item's list (the service's rule), so\n * the bar offers \"replace with…\" (at least one skill) and an explicit \"clear\", never a silent merge.\n */\n\nconst NO_CHANGE = '';\nconst UNASSIGN = '__unassign__';\nconst NO_SPRINT = '__no_sprint__';\nconst SKILLS_REPLACE = 'replace';\nconst SKILLS_CLEAR = 'clear';\nconst OPEN_SPRINT_STATUSES = new Set(['PLANNING', 'ACTIVE', 'REVIEW']);\n\nconst BULK_UPDATE = gql`\n mutation ProjectBoardBulkUpdate($input: PlanmagnetBulkUpdateWorkItemsInput!) {\n planmagnetBulkUpdateWorkItems(input: $input) {\n id\n status\n priority\n assigneeId\n sprintId\n }\n }\n`;\n\nconst PROJECT_SPRINTS = gql`\n query BulkActionsProjectSprints($workspaceId: ID!, $projectId: String) {\n planmagnetSprints(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n name\n status\n }\n }\n }\n`;\n\n/** Every query whose numbers a bulk change can move — the same list ProjectBoard's own mutations refetch. */\nexport const BULK_REFETCH = [\n 'ProjectBoard',\n 'ProjectBoards',\n 'GetWorkItems',\n 'GetWorkItemCounts',\n 'GetDashboardData',\n];\n\nexport function BulkActionsBar({\n projectId,\n selectedIds,\n workflow,\n onDone,\n}: {\n projectId: string;\n selectedIds: string[];\n workflow: ProjectWorkflow | null;\n onDone: () => void;\n}) {\n const { workspaceId } = usePlanMagnet();\n const tr = useTr();\n const [status, setStatus] = useState(NO_CHANGE);\n const [priority, setPriority] = useState(NO_CHANGE);\n const [assigneeId, setAssigneeId] = useState(NO_CHANGE);\n const [sprintId, setSprintId] = useState(NO_CHANGE);\n const [skillsMode, setSkillsMode] = useState(NO_CHANGE);\n const [skills, setSkills] = useState<string[]>([]);\n const [skillsError, setSkillsError] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [applied, setApplied] = useState<number | null>(null);\n\n const { options: assigneeOptions } = useAssigneeOptions(null);\n const { data: sprintsData } = useQuery<{\n planmagnetSprints?: { items: { id: string; name: string; status: string }[] };\n }>(PROJECT_SPRINTS, {\n variables: { workspaceId: workspaceId ?? '', projectId },\n skip: !workspaceId,\n });\n const sprints = (sprintsData?.planmagnetSprints?.items ?? []).filter((s) =>\n OPEN_SPRINT_STATUSES.has(s.status)\n );\n\n const [bulkUpdate, state] = useMutation<{ planmagnetBulkUpdateWorkItems: { id: string }[] }>(\n BULK_UPDATE,\n { refetchQueries: BULK_REFETCH, awaitRefetchQueries: true }\n );\n\n // Visible statuses this project uses; a hidden status stays off the bulk picker (matches every other status\n // control here). The server still owns the real per-item transition rule — this only offers sane targets.\n const statuses = (workflow?.statuses ?? []).filter((s) => !s.hidden);\n\n // \"Replace with…\" needs at least one skill: an empty replacement is a clear, which has its own explicit option.\n const skillsDirty =\n skillsMode === SKILLS_CLEAR || (skillsMode === SKILLS_REPLACE && skills.length > 0);\n const dirty =\n status !== NO_CHANGE ||\n priority !== NO_CHANGE ||\n assigneeId !== NO_CHANGE ||\n sprintId !== NO_CHANGE ||\n skillsDirty;\n\n const reset = () => {\n setStatus(NO_CHANGE);\n setPriority(NO_CHANGE);\n setAssigneeId(NO_CHANGE);\n setSprintId(NO_CHANGE);\n setSkillsMode(NO_CHANGE);\n setSkills([]);\n setSkillsError(null);\n };\n\n const apply = async () => {\n setError(null);\n setSkillsError(null);\n setApplied(null);\n const input: Record<string, unknown> = { ids: selectedIds };\n if (status !== NO_CHANGE) input.status = status;\n if (priority !== NO_CHANGE) input.priority = priority;\n if (assigneeId !== NO_CHANGE) input.assigneeId = assigneeId === UNASSIGN ? null : assigneeId;\n if (sprintId !== NO_CHANGE) input.sprintId = sprintId === NO_SPRINT ? null : sprintId;\n if (skillsDirty) input.requiredSkills = skillsMode === SKILLS_CLEAR ? [] : skills;\n try {\n const result = await throwOnError(bulkUpdate({ variables: { input } }));\n // The selection is left in place on success (rather than cleared): clearing it would unmount this bar\n // before its \"N tasks updated\" message could ever be read — the same class of bug as a refusal message\n // sitting on a row a refetch removes. The user clears it themselves once they have seen the result.\n setApplied(result.data?.planmagnetBulkUpdateWorkItems.length ?? selectedIds.length);\n reset();\n } catch (e) {\n // A refusal of the skills (BAD_USER_INPUT tagged requiredSkills) is shown on the skills editor itself.\n const skillsRefusal = skillsMode === SKILLS_REPLACE ? requiredSkillsRefusal(e) : null;\n if (skillsRefusal) setSkillsError(skillsRefusal);\n else setError(e instanceof Error ? e.message : 'The selected tasks could not be updated.');\n }\n };\n\n const onField =\n <T,>(setter: (v: T) => void) =>\n (value: T) => {\n setApplied(null);\n setError(null);\n setter(value);\n };\n\n return (\n <div\n className=\"flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 p-2\"\n data-testid=\"board-bulk-actions\"\n >\n <span className=\"text-sm font-medium\" data-testid=\"board-bulk-count\">\n {selectedIds.length} selected\n </span>\n\n <select\n aria-label=\"Bulk: set status\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={status}\n disabled={state.loading}\n onChange={(e) => onField(setStatus)(e.target.value)}\n >\n <option value={NO_CHANGE}>Status: no change</option>\n {statuses.map((s) => (\n <option key={s.status} value={s.status}>\n Status: {workflowLabel(workflow, s.status)}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Bulk: set priority\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm\"\n value={priority}\n disabled={state.loading}\n onChange={(e) => onField(setPriority)(e.target.value)}\n >\n <option value={NO_CHANGE}>Priority: no change</option>\n {WORK_ITEM_PRIORITIES.map((p) => (\n <option key={p} value={p}>\n Priority: {enumLabel(p)}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Bulk: set assignee\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm max-w-[12rem]\"\n value={assigneeId}\n disabled={state.loading}\n onChange={(e) => onField(setAssigneeId)(e.target.value)}\n >\n <option value={NO_CHANGE}>Assignee: no change</option>\n <option value={UNASSIGN}>Assignee: Unassign</option>\n {assigneeOptions.map((o) => (\n <option key={o.id} value={o.id}>\n Assignee: {o.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Bulk: set sprint\"\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm max-w-[12rem]\"\n value={sprintId}\n disabled={state.loading}\n onChange={(e) => onField(setSprintId)(e.target.value)}\n >\n <option value={NO_CHANGE}>Sprint: no change</option>\n <option value={NO_SPRINT}>Sprint: Remove from sprint</option>\n {sprints.map((s) => (\n <option key={s.id} value={s.id}>\n Sprint: {s.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label={tr('planmagnet.skills.bulk.selectLabel', 'Bulk: set required skills')}\n className=\"rounded-md border border-input bg-background px-2 py-1 text-sm max-w-[14rem]\"\n value={skillsMode}\n disabled={state.loading}\n data-testid=\"board-bulk-skills-mode\"\n onChange={(e) => {\n setSkillsError(null);\n onField(setSkillsMode)(e.target.value);\n }}\n >\n <option value={NO_CHANGE}>\n {tr('planmagnet.skills.bulk.noChange', 'Required skills: no change')}\n </option>\n <option value={SKILLS_REPLACE}>\n {tr('planmagnet.skills.bulk.replace', 'Required skills: replace with…')}\n </option>\n <option value={SKILLS_CLEAR}>\n {tr('planmagnet.skills.bulk.clear', 'Required skills: clear')}\n </option>\n </select>\n\n <Button\n size=\"sm\"\n data-testid=\"board-bulk-apply\"\n disabled={!dirty || state.loading || selectedIds.length === 0}\n onClick={() => void apply()}\n >\n {state.loading ? 'Applying…' : `Apply to ${selectedIds.length}`}\n </Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={onDone} disabled={state.loading}>\n <X className=\"mr-1 h-4 w-4\" />\n Clear selection\n </Button>\n\n {applied !== null && (\n <span role=\"status\" className=\"text-sm text-status-success-text\">\n {applied} task{applied === 1 ? '' : 's'} updated.\n </span>\n )}\n {error && <ErrorLine message={error} />}\n {skillsMode === SKILLS_REPLACE && (\n <div className=\"basis-full\" data-testid=\"board-bulk-skills\">\n <label htmlFor=\"board-bulk-required-skills\" className=\"text-sm font-medium\">\n {tr('planmagnet.skills.bulk.editorLabel', 'Replace required skills with')}\n </label>\n <RequiredSkillsEditor\n id=\"board-bulk-required-skills\"\n value={skills}\n onChange={(next) => {\n setApplied(null);\n setSkillsError(null);\n setSkills(next);\n }}\n disabled={state.loading}\n serverError={skillsError}\n hint={tr(\n 'planmagnet.skills.bulk.hint',\n 'Every selected task gets exactly this list; its current required skills are replaced.'\n )}\n />\n </div>\n )}\n {skillsMode === SKILLS_CLEAR && (\n <p\n className=\"basis-full text-xs text-muted-foreground\"\n data-testid=\"board-bulk-skills-clear\"\n >\n {tr(\n 'planmagnet.skills.bulk.clearHint',\n 'Removes every required skill from the selected tasks.'\n )}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -90,9 +90,9 @@ export declare function RoadmapSelect({ id, value, onChange, noneLabel, productI
90
90
  * Search (client-side — `CrewEmployeeFilterInput` has no text-search field of its own), then pick one
91
91
  * CrewFoundry employee (CWF-30). Unlike every other picker in this file this one never takes a `workspaceId`
92
92
  * variable: `crewEmployees` resolves the roster from the caller's own `x-workspace-id` header, same as the rest
93
- * of the CrewFoundry subgraph. A workspace with no CrewFoundry module provisioned, or an unreachable
94
- * CrewFoundry, surfaces as an empty list here rather than a page crash — the same fails-open contract
95
- * `planmagnetSprintCapacity.crewfoundryUnavailable` documents for the read side.
93
+ * of the CrewFoundry subgraph. An unreachable CrewFoundry, or an empty roster, surfaces as an empty list here
94
+ * rather than a page crash. (`planmagnetSprintCapacity` tells the two apart on the read side: its
95
+ * `crewfoundryUnavailable` means only that the roster could not be read; an empty roster is not unavailable.)
96
96
  */
97
97
  export declare function CrewEmployeeSelect({ id, value, onChange, noneLabel, disabled, }: {
98
98
  id?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"Pickers.js","names":[],"sources":["../../../src/planmagnet/controls/Pickers.tsx"],"sourcesContent":["import { useDeferredValue, useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * Record pickers shared by forms and dialogs: a project select, and search-then-pick selects for work items and\n * docs. Values are record ids. Search runs on the server (never \"the first page, filtered in the browser\"), so a\n * record past the first page can still be found.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst PICKER_PROJECTS = gql`\n query PickerProjects($workspaceId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $pagination) {\n items {\n id\n name\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n/** Projects are read page by page up to this many, so the picker never silently stops at the first 100. */\nconst MAX_PROJECT_OPTIONS = 500;\n\n/** A project select. '' means \"no project\" when `noneLabel` is given. `exclude` hides ids already spoken for. */\nexport function ProjectSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (projectId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjects: {\n items: { id: string; name: string }[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(PICKER_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '', pagination: { first: 100 } },\n skip: !workspaceId,\n });\n const loaded = data?.planmagnetProjects.items ?? [];\n const hasNextPage = data?.planmagnetProjects.pageInfo.hasNextPage ?? false;\n const endCursor = data?.planmagnetProjects.pageInfo.endCursor ?? null;\n useEffect(() => {\n if (!hasNextPage || !endCursor || loaded.length >= MAX_PROJECT_OPTIONS) return;\n fetchMore({\n variables: { pagination: { first: 100, after: endCursor } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjects: {\n ...fetchMoreResult.planmagnetProjects,\n items: [...prev.planmagnetProjects.items, ...fetchMoreResult.planmagnetProjects.items],\n },\n }),\n }).catch(() => {\n // A failed extra page leaves the projects already loaded usable.\n });\n }, [hasNextPage, endCursor, loaded.length, fetchMore]);\n const projects = loaded.filter((p) => !exclude.includes(p.id));\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {projects.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading projects…'\n : error\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n {/* Never a silent cut-off: say so when the workspace has more projects than the picker lists. */}\n {hasNextPage && loaded.length >= MAX_PROJECT_OPTIONS && (\n <option value=\"\" disabled>\n {`Only the first ${MAX_PROJECT_OPTIONS} projects are listed`}\n </option>\n )}\n </select>\n );\n}\n\nconst PICKER_TEAMS = gql`\n query PickerTeams($workspaceId: ID!) {\n planmagnetTeams(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A team select (BOFF-8044). '' means \"no team\" when `noneLabel` is given. `exclude` hides teams already spoken for\n * (PRM-01: the teams already linked to a product).\n */\nexport function TeamSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (teamId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetTeams: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_TEAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const teams = (data?.planmagnetTeams.items ?? []).filter((t) => !exclude.includes(t.id));\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Team'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && (\n <option value=\"\">\n {/* Only when something was excluded: '' keeps its caller's meaning (\"All teams\") otherwise. */}\n {exclude.length > 0 && data && teams.length === 0 ? 'No other team to choose' : noneLabel}\n </option>\n )}\n {teams.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading teams…'\n : error\n ? 'Teams could not be loaded'\n : 'No teams yet'}\n </option>\n )}\n {teams.map((t) => (\n <option key={t.id} value={t.id}>\n {t.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_SPRINTS = gql`\n query PickerSprints($workspaceId: ID!, $filter: PlanmagnetSprintFilter) {\n planmagnetSprints(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n status\n }\n totalCount\n }\n }\n`;\n\n/** A sprint select, optionally scoped to one project (report-builder widgets: BURNDOWN reads one sprint). '' means\n * \"no sprint\" when `noneLabel` is given. */\nexport function SprintSelect({\n id,\n value,\n onChange,\n projectId,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (sprintId: string) => void;\n projectId?: string;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetSprints: {\n items: { id: string; name: string; status: string }[];\n totalCount: number;\n };\n }>(PICKER_SPRINTS, {\n variables: { workspaceId: workspaceId ?? '', filter: projectId ? { projectId } : undefined },\n skip: !workspaceId,\n });\n const sprints = data?.planmagnetSprints.items ?? [];\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Sprint'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {sprints.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading sprints…'\n : error\n ? 'Sprints could not be loaded'\n : 'No sprints yet'}\n </option>\n )}\n {sprints.map((s) => (\n <option key={s.id} value={s.id}>\n {s.name} ({s.status.toLowerCase()})\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_WORK_ITEMS = gql`\n query PickerWorkItems($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n }\n totalCount\n }\n }\n`;\n\n/**\n * Search, then pick one work item. The server matches the key, title and description. `exclude` hides items that are\n * already linked (the result says how many more exist, so an empty list never claims \"nothing left\" falsely).\n */\nexport function WorkItemSearchSelect({\n id,\n projectId,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n projectId?: string;\n value: string;\n onChange: (workItemId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; key: string; title: string }[];\n totalCount: number;\n };\n }>(PICKER_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { ...(deferred && { search: deferred }), ...(projectId && { projectId }) },\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetWorkItems.items ?? [];\n const items = found.filter((w) => !exclude.includes(w.id));\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search work items\"\n placeholder=\"Search by key or title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : items.length ? 'Choose a work item' : 'No match'}\n </option>\n {/* The current value stays selectable (and visible) when it is not in this page of results. */}\n {value && !items.some((w) => w.id === value) && (\n <option value={value}>Currently linked work item</option>\n )}\n {items.map((w) => (\n <option key={w.id} value={w.id}>\n {w.key} — {w.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_DOCS = gql`\n query PickerDocs($workspaceId: String, $filter: PlanmagnetDocFilter) {\n planmagnetDocs(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n title\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one doc. */\nexport function DocSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (docId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetDocs: { items: { id: string; title: string }[]; totalCount: number };\n }>(PICKER_DOCS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetDocs.items ?? [];\n const docs = found.filter((d) => !exclude.includes(d.id));\n const total = data?.planmagnetDocs.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search docs\"\n placeholder=\"Search docs by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : docs.length ? 'Choose a doc' : 'No match'}\n </option>\n {docs.map((d) => (\n <option key={d.id} value={d.id}>\n {d.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PROGRAMS = gql`\n query PickerPrograms($workspaceId: ID!) {\n planmagnetPrograms(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A program select. '' means \"no program\" when `noneLabel` is given. */\nexport function ProgramSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (programId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPrograms: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROGRAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const programs = data?.planmagnetPrograms.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {programs.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading programs…'\n : error\n ? 'Programs could not be loaded'\n : 'No programs yet'}\n </option>\n )}\n {programs.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_PORTFOLIOS = gql`\n query PickerPortfolios($workspaceId: ID!) {\n planmagnetPortfolios(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A portfolio select (BOFF-8047). '' means \"no portfolio\" when `noneLabel` is given. */\nexport function PortfolioSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (portfolioId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPortfolios: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PORTFOLIOS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const portfolios = data?.planmagnetPortfolios.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {portfolios.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading portfolios…'\n : error\n ? 'Portfolios could not be loaded'\n : 'No portfolios yet'}\n </option>\n )}\n {portfolios.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_COMPETITORS = gql`\n query PickerCompetitors($workspaceId: ID!, $filter: PlanmagnetCompetitorFilter) {\n planmagnetCompetitors(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one competitor. `exclude` hides competitors already linked. */\nexport function CompetitorSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (competitorId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetCompetitors: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_COMPETITORS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetCompetitors.items ?? [];\n const competitors = found.filter((c) => !exclude.includes(c.id));\n const total = data?.planmagnetCompetitors.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search competitors\"\n placeholder=\"Search competitors by name\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : competitors.length\n ? 'Choose a competitor'\n : 'No match'}\n </option>\n {competitors.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PRODUCTS = gql`\n query PickerProducts($workspaceId: ID!) {\n planmagnetProducts(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A product select (PRM-27). '' means \"every product\" when `noneLabel` is given. */\nexport function ProductSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (productId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetProducts: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PRODUCTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const products = data?.planmagnetProducts.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {products.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading products…'\n : error\n ? 'Products could not be loaded'\n : 'No products yet'}\n </option>\n )}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_ROADMAPS = gql`\n query PickerRoadmaps($workspaceId: ID!, $filter: PlanmagnetRoadmapFilter) {\n planmagnetRoadmaps(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A roadmap select (PRM-27). '' means \"every roadmap\" when `noneLabel` is given. `productId` narrows the list\n * to that product's roadmaps, same filter FeatureInitiativeCard uses for its own roadmap picker.\n */\nexport function RoadmapSelect({\n id,\n value,\n onChange,\n noneLabel,\n productId,\n}: {\n id?: string;\n value: string;\n onChange: (roadmapId: string) => void;\n noneLabel?: string;\n productId?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetRoadmaps: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_ROADMAPS, {\n variables: { workspaceId: workspaceId ?? '', filter: productId ? { productId } : undefined },\n skip: !workspaceId,\n });\n const roadmaps = data?.planmagnetRoadmaps.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {roadmaps.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading roadmaps…'\n : error\n ? 'Roadmaps could not be loaded'\n : 'No roadmaps yet'}\n </option>\n )}\n {roadmaps.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_CREW_EMPLOYEES = gql`\n query PickerCrewEmployees($pagination: PaginationInput) {\n crewEmployees(pagination: $pagination) {\n id\n firstName\n lastName\n email\n }\n }\n`;\n\n/** CrewFoundry's roster is normally a few hundred people at most; past this the search box narrows what the\n * select shows rather than ever paging further. */\nconst MAX_CREW_EMPLOYEE_OPTIONS = 200;\n\n/**\n * Search (client-side — `CrewEmployeeFilterInput` has no text-search field of its own), then pick one\n * CrewFoundry employee (CWF-30). Unlike every other picker in this file this one never takes a `workspaceId`\n * variable: `crewEmployees` resolves the roster from the caller's own `x-workspace-id` header, same as the rest\n * of the CrewFoundry subgraph. A workspace with no CrewFoundry module provisioned, or an unreachable\n * CrewFoundry, surfaces as an empty list here rather than a page crash — the same fails-open contract\n * `planmagnetSprintCapacity.crewfoundryUnavailable` documents for the read side.\n */\nexport function CrewEmployeeSelect({\n id,\n value,\n onChange,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (crewEmployeeId: string) => void;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim().toLowerCase());\n const { data, loading, error } = useQuery<{\n crewEmployees: { id: string; firstName: string; lastName: string; email: string }[];\n }>(PICKER_CREW_EMPLOYEES, {\n variables: { pagination: { limit: MAX_CREW_EMPLOYEE_OPTIONS } },\n });\n const loaded = error ? [] : (data?.crewEmployees ?? []);\n const employees = deferred\n ? loaded.filter(\n (e) =>\n `${e.firstName} ${e.lastName}`.toLowerCase().includes(deferred) ||\n e.email.toLowerCase().includes(deferred)\n )\n : loaded;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search CrewFoundry employees\"\n placeholder=\"Search by name or e-mail\"\n value={search}\n disabled={disabled}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {employees.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data ? 'Loading…' : error ? 'CrewFoundry roster unavailable' : 'No match'}\n </option>\n )}\n {employees.map((e) => (\n <option key={e.id} value={e.id}>\n {e.firstName} {e.lastName} — {e.email}\n </option>\n ))}\n </select>\n {!deferred && loaded.length >= MAX_CREW_EMPLOYEE_OPTIONS && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the first {MAX_CREW_EMPLOYEE_OPTIONS} employees — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Pickers.js","names":[],"sources":["../../../src/planmagnet/controls/Pickers.tsx"],"sourcesContent":["import { useDeferredValue, useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * Record pickers shared by forms and dialogs: a project select, and search-then-pick selects for work items and\n * docs. Values are record ids. Search runs on the server (never \"the first page, filtered in the browser\"), so a\n * record past the first page can still be found.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst PICKER_PROJECTS = gql`\n query PickerProjects($workspaceId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $pagination) {\n items {\n id\n name\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n/** Projects are read page by page up to this many, so the picker never silently stops at the first 100. */\nconst MAX_PROJECT_OPTIONS = 500;\n\n/** A project select. '' means \"no project\" when `noneLabel` is given. `exclude` hides ids already spoken for. */\nexport function ProjectSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (projectId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjects: {\n items: { id: string; name: string }[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(PICKER_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '', pagination: { first: 100 } },\n skip: !workspaceId,\n });\n const loaded = data?.planmagnetProjects.items ?? [];\n const hasNextPage = data?.planmagnetProjects.pageInfo.hasNextPage ?? false;\n const endCursor = data?.planmagnetProjects.pageInfo.endCursor ?? null;\n useEffect(() => {\n if (!hasNextPage || !endCursor || loaded.length >= MAX_PROJECT_OPTIONS) return;\n fetchMore({\n variables: { pagination: { first: 100, after: endCursor } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjects: {\n ...fetchMoreResult.planmagnetProjects,\n items: [...prev.planmagnetProjects.items, ...fetchMoreResult.planmagnetProjects.items],\n },\n }),\n }).catch(() => {\n // A failed extra page leaves the projects already loaded usable.\n });\n }, [hasNextPage, endCursor, loaded.length, fetchMore]);\n const projects = loaded.filter((p) => !exclude.includes(p.id));\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {projects.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading projects…'\n : error\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n {/* Never a silent cut-off: say so when the workspace has more projects than the picker lists. */}\n {hasNextPage && loaded.length >= MAX_PROJECT_OPTIONS && (\n <option value=\"\" disabled>\n {`Only the first ${MAX_PROJECT_OPTIONS} projects are listed`}\n </option>\n )}\n </select>\n );\n}\n\nconst PICKER_TEAMS = gql`\n query PickerTeams($workspaceId: ID!) {\n planmagnetTeams(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A team select (BOFF-8044). '' means \"no team\" when `noneLabel` is given. `exclude` hides teams already spoken for\n * (PRM-01: the teams already linked to a product).\n */\nexport function TeamSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (teamId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetTeams: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_TEAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const teams = (data?.planmagnetTeams.items ?? []).filter((t) => !exclude.includes(t.id));\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Team'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && (\n <option value=\"\">\n {/* Only when something was excluded: '' keeps its caller's meaning (\"All teams\") otherwise. */}\n {exclude.length > 0 && data && teams.length === 0 ? 'No other team to choose' : noneLabel}\n </option>\n )}\n {teams.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading teams…'\n : error\n ? 'Teams could not be loaded'\n : 'No teams yet'}\n </option>\n )}\n {teams.map((t) => (\n <option key={t.id} value={t.id}>\n {t.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_SPRINTS = gql`\n query PickerSprints($workspaceId: ID!, $filter: PlanmagnetSprintFilter) {\n planmagnetSprints(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n status\n }\n totalCount\n }\n }\n`;\n\n/** A sprint select, optionally scoped to one project (report-builder widgets: BURNDOWN reads one sprint). '' means\n * \"no sprint\" when `noneLabel` is given. */\nexport function SprintSelect({\n id,\n value,\n onChange,\n projectId,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (sprintId: string) => void;\n projectId?: string;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetSprints: {\n items: { id: string; name: string; status: string }[];\n totalCount: number;\n };\n }>(PICKER_SPRINTS, {\n variables: { workspaceId: workspaceId ?? '', filter: projectId ? { projectId } : undefined },\n skip: !workspaceId,\n });\n const sprints = data?.planmagnetSprints.items ?? [];\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Sprint'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {sprints.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading sprints…'\n : error\n ? 'Sprints could not be loaded'\n : 'No sprints yet'}\n </option>\n )}\n {sprints.map((s) => (\n <option key={s.id} value={s.id}>\n {s.name} ({s.status.toLowerCase()})\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_WORK_ITEMS = gql`\n query PickerWorkItems($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n }\n totalCount\n }\n }\n`;\n\n/**\n * Search, then pick one work item. The server matches the key, title and description. `exclude` hides items that are\n * already linked (the result says how many more exist, so an empty list never claims \"nothing left\" falsely).\n */\nexport function WorkItemSearchSelect({\n id,\n projectId,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n projectId?: string;\n value: string;\n onChange: (workItemId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; key: string; title: string }[];\n totalCount: number;\n };\n }>(PICKER_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { ...(deferred && { search: deferred }), ...(projectId && { projectId }) },\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetWorkItems.items ?? [];\n const items = found.filter((w) => !exclude.includes(w.id));\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search work items\"\n placeholder=\"Search by key or title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : items.length ? 'Choose a work item' : 'No match'}\n </option>\n {/* The current value stays selectable (and visible) when it is not in this page of results. */}\n {value && !items.some((w) => w.id === value) && (\n <option value={value}>Currently linked work item</option>\n )}\n {items.map((w) => (\n <option key={w.id} value={w.id}>\n {w.key} — {w.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_DOCS = gql`\n query PickerDocs($workspaceId: String, $filter: PlanmagnetDocFilter) {\n planmagnetDocs(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n title\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one doc. */\nexport function DocSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (docId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetDocs: { items: { id: string; title: string }[]; totalCount: number };\n }>(PICKER_DOCS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetDocs.items ?? [];\n const docs = found.filter((d) => !exclude.includes(d.id));\n const total = data?.planmagnetDocs.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search docs\"\n placeholder=\"Search docs by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : docs.length ? 'Choose a doc' : 'No match'}\n </option>\n {docs.map((d) => (\n <option key={d.id} value={d.id}>\n {d.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PROGRAMS = gql`\n query PickerPrograms($workspaceId: ID!) {\n planmagnetPrograms(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A program select. '' means \"no program\" when `noneLabel` is given. */\nexport function ProgramSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (programId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPrograms: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROGRAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const programs = data?.planmagnetPrograms.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {programs.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading programs…'\n : error\n ? 'Programs could not be loaded'\n : 'No programs yet'}\n </option>\n )}\n {programs.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_PORTFOLIOS = gql`\n query PickerPortfolios($workspaceId: ID!) {\n planmagnetPortfolios(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A portfolio select (BOFF-8047). '' means \"no portfolio\" when `noneLabel` is given. */\nexport function PortfolioSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (portfolioId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPortfolios: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PORTFOLIOS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const portfolios = data?.planmagnetPortfolios.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {portfolios.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading portfolios…'\n : error\n ? 'Portfolios could not be loaded'\n : 'No portfolios yet'}\n </option>\n )}\n {portfolios.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_COMPETITORS = gql`\n query PickerCompetitors($workspaceId: ID!, $filter: PlanmagnetCompetitorFilter) {\n planmagnetCompetitors(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one competitor. `exclude` hides competitors already linked. */\nexport function CompetitorSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (competitorId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetCompetitors: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_COMPETITORS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetCompetitors.items ?? [];\n const competitors = found.filter((c) => !exclude.includes(c.id));\n const total = data?.planmagnetCompetitors.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search competitors\"\n placeholder=\"Search competitors by name\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : competitors.length\n ? 'Choose a competitor'\n : 'No match'}\n </option>\n {competitors.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PRODUCTS = gql`\n query PickerProducts($workspaceId: ID!) {\n planmagnetProducts(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A product select (PRM-27). '' means \"every product\" when `noneLabel` is given. */\nexport function ProductSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (productId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetProducts: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PRODUCTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const products = data?.planmagnetProducts.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {products.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading products…'\n : error\n ? 'Products could not be loaded'\n : 'No products yet'}\n </option>\n )}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_ROADMAPS = gql`\n query PickerRoadmaps($workspaceId: ID!, $filter: PlanmagnetRoadmapFilter) {\n planmagnetRoadmaps(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A roadmap select (PRM-27). '' means \"every roadmap\" when `noneLabel` is given. `productId` narrows the list\n * to that product's roadmaps, same filter FeatureInitiativeCard uses for its own roadmap picker.\n */\nexport function RoadmapSelect({\n id,\n value,\n onChange,\n noneLabel,\n productId,\n}: {\n id?: string;\n value: string;\n onChange: (roadmapId: string) => void;\n noneLabel?: string;\n productId?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetRoadmaps: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_ROADMAPS, {\n variables: { workspaceId: workspaceId ?? '', filter: productId ? { productId } : undefined },\n skip: !workspaceId,\n });\n const roadmaps = data?.planmagnetRoadmaps.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {roadmaps.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading roadmaps…'\n : error\n ? 'Roadmaps could not be loaded'\n : 'No roadmaps yet'}\n </option>\n )}\n {roadmaps.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_CREW_EMPLOYEES = gql`\n query PickerCrewEmployees($pagination: PaginationInput) {\n crewEmployees(pagination: $pagination) {\n id\n firstName\n lastName\n email\n }\n }\n`;\n\n/** CrewFoundry's roster is normally a few hundred people at most; past this the search box narrows what the\n * select shows rather than ever paging further. */\nconst MAX_CREW_EMPLOYEE_OPTIONS = 200;\n\n/**\n * Search (client-side — `CrewEmployeeFilterInput` has no text-search field of its own), then pick one\n * CrewFoundry employee (CWF-30). Unlike every other picker in this file this one never takes a `workspaceId`\n * variable: `crewEmployees` resolves the roster from the caller's own `x-workspace-id` header, same as the rest\n * of the CrewFoundry subgraph. An unreachable CrewFoundry, or an empty roster, surfaces as an empty list here\n * rather than a page crash. (`planmagnetSprintCapacity` tells the two apart on the read side: its\n * `crewfoundryUnavailable` means only that the roster could not be read; an empty roster is not unavailable.)\n */\nexport function CrewEmployeeSelect({\n id,\n value,\n onChange,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (crewEmployeeId: string) => void;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim().toLowerCase());\n const { data, loading, error } = useQuery<{\n crewEmployees: { id: string; firstName: string; lastName: string; email: string }[];\n }>(PICKER_CREW_EMPLOYEES, {\n variables: { pagination: { limit: MAX_CREW_EMPLOYEE_OPTIONS } },\n });\n const loaded = error ? [] : (data?.crewEmployees ?? []);\n const employees = deferred\n ? loaded.filter(\n (e) =>\n `${e.firstName} ${e.lastName}`.toLowerCase().includes(deferred) ||\n e.email.toLowerCase().includes(deferred)\n )\n : loaded;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search CrewFoundry employees\"\n placeholder=\"Search by name or e-mail\"\n value={search}\n disabled={disabled}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {employees.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data ? 'Loading…' : error ? 'CrewFoundry roster unavailable' : 'No match'}\n </option>\n )}\n {employees.map((e) => (\n <option key={e.id} value={e.id}>\n {e.firstName} {e.lastName} — {e.email}\n </option>\n ))}\n </select>\n {!deferred && loaded.length >= MAX_CREW_EMPLOYEE_OPTIONS && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the first {MAX_CREW_EMPLOYEE_OPTIONS} employees — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAYA,IAAM,IACJ,6FAEI,IAAkB,CAAG;;;;;;;;;;;;;;GAiBrB,IAAsB;AAG5B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,cACA,aAAU,EAAE,EACZ,cAAW,MAQV;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,UAAO,iBAAc,EAMzC,GAAiB;EAClB,WAAW;GAAE,aAAa,KAAe;GAAI,YAAY,EAAE,OAAO,KAAK;GAAE;EACzE,MAAM,CAAC;EACR,CAAC,EACI,IAAS,GAAM,mBAAmB,SAAS,EAAE,EAC7C,IAAc,GAAM,mBAAmB,SAAS,eAAe,IAC/D,IAAY,GAAM,mBAAmB,SAAS,aAAa;AACjE,SAAgB;AACV,GAAC,KAAe,CAAC,KAAa,EAAO,UAAU,KACnD,EAAU;GACR,WAAW,EAAE,YAAY;IAAE,OAAO;IAAK,OAAO;IAAW,EAAE;GAC3D,cAAc,GAAM,EAAE,0BAAuB,EAC3C,oBAAoB;IAClB,GAAG,EAAgB;IACnB,OAAO,CAAC,GAAG,EAAK,mBAAmB,OAAO,GAAG,EAAgB,mBAAmB,MAAM;IACvF,EACF;GACF,CAAC,CAAC,YAAY,GAEb;IACD;EAAC;EAAa;EAAW,EAAO;EAAQ;EAAU,CAAC;CACtD,IAAM,IAAW,EAAO,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC;AAC9D,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACG;EACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAL3C;GAOG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GAED,KAAe,EAAO,UAAU,KAC/B,kBAAC,UAAD;IAAQ,OAAM;IAAG,UAAA;cACd,kBAAkB,EAAoB;IAChC,CAAA;GAEJ;;;AAIb,IAAM,IAAe,CAAG;;;;;;;;;;;AAgBxB,SAAgB,EAAW,EACzB,OACA,UACA,aACA,cACA,aAAU,EAAE,EACZ,cAAW,MAQV;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAc;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACjF,KAAS,GAAM,gBAAgB,SAAS,EAAE,EAAE,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC;AACxF,QACE,kBAAC,UAAD;EACM;EACJ,cAAY,IAAK,KAAA,IAAY;EAC7B,WAAW;EACJ;EACG;EACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAN3C;GAQG,MAAc,KAAA,KACb,kBAAC,UAAD;IAAQ,OAAM;cAEX,EAAQ,SAAS,KAAK,KAAQ,EAAM,WAAW,IAAI,4BAA4B;IACzE,CAAA;GAEV,EAAM,WAAW,KAAK,MAAc,KAAA,KACnC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,mBACA,IACE,8BACA;IACC,CAAA;GAEV,EAAM,KAAK,MACV,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAiB,CAAG;;;;;;;;;;;;AAe1B,SAAgB,EAAa,EAC3B,OACA,UACA,aACA,cACA,cACA,cAAW,MAQV;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAK9B,GAAgB;EACjB,WAAW;GAAE,aAAa,KAAe;GAAI,QAAQ,IAAY,EAAE,cAAW,GAAG,KAAA;GAAW;EAC5F,MAAM,CAAC;EACR,CAAC,EACI,IAAU,GAAM,kBAAkB,SAAS,EAAE;AACnD,QACE,kBAAC,UAAD;EACM;EACJ,cAAY,IAAK,KAAA,IAAY;EAC7B,WAAW;EACJ;EACG;EACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAN3C;GAQG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAQ,WAAW,KAAK,MAAc,KAAA,KACrC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,qBACA,IACE,gCACA;IACC,CAAA;GAEV,EAAQ,KAAK,MACZ,kBAAC,UAAD;IAAmB,OAAO,EAAE;cAA5B;KACG,EAAE;KAAK;KAAG,EAAE,OAAO,aAAa;KAAC;KAC3B;MAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAoB,CAAG;;;;;;;;;;;;AAiB7B,SAAgB,EAAqB,EACnC,OACA,cACA,UACA,aACA,aAAU,EAAE,IAOX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAKvB,GAAmB;EACpB,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ;IAAE,GAAI,KAAY,EAAE,QAAQ,GAAU;IAAG,GAAI,KAAa,EAAE,cAAW;IAAG;GACnF;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,oBAAoB,SAAS,EAAE,EAC7C,IAAQ,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EACpD,IAAQ,GAAM,oBAAoB,cAAc;AACtD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C;KAME,kBAAC,UAAD;MAAQ,OAAM;gBACX,KAAW,CAAC,IAAO,eAAe,EAAM,SAAS,uBAAuB;MAClE,CAAA;KAER,KAAS,CAAC,EAAM,MAAM,MAAM,EAAE,OAAO,EAAM,IAC1C,kBAAC,UAAD;MAAe;gBAAO;MAAmC,CAAA;KAE1D,EAAM,KAAK,MACV,kBAAC,UAAD;MAAmB,OAAO,EAAE;gBAA5B;OACG,EAAE;OAAI;OAAI,EAAE;OACN;QAFI,EAAE,GAEN,CACT;KACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAc,CAAG;;;;;;;;;;;AAavB,SAAgB,EAAgB,EAC9B,OACA,UACA,aACA,aAAU,EAAE,IAMX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAEvB,GAAa;EACd,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ,IAAW,EAAE,QAAQ,GAAU,GAAG,KAAA;GAC3C;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,eAAe,SAAS,EAAE,EACxC,IAAO,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EACnD,IAAQ,GAAM,eAAe,cAAc;AACjD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C,CAME,kBAAC,UAAD;KAAQ,OAAM;eACX,KAAW,CAAC,IAAO,eAAe,EAAK,SAAS,iBAAiB;KAC3D,CAAA,EACR,EAAK,KAAK,MACT,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAkB,CAAG;;;;;;;;;;;AAa3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACpF,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAoB,CAAG;;;;;;;;;;;AAa7B,SAAgB,EAAgB,EAC9B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAmB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACtF,IAAa,GAAM,qBAAqB,SAAS,EAAE;AACzD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAW,WAAW,KAAK,MAAc,KAAA,KACxC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,wBACA,IACE,mCACA;IACC,CAAA;GAEV,EAAW,KAAK,MACf,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAqB,CAAG;;;;;;;;;;;AAa9B,SAAgB,EAAuB,EACrC,OACA,UACA,aACA,aAAU,EAAE,IAMX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAEvB,GAAoB;EACrB,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ,IAAW,EAAE,QAAQ,GAAU,GAAG,KAAA;GAC3C;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,sBAAsB,SAAS,EAAE,EAC/C,IAAc,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EAC1D,IAAQ,GAAM,sBAAsB,cAAc;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C,CAME,kBAAC,UAAD;KAAQ,OAAM;eACX,KAAW,CAAC,IACT,eACA,EAAY,SACV,wBACA;KACC,CAAA,EACR,EAAY,KAAK,MAChB,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAkB,CAAG;;;;;;;;;;;AAa3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACpF,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAkB,CAAG;;;;;;;;;;;AAgB3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,cACA,gBAOC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAClB,WAAW;GAAE,aAAa,KAAe;GAAI,QAAQ,IAAY,EAAE,cAAW,GAAG,KAAA;GAAW;EAC5F,MAAM,CAAC;EACR,CAAC,EACI,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAwB,CAAG;;;;;;;;;GAa3B,IAA4B;AAUlC,SAAgB,EAAmB,EACjC,OACA,UACA,aACA,cACA,cAAW,MAOV;CACD,IAAM,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,aAAa,CAAC,EACxD,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAuB,EACxB,WAAW,EAAE,YAAY,EAAE,OAAO,GAA2B,EAAE,EAChE,CAAC,EACI,IAAS,IAAQ,EAAE,GAAI,GAAM,iBAAiB,EAAE,EAChD,IAAY,IACd,EAAO,QACJ,MACC,GAAG,EAAE,UAAU,GAAG,EAAE,WAAW,aAAa,CAAC,SAAS,EAAS,IAC/D,EAAE,MAAM,aAAa,CAAC,SAAS,EAAS,CAC3C,GACD;AACJ,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACG;IACV,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACG;IACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAL3C;KAOG,MAAc,KAAA,KAAa,kBAAC,UAAD;MAAQ,OAAM;gBAAI;MAAmB,CAAA;KAChE,EAAU,WAAW,KAAK,MAAc,KAAA,KACvC,kBAAC,UAAD;MAAQ,OAAM;gBACX,KAAW,CAAC,IAAO,aAAa,IAAQ,mCAAmC;MACrE,CAAA;KAEV,EAAU,KAAK,MACd,kBAAC,UAAD;MAAmB,OAAO,EAAE;gBAA5B;OACG,EAAE;OAAU;OAAE,EAAE;OAAS;OAAI,EAAE;OACzB;QAFI,EAAE,GAEN,CACT;KACK;;GACR,CAAC,KAAY,EAAO,UAAU,KAC7B,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KACxB;KAA0B;KAC3C;;GAEF"}
@@ -49,10 +49,6 @@ export declare function DashboardCardShell({ drillDown, children, }: {
49
49
  };
50
50
  children: ReactNode;
51
51
  }): import("react/jsx-runtime").JSX.Element;
52
- /** "There is no skill concept in PlanMagnet" — stated once per page, never invented. */
53
- export declare function NoSkillsNote({ message }: {
54
- message: string;
55
- }): import("react/jsx-runtime").JSX.Element;
56
52
  /** A small number-and-label tile, the same shape `analytics/DeliveryHealthCards.tsx`'s local `StatTile` uses. */
57
53
  export declare function StatTile({ label, value, hint }: {
58
54
  label: string;
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardUI.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC;;;;;;;GAOG;AAEH;4FAC4F;AAC5F,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,2CAWvF;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,YAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,kBAAkB,2CA+CpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAOA;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,2CAM5D;AAED,iHAAiH;AACjH,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,2CAQ/F;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,2CAyCxF;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAExE;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjF"}
1
+ {"version":3,"file":"DashboardUI.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC;;;;;;;GAOG;AAEH;4FAC4F;AAC5F,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,2CAWvF;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,YAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,kBAAkB,2CA+CpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAOA;AAED,iHAAiH;AACjH,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,2CAQ/F;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,2CAyCxF;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAExE;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjF"}
@@ -53,14 +53,7 @@ function d({ drillDown: e, children: t }) {
53
53
  }) : null]
54
54
  });
55
55
  }
56
- function f({ message: e }) {
57
- return /* @__PURE__ */ n("p", {
58
- "data-testid": "dashboard-no-skills-note",
59
- className: "text-xs italic text-muted-foreground",
60
- children: e
61
- });
62
- }
63
- function p({ label: e, value: t, hint: i }) {
56
+ function f({ label: e, value: t, hint: i }) {
64
57
  return /* @__PURE__ */ r("div", {
65
58
  className: "rounded-md border p-3",
66
59
  children: [
@@ -79,7 +72,7 @@ function p({ label: e, value: t, hint: i }) {
79
72
  ]
80
73
  });
81
74
  }
82
- function m({ percent: e, label: t }) {
75
+ function p({ percent: e, label: t }) {
83
76
  if (e == null) return /* @__PURE__ */ r("div", {
84
77
  className: "space-y-1",
85
78
  children: [t ? /* @__PURE__ */ r("div", {
@@ -110,17 +103,17 @@ function m({ percent: e, label: t }) {
110
103
  })]
111
104
  });
112
105
  }
113
- function h(e) {
106
+ function m(e) {
114
107
  return e == null ? "—" : `${Math.round(e * 100)}%`;
115
108
  }
116
- function g(e) {
109
+ function h(e) {
117
110
  let t = e / 60;
118
111
  return `${t < 10 ? t.toFixed(1) : Math.round(t)} h`;
119
112
  }
120
- function _(e, t, n) {
113
+ function g(e, t, n) {
121
114
  return t > e ? `Showing ${e} of ${t} ${n}` : `${t} ${n}`;
122
115
  }
123
116
  //#endregion
124
- export { u as DashboardCard, d as DashboardCardShell, l as DrillDownLink, f as NoSkillsNote, m as PercentBar, p as StatTile, _ as cappedSubtitle, g as formatHours, h as ratePercentLabel };
117
+ export { u as DashboardCard, d as DashboardCardShell, l as DrillDownLink, p as PercentBar, f as StatTile, g as cappedSubtitle, h as formatHours, m as ratePercentLabel };
125
118
 
126
119
  //# sourceMappingURL=DashboardUI.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardUI.js","names":[],"sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowRight } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\n\n/**\n * Shared building blocks for the four role dashboards (ANR-06/07/28, CWF-31): every card on every dashboard\n * follows the same contract — a title, a one-line definition of what the number means, and loading/empty/\n * error/forbidden states, so a viewer never has to guess whether a blank card means \"nothing to show\" or \"this\n * broke\". `isForbidden` is the same RBAC-denial detector `releases/ReleaseReadinessCard.tsx`'s local `denied()`\n * generalizes from (see `utils/graphqlErrors.ts`) — a card whose resource the caller's role cannot read says so\n * instead of leaking an unreadable error string or silently rendering empty.\n */\n\n/** A one-line link out of a card into the page that can show more. Rendered as a real button, not a link styled\n * to look clickable, since it runs client-side navigation rather than following an href. */\nexport function DrillDownLink({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline\"\n >\n {label}\n <ArrowRight className=\"h-3 w-3\" aria-hidden={true} />\n </button>\n );\n}\n\nexport interface DashboardCardProps {\n icon: ReactNode;\n title: string;\n /** One line: what the number(s) on this card mean and where they come from. */\n definition: string;\n loading: boolean;\n error?: unknown;\n /** True when the query succeeded but there is nothing to show. */\n empty?: boolean;\n emptyMessage?: string;\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}\n\n/**\n * The full card shell: title + definition + icon in the header, then one of loading / forbidden / error / empty\n * / content in the body, then an optional drill-down link. Use this for a card built fresh for the dashboards.\n * For a card that already exists as its own `<Card>` (e.g. `BudgetPerformanceCard`), wrap it in\n * `DashboardCardShell` instead so its own header isn't duplicated.\n */\nexport function DashboardCard({\n icon,\n title,\n definition,\n loading,\n error,\n empty,\n emptyMessage,\n drillDown,\n children,\n}: DashboardCardProps) {\n const { t } = useI18n();\n const forbidden = isForbidden(error);\n const testId = forbidden ? 'dashboard-card-forbidden' : 'dashboard-card';\n\n let body: ReactNode;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {forbidden\n ? t('planmagnet.dashboards.forbidden', 'You do not have access to {{title}}.', { title })\n : t('planmagnet.dashboards.error', 'Could not be loaded: {{message}}', {\n message: error instanceof Error ? error.message : 'unknown error',\n })}\n </p>\n );\n } else if (loading) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.dashboards.loading', 'Loading…')}\n </p>\n );\n } else if (empty) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {emptyMessage ?? t('planmagnet.dashboards.empty', 'Nothing to show yet.')}\n </p>\n );\n } else {\n body = children;\n }\n\n return (\n <Card data-testid={testId}>\n <CardHeader className=\"space-y-1\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n {icon}\n {title}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">{definition}</p>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {body}\n {drillDown && !loading ? <DrillDownLink {...drillDown} /> : null}\n </CardContent>\n </Card>\n );\n}\n\n/**\n * Wraps an already-complete card component (it renders its own `<Card>`, title and loading/empty/error states —\n * `BudgetPerformanceCard`, `CycleTimeCard`, `SprintBurndownCard`, `SprintVelocityCard`) so it carries the\n * `dashboard-card` testid and an optional drill-down link without duplicating its header.\n */\nexport function DashboardCardShell({\n drillDown,\n children,\n}: {\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}) {\n return (\n <div data-testid=\"dashboard-card\" className=\"space-y-2\">\n {children}\n {drillDown ? <div className=\"px-1\">{<DrillDownLink {...drillDown} />}</div> : null}\n </div>\n );\n}\n\n/** \"There is no skill concept in PlanMagnet\" — stated once per page, never invented. */\nexport function NoSkillsNote({ message }: { message: string }) {\n return (\n <p data-testid=\"dashboard-no-skills-note\" className=\"text-xs italic text-muted-foreground\">\n {message}\n </p>\n );\n}\n\n/** A small number-and-label tile, the same shape `analytics/DeliveryHealthCards.tsx`'s local `StatTile` uses. */\nexport function 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\n/**\n * A labelled 0-100% meter, using the semantic `primary` token — never a raw color. `percent: null` means\n * \"not measured\" (e.g. a rate whose denominator was 0 — nothing completed in the window to score), and\n * renders an empty dashed track with a \"—\" label instead of a fabricated 0%. A 0%-filled bar is visually\n * indistinguishable from \"measured and it's zero\" (total delivery failure), so coercing null to 0 here\n * would mislead a viewer skimming the card — the caller's null case must flow through, never `?? 0`.\n */\nexport function PercentBar({ percent, label }: { percent: number | null; label?: string }) {\n if (percent == null) {\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>—</span>\n </div>\n ) : null}\n <div\n aria-hidden=\"true\"\n className=\"h-2 w-full overflow-hidden rounded-full border border-dashed border-muted-foreground/30\"\n />\n </div>\n );\n }\n const pct = Math.max(0, Math.min(100, Math.round(percent)));\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>{pct}%</span>\n </div>\n ) : null}\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n aria-label={label ?? `${pct}%`}\n className=\"h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n}\n\n/** \"—\" for a rate that is null because its denominator was 0 — never a fabricated 0% or 100%. */\nexport function ratePercentLabel(rate: number | null | undefined): string {\n return rate == null ? '—' : `${Math.round(rate * 100)}%`;\n}\n\n/** Minutes read naturally as hours (one decimal below 10h, whole hours above). */\nexport function formatHours(minutes: number): string {\n const hours = minutes / 60;\n return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)} h`;\n}\n\n/** Cap-aware \"N of TOTAL\" wording for a list a card intentionally truncates instead of paginating in place. */\nexport function cappedSubtitle(shown: number, total: number, noun: string): string {\n return total > shown ? `Showing ${shown} of ${total} ${noun}` : `${total} ${noun}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardUI.js","names":[],"sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowRight } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\n\n/**\n * Shared building blocks for the four role dashboards (ANR-06/07/28, CWF-31): every card on every dashboard\n * follows the same contract — a title, a one-line definition of what the number means, and loading/empty/\n * error/forbidden states, so a viewer never has to guess whether a blank card means \"nothing to show\" or \"this\n * broke\". `isForbidden` is the same RBAC-denial detector `releases/ReleaseReadinessCard.tsx`'s local `denied()`\n * generalizes from (see `utils/graphqlErrors.ts`) — a card whose resource the caller's role cannot read says so\n * instead of leaking an unreadable error string or silently rendering empty.\n */\n\n/** A one-line link out of a card into the page that can show more. Rendered as a real button, not a link styled\n * to look clickable, since it runs client-side navigation rather than following an href. */\nexport function DrillDownLink({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline\"\n >\n {label}\n <ArrowRight className=\"h-3 w-3\" aria-hidden={true} />\n </button>\n );\n}\n\nexport interface DashboardCardProps {\n icon: ReactNode;\n title: string;\n /** One line: what the number(s) on this card mean and where they come from. */\n definition: string;\n loading: boolean;\n error?: unknown;\n /** True when the query succeeded but there is nothing to show. */\n empty?: boolean;\n emptyMessage?: string;\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}\n\n/**\n * The full card shell: title + definition + icon in the header, then one of loading / forbidden / error / empty\n * / content in the body, then an optional drill-down link. Use this for a card built fresh for the dashboards.\n * For a card that already exists as its own `<Card>` (e.g. `BudgetPerformanceCard`), wrap it in\n * `DashboardCardShell` instead so its own header isn't duplicated.\n */\nexport function DashboardCard({\n icon,\n title,\n definition,\n loading,\n error,\n empty,\n emptyMessage,\n drillDown,\n children,\n}: DashboardCardProps) {\n const { t } = useI18n();\n const forbidden = isForbidden(error);\n const testId = forbidden ? 'dashboard-card-forbidden' : 'dashboard-card';\n\n let body: ReactNode;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {forbidden\n ? t('planmagnet.dashboards.forbidden', 'You do not have access to {{title}}.', { title })\n : t('planmagnet.dashboards.error', 'Could not be loaded: {{message}}', {\n message: error instanceof Error ? error.message : 'unknown error',\n })}\n </p>\n );\n } else if (loading) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.dashboards.loading', 'Loading…')}\n </p>\n );\n } else if (empty) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {emptyMessage ?? t('planmagnet.dashboards.empty', 'Nothing to show yet.')}\n </p>\n );\n } else {\n body = children;\n }\n\n return (\n <Card data-testid={testId}>\n <CardHeader className=\"space-y-1\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n {icon}\n {title}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">{definition}</p>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {body}\n {drillDown && !loading ? <DrillDownLink {...drillDown} /> : null}\n </CardContent>\n </Card>\n );\n}\n\n/**\n * Wraps an already-complete card component (it renders its own `<Card>`, title and loading/empty/error states —\n * `BudgetPerformanceCard`, `CycleTimeCard`, `SprintBurndownCard`, `SprintVelocityCard`) so it carries the\n * `dashboard-card` testid and an optional drill-down link without duplicating its header.\n */\nexport function DashboardCardShell({\n drillDown,\n children,\n}: {\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}) {\n return (\n <div data-testid=\"dashboard-card\" className=\"space-y-2\">\n {children}\n {drillDown ? <div className=\"px-1\">{<DrillDownLink {...drillDown} />}</div> : null}\n </div>\n );\n}\n\n/** A small number-and-label tile, the same shape `analytics/DeliveryHealthCards.tsx`'s local `StatTile` uses. */\nexport function 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\n/**\n * A labelled 0-100% meter, using the semantic `primary` token — never a raw color. `percent: null` means\n * \"not measured\" (e.g. a rate whose denominator was 0 — nothing completed in the window to score), and\n * renders an empty dashed track with a \"—\" label instead of a fabricated 0%. A 0%-filled bar is visually\n * indistinguishable from \"measured and it's zero\" (total delivery failure), so coercing null to 0 here\n * would mislead a viewer skimming the card — the caller's null case must flow through, never `?? 0`.\n */\nexport function PercentBar({ percent, label }: { percent: number | null; label?: string }) {\n if (percent == null) {\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>—</span>\n </div>\n ) : null}\n <div\n aria-hidden=\"true\"\n className=\"h-2 w-full overflow-hidden rounded-full border border-dashed border-muted-foreground/30\"\n />\n </div>\n );\n }\n const pct = Math.max(0, Math.min(100, Math.round(percent)));\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>{pct}%</span>\n </div>\n ) : null}\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n aria-label={label ?? `${pct}%`}\n className=\"h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n}\n\n/** \"—\" for a rate that is null because its denominator was 0 — never a fabricated 0% or 100%. */\nexport function ratePercentLabel(rate: number | null | undefined): string {\n return rate == null ? '—' : `${Math.round(rate * 100)}%`;\n}\n\n/** Minutes read naturally as hours (one decimal below 10h, whole hours above). */\nexport function formatHours(minutes: number): string {\n const hours = minutes / 60;\n return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)} h`;\n}\n\n/** Cap-aware \"N of TOTAL\" wording for a list a card intentionally truncates instead of paginating in place. */\nexport function cappedSubtitle(shown: number, total: number, noun: string): string {\n return total > shown ? `Showing ${shown} of ${total} ${noun}` : `${total} ${noun}`;\n}\n"],"mappings":";;;;;;AAiBA,SAAgB,EAAc,EAAE,UAAO,cAAmD;AACxF,QACE,kBAAC,UAAD;EACE,MAAK;EACI;EACT,WAAU;YAHZ,CAKG,GACD,kBAAC,GAAD;GAAY,WAAU;GAAU,eAAa;GAAQ,CAAA,CAC9C;;;AAwBb,SAAgB,EAAc,EAC5B,SACA,UACA,eACA,YACA,UACA,UACA,iBACA,cACA,eACqB;CACrB,IAAM,EAAE,SAAM,GAAS,EACjB,IAAY,EAAY,EAAM,EAC9B,IAAS,IAAY,6BAA6B,kBAEpD;AA2BJ,QA1BA,AAuBE,IAvBE,IAEA,kBAAC,KAAD;EAAG,WAAU;YACV,IACG,EAAE,mCAAmC,wCAAwC,EAAE,UAAO,CAAC,GACvF,EAAE,+BAA+B,oCAAoC,EACnE,SAAS,aAAiB,QAAQ,EAAM,UAAU,iBACnD,CAAC;EACJ,CAAA,GAEG,IAEP,kBAAC,KAAD;EAAG,WAAU;YACV,EAAE,iCAAiC,WAAW;EAC7C,CAAA,GAEG,IAEP,kBAAC,KAAD;EAAG,WAAU;YACV,KAAgB,EAAE,+BAA+B,uBAAuB;EACvE,CAAA,GAGC,GAIP,kBAAC,GAAD;EAAM,eAAa;YAAnB,CACE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,GAAD;IAAW,WAAU;cAArB,CACG,GACA,EACS;OACZ,kBAAC,KAAD;IAAG,WAAU;cAAiC;IAAe,CAAA,CAClD;MACb,kBAAC,GAAD;GAAa,WAAU;aAAvB,CACG,GACA,KAAa,CAAC,IAAU,kBAAC,GAAD,EAAe,GAAI,GAAa,CAAA,GAAG,KAChD;KACT;;;AASX,SAAgB,EAAmB,EACjC,cACA,eAIC;AACD,QACE,kBAAC,OAAD;EAAK,eAAY;EAAiB,WAAU;YAA5C,CACG,GACA,IAAY,kBAAC,OAAD;GAAK,WAAU;aAAQ,kBAAC,GAAD,EAAe,GAAI,GAAa,CAAA;GAAO,CAAA,GAAG,KAC1E;;;AAKV,SAAgB,EAAS,EAAE,UAAO,UAAO,WAAyD;AAChG,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,KAAD;IAAG,WAAU;cAAiC;IAAU,CAAA;GACxD,kBAAC,KAAD;IAAG,WAAU;cAA+B;IAAU,CAAA;GACrD,IAAO,kBAAC,KAAD;IAAG,WAAU;cAAwC;IAAS,CAAA,GAAG;GACrE;;;AAWV,SAAgB,EAAW,EAAE,YAAS,YAAqD;AACzF,KAAI,KAAW,KACb,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD,EAAA,UAAO,GAAa,CAAA,EACpB,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA,CACV;OACJ,MACJ,kBAAC,OAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,CACE;;CAGV,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAM,EAAQ,CAAC,CAAC;AAC3D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD,EAAA,UAAO,GAAa,CAAA,EACpB,kBAAC,QAAD,EAAA,UAAA,CAAO,GAAI,IAAQ,EAAA,CAAA,CACf;OACJ,MACJ,kBAAC,OAAD;GACE,MAAK;GACL,iBAAe;GACf,iBAAe;GACf,iBAAe;GACf,cAAY,KAAS,GAAG,EAAI;GAC5B,WAAU;aAEV,kBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,OAAO,GAAG,EAAI,IAAI;IAC3B,CAAA;GACE,CAAA,CACF;;;AAKV,SAAgB,EAAiB,GAAyC;AACxE,QAAO,KAAQ,OAAO,MAAM,GAAG,KAAK,MAAM,IAAO,IAAI,CAAC;;AAIxD,SAAgB,EAAY,GAAyB;CACnD,IAAM,IAAQ,IAAU;AACxB,QAAO,GAAG,IAAQ,KAAK,EAAM,QAAQ,EAAE,GAAG,KAAK,MAAM,EAAM,CAAC;;AAI9D,SAAgB,EAAe,GAAe,GAAe,GAAsB;AACjF,QAAO,IAAQ,IAAQ,WAAW,EAAM,MAAM,EAAM,GAAG,MAAS,GAAG,EAAM,GAAG"}
@@ -1 +1 @@
1
- {"version":3,"file":"SprintCapacityCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,2CAqHA"}
1
+ {"version":3,"file":"SprintCapacityCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,2CA0HA"}
@@ -1,13 +1,14 @@
1
1
  import { usePlanMagnetNavigate as e } from "../../providers/PlanMagnetProvider.js";
2
2
  import { PersonAvatar as t } from "../../people/People.js";
3
3
  import { DashboardCard as n, DrillDownLink as r } from "../DashboardUI.js";
4
- import { Users as i } from "lucide-react";
5
- import { gql as a } from "@apollo/client";
6
- import { useQuery as o } from "@apollo/client/react";
7
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
8
- import { useI18n as l } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
4
+ import { MemberSkills as i } from "../../skills/SkillChips.js";
5
+ import { Users as a } from "lucide-react";
6
+ import { gql as o } from "@apollo/client";
7
+ import { useQuery as s } from "@apollo/client/react";
8
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
9
+ import { useI18n as u } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
9
10
  //#region src/planmagnet/dashboards/cards/SprintCapacityCard.tsx
10
- var u = a`
11
+ var d = o`
11
12
  query DashboardSprintCapacity($sprintId: ID!) {
12
13
  planmagnetSprintCapacity(sprintId: $sprintId) {
13
14
  sprintId
@@ -15,7 +16,9 @@ var u = a`
15
16
  unmatchedCount
16
17
  members {
17
18
  actorId
19
+ employeeId
18
20
  employeeName
21
+ skills
19
22
  position
20
23
  department
21
24
  allocatedHours
@@ -27,82 +30,90 @@ var u = a`
27
30
  }
28
31
  }
29
32
  `;
30
- function d({ sprintId: a, onDrillDown: d }) {
31
- let { t: f } = l(), p = e(), { data: m, loading: h, error: g } = o(u, {
32
- variables: { sprintId: a },
33
- skip: !a
34
- }), _ = m?.planmagnetSprintCapacity;
35
- return /* @__PURE__ */ s(n, {
36
- icon: /* @__PURE__ */ s(i, { className: "h-4 w-4" }),
37
- title: f("planmagnet.dashboards.pm.capacity.title", "Capacity"),
38
- definition: f("planmagnet.dashboards.pm.capacity.definition", "Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person."),
39
- loading: h && !m,
40
- error: g,
41
- empty: !a || !!_ && _.members.length === 0 && !_.crewfoundryUnavailable,
42
- emptyMessage: a ? f("planmagnet.dashboards.pm.capacity.noMembers", "No CrewFoundry-matched assignees in this sprint.") : f("planmagnet.dashboards.pm.capacity.noSprint", "No active sprint for this project."),
33
+ function f({ sprintId: o, onDrillDown: f }) {
34
+ let { t: p } = u(), m = e(), { data: h, loading: g, error: _ } = s(d, {
35
+ variables: { sprintId: o },
36
+ skip: !o
37
+ }), v = h?.planmagnetSprintCapacity;
38
+ return /* @__PURE__ */ c(n, {
39
+ icon: /* @__PURE__ */ c(a, { className: "h-4 w-4" }),
40
+ title: p("planmagnet.dashboards.pm.capacity.title", "Capacity"),
41
+ definition: p("planmagnet.dashboards.pm.capacity.definition", "Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person."),
42
+ loading: g && !h,
43
+ error: _,
44
+ empty: !o || !!v && v.members.length === 0 && !v.crewfoundryUnavailable,
45
+ emptyMessage: o ? p("planmagnet.dashboards.pm.capacity.noMembers", "No CrewFoundry-matched assignees in this sprint.") : p("planmagnet.dashboards.pm.capacity.noSprint", "No active sprint for this project."),
43
46
  drillDown: {
44
- label: f("planmagnet.dashboards.pm.capacity.drillDown", "Open project"),
45
- onClick: d
47
+ label: p("planmagnet.dashboards.pm.capacity.drillDown", "Open project"),
48
+ onClick: f
46
49
  },
47
- children: _ ? _.crewfoundryUnavailable ? /* @__PURE__ */ s("p", {
50
+ children: v ? v.crewfoundryUnavailable ? /* @__PURE__ */ c("p", {
48
51
  className: "text-sm text-muted-foreground",
49
- children: f("planmagnet.dashboards.pm.capacity.unavailable", "Capacity data is unavailable — CrewFoundry could not be reached, or this workspace has no CrewFoundry roster.")
50
- }) : /* @__PURE__ */ c("div", {
52
+ children: p("planmagnet.dashboards.pm.capacity.crewfoundryUnavailable", "CrewFoundry is temporarily unavailable, so capacity and skills can’t be shown right now. Try again shortly.")
53
+ }) : /* @__PURE__ */ l("div", {
51
54
  className: "space-y-2",
52
- children: [/* @__PURE__ */ c("table", {
55
+ children: [/* @__PURE__ */ l("table", {
53
56
  className: "w-full text-sm",
54
- children: [/* @__PURE__ */ s("thead", { children: /* @__PURE__ */ c("tr", {
57
+ children: [/* @__PURE__ */ c("thead", { children: /* @__PURE__ */ l("tr", {
55
58
  className: "text-left text-xs text-muted-foreground",
56
59
  children: [
57
- /* @__PURE__ */ s("th", {
60
+ /* @__PURE__ */ c("th", {
58
61
  className: "py-1 pr-2 font-normal",
59
- children: f("planmagnet.dashboards.pm.capacity.person", "Person")
62
+ children: p("planmagnet.dashboards.pm.capacity.person", "Person")
60
63
  }),
61
- /* @__PURE__ */ s("th", {
64
+ /* @__PURE__ */ c("th", {
62
65
  className: "py-1 pr-2 text-right font-normal",
63
- children: f("planmagnet.dashboards.pm.capacity.allocated", "Allocated h")
66
+ children: p("planmagnet.dashboards.pm.capacity.allocated", "Allocated h")
64
67
  }),
65
- /* @__PURE__ */ s("th", {
68
+ /* @__PURE__ */ c("th", {
66
69
  className: "py-1 pr-2 text-right font-normal",
67
- children: f("planmagnet.dashboards.pm.capacity.utilization", "Utilization")
70
+ children: p("planmagnet.dashboards.pm.capacity.utilization", "Utilization")
68
71
  }),
69
- /* @__PURE__ */ s("th", {
72
+ /* @__PURE__ */ c("th", {
70
73
  className: "py-1 text-right font-normal",
71
- children: f("planmagnet.dashboards.pm.capacity.items", "Items (pts)")
74
+ children: p("planmagnet.dashboards.pm.capacity.items", "Items (pts)")
72
75
  })
73
76
  ]
74
- }) }), /* @__PURE__ */ s("tbody", { children: _.members.map((e) => /* @__PURE__ */ c("tr", {
77
+ }) }), /* @__PURE__ */ c("tbody", { children: v.members.map((e) => /* @__PURE__ */ l("tr", {
75
78
  className: "border-t",
76
79
  children: [
77
- /* @__PURE__ */ s("td", {
80
+ /* @__PURE__ */ c("td", {
78
81
  className: "py-1.5 pr-2",
79
- children: /* @__PURE__ */ c("div", {
82
+ children: /* @__PURE__ */ l("div", {
80
83
  className: "flex items-center gap-2",
81
- children: [/* @__PURE__ */ s(t, {
84
+ children: [/* @__PURE__ */ c(t, {
82
85
  id: e.actorId,
83
86
  className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"
84
- }), /* @__PURE__ */ c("div", { children: [/* @__PURE__ */ s("div", {
85
- className: "font-medium",
86
- children: e.employeeName
87
- }), /* @__PURE__ */ c("div", {
88
- className: "text-xs text-muted-foreground",
89
- children: [
90
- e.position,
91
- " · ",
92
- e.department
93
- ]
94
- })] })]
87
+ }), /* @__PURE__ */ l("div", { children: [
88
+ /* @__PURE__ */ c("div", {
89
+ className: "font-medium",
90
+ children: e.employeeName
91
+ }),
92
+ /* @__PURE__ */ l("div", {
93
+ className: "text-xs text-muted-foreground",
94
+ children: [
95
+ e.position,
96
+ " · ",
97
+ e.department
98
+ ]
99
+ }),
100
+ /* @__PURE__ */ c(i, {
101
+ skills: e.skills,
102
+ employeeId: e.employeeId,
103
+ employeeName: e.employeeName
104
+ })
105
+ ] })]
95
106
  })
96
107
  }),
97
- /* @__PURE__ */ s("td", {
108
+ /* @__PURE__ */ c("td", {
98
109
  className: "py-1.5 pr-2 text-right tabular-nums",
99
110
  children: e.allocatedHours.toFixed(1)
100
111
  }),
101
- /* @__PURE__ */ s("td", {
112
+ /* @__PURE__ */ c("td", {
102
113
  className: "py-1.5 pr-2 text-right tabular-nums",
103
114
  children: e.utilizationPercent == null ? "—" : `${e.utilizationPercent}%`
104
115
  }),
105
- /* @__PURE__ */ c("td", {
116
+ /* @__PURE__ */ l("td", {
106
117
  className: "py-1.5 text-right tabular-nums",
107
118
  children: [
108
119
  e.assignedWorkItemCount,
@@ -113,21 +124,21 @@ function d({ sprintId: a, onDrillDown: d }) {
113
124
  })
114
125
  ]
115
126
  }, e.actorId)) })]
116
- }), _.unmatchedCount > 0 ? /* @__PURE__ */ c("div", {
127
+ }), v.unmatchedCount > 0 ? /* @__PURE__ */ l("div", {
117
128
  className: "flex items-center justify-between gap-2",
118
129
  "data-testid": "sprint-capacity-card-unmatched",
119
- children: [/* @__PURE__ */ s("p", {
130
+ children: [/* @__PURE__ */ c("p", {
120
131
  className: "text-xs text-muted-foreground",
121
- children: f("planmagnet.dashboards.pm.capacity.unmatched", "{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.", { count: _.unmatchedCount })
122
- }), /* @__PURE__ */ s(r, {
123
- label: f("planmagnet.dashboards.pm.capacity.fixUnmatched", "Fix"),
124
- onClick: () => p("/team/team-page?focus=unmatched-crew-identities")
132
+ children: p("planmagnet.dashboards.pm.capacity.unmatched", "{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.", { count: v.unmatchedCount })
133
+ }), /* @__PURE__ */ c(r, {
134
+ label: p("planmagnet.dashboards.pm.capacity.fixUnmatched", "Fix"),
135
+ onClick: () => m("/team/team-page?focus=unmatched-crew-identities")
125
136
  })]
126
137
  }) : null]
127
138
  }) : null
128
139
  });
129
140
  }
130
141
  //#endregion
131
- export { d as SprintCapacityCard };
142
+ export { f as SprintCapacityCard };
132
143
 
133
144
  //# sourceMappingURL=SprintCapacityCard.js.map