@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":"SprintCapacityCard.js","names":[],"sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Users } from 'lucide-react';\nimport { DashboardCard, DrillDownLink } from '../DashboardUI';\nimport { PersonAvatar } from '../../people/People';\nimport { usePlanMagnetNavigate } from '../../providers/PlanMagnetProvider';\n\n/** Capacity (ANR-07): `planmagnetSprintCapacity` (BOFF-4444) of the active sprint — real CrewFoundry\n * allocation/availability per assignee, cross-layer. There is no \"skill\" concept anywhere in this data:\n * capacity is hours and story points, per person, never per skill (the PM dashboard says so separately). */\nconst GET_SPRINT_CAPACITY = gql`\n query DashboardSprintCapacity($sprintId: ID!) {\n planmagnetSprintCapacity(sprintId: $sprintId) {\n sprintId\n crewfoundryUnavailable\n unmatchedCount\n members {\n actorId\n employeeName\n position\n department\n allocatedHours\n billableHours\n utilizationPercent\n assignedWorkItemCount\n assignedStoryPoints\n }\n }\n }\n`;\n\ninterface SprintCapacityData {\n planmagnetSprintCapacity: {\n sprintId: string;\n crewfoundryUnavailable: boolean;\n unmatchedCount: number;\n members: {\n actorId: string;\n employeeName: string;\n position: string;\n department: string;\n allocatedHours: number;\n billableHours: number;\n utilizationPercent: number | null;\n assignedWorkItemCount: number;\n assignedStoryPoints: number;\n }[];\n };\n}\n\nexport function SprintCapacityCard({\n sprintId,\n onDrillDown,\n}: {\n sprintId: string;\n onDrillDown: () => void;\n}) {\n const { t } = useI18n();\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<SprintCapacityData>(GET_SPRINT_CAPACITY, {\n variables: { sprintId },\n skip: !sprintId,\n });\n const capacity = data?.planmagnetSprintCapacity;\n\n return (\n <DashboardCard\n icon={<Users className=\"h-4 w-4\" />}\n title={t('planmagnet.dashboards.pm.capacity.title', 'Capacity')}\n definition={t(\n 'planmagnet.dashboards.pm.capacity.definition',\n \"Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person.\"\n )}\n loading={loading && !data}\n error={error}\n empty={\n !sprintId ||\n (!!capacity && capacity.members.length === 0 && !capacity.crewfoundryUnavailable)\n }\n emptyMessage={\n !sprintId\n ? t('planmagnet.dashboards.pm.capacity.noSprint', 'No active sprint for this project.')\n : t(\n 'planmagnet.dashboards.pm.capacity.noMembers',\n 'No CrewFoundry-matched assignees in this sprint.'\n )\n }\n drillDown={{\n label: t('planmagnet.dashboards.pm.capacity.drillDown', 'Open project'),\n onClick: onDrillDown,\n }}\n >\n {capacity ? (\n capacity.crewfoundryUnavailable ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.capacity.unavailable',\n 'Capacity data is unavailable — CrewFoundry could not be reached, or this workspace has no CrewFoundry roster.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.dashboards.pm.capacity.person', 'Person')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.allocated', 'Allocated h')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.utilization', 'Utilization')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.items', 'Items (pts)')}\n </th>\n </tr>\n </thead>\n <tbody>\n {capacity.members.map((m) => (\n <tr key={m.actorId} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">\n <div className=\"flex items-center gap-2\">\n <PersonAvatar\n id={m.actorId}\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary\"\n />\n <div>\n <div className=\"font-medium\">{m.employeeName}</div>\n <div className=\"text-xs text-muted-foreground\">\n {m.position} · {m.department}\n </div>\n </div>\n </div>\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {m.allocatedHours.toFixed(1)}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {m.utilizationPercent == null ? '—' : `${m.utilizationPercent}%`}\n </td>\n <td className=\"py-1.5 text-right tabular-nums\">\n {m.assignedWorkItemCount} ({m.assignedStoryPoints})\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n {capacity.unmatchedCount > 0 ? (\n <div\n className=\"flex items-center justify-between gap-2\"\n data-testid=\"sprint-capacity-card-unmatched\"\n >\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.capacity.unmatched',\n '{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.',\n { count: capacity.unmatchedCount }\n )}\n </p>\n {/* CWF-30: the admin remediation panel on the Members page. */}\n <DrillDownLink\n label={t('planmagnet.dashboards.pm.capacity.fixUnmatched', 'Fix')}\n onClick={() => navigate('/team/team-page?focus=unmatched-crew-identities')}\n />\n </div>\n ) : null}\n </div>\n )\n ) : null}\n </DashboardCard>\n );\n}\n"],"mappings":";;;;;;;;;AAWA,IAAM,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;AAwC/B,SAAgB,EAAmB,EACjC,aACA,kBAIC;CACD,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAuB,EAClC,EAAE,SAAM,YAAS,aAAU,EAA6B,GAAqB;EACjF,WAAW,EAAE,aAAU;EACvB,MAAM,CAAC;EACR,CAAC,EACI,IAAW,GAAM;AAEvB,QACE,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,OAAO,EAAE,2CAA2C,WAAW;EAC/D,YAAY,EACV,gDACA,sGACD;EACD,SAAS,KAAW,CAAC;EACd;EACP,OACE,CAAC,KACA,CAAC,CAAC,KAAY,EAAS,QAAQ,WAAW,KAAK,CAAC,EAAS;EAE5D,cACG,IAEG,EACE,+CACA,mDACD,GAJD,EAAE,8CAA8C,qCAAqC;EAM3F,WAAW;GACT,OAAO,EAAE,+CAA+C,eAAe;GACvE,SAAS;GACV;YAEA,IACC,EAAS,yBACP,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,iDACA,gHACD;GACC,CAAA,GAEJ,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eAAd;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,4CAA4C,SAAS;OACrD,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,+CAA+C,cAAc;OAC7D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,iDAAiD,cAAc;OAC/D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,2CAA2C,cAAc;OACzD,CAAA;MACF;QACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAS,QAAQ,KAAK,MACrB,kBAAC,MAAD;KAAoB,WAAU;eAA9B;MACE,kBAAC,MAAD;OAAI,WAAU;iBACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,IAAI,EAAE;SACN,WAAU;SACV,CAAA,EACF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAe,EAAE;SAAmB,CAAA,EACnD,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACG,EAAE;UAAS;UAAI,EAAE;UACd;WACF,EAAA,CAAA,CACF;;OACH,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,eAAe,QAAQ,EAAE;OACzB,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,sBAAsB,OAAO,MAAM,GAAG,EAAE,mBAAmB;OAC3D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACG,EAAE;QAAsB;QAAG,EAAE;QAAoB;QAC/C;;MACF;OAxBI,EAAE,QAwBN,CACL,EACI,CAAA,CACF;OACP,EAAS,iBAAiB,IACzB,kBAAC,OAAD;IACE,WAAU;IACV,eAAY;cAFd,CAIE,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,+CACA,uFACA,EAAE,OAAO,EAAS,gBAAgB,CACnC;KACC,CAAA,EAEJ,kBAAC,GAAD;KACE,OAAO,EAAE,kDAAkD,MAAM;KACjE,eAAe,EAAS,kDAAkD;KAC1E,CAAA,CACE;QACJ,KACA;OAEN;EACU,CAAA"}
1
+ {"version":3,"file":"SprintCapacityCard.js","names":[],"sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Users } from 'lucide-react';\nimport { DashboardCard, DrillDownLink } from '../DashboardUI';\nimport { PersonAvatar } from '../../people/People';\nimport { usePlanMagnetNavigate } from '../../providers/PlanMagnetProvider';\nimport { MemberSkills } from '../../skills/SkillChips';\n\n/** Capacity (ANR-07): `planmagnetSprintCapacity` (BOFF-4444) of the active sprint — real CrewFoundry\n * allocation/availability per assignee, cross-layer. Capacity is hours and story points per person, never per\n * skill; each person's CrewFoundry skills are shown read-only (CrewFoundry owns them — the link edits them there),\n * and the dashboard's Skills coverage card matches them against the open work's required skills. `skills` rides on\n * this query rather than a second one: a second planmagnetSprintCapacity would page the whole CrewFoundry roster\n * again for every view. */\nconst GET_SPRINT_CAPACITY = gql`\n query DashboardSprintCapacity($sprintId: ID!) {\n planmagnetSprintCapacity(sprintId: $sprintId) {\n sprintId\n crewfoundryUnavailable\n unmatchedCount\n members {\n actorId\n employeeId\n employeeName\n skills\n position\n department\n allocatedHours\n billableHours\n utilizationPercent\n assignedWorkItemCount\n assignedStoryPoints\n }\n }\n }\n`;\n\ninterface SprintCapacityData {\n planmagnetSprintCapacity: {\n sprintId: string;\n crewfoundryUnavailable: boolean;\n unmatchedCount: number;\n members: {\n actorId: string;\n employeeId: string;\n employeeName: string;\n skills: string[];\n position: string;\n department: string;\n allocatedHours: number;\n billableHours: number;\n utilizationPercent: number | null;\n assignedWorkItemCount: number;\n assignedStoryPoints: number;\n }[];\n };\n}\n\nexport function SprintCapacityCard({\n sprintId,\n onDrillDown,\n}: {\n sprintId: string;\n onDrillDown: () => void;\n}) {\n const { t } = useI18n();\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<SprintCapacityData>(GET_SPRINT_CAPACITY, {\n variables: { sprintId },\n skip: !sprintId,\n });\n const capacity = data?.planmagnetSprintCapacity;\n\n return (\n <DashboardCard\n icon={<Users className=\"h-4 w-4\" />}\n title={t('planmagnet.dashboards.pm.capacity.title', 'Capacity')}\n definition={t(\n 'planmagnet.dashboards.pm.capacity.definition',\n \"Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person.\"\n )}\n loading={loading && !data}\n error={error}\n empty={\n !sprintId ||\n (!!capacity && capacity.members.length === 0 && !capacity.crewfoundryUnavailable)\n }\n emptyMessage={\n !sprintId\n ? t('planmagnet.dashboards.pm.capacity.noSprint', 'No active sprint for this project.')\n : t(\n 'planmagnet.dashboards.pm.capacity.noMembers',\n 'No CrewFoundry-matched assignees in this sprint.'\n )\n }\n drillDown={{\n label: t('planmagnet.dashboards.pm.capacity.drillDown', 'Open project'),\n onClick: onDrillDown,\n }}\n >\n {capacity ? (\n capacity.crewfoundryUnavailable ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.capacity.crewfoundryUnavailable',\n 'CrewFoundry is temporarily unavailable, so capacity and skills can’t be shown right now. Try again shortly.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.dashboards.pm.capacity.person', 'Person')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.allocated', 'Allocated h')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.utilization', 'Utilization')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.dashboards.pm.capacity.items', 'Items (pts)')}\n </th>\n </tr>\n </thead>\n <tbody>\n {capacity.members.map((m) => (\n <tr key={m.actorId} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">\n <div className=\"flex items-center gap-2\">\n <PersonAvatar\n id={m.actorId}\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary\"\n />\n <div>\n <div className=\"font-medium\">{m.employeeName}</div>\n <div className=\"text-xs text-muted-foreground\">\n {m.position} · {m.department}\n </div>\n <MemberSkills\n skills={m.skills}\n employeeId={m.employeeId}\n employeeName={m.employeeName}\n />\n </div>\n </div>\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {m.allocatedHours.toFixed(1)}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {m.utilizationPercent == null ? '—' : `${m.utilizationPercent}%`}\n </td>\n <td className=\"py-1.5 text-right tabular-nums\">\n {m.assignedWorkItemCount} ({m.assignedStoryPoints})\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n {capacity.unmatchedCount > 0 ? (\n <div\n className=\"flex items-center justify-between gap-2\"\n data-testid=\"sprint-capacity-card-unmatched\"\n >\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.capacity.unmatched',\n '{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.',\n { count: capacity.unmatchedCount }\n )}\n </p>\n {/* CWF-30: the admin remediation panel on the Members page. */}\n <DrillDownLink\n label={t('planmagnet.dashboards.pm.capacity.fixUnmatched', 'Fix')}\n onClick={() => navigate('/team/team-page?focus=unmatched-crew-identities')}\n />\n </div>\n ) : null}\n </div>\n )\n ) : null}\n </DashboardCard>\n );\n}\n"],"mappings":";;;;;;;;;;AAeA,IAAM,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;;;AA4C/B,SAAgB,EAAmB,EACjC,aACA,kBAIC;CACD,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAuB,EAClC,EAAE,SAAM,YAAS,aAAU,EAA6B,GAAqB;EACjF,WAAW,EAAE,aAAU;EACvB,MAAM,CAAC;EACR,CAAC,EACI,IAAW,GAAM;AAEvB,QACE,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACnC,OAAO,EAAE,2CAA2C,WAAW;EAC/D,YAAY,EACV,gDACA,sGACD;EACD,SAAS,KAAW,CAAC;EACd;EACP,OACE,CAAC,KACA,CAAC,CAAC,KAAY,EAAS,QAAQ,WAAW,KAAK,CAAC,EAAS;EAE5D,cACG,IAEG,EACE,+CACA,mDACD,GAJD,EAAE,8CAA8C,qCAAqC;EAM3F,WAAW;GACT,OAAO,EAAE,+CAA+C,eAAe;GACvE,SAAS;GACV;YAEA,IACC,EAAS,yBACP,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,4DACA,8GACD;GACC,CAAA,GAEJ,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eAAd;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,4CAA4C,SAAS;OACrD,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,+CAA+C,cAAc;OAC7D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,iDAAiD,cAAc;OAC/D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,2CAA2C,cAAc;OACzD,CAAA;MACF;QACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAS,QAAQ,KAAK,MACrB,kBAAC,MAAD;KAAoB,WAAU;eAA9B;MACE,kBAAC,MAAD;OAAI,WAAU;iBACZ,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,IAAI,EAAE;SACN,WAAU;SACV,CAAA,EACF,kBAAC,OAAD,EAAA,UAAA;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAe,EAAE;UAAmB,CAAA;SACnD,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACG,EAAE;WAAS;WAAI,EAAE;WACd;;SACN,kBAAC,GAAD;UACE,QAAQ,EAAE;UACV,YAAY,EAAE;UACd,cAAc,EAAE;UAChB,CAAA;SACE,EAAA,CAAA,CACF;;OACH,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,eAAe,QAAQ,EAAE;OACzB,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAE,sBAAsB,OAAO,MAAM,GAAG,EAAE,mBAAmB;OAC3D,CAAA;MACL,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACG,EAAE;QAAsB;QAAG,EAAE;QAAoB;QAC/C;;MACF;OA7BI,EAAE,QA6BN,CACL,EACI,CAAA,CACF;OACP,EAAS,iBAAiB,IACzB,kBAAC,OAAD;IACE,WAAU;IACV,eAAY;cAFd,CAIE,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,+CACA,uFACA,EAAE,OAAO,EAAS,gBAAgB,CACnC;KACC,CAAA,EAEJ,kBAAC,GAAD;KACE,OAAO,EAAE,kDAAkD,MAAM;KACjE,eAAe,EAAS,kDAAkD;KAC1E,CAAA,CACE;QACJ,KACA;OAEN;EACU,CAAA"}
@@ -159,7 +159,7 @@ function p({ workspaceId: r, teamId: p, projectId: m, horizonSprints: h = 3 }) {
159
159
  }),
