@burdenoff/microfe-planmagnet 2026.1002.1 → 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.
- package/dist/generated/wspace-operations.d.ts +2533 -546
- package/dist/generated/wspace-operations.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +5654 -71
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +180 -162
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +100 -84
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/board/BulkActions.d.ts.map +1 -1
- package/dist/planmagnet/board/BulkActions.js +106 -55
- package/dist/planmagnet/board/BulkActions.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +10 -5
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +10 -9
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.d.ts +0 -4
- package/dist/planmagnet/dashboards/DashboardUI.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +6 -13
- package/dist/planmagnet/dashboards/DashboardUI.js.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +70 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js +1 -1
- package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -1
- package/dist/planmagnet/index.d.ts +1 -1
- package/dist/planmagnet/index.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts +3 -3
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +26 -19
- package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts +2 -2
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +18 -12
- package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js +251 -0
- package/dist/planmagnet/pages/ProductsCustomerSatisfactionPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js +4 -4
- package/dist/planmagnet/pages/ProductsFeatureAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +133 -131
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +5 -5
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +6 -6
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +107 -82
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js +174 -0
- package/dist/planmagnet/pages/ProductsProductFeatureDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js +350 -0
- package/dist/planmagnet/pages/ProductsProductHierarchyPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js +168 -0
- package/dist/planmagnet/pages/ProductsProductLinesPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +116 -114
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +174 -159
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js +159 -145
- package/dist/planmagnet/pages/TasksCreateTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js +161 -143
- package/dist/planmagnet/pages/TasksEditTaskPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +160 -156
- package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +97 -99
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js.map +1 -1
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts +29 -0
- package/dist/planmagnet/product/FeatureLinkPanels.d.ts.map +1 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js +373 -0
- package/dist/planmagnet/product/FeatureLinkPanels.js.map +1 -0
- package/dist/planmagnet/product/ProductControls.js +1 -1
- package/dist/planmagnet/product/ProductControls.js.map +1 -1
- package/dist/planmagnet/product/ProductFeatureControls.d.ts +39 -0
- package/dist/planmagnet/product/ProductFeatureControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureControls.js +315 -0
- package/dist/planmagnet/product/ProductFeatureControls.js.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts +4 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js +192 -0
- package/dist/planmagnet/product/ProductFeatureLinkCard.js.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts +52 -0
- package/dist/planmagnet/product/ProductLineControls.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductLineControls.js +417 -0
- package/dist/planmagnet/product/ProductLineControls.js.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts +4 -0
- package/dist/planmagnet/product/ProductTeamsPanel.d.ts.map +1 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js +129 -0
- package/dist/planmagnet/product/ProductTeamsPanel.js.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts +4 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.d.ts.map +1 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js +41 -0
- package/dist/planmagnet/product/WorkItemProductFeatureChip.js.map +1 -0
- package/dist/planmagnet/product/hierarchy.d.ts +127 -0
- package/dist/planmagnet/product/hierarchy.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchy.js +173 -0
- package/dist/planmagnet/product/hierarchy.js.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts +35 -0
- package/dist/planmagnet/product/hierarchyOperations.d.ts.map +1 -0
- package/dist/planmagnet/product/hierarchyOperations.js +440 -0
- package/dist/planmagnet/product/hierarchyOperations.js.map +1 -0
- package/dist/planmagnet/product/refusal.d.ts +14 -0
- package/dist/planmagnet/product/refusal.d.ts.map +1 -0
- package/dist/planmagnet/product/refusal.js +10 -0
- package/dist/planmagnet/product/refusal.js.map +1 -0
- package/dist/planmagnet/product/routes.d.ts +10 -0
- package/dist/planmagnet/product/routes.d.ts.map +1 -1
- package/dist/planmagnet/product/routes.js +2 -2
- package/dist/planmagnet/product/routes.js.map +1 -1
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts +20 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js +55 -0
- package/dist/planmagnet/satisfaction/DeliverySatisfactionSection.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts +30 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js +282 -0
- package/dist/planmagnet/satisfaction/SatisfactionFigures.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts +9 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js +390 -0
- package/dist/planmagnet/satisfaction/SatisfactionImportDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts +17 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js +77 -0
- package/dist/planmagnet/satisfaction/SatisfactionLinkFields.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts +18 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js +87 -0
- package/dist/planmagnet/satisfaction/SatisfactionReleaseSelect.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts +13 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js +326 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponseDialog.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts +23 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js +258 -0
- package/dist/planmagnet/satisfaction/SatisfactionResponsesTable.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts +6 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js +56 -0
- package/dist/planmagnet/satisfaction/SatisfactionSummaryCard.js.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts +16 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js +102 -0
- package/dist/planmagnet/satisfaction/SatisfactionWindowPicker.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts +154 -0
- package/dist/planmagnet/satisfaction/satisfaction.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfaction.js +210 -0
- package/dist/planmagnet/satisfaction/satisfaction.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts +108 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js +211 -0
- package/dist/planmagnet/satisfaction/satisfactionCsv.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js +90 -0
- package/dist/planmagnet/satisfaction/satisfactionForm.js.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts +116 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js +86 -0
- package/dist/planmagnet/satisfaction/satisfactionOperations.js.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts +20 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.d.ts.map +1 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js +31 -0
- package/dist/planmagnet/satisfaction/useSatisfactionAccess.js.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts +35 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.d.ts.map +1 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js +124 -0
- package/dist/planmagnet/skills/RequiredSkillsEditor.js.map +1 -0
- package/dist/planmagnet/skills/SkillChips.d.ts +30 -0
- package/dist/planmagnet/skills/SkillChips.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillChips.js +57 -0
- package/dist/planmagnet/skills/SkillChips.js.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts +24 -0
- package/dist/planmagnet/skills/SkillCoverageCard.d.ts.map +1 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js +256 -0
- package/dist/planmagnet/skills/SkillCoverageCard.js.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts +10 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.d.ts.map +1 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js +65 -0
- package/dist/planmagnet/skills/WorkItemRequiredSkills.js.map +1 -0
- package/dist/planmagnet/skills/skillOperations.d.ts +52 -0
- package/dist/planmagnet/skills/skillOperations.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillOperations.js +44 -0
- package/dist/planmagnet/skills/skillOperations.js.map +1 -0
- package/dist/planmagnet/skills/skillTags.d.ts +103 -0
- package/dist/planmagnet/skills/skillTags.d.ts.map +1 -0
- package/dist/planmagnet/skills/skillTags.js +115 -0
- package/dist/planmagnet/skills/skillTags.js.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts +28 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.d.ts.map +1 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js +30 -0
- package/dist/planmagnet/skills/useRequiredSkillsDraft.js.map +1 -0
- package/dist/shared/utils/i18n.d.ts +3 -0
- package/dist/shared/utils/i18n.d.ts.map +1 -1
- package/dist/shared/utils/i18n.js +1 -1
- package/dist/shared/utils/i18n.js.map +1 -1
- package/package.json +4 -4
|
@@ -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.
|
|
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.
|
|
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 } 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;
|
|
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
|
-
*
|
|
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":"
|
|
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
|
|
8
|
-
import { TaskCompletionCard as
|
|
9
|
-
import { QualityMetricsCard as
|
|
10
|
-
import { RiskSeverityCard as
|
|
11
|
-
import { SprintCapacityCard as
|
|
12
|
-
import { SprintBurndownCard as
|
|
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.
|
|
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(
|
|
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(
|
|
108
|
+
children: /* @__PURE__ */ x(m, {
|
|
107
109
|
workspaceId: y,
|
|
108
110
|
sprintId: I
|
|
109
111
|
})
|
|
110
112
|
}),
|
|
111
|
-
/* @__PURE__ */ x(
|
|
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(
|
|
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(
|
|
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__ */
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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":"
|
|
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 {
|
|
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.
|
|
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__ */
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 {
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"ProductsCustomerSatisfactionPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsCustomerSatisfactionPagePage.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,oCAAoC,4CAiUnD"}
|