160
160
  /* @__PURE__ */ a("p", {
161
161
  className: "text-xs text-muted-foreground",
162
- children: g("planmagnet.forecast.resource.skillsNote", "Skills demand is not modelled — this forecasts total story points only, not which skills the backlog actually needs.")
162
+ children: g("planmagnet.forecast.resource.skillsNoteCoverage", "This forecasts total story points only, not which skills the backlog needs — the Skills coverage card on the project manager and team dashboards shows that.")
163
163
  }),
164
164
  /* @__PURE__ */ a("p", {
165
165
  className: "text-xs text-muted-foreground",
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceForecastCard.js","names":[],"sources":["../../../src/planmagnet/forecast/ResourceForecastCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Gauge } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { StatTile } from '../pages/analytics/DeliveryHealthCards';\n\n/**\n * ANR-11: `planmagnetResourceForecast` — open backlog vs historical velocity, and the resulting\n * per-sprint capacity gap over the horizon. Used on the team detail page (team scope) and the\n * project detail page (project scope) — same card either way, one of `teamId`/`projectId` set.\n *\n * Statistical, never AI/LLM: reuses the same trailing-sprint-velocity resolution\n * `planmagnetForecastDelivery` does. Demand is the CURRENT open backlog split evenly across the\n * horizon (not a burndown simulation) and capacity never trends — see the field doc comments in\n * graphql/wspace.graphql. Skills/roles are not modelled: this is a total-points forecast only.\n */\n\nconst GET_RESOURCE_FORECAST = gql`\n query PlanmagnetResourceForecast(\n $workspaceId: ID!\n $teamId: ID\n $projectId: ID\n $horizonSprints: Int\n ) {\n planmagnetResourceForecast(\n workspaceId: $workspaceId\n teamId: $teamId\n projectId: $projectId\n horizonSprints: $horizonSprints\n ) {\n scopeType\n horizonSprints\n openEstimatedPoints\n unestimatedOpenItems\n backlogTruncated\n insufficientHistory\n velocityP50\n sprintsToClear\n confidenceNote\n capacityGaps {\n sprintNumber\n demandPoints\n forecastCapacityPoints\n gapPoints\n }\n }\n }\n`;\n\ninterface ResourceForecast {\n scopeType: 'TEAM' | 'PROJECT';\n horizonSprints: number;\n openEstimatedPoints: number;\n unestimatedOpenItems: number;\n backlogTruncated: boolean;\n insufficientHistory: boolean;\n velocityP50: number | null;\n sprintsToClear: number | null;\n confidenceNote: string;\n capacityGaps: {\n sprintNumber: number;\n demandPoints: number;\n forecastCapacityPoints: number;\n gapPoints: number;\n }[];\n}\n\ninterface ResourceForecastData {\n planmagnetResourceForecast: ResourceForecast;\n}\n\nexport function ResourceForecastCard({\n workspaceId,\n teamId,\n projectId,\n horizonSprints = 3,\n}: {\n workspaceId?: string | null;\n /** Exactly one of teamId/projectId — the server requires it (`horizonSprints` schema doc). */\n teamId?: string;\n projectId?: string;\n horizonSprints?: number;\n}) {\n const { t } = useI18n();\n const { data, loading, error } = useQuery<ResourceForecastData>(GET_RESOURCE_FORECAST, {\n variables: { workspaceId, teamId, projectId, horizonSprints },\n skip: !workspaceId || (!teamId && !projectId),\n });\n const forecast = data?.planmagnetResourceForecast;\n const maxGapMagnitude = Math.max(\n 1,\n ...(forecast?.capacityGaps.map((g) => Math.abs(g.gapPoints)) ?? [1])\n );\n\n return (\n <Card data-testid=\"resource-forecast\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Gauge className=\"h-4 w-4\" />\n {t('planmagnet.forecast.resource.title', 'Resource forecast')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.forecast.resource.subtitle',\n 'Open backlog vs. historical velocity, over the next {{count}} sprint(s).',\n { count: horizonSprints }\n )}\n </p>\n </CardHeader>\n <CardContent>\n {!teamId && !projectId ? null : loading && !data ? (\n <div className=\"flex justify-center py-8\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.forecast.resource.forbidden',\n 'You do not have access to the resource forecast for this scope.'\n )\n : `${t('planmagnet.forecast.resource.error', 'Resource forecast could not be loaded')}: ${error.message}`}\n </p>\n ) : !forecast ? null : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatTile\n label={t('planmagnet.forecast.resource.openPoints', 'Open est. points')}\n value={String(forecast.openEstimatedPoints)}\n />\n <StatTile\n label={t('planmagnet.forecast.resource.unestimated', 'Unestimated items')}\n value={String(forecast.unestimatedOpenItems)}\n />\n <StatTile\n label={t('planmagnet.forecast.resource.velocity', 'Velocity (p50)')}\n value={\n forecast.velocityP50 === null ? '—' : String(Math.round(forecast.velocityP50))\n }\n />\n <StatTile\n label={t('planmagnet.forecast.resource.sprintsToClear', 'Sprints to clear')}\n value={forecast.sprintsToClear === null ? '—' : forecast.sprintsToClear.toFixed(1)}\n />\n </div>\n\n {forecast.insufficientHistory ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.forecast.resource.insufficient',\n 'Not enough completed-sprint history yet to forecast capacity for this scope.'\n )}\n </p>\n ) : (\n <div>\n <h3 className=\"mb-1 text-sm font-medium\">\n {t('planmagnet.forecast.resource.capacityGaps', 'Capacity vs. demand, by sprint')}\n </h3>\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"text-left text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.forecast.resource.sprint', 'Sprint')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.forecast.resource.demand', 'Demand')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.forecast.resource.capacity', 'Capacity')}\n </th>\n <th className=\"w-1/3 py-1 text-right font-normal\">\n {t('planmagnet.forecast.resource.gap', 'Gap')}\n </th>\n </tr>\n </thead>\n <tbody>\n {forecast.capacityGaps.map((row) => {\n const short = row.gapPoints > 0;\n return (\n <tr key={row.sprintNumber} className=\"border-t align-middle\">\n <td className=\"py-1.5 pr-2\">\n {t('planmagnet.forecast.resource.sprintN', '+{{n}}', {\n n: row.sprintNumber,\n })}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {row.demandPoints}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {Math.round(row.forecastCapacityPoints)}\n </td>\n <td className=\"py-1.5\">\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={maxGapMagnitude}\n aria-valuenow={Math.abs(row.gapPoints)}\n aria-label={`${short ? 'short' : 'surplus'} ${Math.abs(Math.round(row.gapPoints))} points`}\n className=\"flex h-2 w-full items-center overflow-hidden rounded-full bg-muted\"\n >\n <div\n className={`h-full rounded-full ${short ? 'bg-status-error' : 'bg-status-success'}`}\n style={{\n width: `${Math.min(100, (Math.abs(row.gapPoints) / maxGapMagnitude) * 100)}%`,\n }}\n />\n </div>\n <span\n className={`text-right block tabular-nums ${short ? 'text-status-error-text' : 'text-status-success-text'}`}\n >\n {short ? '+' : ''}\n {Math.round(row.gapPoints)}\n </span>\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n )}\n\n {forecast.backlogTruncated && (\n <p className=\"text-xs text-status-warning-text\">\n {t(\n 'planmagnet.forecast.resource.truncated',\n 'This backlog is large enough that the scan stopped early — open points and unestimated items reflect only the first items found.'\n )}\n </p>\n )}\n\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.forecast.resource.skillsNote',\n 'Skills demand is not modelled — this forecasts total story points only, not which skills the backlog actually needs.'\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">{forecast.confidenceNote}</p>\n\n <details className=\"text-xs text-muted-foreground\">\n <summary className=\"cursor-pointer select-none font-medium text-foreground\">\n {t('planmagnet.forecast.whatIsThis', 'What is this?')}\n </summary>\n <p className=\"mt-1\">\n {t(\n 'planmagnet.forecast.resource.explainer',\n 'Statistical forecast (not AI-generated): capacity is the median of the trailing completed sprints’ real velocity; demand is today’s open backlog split evenly across the horizon. Never an AI/LLM call.'\n )}\n </p>\n </details>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ResourceForecastCard.js","names":[],"sources":["../../../src/planmagnet/forecast/ResourceForecastCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Gauge } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { StatTile } from '../pages/analytics/DeliveryHealthCards';\n\n/**\n * ANR-11: `planmagnetResourceForecast` — open backlog vs historical velocity, and the resulting\n * per-sprint capacity gap over the horizon. Used on the team detail page (team scope) and the\n * project detail page (project scope) — same card either way, one of `teamId`/`projectId` set.\n *\n * Statistical, never AI/LLM: reuses the same trailing-sprint-velocity resolution\n * `planmagnetForecastDelivery` does. Demand is the CURRENT open backlog split evenly across the\n * horizon (not a burndown simulation) and capacity never trends — see the field doc comments in\n * graphql/wspace.graphql. Skills/roles are not modelled: this is a total-points forecast only.\n */\n\nconst GET_RESOURCE_FORECAST = gql`\n query PlanmagnetResourceForecast(\n $workspaceId: ID!\n $teamId: ID\n $projectId: ID\n $horizonSprints: Int\n ) {\n planmagnetResourceForecast(\n workspaceId: $workspaceId\n teamId: $teamId\n projectId: $projectId\n horizonSprints: $horizonSprints\n ) {\n scopeType\n horizonSprints\n openEstimatedPoints\n unestimatedOpenItems\n backlogTruncated\n insufficientHistory\n velocityP50\n sprintsToClear\n confidenceNote\n capacityGaps {\n sprintNumber\n demandPoints\n forecastCapacityPoints\n gapPoints\n }\n }\n }\n`;\n\ninterface ResourceForecast {\n scopeType: 'TEAM' | 'PROJECT';\n horizonSprints: number;\n openEstimatedPoints: number;\n unestimatedOpenItems: number;\n backlogTruncated: boolean;\n insufficientHistory: boolean;\n velocityP50: number | null;\n sprintsToClear: number | null;\n confidenceNote: string;\n capacityGaps: {\n sprintNumber: number;\n demandPoints: number;\n forecastCapacityPoints: number;\n gapPoints: number;\n }[];\n}\n\ninterface ResourceForecastData {\n planmagnetResourceForecast: ResourceForecast;\n}\n\nexport function ResourceForecastCard({\n workspaceId,\n teamId,\n projectId,\n horizonSprints = 3,\n}: {\n workspaceId?: string | null;\n /** Exactly one of teamId/projectId — the server requires it (`horizonSprints` schema doc). */\n teamId?: string;\n projectId?: string;\n horizonSprints?: number;\n}) {\n const { t } = useI18n();\n const { data, loading, error } = useQuery<ResourceForecastData>(GET_RESOURCE_FORECAST, {\n variables: { workspaceId, teamId, projectId, horizonSprints },\n skip: !workspaceId || (!teamId && !projectId),\n });\n const forecast = data?.planmagnetResourceForecast;\n const maxGapMagnitude = Math.max(\n 1,\n ...(forecast?.capacityGaps.map((g) => Math.abs(g.gapPoints)) ?? [1])\n );\n\n return (\n <Card data-testid=\"resource-forecast\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Gauge className=\"h-4 w-4\" />\n {t('planmagnet.forecast.resource.title', 'Resource forecast')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.forecast.resource.subtitle',\n 'Open backlog vs. historical velocity, over the next {{count}} sprint(s).',\n { count: horizonSprints }\n )}\n </p>\n </CardHeader>\n <CardContent>\n {!teamId && !projectId ? null : loading && !data ? (\n <div className=\"flex justify-center py-8\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.forecast.resource.forbidden',\n 'You do not have access to the resource forecast for this scope.'\n )\n : `${t('planmagnet.forecast.resource.error', 'Resource forecast could not be loaded')}: ${error.message}`}\n </p>\n ) : !forecast ? null : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatTile\n label={t('planmagnet.forecast.resource.openPoints', 'Open est. points')}\n value={String(forecast.openEstimatedPoints)}\n />\n <StatTile\n label={t('planmagnet.forecast.resource.unestimated', 'Unestimated items')}\n value={String(forecast.unestimatedOpenItems)}\n />\n <StatTile\n label={t('planmagnet.forecast.resource.velocity', 'Velocity (p50)')}\n value={\n forecast.velocityP50 === null ? '—' : String(Math.round(forecast.velocityP50))\n }\n />\n <StatTile\n label={t('planmagnet.forecast.resource.sprintsToClear', 'Sprints to clear')}\n value={forecast.sprintsToClear === null ? '—' : forecast.sprintsToClear.toFixed(1)}\n />\n </div>\n\n {forecast.insufficientHistory ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.forecast.resource.insufficient',\n 'Not enough completed-sprint history yet to forecast capacity for this scope.'\n )}\n </p>\n ) : (\n <div>\n <h3 className=\"mb-1 text-sm font-medium\">\n {t('planmagnet.forecast.resource.capacityGaps', 'Capacity vs. demand, by sprint')}\n </h3>\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"text-left text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.forecast.resource.sprint', 'Sprint')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.forecast.resource.demand', 'Demand')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {t('planmagnet.forecast.resource.capacity', 'Capacity')}\n </th>\n <th className=\"w-1/3 py-1 text-right font-normal\">\n {t('planmagnet.forecast.resource.gap', 'Gap')}\n </th>\n </tr>\n </thead>\n <tbody>\n {forecast.capacityGaps.map((row) => {\n const short = row.gapPoints > 0;\n return (\n <tr key={row.sprintNumber} className=\"border-t align-middle\">\n <td className=\"py-1.5 pr-2\">\n {t('planmagnet.forecast.resource.sprintN', '+{{n}}', {\n n: row.sprintNumber,\n })}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {row.demandPoints}\n </td>\n <td className=\"py-1.5 pr-2 text-right tabular-nums\">\n {Math.round(row.forecastCapacityPoints)}\n </td>\n <td className=\"py-1.5\">\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={maxGapMagnitude}\n aria-valuenow={Math.abs(row.gapPoints)}\n aria-label={`${short ? 'short' : 'surplus'} ${Math.abs(Math.round(row.gapPoints))} points`}\n className=\"flex h-2 w-full items-center overflow-hidden rounded-full bg-muted\"\n >\n <div\n className={`h-full rounded-full ${short ? 'bg-status-error' : 'bg-status-success'}`}\n style={{\n width: `${Math.min(100, (Math.abs(row.gapPoints) / maxGapMagnitude) * 100)}%`,\n }}\n />\n </div>\n <span\n className={`text-right block tabular-nums ${short ? 'text-status-error-text' : 'text-status-success-text'}`}\n >\n {short ? '+' : ''}\n {Math.round(row.gapPoints)}\n </span>\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n )}\n\n {forecast.backlogTruncated && (\n <p className=\"text-xs text-status-warning-text\">\n {t(\n 'planmagnet.forecast.resource.truncated',\n 'This backlog is large enough that the scan stopped early — open points and unestimated items reflect only the first items found.'\n )}\n </p>\n )}\n\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.forecast.resource.skillsNoteCoverage',\n 'This forecasts total story points only, not which skills the backlog needs — the Skills coverage card on the project manager and team dashboards shows that.'\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">{forecast.confidenceNote}</p>\n\n <details className=\"text-xs text-muted-foreground\">\n <summary className=\"cursor-pointer select-none font-medium text-foreground\">\n {t('planmagnet.forecast.whatIsThis', 'What is this?')}\n </summary>\n <p className=\"mt-1\">\n {t(\n 'planmagnet.forecast.resource.explainer',\n 'Statistical forecast (not AI-generated): capacity is the median of the trailing completed sprints’ real velocity; demand is today’s open backlog split evenly across the horizon. Never an AI/LLM call.'\n )}\n </p>\n </details>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ export type { PlanMagnetRootProps, NavItem, FooterItem } from './types';
4
4
  export { usePlanMagnet, usePlanMagnetNavigate } from './providers/PlanMagnetProvider';
5
5
  export { usePlanMagnetStore } from './store/planmagnetStore';
6
6
  export { INTEGRATIONS_SYNCS_PATH, INTEGRATIONS_NEW_SYNC_PATH, INTEGRATIONS_SYNC_DETAIL_PATH, integrationsSyncDetailPath, } from './integrations/routes';
7
- export { PRIORITIZATION_PATH, PRODUCT_LINES_PATH, PRODUCT_HIERARCHY_PATH } from './product/routes';
7
+ export { PRIORITIZATION_PATH, PRODUCT_LINES_PATH, PRODUCT_HIERARCHY_PATH, CUSTOMER_SATISFACTION_PATH, } from './product/routes';
8
8
  export { SharedReportPage } from './reports/SharedReportPage';
9
9
  export { SHARED_REPORT_ROUTE_PATH, SHARED_REPORT_BASE_PATH } from './reports/sharedReportRoute';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/planmagnet/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAKnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/planmagnet/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAK1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Project manager dashboard (ANR-07): velocity, burndown, task completion, quality/defects, risk tracking
3
- * and sprint capacity for one picked project. The project stays in the URL (`?project=`) so the page can be
4
- * shared or bookmarked pointed at a specific project.
2
+ * Project manager dashboard (ANR-07): velocity, burndown, task completion, quality/defects, risk tracking,
3
+ * sprint capacity and skills coverage for one picked project. The project stays in the URL (`?project=`) so
4
+ * the page can be shared or bookmarked pointed at a specific project.
5
5
  */
6
6
  export declare function DashboardsProjectManagerPagePage(): import("react/jsx-runtime").JSX.Element;
7
7
  //# sourceMappingURL=DashboardsProjectManagerPagePage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardsProjectManagerPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardsProjectManagerPagePage.tsx"],"names":[],"mappings":"AAoDA;;;;GAIG;AACH,wBAAgB,gCAAgC,4CAuI/C"}
1
+ {"version":3,"file":"DashboardsProjectManagerPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardsProjectManagerPagePage.tsx"],"names":[],"mappings":"AAsDA;;;;GAIG;AACH,wBAAgB,gCAAgC,4CA0I/C"}
@@ -4,12 +4,13 @@ import { ProjectSelect as r } from "../controls/Pickers.js";
4
4
  import { readUrlParam as i, writeUrlParam as a } from "../utils/urlParam.js";
5
5
  import { combineLiveRefreshModes as o, useLiveRefresh as s } from "../hooks/useLiveRefresh.js";
6
6
  import { LiveStatus as c } from "../controls/LiveStatus.js";
7
- import { DashboardCardShell as l, NoSkillsNote as u } from "../dashboards/DashboardUI.js";
8
- import { TaskCompletionCard as d } from "../dashboards/cards/TaskCompletionCard.js";
9
- import { QualityMetricsCard as f } from "../dashboards/cards/QualityMetricsCard.js";
10
- import { RiskSeverityCard as p } from "../dashboards/cards/RiskSeverityCard.js";
11
- import { SprintCapacityCard as m } from "../dashboards/cards/SprintCapacityCard.js";
12
- import { SprintBurndownCard as h, SprintVelocityCard as g } from "./analytics/SprintDeliveryCards.js";
7
+ import { DashboardCardShell as l } from "../dashboards/DashboardUI.js";
8
+ import { TaskCompletionCard as u } from "../dashboards/cards/TaskCompletionCard.js";
9
+ import { QualityMetricsCard as d } from "../dashboards/cards/QualityMetricsCard.js";
10
+ import { RiskSeverityCard as f } from "../dashboards/cards/RiskSeverityCard.js";
11
+ import { SprintCapacityCard as p } from "../dashboards/cards/SprintCapacityCard.js";
12
+ import { SprintBurndownCard as m, SprintVelocityCard as h } from "./analytics/SprintDeliveryCards.js";
13
+ import { SkillCoverageCard as g } from "../skills/SkillCoverageCard.js";
13
14
  import { useState as _ } from "react";
14
15
  import { ClipboardList as v } from "lucide-react";
15
16
  import { gql as y } from "@apollo/client";
@@ -24,7 +25,8 @@ var w = 720 * 60 * 60 * 1e3, T = [
24
25
  "DashboardProjectWorkItemCounts",
25
26
  "DashboardQualityMetrics",
26
27
  "DashboardProjectRisks",
27
- "DashboardSprintCapacity"
28
+ "DashboardSprintCapacity",
29
+ "SkillCoverage"
28
30
  ], E = y`
29
31
  query DashboardProjectSprints($workspaceId: ID!, $projectId: String) {
30
32
  planmagnetSprints(
@@ -74,7 +76,7 @@ function D() {
74
76
  }), /* @__PURE__ */ x(c, { mode: z })]
75
77
  }), /* @__PURE__ */ x("p", {
76
78
  className: "text-sm text-muted-foreground",
77
- children: O("planmagnet.dashboards.pm.subtitle", "Velocity, burndown, task completion, quality, risk and capacity for one project.")
79
+ children: O("planmagnet.dashboards.pm.subtitleWithSkills", "Velocity, burndown, task completion, quality, risk, capacity and skills coverage for one project.")
78
80
  })] }), /* @__PURE__ */ x("div", {
79
81
  className: "w-full max-w-xs",
80
82
  children: /* @__PURE__ */ x(r, {
@@ -92,7 +94,7 @@ function D() {
92
94
  label: O("planmagnet.dashboards.pm.openAnalytics", "Open delivery analytics"),
93
95
  onClick: () => D(`/analytics/analytics-page?project=${encodeURIComponent(k)}`)
94
96
  } : void 0,
95
- children: /* @__PURE__ */ x(g, {
97
+ children: /* @__PURE__ */ x(h, {
96
98
  workspaceId: y,
97
99
  projectId: k,
98
100
  trailingSprintCount: 6
@@ -103,34 +105,39 @@ function D() {
103
105
  label: O("planmagnet.dashboards.pm.openAnalytics", "Open delivery analytics"),
104
106
  onClick: () => D(`/analytics/analytics-page?project=${encodeURIComponent(k)}`)
105
107
  } : void 0,
106
- children: /* @__PURE__ */ x(h, {
108
+ children: /* @__PURE__ */ x(m, {
107
109
  workspaceId: y,
108
110
  sprintId: I
109
111
  })
110
112
  }),
111
- /* @__PURE__ */ x(d, {
113
+ /* @__PURE__ */ x(u, {
112
114
  workspaceId: y,
113
115
  projectId: k,
114
116
  onDrillDown: () => D(n("/projects/project-detail-page", k))
115
117
  }),
116
- /* @__PURE__ */ x(f, {
118
+ /* @__PURE__ */ x(d, {
117
119
  workspaceId: y,
118
120
  projectId: k,
119
121
  startDate: N,
120
122
  endDate: P,
121
123
  onDrillDown: () => D(`/projects/quality-gates-page?projectId=${encodeURIComponent(k)}`)
122
124
  }),
123
- /* @__PURE__ */ x(p, {
125
+ /* @__PURE__ */ x(f, {
124
126
  workspaceId: y,
125
127
  projectId: k,
126
128
  onDrillDown: () => D(`/risks/risk-register-page?projectId=${encodeURIComponent(k)}`)
127
129
  }),
128
- /* @__PURE__ */ S("div", {
129
- className: "space-y-2",
130
- children: [/* @__PURE__ */ x(m, {
131
- sprintId: I,
132
- onDrillDown: () => D(n("/projects/project-detail-page", k))
133
- }), /* @__PURE__ */ x(u, { message: O("planmagnet.dashboards.pm.noSkillView", "There is no skill concept in PlanMagnet — capacity above is per person, never per skill.") })]
130
+ /* @__PURE__ */ x(p, {
131
+ sprintId: I,
132
+ onDrillDown: () => D(n("/projects/project-detail-page", k))
133
+ }),
134
+ /* @__PURE__ */ x("div", {
135
+ className: "lg:col-span-2",
136
+ children: /* @__PURE__ */ x(g, {
137
+ workspaceId: y,
138
+ projectId: k,
139
+ notReadyMessage: O("planmagnet.skills.coverage.pickProject", "Select a project to see which skills its open work needs.")
140
+ })
134
141
  })
135
142
  ]
136
143
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardsProjectManagerPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardsProjectManagerPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ClipboardList } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectSelect } from '../controls/Pickers';\nimport { readUrlParam, writeUrlParam } from '../utils/urlParam';\nimport { detailPath } from '../utils/rowNavigation';\nimport { DashboardCardShell, NoSkillsNote } from '../dashboards/DashboardUI';\nimport { TaskCompletionCard } from '../dashboards/cards/TaskCompletionCard';\nimport { QualityMetricsCard } from '../dashboards/cards/QualityMetricsCard';\nimport { RiskSeverityCard } from '../dashboards/cards/RiskSeverityCard';\nimport { SprintCapacityCard } from '../dashboards/cards/SprintCapacityCard';\nimport { SprintBurndownCard, SprintVelocityCard } from './analytics/SprintDeliveryCards';\nimport { useLiveRefresh, combineLiveRefreshModes } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\n\nconst THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** Every card's own query on this page (ANR-07), scoped to the picked project. */\nconst PM_QUERIES = [\n 'DashboardProjectSprints',\n 'DashboardVelocity',\n 'DashboardSprintBurndown',\n 'DashboardProjectWorkItemCounts',\n 'DashboardQualityMetrics',\n 'DashboardProjectRisks',\n 'DashboardSprintCapacity',\n];\n\n/** The project's sprints, to find the running one for burndown/capacity — same lookup\n * `pages/AnalyticsAnalyticsPagePage.tsx` runs, scoped to the picked project. */\nconst GET_PROJECT_SPRINTS = gql`\n query DashboardProjectSprints($workspaceId: ID!, $projectId: String) {\n planmagnetSprints(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n status\n }\n }\n }\n`;\n\ninterface ProjectSprintsData {\n planmagnetSprints: { items: { id: string; status: string }[] };\n}\n\n/**\n * Project manager dashboard (ANR-07): velocity, burndown, task completion, quality/defects, risk tracking\n * and sprint capacity for one picked project. The project stays in the URL (`?project=`) so the page can be\n * shared or bookmarked pointed at a specific project.\n */\nexport function DashboardsProjectManagerPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { t } = useI18n();\n const [projectId, setProjectId] = useState(() => readUrlParam('project') ?? '');\n const handleProjectChange = (id: string) => {\n setProjectId(id);\n writeUrlParam('project', id || null);\n };\n // Fixed for the life of the page.\n const [now] = useState(() => Date.now());\n const startDate = new Date(now - THIRTY_DAYS_MS).toISOString();\n const endDate = new Date(now).toISOString();\n\n const { data: sprintsData } = useQuery<ProjectSprintsData>(GET_PROJECT_SPRINTS, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n const sprints = sprintsData?.planmagnetSprints.items ?? [];\n const activeSprintId =\n sprints.find((s) => s.status === 'ACTIVE' || s.status === 'REVIEW')?.id ?? '';\n\n // Live updates (ANR-23): work item AND sprint events for the picked project, throttled to at most once per\n // 15s — off entirely until a project is picked (no `projectId` means no scope to subscribe to).\n const workItemLive = useLiveRefresh({\n kind: 'workItem',\n projectId,\n enabled: Boolean(projectId),\n queries: PM_QUERIES,\n minIntervalMs: 15000,\n });\n const sprintLive = useLiveRefresh({\n kind: 'sprint',\n projectId,\n enabled: Boolean(projectId),\n queries: PM_QUERIES,\n minIntervalMs: 15000,\n });\n const liveMode = combineLiveRefreshModes(workItemLive.mode, sprintLive.mode);\n\n return (\n <div data-testid=\"dashboard-pm\" className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <ClipboardList className=\"h-5 w-5 text-primary\" />\n {t('planmagnet.dashboards.pm.title', 'Project manager dashboard')}\n </h1>\n <LiveStatus mode={liveMode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.subtitle',\n 'Velocity, burndown, task completion, quality, risk and capacity for one project.'\n )}\n </p>\n </div>\n <div className=\"w-full max-w-xs\">\n <ProjectSelect\n id=\"pm-dashboard-project\"\n value={projectId}\n onChange={handleProjectChange}\n noneLabel={t('planmagnet.dashboards.pm.selectProject', 'Select a project…')}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <DashboardCardShell\n drillDown={\n projectId\n ? {\n label: t('planmagnet.dashboards.pm.openAnalytics', 'Open delivery analytics'),\n onClick: () =>\n navigate(`/analytics/analytics-page?project=${encodeURIComponent(projectId)}`),\n }\n : undefined\n }\n >\n <SprintVelocityCard\n workspaceId={workspaceId}\n projectId={projectId}\n trailingSprintCount={6}\n />\n </DashboardCardShell>\n <DashboardCardShell\n drillDown={\n projectId\n ? {\n label: t('planmagnet.dashboards.pm.openAnalytics', 'Open delivery analytics'),\n onClick: () =>\n navigate(`/analytics/analytics-page?project=${encodeURIComponent(projectId)}`),\n }\n : undefined\n }\n >\n <SprintBurndownCard workspaceId={workspaceId} sprintId={activeSprintId} />\n </DashboardCardShell>\n <TaskCompletionCard\n workspaceId={workspaceId}\n projectId={projectId}\n onDrillDown={() => navigate(detailPath('/projects/project-detail-page', projectId))}\n />\n <QualityMetricsCard\n workspaceId={workspaceId}\n projectId={projectId}\n startDate={startDate}\n endDate={endDate}\n onDrillDown={() =>\n navigate(`/projects/quality-gates-page?projectId=${encodeURIComponent(projectId)}`)\n }\n />\n <RiskSeverityCard\n workspaceId={workspaceId}\n projectId={projectId}\n onDrillDown={() =>\n navigate(`/risks/risk-register-page?projectId=${encodeURIComponent(projectId)}`)\n }\n />\n <div className=\"space-y-2\">\n <SprintCapacityCard\n sprintId={activeSprintId}\n onDrillDown={() => navigate(detailPath('/projects/project-detail-page', projectId))}\n />\n <NoSkillsNote\n message={t(\n 'planmagnet.dashboards.pm.noSkillView',\n 'There is no skill concept in PlanMagnet — capacity above is per person, never per skill.'\n )}\n />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardsProjectManagerPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardsProjectManagerPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ClipboardList } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectSelect } from '../controls/Pickers';\nimport { readUrlParam, writeUrlParam } from '../utils/urlParam';\nimport { detailPath } from '../utils/rowNavigation';\nimport { DashboardCardShell } from '../dashboards/DashboardUI';\nimport { TaskCompletionCard } from '../dashboards/cards/TaskCompletionCard';\nimport { QualityMetricsCard } from '../dashboards/cards/QualityMetricsCard';\nimport { RiskSeverityCard } from '../dashboards/cards/RiskSeverityCard';\nimport { SprintCapacityCard } from '../dashboards/cards/SprintCapacityCard';\nimport { SprintBurndownCard, SprintVelocityCard } from './analytics/SprintDeliveryCards';\nimport { SkillCoverageCard } from '../skills/SkillCoverageCard';\nimport { useLiveRefresh, combineLiveRefreshModes } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\n\nconst THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** Every card's own query on this page (ANR-07), scoped to the picked project. */\nconst PM_QUERIES = [\n 'DashboardProjectSprints',\n 'DashboardVelocity',\n 'DashboardSprintBurndown',\n 'DashboardProjectWorkItemCounts',\n 'DashboardQualityMetrics',\n 'DashboardProjectRisks',\n 'DashboardSprintCapacity',\n 'SkillCoverage',\n];\n\n/** The project's sprints, to find the running one for burndown/capacity — same lookup\n * `pages/AnalyticsAnalyticsPagePage.tsx` runs, scoped to the picked project. */\nconst GET_PROJECT_SPRINTS = gql`\n query DashboardProjectSprints($workspaceId: ID!, $projectId: String) {\n planmagnetSprints(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n status\n }\n }\n }\n`;\n\ninterface ProjectSprintsData {\n planmagnetSprints: { items: { id: string; status: string }[] };\n}\n\n/**\n * Project manager dashboard (ANR-07): velocity, burndown, task completion, quality/defects, risk tracking,\n * sprint capacity and skills coverage for one picked project. The project stays in the URL (`?project=`) so\n * the page can be shared or bookmarked pointed at a specific project.\n */\nexport function DashboardsProjectManagerPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { t } = useI18n();\n const [projectId, setProjectId] = useState(() => readUrlParam('project') ?? '');\n const handleProjectChange = (id: string) => {\n setProjectId(id);\n writeUrlParam('project', id || null);\n };\n // Fixed for the life of the page.\n const [now] = useState(() => Date.now());\n const startDate = new Date(now - THIRTY_DAYS_MS).toISOString();\n const endDate = new Date(now).toISOString();\n\n const { data: sprintsData } = useQuery<ProjectSprintsData>(GET_PROJECT_SPRINTS, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n const sprints = sprintsData?.planmagnetSprints.items ?? [];\n const activeSprintId =\n sprints.find((s) => s.status === 'ACTIVE' || s.status === 'REVIEW')?.id ?? '';\n\n // Live updates (ANR-23): work item AND sprint events for the picked project, throttled to at most once per\n // 15s — off entirely until a project is picked (no `projectId` means no scope to subscribe to).\n const workItemLive = useLiveRefresh({\n kind: 'workItem',\n projectId,\n enabled: Boolean(projectId),\n queries: PM_QUERIES,\n minIntervalMs: 15000,\n });\n const sprintLive = useLiveRefresh({\n kind: 'sprint',\n projectId,\n enabled: Boolean(projectId),\n queries: PM_QUERIES,\n minIntervalMs: 15000,\n });\n const liveMode = combineLiveRefreshModes(workItemLive.mode, sprintLive.mode);\n\n return (\n <div data-testid=\"dashboard-pm\" className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <ClipboardList className=\"h-5 w-5 text-primary\" />\n {t('planmagnet.dashboards.pm.title', 'Project manager dashboard')}\n </h1>\n <LiveStatus mode={liveMode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.pm.subtitleWithSkills',\n 'Velocity, burndown, task completion, quality, risk, capacity and skills coverage for one project.'\n )}\n </p>\n </div>\n <div className=\"w-full max-w-xs\">\n <ProjectSelect\n id=\"pm-dashboard-project\"\n value={projectId}\n onChange={handleProjectChange}\n noneLabel={t('planmagnet.dashboards.pm.selectProject', 'Select a project…')}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <DashboardCardShell\n drillDown={\n projectId\n ? {\n label: t('planmagnet.dashboards.pm.openAnalytics', 'Open delivery analytics'),\n onClick: () =>\n navigate(`/analytics/analytics-page?project=${encodeURIComponent(projectId)}`),\n }\n : undefined\n }\n >\n <SprintVelocityCard\n workspaceId={workspaceId}\n projectId={projectId}\n trailingSprintCount={6}\n />\n </DashboardCardShell>\n <DashboardCardShell\n drillDown={\n projectId\n ? {\n label: t('planmagnet.dashboards.pm.openAnalytics', 'Open delivery analytics'),\n onClick: () =>\n navigate(`/analytics/analytics-page?project=${encodeURIComponent(projectId)}`),\n }\n : undefined\n }\n >\n <SprintBurndownCard workspaceId={workspaceId} sprintId={activeSprintId} />\n </DashboardCardShell>\n <TaskCompletionCard\n workspaceId={workspaceId}\n projectId={projectId}\n onDrillDown={() => navigate(detailPath('/projects/project-detail-page', projectId))}\n />\n <QualityMetricsCard\n workspaceId={workspaceId}\n projectId={projectId}\n startDate={startDate}\n endDate={endDate}\n onDrillDown={() =>\n navigate(`/projects/quality-gates-page?projectId=${encodeURIComponent(projectId)}`)\n }\n />\n <RiskSeverityCard\n workspaceId={workspaceId}\n projectId={projectId}\n onDrillDown={() =>\n navigate(`/risks/risk-register-page?projectId=${encodeURIComponent(projectId)}`)\n }\n />\n <SprintCapacityCard\n sprintId={activeSprintId}\n onDrillDown={() => navigate(detailPath('/projects/project-detail-page', projectId))}\n />\n {/* ANR-07 skill view: the project's open work by required skill, against CrewFoundry skills. */}\n <div className=\"lg:col-span-2\">\n <SkillCoverageCard\n workspaceId={workspaceId}\n projectId={projectId}\n notReadyMessage={t(\n 'planmagnet.skills.coverage.pickProject',\n 'Select a project to see which skills its open work needs.'\n )}\n />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Team utilization dashboard (ANR-28): capacity/allocation, workload distribution, individual delivery and
3
- * time logged for one picked team. The team stays in the URL (`?team=`).
2
+ * Team utilization dashboard (ANR-28): capacity/allocation, workload distribution, individual delivery,
3
+ * time logged and skill coverage for one picked team. The team stays in the URL (`?team=`).
4
4
  */
5
5
  export declare function DashboardsTeamUtilizationPagePage(): import("react/jsx-runtime").JSX.Element;
6
6
  //# sourceMappingURL=DashboardsTeamUtilizationPagePage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardsTeamUtilizationPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardsTeamUtilizationPagePage.tsx"],"names":[],"mappings":"AA2CA;;;GAGG;AACH,wBAAgB,iCAAiC,4CAsFhD"}
1
+ {"version":3,"file":"DashboardsTeamUtilizationPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardsTeamUtilizationPagePage.tsx"],"names":[],"mappings":"AA4CA;;;GAGG;AACH,wBAAgB,iCAAiC,4CA0FhD"}
@@ -4,7 +4,7 @@ import { TeamSelect as r } from "../controls/Pickers.js";
4
4
  import { readUrlParam as i, writeUrlParam as a } from "../utils/urlParam.js";
5
5
  import { useLiveRefresh as o } from "../hooks/useLiveRefresh.js";
6
6
  import { LiveStatus as s } from "../controls/LiveStatus.js";
7
- import { NoSkillsNote as c } from "../dashboards/DashboardUI.js";
7
+ import { SkillCoverageCard as c } from "../skills/SkillCoverageCard.js";
8
8
  import { TeamOverviewCard as l } from "../dashboards/cards/TeamOverviewCard.js";
9
9
  import { TeamWorkloadCard as u } from "../dashboards/cards/TeamWorkloadCard.js";
10
10
  import { TimeLoggedCard as d } from "../dashboards/cards/TimeLoggedCard.js";
@@ -19,7 +19,8 @@ var y = 672 * 60 * 60 * 1e3, b = [
19
19
  "DashboardTeamMemberIds",
20
20
  "DashboardTeamOverview",
21
21
  "TeamMemberWorkload",
22
- "TeamMemberTimeLogged"
22
+ "TeamMemberTimeLogged",
23
+ "SkillCoverage"
23
24
  ], x = m`
24
25
  query DashboardTeamMemberIds($id: ID!) {
25
26
  planmagnetTeam(id: $id) {
@@ -55,7 +56,7 @@ function S() {
55
56
  }), /* @__PURE__ */ g(s, { mode: k.mode })]
56
57
  }), /* @__PURE__ */ g("p", {
57
58
  className: "text-sm text-muted-foreground",
58
- children: C("planmagnet.dashboards.team.subtitle", "Capacity, workload, individual delivery and time logged for one team.")
59
+ children: C("planmagnet.dashboards.team.subtitleWithSkills", "Capacity, workload, individual delivery, time logged and skill coverage for one team.")
59
60
  })] }), /* @__PURE__ */ g("div", {
60
61
  className: "w-full max-w-xs",
61
62
  children: /* @__PURE__ */ g(r, {
@@ -72,15 +73,12 @@ function S() {
72
73
  teamId: w,
73
74
  onDrillDown: () => S(n("/team/team-detail-page", w))
74
75
  }),
75
- /* @__PURE__ */ _("div", {
76
- className: "space-y-2",
77
- children: [/* @__PURE__ */ g(u, {
78
- workspaceId: m,
79
- memberActorIds: A,
80
- sinceIso: D,
81
- weeks: 4,
82
- onDrillDown: () => S(n("/team/team-detail-page", w))
83
- }), /* @__PURE__ */ g(c, { message: C("planmagnet.dashboards.team.noSkillUtilization", "Skill utilization is not modelled in PlanMagnet — workload above is by person only.") })]
76
+ /* @__PURE__ */ g(u, {
77
+ workspaceId: m,
78
+ memberActorIds: A,
79
+ sinceIso: D,
80
+ weeks: 4,
81
+ onDrillDown: () => S(n("/team/team-detail-page", w))
84
82
  }),
85
83
  /* @__PURE__ */ g(d, {
86
84
  workspaceId: m,
@@ -88,6 +86,14 @@ function S() {
88
86
  sinceIso: D,
89
87
  weeks: 4,
90
88
  onDrillDown: () => S("/tasks/time-entries-page")
89
+ }),
90
+ /* @__PURE__ */ g("div", {
91
+ className: "lg:col-span-2",
92
+ children: /* @__PURE__ */ g(c, {
93
+ workspaceId: m,
94
+ teamId: w,
95
+ notReadyMessage: C("planmagnet.skills.coverage.pickTeam", "Select a team to see which skills its open work needs.")
96
+ })
91
97
  })
92
98
  ]
93
99
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardsTeamUtilizationPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardsTeamUtilizationPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { UsersRound } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { TeamSelect } from '../controls/Pickers';\nimport { readUrlParam, writeUrlParam } from '../utils/urlParam';\nimport { detailPath } from '../utils/rowNavigation';\nimport { NoSkillsNote } from '../dashboards/DashboardUI';\nimport { TeamOverviewCard } from '../dashboards/cards/TeamOverviewCard';\nimport { TeamWorkloadCard } from '../dashboards/cards/TeamWorkloadCard';\nimport { TimeLoggedCard } from '../dashboards/cards/TimeLoggedCard';\nimport { useLiveRefresh } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\n\nconst FOUR_WEEKS_MS = 28 * 24 * 60 * 60 * 1000;\n\n/** Every card's own query on this page (ANR-28). `DashboardTeamMemberIds` is this page's own query, above. */\nconst TEAM_QUERIES = [\n 'DashboardTeamMemberIds',\n 'DashboardTeamOverview',\n 'TeamMemberWorkload',\n 'TeamMemberTimeLogged',\n];\n\n/** Just the member actor ids, for the workload/time-logged cards below — the team's own capacity/lead comes\n * from `TeamOverviewCard`'s own query; this is separate so a team switch only refetches what changed. */\nconst GET_TEAM_MEMBER_IDS = gql`\n query DashboardTeamMemberIds($id: ID!) {\n planmagnetTeam(id: $id) {\n id\n members {\n actorId\n }\n }\n }\n`;\n\ninterface TeamMemberIdsData {\n planmagnetTeam: { id: string; members: { actorId: string }[] } | null;\n}\n\n/**\n * Team utilization dashboard (ANR-28): capacity/allocation, workload distribution, individual delivery and\n * time logged for one picked team. The team stays in the URL (`?team=`).\n */\nexport function DashboardsTeamUtilizationPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { t } = useI18n();\n const [teamId, setTeamId] = useState(() => readUrlParam('team') ?? '');\n const handleTeamChange = (id: string) => {\n setTeamId(id);\n writeUrlParam('team', id || null);\n };\n // Fixed for the life of the page.\n const [since] = useState(() => new Date(Date.now() - FOUR_WEEKS_MS).toISOString());\n\n const { data } = useQuery<TeamMemberIdsData>(GET_TEAM_MEMBER_IDS, {\n variables: { id: teamId },\n skip: !teamId,\n });\n\n // Live updates (ANR-23): no subscription is scoped by team (a team's members' work can span several\n // projects), so this is workspace-wide work item events, throttled to at most once per 15s — off until a\n // team is actually picked, since nothing on the page depends on it before then.\n const live = useLiveRefresh({\n kind: 'workItem',\n enabled: Boolean(teamId),\n queries: TEAM_QUERIES,\n minIntervalMs: 15000,\n });\n const memberActorIds = data?.planmagnetTeam?.members.map((m) => m.actorId) ?? [];\n\n return (\n <div data-testid=\"dashboard-team\" className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <UsersRound className=\"h-5 w-5 text-primary\" />\n {t('planmagnet.dashboards.team.title', 'Team utilization dashboard')}\n </h1>\n <LiveStatus mode={live.mode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.team.subtitle',\n 'Capacity, workload, individual delivery and time logged for one team.'\n )}\n </p>\n </div>\n <div className=\"w-full max-w-xs\">\n <TeamSelect\n id=\"team-dashboard-team\"\n value={teamId}\n onChange={handleTeamChange}\n noneLabel={t('planmagnet.dashboards.team.selectTeam', 'Select a team…')}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <TeamOverviewCard\n teamId={teamId}\n onDrillDown={() => navigate(detailPath('/team/team-detail-page', teamId))}\n />\n <div className=\"space-y-2\">\n <TeamWorkloadCard\n workspaceId={workspaceId}\n memberActorIds={memberActorIds}\n sinceIso={since}\n weeks={4}\n onDrillDown={() => navigate(detailPath('/team/team-detail-page', teamId))}\n />\n <NoSkillsNote\n message={t(\n 'planmagnet.dashboards.team.noSkillUtilization',\n 'Skill utilization is not modelled in PlanMagnet — workload above is by person only.'\n )}\n />\n </div>\n <TimeLoggedCard\n workspaceId={workspaceId}\n memberActorIds={memberActorIds}\n sinceIso={since}\n weeks={4}\n onDrillDown={() => navigate('/tasks/time-entries-page')}\n />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,IAAM,IAAgB,MAAU,KAAK,KAAK,KAGpC,IAAe;CACnB;CACA;CACA;CACA;CACD,EAIK,IAAsB,CAAG;;;;;;;;;;AAmB/B,SAAgB,IAAoC;CAClD,IAAM,EAAE,mBAAgB,GAAe,EACjC,IAAW,GAAuB,EAClC,EAAE,SAAM,GAAS,EACjB,CAAC,GAAQ,KAAa,QAAe,EAAa,OAAO,IAAI,GAAG,EAChE,KAAoB,MAAe;AAEvC,EADA,EAAU,EAAG,EACb,EAAc,QAAQ,KAAM,KAAK;IAG7B,CAAC,KAAS,QAAe,IAAI,KAAK,KAAK,KAAK,GAAG,EAAc,CAAC,aAAa,CAAC,EAE5E,EAAE,YAAS,EAA4B,GAAqB;EAChE,WAAW,EAAE,IAAI,GAAQ;EACzB,MAAM,CAAC;EACR,CAAC,EAKI,IAAO,EAAe;EAC1B,MAAM;EACN,SAAS,EAAQ;EACjB,SAAS;EACT,eAAe;EAChB,CAAC,EACI,IAAiB,GAAM,gBAAgB,QAAQ,KAAK,MAAM,EAAE,QAAQ,IAAI,EAAE;AAEhF,QACE,kBAAC,OAAD;EAAK,eAAY;EAAiB,WAAU;YAA5C,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD,EAAY,WAAU,wBAAyB,CAAA,EAC9C,EAAE,oCAAoC,6BAA6B,CACjE;QACL,kBAAC,GAAD,EAAY,MAAM,EAAK,MAAQ,CAAA,CAC3B;OACN,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,uCACA,wEACD;IACC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,IAAG;KACH,OAAO;KACP,UAAU;KACV,WAAW,EAAE,yCAAyC,iBAAiB;KACvE,CAAA;IACE,CAAA,CACF;MAEN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACU;KACR,mBAAmB,EAAS,EAAW,0BAA0B,EAAO,CAAC;KACzE,CAAA;IACF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACe;MACG;MAChB,UAAU;MACV,OAAO;MACP,mBAAmB,EAAS,EAAW,0BAA0B,EAAO,CAAC;MACzE,CAAA,EACF,kBAAC,GAAD,EACE,SAAS,EACP,iDACA,sFACD,EACD,CAAA,CACE;;IACN,kBAAC,GAAD;KACe;KACG;KAChB,UAAU;KACV,OAAO;KACP,mBAAmB,EAAS,2BAA2B;KACvD,CAAA;IACE;KACF"}
1
+ {"version":3,"file":"DashboardsTeamUtilizationPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/DashboardsTeamUtilizationPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { UsersRound } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { TeamSelect } from '../controls/Pickers';\nimport { readUrlParam, writeUrlParam } from '../utils/urlParam';\nimport { detailPath } from '../utils/rowNavigation';\nimport { TeamOverviewCard } from '../dashboards/cards/TeamOverviewCard';\nimport { TeamWorkloadCard } from '../dashboards/cards/TeamWorkloadCard';\nimport { TimeLoggedCard } from '../dashboards/cards/TimeLoggedCard';\nimport { SkillCoverageCard } from '../skills/SkillCoverageCard';\nimport { useLiveRefresh } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\n\nconst FOUR_WEEKS_MS = 28 * 24 * 60 * 60 * 1000;\n\n/** Every card's own query on this page (ANR-28). `DashboardTeamMemberIds` is this page's own query, above. */\nconst TEAM_QUERIES = [\n 'DashboardTeamMemberIds',\n 'DashboardTeamOverview',\n 'TeamMemberWorkload',\n 'TeamMemberTimeLogged',\n 'SkillCoverage',\n];\n\n/** Just the member actor ids, for the workload/time-logged cards below — the team's own capacity/lead comes\n * from `TeamOverviewCard`'s own query; this is separate so a team switch only refetches what changed. */\nconst GET_TEAM_MEMBER_IDS = gql`\n query DashboardTeamMemberIds($id: ID!) {\n planmagnetTeam(id: $id) {\n id\n members {\n actorId\n }\n }\n }\n`;\n\ninterface TeamMemberIdsData {\n planmagnetTeam: { id: string; members: { actorId: string }[] } | null;\n}\n\n/**\n * Team utilization dashboard (ANR-28): capacity/allocation, workload distribution, individual delivery,\n * time logged and skill coverage for one picked team. The team stays in the URL (`?team=`).\n */\nexport function DashboardsTeamUtilizationPagePage() {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const { t } = useI18n();\n const [teamId, setTeamId] = useState(() => readUrlParam('team') ?? '');\n const handleTeamChange = (id: string) => {\n setTeamId(id);\n writeUrlParam('team', id || null);\n };\n // Fixed for the life of the page.\n const [since] = useState(() => new Date(Date.now() - FOUR_WEEKS_MS).toISOString());\n\n const { data } = useQuery<TeamMemberIdsData>(GET_TEAM_MEMBER_IDS, {\n variables: { id: teamId },\n skip: !teamId,\n });\n\n // Live updates (ANR-23): no subscription is scoped by team (a team's members' work can span several\n // projects), so this is workspace-wide work item events, throttled to at most once per 15s — off until a\n // team is actually picked, since nothing on the page depends on it before then.\n const live = useLiveRefresh({\n kind: 'workItem',\n enabled: Boolean(teamId),\n queries: TEAM_QUERIES,\n minIntervalMs: 15000,\n });\n const memberActorIds = data?.planmagnetTeam?.members.map((m) => m.actorId) ?? [];\n\n return (\n <div data-testid=\"dashboard-team\" className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h1 className=\"flex items-center gap-2 text-2xl font-semibold\">\n <UsersRound className=\"h-5 w-5 text-primary\" />\n {t('planmagnet.dashboards.team.title', 'Team utilization dashboard')}\n </h1>\n <LiveStatus mode={live.mode} />\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'planmagnet.dashboards.team.subtitleWithSkills',\n 'Capacity, workload, individual delivery, time logged and skill coverage for one team.'\n )}\n </p>\n </div>\n <div className=\"w-full max-w-xs\">\n <TeamSelect\n id=\"team-dashboard-team\"\n value={teamId}\n onChange={handleTeamChange}\n noneLabel={t('planmagnet.dashboards.team.selectTeam', 'Select a team…')}\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <TeamOverviewCard\n teamId={teamId}\n onDrillDown={() => navigate(detailPath('/team/team-detail-page', teamId))}\n />\n <TeamWorkloadCard\n workspaceId={workspaceId}\n memberActorIds={memberActorIds}\n sinceIso={since}\n weeks={4}\n onDrillDown={() => navigate(detailPath('/team/team-detail-page', teamId))}\n />\n <TimeLoggedCard\n workspaceId={workspaceId}\n memberActorIds={memberActorIds}\n sinceIso={since}\n weeks={4}\n onDrillDown={() => navigate('/tasks/time-entries-page')}\n />\n {/* ANR-28 skill utilization: the team's open work by required skill, against its members' CrewFoundry\n skills (only this team's members count as qualified). */}\n <div className=\"lg:col-span-2\">\n <SkillCoverageCard\n workspaceId={workspaceId}\n teamId={teamId}\n notReadyMessage={t(\n 'planmagnet.skills.coverage.pickTeam',\n 'Select a team to see which skills its open work needs.'\n )}\n />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,IAAM,IAAgB,MAAU,KAAK,KAAK,KAGpC,IAAe;CACnB;CACA;CACA;CACA;CACA;CACD,EAIK,IAAsB,CAAG;;;;;;;;;;AAmB/B,SAAgB,IAAoC;CAClD,IAAM,EAAE,mBAAgB,GAAe,EACjC,IAAW,GAAuB,EAClC,EAAE,SAAM,GAAS,EACjB,CAAC,GAAQ,KAAa,QAAe,EAAa,OAAO,IAAI,GAAG,EAChE,KAAoB,MAAe;AAEvC,EADA,EAAU,EAAG,EACb,EAAc,QAAQ,KAAM,KAAK;IAG7B,CAAC,KAAS,QAAe,IAAI,KAAK,KAAK,KAAK,GAAG,EAAc,CAAC,aAAa,CAAC,EAE5E,EAAE,YAAS,EAA4B,GAAqB;EAChE,WAAW,EAAE,IAAI,GAAQ;EACzB,MAAM,CAAC;EACR,CAAC,EAKI,IAAO,EAAe;EAC1B,MAAM;EACN,SAAS,EAAQ;EACjB,SAAS;EACT,eAAe;EAChB,CAAC,EACI,IAAiB,GAAM,gBAAgB,QAAQ,KAAK,MAAM,EAAE,QAAQ,IAAI,EAAE;AAEhF,QACE,kBAAC,OAAD;EAAK,eAAY;EAAiB,WAAU;YAA5C,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD,EAAY,WAAU,wBAAyB,CAAA,EAC9C,EAAE,oCAAoC,6BAA6B,CACjE;QACL,kBAAC,GAAD,EAAY,MAAM,EAAK,MAAQ,CAAA,CAC3B;OACN,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,iDACA,wFACD;IACC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,IAAG;KACH,OAAO;KACP,UAAU;KACV,WAAW,EAAE,yCAAyC,iBAAiB;KACvE,CAAA;IACE,CAAA,CACF;MAEN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACU;KACR,mBAAmB,EAAS,EAAW,0BAA0B,EAAO,CAAC;KACzE,CAAA;IACF,kBAAC,GAAD;KACe;KACG;KAChB,UAAU;KACV,OAAO;KACP,mBAAmB,EAAS,EAAW,0BAA0B,EAAO,CAAC;KACzE,CAAA;IACF,kBAAC,GAAD;KACe;KACG;KAChB,UAAU;KACV,OAAO;KACP,mBAAmB,EAAS,2BAA2B;KACvD,CAAA;IAGF,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACe;MACL;MACR,iBAAiB,EACf,uCACA,yDACD;MACD,CAAA;KACE,CAAA;IACF;KACF"}
@@ -0,0 +1,2 @@
1
+ export declare function ProductsCustomerSatisfactionPagePage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=ProductsCustomerSatisfactionPagePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductsCustomerSatisfactionPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsCustomerSatisfactionPagePage.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,oCAAoC,4CAiUnD"}