@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
|
@@ -7,12 +7,17 @@ export declare function ProjectSelect({ id, value, onChange, noneLabel, exclude,
|
|
|
7
7
|
exclude?: string[];
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
}): import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A team select (BOFF-8044). '' means "no team" when `noneLabel` is given. `exclude` hides teams already spoken for
|
|
12
|
+
* (PRM-01: the teams already linked to a product).
|
|
13
|
+
*/
|
|
14
|
+
export declare function TeamSelect({ id, value, onChange, noneLabel, exclude, disabled, }: {
|
|
12
15
|
id?: string;
|
|
13
16
|
value: string;
|
|
14
17
|
onChange: (teamId: string) => void;
|
|
15
18
|
noneLabel?: string;
|
|
19
|
+
exclude?: string[];
|
|
20
|
+
disabled?: boolean;
|
|
16
21
|
}): import("react/jsx-runtime").JSX.Element;
|
|
17
22
|
/** A sprint select, optionally scoped to one project (report-builder widgets: BURNDOWN reads one sprint). '' means
|
|
18
23
|
* "no sprint" when `noneLabel` is given. */
|
|
@@ -85,9 +90,9 @@ export declare function RoadmapSelect({ id, value, onChange, noneLabel, productI
|
|
|
85
90
|
* Search (client-side — `CrewEmployeeFilterInput` has no text-search field of its own), then pick one
|
|
86
91
|
* CrewFoundry employee (CWF-30). Unlike every other picker in this file this one never takes a `workspaceId`
|
|
87
92
|
* variable: `crewEmployees` resolves the roster from the caller's own `x-workspace-id` header, same as the rest
|
|
88
|
-
* of the CrewFoundry subgraph.
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
93
|
+
* of the CrewFoundry subgraph. An unreachable CrewFoundry, or an empty roster, surfaces as an empty list here
|
|
94
|
+
* rather than a page crash. (`planmagnetSprintCapacity` tells the two apart on the read side: its
|
|
95
|
+
* `crewfoundryUnavailable` means only that the roster could not be read; an empty roster is not unavailable.)
|
|
91
96
|
*/
|
|
92
97
|
export declare function CrewEmployeeSelect({ id, value, onChange, noneLabel, disabled, }: {
|
|
93
98
|
id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pickers.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/Pickers.tsx"],"names":[],"mappings":"AAkCA,iHAAiH;AACjH,wBAAgB,aAAa,CAAC,EAC5B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAY,EACZ,QAAgB,GACjB,EAAE;IACD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,2CA6DA;AAcD
|
|
1
|
+
{"version":3,"file":"Pickers.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/controls/Pickers.tsx"],"names":[],"mappings":"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"}
|
|
@@ -84,27 +84,28 @@ var p = i`
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
`;
|
|
87
|
-
function m({ id: t, value: n, onChange: r, noneLabel: i }) {
|
|
88
|
-
let { workspaceId:
|
|
89
|
-
variables: { workspaceId:
|
|
90
|
-
skip: !
|
|
91
|
-
}),
|
|
87
|
+
function m({ id: t, value: n, onChange: r, noneLabel: i, exclude: c = [], disabled: u = !1 }) {
|
|
88
|
+
let { workspaceId: d } = e(), { data: f, loading: m, error: h } = a(p, {
|
|
89
|
+
variables: { workspaceId: d ?? "" },
|
|
90
|
+
skip: !d
|
|
91
|
+
}), g = (f?.planmagnetTeams.items ?? []).filter((e) => !c.includes(e.id));
|
|
92
92
|
return /* @__PURE__ */ s("select", {
|
|
93
93
|
id: t,
|
|
94
94
|
"aria-label": t ? void 0 : "Team",
|
|
95
95
|
className: l,
|
|
96
96
|
value: n,
|
|
97
|
+
disabled: u,
|
|
97
98
|
onChange: (e) => r(e.target.value),
|
|
98
99
|
children: [
|
|
99
100
|
i !== void 0 && /* @__PURE__ */ o("option", {
|
|
100
101
|
value: "",
|
|
101
|
-
children: i
|
|
102
|
+
children: c.length > 0 && f && g.length === 0 ? "No other team to choose" : i
|
|
102
103
|
}),
|
|
103
|
-
|
|
104
|
+
g.length === 0 && i === void 0 && /* @__PURE__ */ o("option", {
|
|
104
105
|
value: "",
|
|
105
|
-
children:
|
|
106
|
+
children: m && !f ? "Loading teams…" : h ? "Teams could not be loaded" : "No teams yet"
|
|
106
107
|
}),
|
|
107
|
-
|
|
108
|
+
g.map((e) => /* @__PURE__ */ o("option", {
|
|
108
109
|
value: e.id,
|
|
109
110
|
children: e.name
|
|
110
111
|
}, e.id))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pickers.js","names":[],"sources":["../../../src/planmagnet/controls/Pickers.tsx"],"sourcesContent":["import { useDeferredValue, useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * Record pickers shared by forms and dialogs: a project select, and search-then-pick selects for work items and\n * docs. Values are record ids. Search runs on the server (never \"the first page, filtered in the browser\"), so a\n * record past the first page can still be found.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst PICKER_PROJECTS = gql`\n query PickerProjects($workspaceId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $pagination) {\n items {\n id\n name\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n/** Projects are read page by page up to this many, so the picker never silently stops at the first 100. */\nconst MAX_PROJECT_OPTIONS = 500;\n\n/** A project select. '' means \"no project\" when `noneLabel` is given. `exclude` hides ids already spoken for. */\nexport function ProjectSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (projectId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjects: {\n items: { id: string; name: string }[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(PICKER_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '', pagination: { first: 100 } },\n skip: !workspaceId,\n });\n const loaded = data?.planmagnetProjects.items ?? [];\n const hasNextPage = data?.planmagnetProjects.pageInfo.hasNextPage ?? false;\n const endCursor = data?.planmagnetProjects.pageInfo.endCursor ?? null;\n useEffect(() => {\n if (!hasNextPage || !endCursor || loaded.length >= MAX_PROJECT_OPTIONS) return;\n fetchMore({\n variables: { pagination: { first: 100, after: endCursor } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjects: {\n ...fetchMoreResult.planmagnetProjects,\n items: [...prev.planmagnetProjects.items, ...fetchMoreResult.planmagnetProjects.items],\n },\n }),\n }).catch(() => {\n // A failed extra page leaves the projects already loaded usable.\n });\n }, [hasNextPage, endCursor, loaded.length, fetchMore]);\n const projects = loaded.filter((p) => !exclude.includes(p.id));\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {projects.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading projects…'\n : error\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n {/* Never a silent cut-off: say so when the workspace has more projects than the picker lists. */}\n {hasNextPage && loaded.length >= MAX_PROJECT_OPTIONS && (\n <option value=\"\" disabled>\n {`Only the first ${MAX_PROJECT_OPTIONS} projects are listed`}\n </option>\n )}\n </select>\n );\n}\n\nconst PICKER_TEAMS = gql`\n query PickerTeams($workspaceId: ID!) {\n planmagnetTeams(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A team select (BOFF-8044). '' means \"no team\" when `noneLabel` is given. */\nexport function TeamSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (teamId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetTeams: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_TEAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const teams = data?.planmagnetTeams.items ?? [];\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Team'}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {teams.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading teams…'\n : error\n ? 'Teams could not be loaded'\n : 'No teams yet'}\n </option>\n )}\n {teams.map((t) => (\n <option key={t.id} value={t.id}>\n {t.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_SPRINTS = gql`\n query PickerSprints($workspaceId: ID!, $filter: PlanmagnetSprintFilter) {\n planmagnetSprints(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n status\n }\n totalCount\n }\n }\n`;\n\n/** A sprint select, optionally scoped to one project (report-builder widgets: BURNDOWN reads one sprint). '' means\n * \"no sprint\" when `noneLabel` is given. */\nexport function SprintSelect({\n id,\n value,\n onChange,\n projectId,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (sprintId: string) => void;\n projectId?: string;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetSprints: {\n items: { id: string; name: string; status: string }[];\n totalCount: number;\n };\n }>(PICKER_SPRINTS, {\n variables: { workspaceId: workspaceId ?? '', filter: projectId ? { projectId } : undefined },\n skip: !workspaceId,\n });\n const sprints = data?.planmagnetSprints.items ?? [];\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Sprint'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {sprints.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading sprints…'\n : error\n ? 'Sprints could not be loaded'\n : 'No sprints yet'}\n </option>\n )}\n {sprints.map((s) => (\n <option key={s.id} value={s.id}>\n {s.name} ({s.status.toLowerCase()})\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_WORK_ITEMS = gql`\n query PickerWorkItems($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n }\n totalCount\n }\n }\n`;\n\n/**\n * Search, then pick one work item. The server matches the key, title and description. `exclude` hides items that are\n * already linked (the result says how many more exist, so an empty list never claims \"nothing left\" falsely).\n */\nexport function WorkItemSearchSelect({\n id,\n projectId,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n projectId?: string;\n value: string;\n onChange: (workItemId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; key: string; title: string }[];\n totalCount: number;\n };\n }>(PICKER_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { ...(deferred && { search: deferred }), ...(projectId && { projectId }) },\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetWorkItems.items ?? [];\n const items = found.filter((w) => !exclude.includes(w.id));\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search work items\"\n placeholder=\"Search by key or title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : items.length ? 'Choose a work item' : 'No match'}\n </option>\n {/* The current value stays selectable (and visible) when it is not in this page of results. */}\n {value && !items.some((w) => w.id === value) && (\n <option value={value}>Currently linked work item</option>\n )}\n {items.map((w) => (\n <option key={w.id} value={w.id}>\n {w.key} — {w.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_DOCS = gql`\n query PickerDocs($workspaceId: String, $filter: PlanmagnetDocFilter) {\n planmagnetDocs(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n title\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one doc. */\nexport function DocSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (docId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetDocs: { items: { id: string; title: string }[]; totalCount: number };\n }>(PICKER_DOCS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetDocs.items ?? [];\n const docs = found.filter((d) => !exclude.includes(d.id));\n const total = data?.planmagnetDocs.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search docs\"\n placeholder=\"Search docs by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : docs.length ? 'Choose a doc' : 'No match'}\n </option>\n {docs.map((d) => (\n <option key={d.id} value={d.id}>\n {d.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PROGRAMS = gql`\n query PickerPrograms($workspaceId: ID!) {\n planmagnetPrograms(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A program select. '' means \"no program\" when `noneLabel` is given. */\nexport function ProgramSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (programId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPrograms: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROGRAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const programs = data?.planmagnetPrograms.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {programs.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading programs…'\n : error\n ? 'Programs could not be loaded'\n : 'No programs yet'}\n </option>\n )}\n {programs.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_PORTFOLIOS = gql`\n query PickerPortfolios($workspaceId: ID!) {\n planmagnetPortfolios(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A portfolio select (BOFF-8047). '' means \"no portfolio\" when `noneLabel` is given. */\nexport function PortfolioSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (portfolioId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPortfolios: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PORTFOLIOS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const portfolios = data?.planmagnetPortfolios.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {portfolios.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading portfolios…'\n : error\n ? 'Portfolios could not be loaded'\n : 'No portfolios yet'}\n </option>\n )}\n {portfolios.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_COMPETITORS = gql`\n query PickerCompetitors($workspaceId: ID!, $filter: PlanmagnetCompetitorFilter) {\n planmagnetCompetitors(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one competitor. `exclude` hides competitors already linked. */\nexport function CompetitorSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (competitorId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetCompetitors: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_COMPETITORS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetCompetitors.items ?? [];\n const competitors = found.filter((c) => !exclude.includes(c.id));\n const total = data?.planmagnetCompetitors.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search competitors\"\n placeholder=\"Search competitors by name\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : competitors.length\n ? 'Choose a competitor'\n : 'No match'}\n </option>\n {competitors.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PRODUCTS = gql`\n query PickerProducts($workspaceId: ID!) {\n planmagnetProducts(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A product select (PRM-27). '' means \"every product\" when `noneLabel` is given. */\nexport function ProductSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (productId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetProducts: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PRODUCTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const products = data?.planmagnetProducts.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {products.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading products…'\n : error\n ? 'Products could not be loaded'\n : 'No products yet'}\n </option>\n )}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_ROADMAPS = gql`\n query PickerRoadmaps($workspaceId: ID!, $filter: PlanmagnetRoadmapFilter) {\n planmagnetRoadmaps(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A roadmap select (PRM-27). '' means \"every roadmap\" when `noneLabel` is given. `productId` narrows the list\n * to that product's roadmaps, same filter FeatureInitiativeCard uses for its own roadmap picker.\n */\nexport function RoadmapSelect({\n id,\n value,\n onChange,\n noneLabel,\n productId,\n}: {\n id?: string;\n value: string;\n onChange: (roadmapId: string) => void;\n noneLabel?: string;\n productId?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetRoadmaps: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_ROADMAPS, {\n variables: { workspaceId: workspaceId ?? '', filter: productId ? { productId } : undefined },\n skip: !workspaceId,\n });\n const roadmaps = data?.planmagnetRoadmaps.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {roadmaps.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading roadmaps…'\n : error\n ? 'Roadmaps could not be loaded'\n : 'No roadmaps yet'}\n </option>\n )}\n {roadmaps.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_CREW_EMPLOYEES = gql`\n query PickerCrewEmployees($pagination: PaginationInput) {\n crewEmployees(pagination: $pagination) {\n id\n firstName\n lastName\n email\n }\n }\n`;\n\n/** CrewFoundry's roster is normally a few hundred people at most; past this the search box narrows what the\n * select shows rather than ever paging further. */\nconst MAX_CREW_EMPLOYEE_OPTIONS = 200;\n\n/**\n * Search (client-side — `CrewEmployeeFilterInput` has no text-search field of its own), then pick one\n * CrewFoundry employee (CWF-30). Unlike every other picker in this file this one never takes a `workspaceId`\n * variable: `crewEmployees` resolves the roster from the caller's own `x-workspace-id` header, same as the rest\n * of the CrewFoundry subgraph. A workspace with no CrewFoundry module provisioned, or an unreachable\n * CrewFoundry, surfaces as an empty list here rather than a page crash — the same fails-open contract\n * `planmagnetSprintCapacity.crewfoundryUnavailable` documents for the read side.\n */\nexport function CrewEmployeeSelect({\n id,\n value,\n onChange,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (crewEmployeeId: string) => void;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim().toLowerCase());\n const { data, loading, error } = useQuery<{\n crewEmployees: { id: string; firstName: string; lastName: string; email: string }[];\n }>(PICKER_CREW_EMPLOYEES, {\n variables: { pagination: { limit: MAX_CREW_EMPLOYEE_OPTIONS } },\n });\n const loaded = error ? [] : (data?.crewEmployees ?? []);\n const employees = deferred\n ? loaded.filter(\n (e) =>\n `${e.firstName} ${e.lastName}`.toLowerCase().includes(deferred) ||\n e.email.toLowerCase().includes(deferred)\n )\n : loaded;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search CrewFoundry employees\"\n placeholder=\"Search by name or e-mail\"\n value={search}\n disabled={disabled}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {employees.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data ? 'Loading…' : error ? 'CrewFoundry roster unavailable' : 'No match'}\n </option>\n )}\n {employees.map((e) => (\n <option key={e.id} value={e.id}>\n {e.firstName} {e.lastName} — {e.email}\n </option>\n ))}\n </select>\n {!deferred && loaded.length >= MAX_CREW_EMPLOYEE_OPTIONS && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the first {MAX_CREW_EMPLOYEE_OPTIONS} employees — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Pickers.js","names":[],"sources":["../../../src/planmagnet/controls/Pickers.tsx"],"sourcesContent":["import { useDeferredValue, useEffect, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\n/**\n * Record pickers shared by forms and dialogs: a project select, and search-then-pick selects for work items and\n * docs. Values are record ids. Search runs on the server (never \"the first page, filtered in the browser\"), so a\n * record past the first page can still be found.\n */\n\nconst selectClass =\n 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\nconst PICKER_PROJECTS = gql`\n query PickerProjects($workspaceId: ID!, $pagination: PlanmagnetPaginationInput) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $pagination) {\n items {\n id\n name\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n totalCount\n }\n }\n`;\n\n/** Projects are read page by page up to this many, so the picker never silently stops at the first 100. */\nconst MAX_PROJECT_OPTIONS = 500;\n\n/** A project select. '' means \"no project\" when `noneLabel` is given. `exclude` hides ids already spoken for. */\nexport function ProjectSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (projectId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetProjects: {\n items: { id: string; name: string }[];\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n totalCount: number;\n };\n }>(PICKER_PROJECTS, {\n variables: { workspaceId: workspaceId ?? '', pagination: { first: 100 } },\n skip: !workspaceId,\n });\n const loaded = data?.planmagnetProjects.items ?? [];\n const hasNextPage = data?.planmagnetProjects.pageInfo.hasNextPage ?? false;\n const endCursor = data?.planmagnetProjects.pageInfo.endCursor ?? null;\n useEffect(() => {\n if (!hasNextPage || !endCursor || loaded.length >= MAX_PROJECT_OPTIONS) return;\n fetchMore({\n variables: { pagination: { first: 100, after: endCursor } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n planmagnetProjects: {\n ...fetchMoreResult.planmagnetProjects,\n items: [...prev.planmagnetProjects.items, ...fetchMoreResult.planmagnetProjects.items],\n },\n }),\n }).catch(() => {\n // A failed extra page leaves the projects already loaded usable.\n });\n }, [hasNextPage, endCursor, loaded.length, fetchMore]);\n const projects = loaded.filter((p) => !exclude.includes(p.id));\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {projects.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading projects…'\n : error\n ? 'Projects could not be loaded'\n : 'No projects yet'}\n </option>\n )}\n {projects.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n {/* Never a silent cut-off: say so when the workspace has more projects than the picker lists. */}\n {hasNextPage && loaded.length >= MAX_PROJECT_OPTIONS && (\n <option value=\"\" disabled>\n {`Only the first ${MAX_PROJECT_OPTIONS} projects are listed`}\n </option>\n )}\n </select>\n );\n}\n\nconst PICKER_TEAMS = gql`\n query PickerTeams($workspaceId: ID!) {\n planmagnetTeams(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A team select (BOFF-8044). '' means \"no team\" when `noneLabel` is given. `exclude` hides teams already spoken for\n * (PRM-01: the teams already linked to a product).\n */\nexport function TeamSelect({\n id,\n value,\n onChange,\n noneLabel,\n exclude = [],\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (teamId: string) => void;\n noneLabel?: string;\n exclude?: string[];\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetTeams: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_TEAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const teams = (data?.planmagnetTeams.items ?? []).filter((t) => !exclude.includes(t.id));\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Team'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && (\n <option value=\"\">\n {/* Only when something was excluded: '' keeps its caller's meaning (\"All teams\") otherwise. */}\n {exclude.length > 0 && data && teams.length === 0 ? 'No other team to choose' : noneLabel}\n </option>\n )}\n {teams.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading teams…'\n : error\n ? 'Teams could not be loaded'\n : 'No teams yet'}\n </option>\n )}\n {teams.map((t) => (\n <option key={t.id} value={t.id}>\n {t.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_SPRINTS = gql`\n query PickerSprints($workspaceId: ID!, $filter: PlanmagnetSprintFilter) {\n planmagnetSprints(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n status\n }\n totalCount\n }\n }\n`;\n\n/** A sprint select, optionally scoped to one project (report-builder widgets: BURNDOWN reads one sprint). '' means\n * \"no sprint\" when `noneLabel` is given. */\nexport function SprintSelect({\n id,\n value,\n onChange,\n projectId,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (sprintId: string) => void;\n projectId?: string;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetSprints: {\n items: { id: string; name: string; status: string }[];\n totalCount: number;\n };\n }>(PICKER_SPRINTS, {\n variables: { workspaceId: workspaceId ?? '', filter: projectId ? { projectId } : undefined },\n skip: !workspaceId,\n });\n const sprints = data?.planmagnetSprints.items ?? [];\n return (\n <select\n id={id}\n aria-label={id ? undefined : 'Sprint'}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {sprints.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading sprints…'\n : error\n ? 'Sprints could not be loaded'\n : 'No sprints yet'}\n </option>\n )}\n {sprints.map((s) => (\n <option key={s.id} value={s.id}>\n {s.name} ({s.status.toLowerCase()})\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_WORK_ITEMS = gql`\n query PickerWorkItems($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n }\n totalCount\n }\n }\n`;\n\n/**\n * Search, then pick one work item. The server matches the key, title and description. `exclude` hides items that are\n * already linked (the result says how many more exist, so an empty list never claims \"nothing left\" falsely).\n */\nexport function WorkItemSearchSelect({\n id,\n projectId,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n projectId?: string;\n value: string;\n onChange: (workItemId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetWorkItems: {\n items: { id: string; key: string; title: string }[];\n totalCount: number;\n };\n }>(PICKER_WORK_ITEMS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: { ...(deferred && { search: deferred }), ...(projectId && { projectId }) },\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetWorkItems.items ?? [];\n const items = found.filter((w) => !exclude.includes(w.id));\n const total = data?.planmagnetWorkItems.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search work items\"\n placeholder=\"Search by key or title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : items.length ? 'Choose a work item' : 'No match'}\n </option>\n {/* The current value stays selectable (and visible) when it is not in this page of results. */}\n {value && !items.some((w) => w.id === value) && (\n <option value={value}>Currently linked work item</option>\n )}\n {items.map((w) => (\n <option key={w.id} value={w.id}>\n {w.key} — {w.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_DOCS = gql`\n query PickerDocs($workspaceId: String, $filter: PlanmagnetDocFilter) {\n planmagnetDocs(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n title\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one doc. */\nexport function DocSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (docId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetDocs: { items: { id: string; title: string }[]; totalCount: number };\n }>(PICKER_DOCS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetDocs.items ?? [];\n const docs = found.filter((d) => !exclude.includes(d.id));\n const total = data?.planmagnetDocs.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search docs\"\n placeholder=\"Search docs by title\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data ? 'Searching…' : docs.length ? 'Choose a doc' : 'No match'}\n </option>\n {docs.map((d) => (\n <option key={d.id} value={d.id}>\n {d.title}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PROGRAMS = gql`\n query PickerPrograms($workspaceId: ID!) {\n planmagnetPrograms(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A program select. '' means \"no program\" when `noneLabel` is given. */\nexport function ProgramSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (programId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPrograms: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PROGRAMS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const programs = data?.planmagnetPrograms.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {programs.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading programs…'\n : error\n ? 'Programs could not be loaded'\n : 'No programs yet'}\n </option>\n )}\n {programs.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_PORTFOLIOS = gql`\n query PickerPortfolios($workspaceId: ID!) {\n planmagnetPortfolios(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A portfolio select (BOFF-8047). '' means \"no portfolio\" when `noneLabel` is given. */\nexport function PortfolioSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (portfolioId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetPortfolios: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PORTFOLIOS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const portfolios = data?.planmagnetPortfolios.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {portfolios.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading portfolios…'\n : error\n ? 'Portfolios could not be loaded'\n : 'No portfolios yet'}\n </option>\n )}\n {portfolios.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_COMPETITORS = gql`\n query PickerCompetitors($workspaceId: ID!, $filter: PlanmagnetCompetitorFilter) {\n planmagnetCompetitors(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** Search, then pick one competitor. `exclude` hides competitors already linked. */\nexport function CompetitorSearchSelect({\n id,\n value,\n onChange,\n exclude = [],\n}: {\n id: string;\n value: string;\n onChange: (competitorId: string) => void;\n exclude?: string[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim());\n const { data, loading } = useQuery<{\n planmagnetCompetitors: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_COMPETITORS, {\n variables: {\n workspaceId: workspaceId ?? '',\n filter: deferred ? { search: deferred } : undefined,\n },\n skip: !workspaceId,\n });\n const found = data?.planmagnetCompetitors.items ?? [];\n const competitors = found.filter((c) => !exclude.includes(c.id));\n const total = data?.planmagnetCompetitors.totalCount ?? 0;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search competitors\"\n placeholder=\"Search competitors by name\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n <option value=\"\">\n {loading && !data\n ? 'Searching…'\n : competitors.length\n ? 'Choose a competitor'\n : 'No match'}\n </option>\n {competitors.map((c) => (\n <option key={c.id} value={c.id}>\n {c.name}\n </option>\n ))}\n </select>\n {total > found.length && (\n <p className=\"text-xs text-muted-foreground\">\n Showing {found.length} of {total} — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n\nconst PICKER_PRODUCTS = gql`\n query PickerProducts($workspaceId: ID!) {\n planmagnetProducts(workspaceId: $workspaceId, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/** A product select (PRM-27). '' means \"every product\" when `noneLabel` is given. */\nexport function ProductSelect({\n id,\n value,\n onChange,\n noneLabel,\n}: {\n id?: string;\n value: string;\n onChange: (productId: string) => void;\n noneLabel?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetProducts: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_PRODUCTS, { variables: { workspaceId: workspaceId ?? '' }, skip: !workspaceId });\n const products = data?.planmagnetProducts.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {products.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading products…'\n : error\n ? 'Products could not be loaded'\n : 'No products yet'}\n </option>\n )}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_ROADMAPS = gql`\n query PickerRoadmaps($workspaceId: ID!, $filter: PlanmagnetRoadmapFilter) {\n planmagnetRoadmaps(workspaceId: $workspaceId, filter: $filter, pagination: { first: 100 }) {\n items {\n id\n name\n }\n totalCount\n }\n }\n`;\n\n/**\n * A roadmap select (PRM-27). '' means \"every roadmap\" when `noneLabel` is given. `productId` narrows the list\n * to that product's roadmaps, same filter FeatureInitiativeCard uses for its own roadmap picker.\n */\nexport function RoadmapSelect({\n id,\n value,\n onChange,\n noneLabel,\n productId,\n}: {\n id?: string;\n value: string;\n onChange: (roadmapId: string) => void;\n noneLabel?: string;\n productId?: string;\n}) {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetRoadmaps: { items: { id: string; name: string }[]; totalCount: number };\n }>(PICKER_ROADMAPS, {\n variables: { workspaceId: workspaceId ?? '', filter: productId ? { productId } : undefined },\n skip: !workspaceId,\n });\n const roadmaps = data?.planmagnetRoadmaps.items ?? [];\n return (\n <select\n id={id}\n className={selectClass}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {roadmaps.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data\n ? 'Loading roadmaps…'\n : error\n ? 'Roadmaps could not be loaded'\n : 'No roadmaps yet'}\n </option>\n )}\n {roadmaps.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n );\n}\n\nconst PICKER_CREW_EMPLOYEES = gql`\n query PickerCrewEmployees($pagination: PaginationInput) {\n crewEmployees(pagination: $pagination) {\n id\n firstName\n lastName\n email\n }\n }\n`;\n\n/** CrewFoundry's roster is normally a few hundred people at most; past this the search box narrows what the\n * select shows rather than ever paging further. */\nconst MAX_CREW_EMPLOYEE_OPTIONS = 200;\n\n/**\n * Search (client-side — `CrewEmployeeFilterInput` has no text-search field of its own), then pick one\n * CrewFoundry employee (CWF-30). Unlike every other picker in this file this one never takes a `workspaceId`\n * variable: `crewEmployees` resolves the roster from the caller's own `x-workspace-id` header, same as the rest\n * of the CrewFoundry subgraph. An unreachable CrewFoundry, or an empty roster, surfaces as an empty list here\n * rather than a page crash. (`planmagnetSprintCapacity` tells the two apart on the read side: its\n * `crewfoundryUnavailable` means only that the roster could not be read; an empty roster is not unavailable.)\n */\nexport function CrewEmployeeSelect({\n id,\n value,\n onChange,\n noneLabel,\n disabled = false,\n}: {\n id?: string;\n value: string;\n onChange: (crewEmployeeId: string) => void;\n noneLabel?: string;\n disabled?: boolean;\n}) {\n const [search, setSearch] = useState('');\n const deferred = useDeferredValue(search.trim().toLowerCase());\n const { data, loading, error } = useQuery<{\n crewEmployees: { id: string; firstName: string; lastName: string; email: string }[];\n }>(PICKER_CREW_EMPLOYEES, {\n variables: { pagination: { limit: MAX_CREW_EMPLOYEE_OPTIONS } },\n });\n const loaded = error ? [] : (data?.crewEmployees ?? []);\n const employees = deferred\n ? loaded.filter(\n (e) =>\n `${e.firstName} ${e.lastName}`.toLowerCase().includes(deferred) ||\n e.email.toLowerCase().includes(deferred)\n )\n : loaded;\n return (\n <div className=\"space-y-1\">\n <Input\n aria-label=\"Search CrewFoundry employees\"\n placeholder=\"Search by name or e-mail\"\n value={search}\n disabled={disabled}\n onChange={(e) => setSearch(e.target.value)}\n />\n <select\n id={id}\n className={selectClass}\n value={value}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n >\n {noneLabel !== undefined && <option value=\"\">{noneLabel}</option>}\n {employees.length === 0 && noneLabel === undefined && (\n <option value=\"\">\n {loading && !data ? 'Loading…' : error ? 'CrewFoundry roster unavailable' : 'No match'}\n </option>\n )}\n {employees.map((e) => (\n <option key={e.id} value={e.id}>\n {e.firstName} {e.lastName} — {e.email}\n </option>\n ))}\n </select>\n {!deferred && loaded.length >= MAX_CREW_EMPLOYEE_OPTIONS && (\n <p className=\"text-xs text-muted-foreground\">\n Showing the first {MAX_CREW_EMPLOYEE_OPTIONS} employees — type to narrow the list.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAYA,IAAM,IACJ,6FAEI,IAAkB,CAAG;;;;;;;;;;;;;;GAiBrB,IAAsB;AAG5B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,cACA,aAAU,EAAE,EACZ,cAAW,MAQV;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,UAAO,iBAAc,EAMzC,GAAiB;EAClB,WAAW;GAAE,aAAa,KAAe;GAAI,YAAY,EAAE,OAAO,KAAK;GAAE;EACzE,MAAM,CAAC;EACR,CAAC,EACI,IAAS,GAAM,mBAAmB,SAAS,EAAE,EAC7C,IAAc,GAAM,mBAAmB,SAAS,eAAe,IAC/D,IAAY,GAAM,mBAAmB,SAAS,aAAa;AACjE,SAAgB;AACV,GAAC,KAAe,CAAC,KAAa,EAAO,UAAU,KACnD,EAAU;GACR,WAAW,EAAE,YAAY;IAAE,OAAO;IAAK,OAAO;IAAW,EAAE;GAC3D,cAAc,GAAM,EAAE,0BAAuB,EAC3C,oBAAoB;IAClB,GAAG,EAAgB;IACnB,OAAO,CAAC,GAAG,EAAK,mBAAmB,OAAO,GAAG,EAAgB,mBAAmB,MAAM;IACvF,EACF;GACF,CAAC,CAAC,YAAY,GAEb;IACD;EAAC;EAAa;EAAW,EAAO;EAAQ;EAAU,CAAC;CACtD,IAAM,IAAW,EAAO,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC;AAC9D,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACG;EACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAL3C;GAOG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GAED,KAAe,EAAO,UAAU,KAC/B,kBAAC,UAAD;IAAQ,OAAM;IAAG,UAAA;cACd,kBAAkB,EAAoB;IAChC,CAAA;GAEJ;;;AAIb,IAAM,IAAe,CAAG;;;;;;;;;;;AAgBxB,SAAgB,EAAW,EACzB,OACA,UACA,aACA,cACA,aAAU,EAAE,EACZ,cAAW,MAQV;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAc;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACjF,KAAS,GAAM,gBAAgB,SAAS,EAAE,EAAE,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC;AACxF,QACE,kBAAC,UAAD;EACM;EACJ,cAAY,IAAK,KAAA,IAAY;EAC7B,WAAW;EACJ;EACG;EACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAN3C;GAQG,MAAc,KAAA,KACb,kBAAC,UAAD;IAAQ,OAAM;cAEX,EAAQ,SAAS,KAAK,KAAQ,EAAM,WAAW,IAAI,4BAA4B;IACzE,CAAA;GAEV,EAAM,WAAW,KAAK,MAAc,KAAA,KACnC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,mBACA,IACE,8BACA;IACC,CAAA;GAEV,EAAM,KAAK,MACV,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAiB,CAAG;;;;;;;;;;;;AAe1B,SAAgB,EAAa,EAC3B,OACA,UACA,aACA,cACA,cACA,cAAW,MAQV;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAK9B,GAAgB;EACjB,WAAW;GAAE,aAAa,KAAe;GAAI,QAAQ,IAAY,EAAE,cAAW,GAAG,KAAA;GAAW;EAC5F,MAAM,CAAC;EACR,CAAC,EACI,IAAU,GAAM,kBAAkB,SAAS,EAAE;AACnD,QACE,kBAAC,UAAD;EACM;EACJ,cAAY,IAAK,KAAA,IAAY;EAC7B,WAAW;EACJ;EACG;EACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAN3C;GAQG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAQ,WAAW,KAAK,MAAc,KAAA,KACrC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,qBACA,IACE,gCACA;IACC,CAAA;GAEV,EAAQ,KAAK,MACZ,kBAAC,UAAD;IAAmB,OAAO,EAAE;cAA5B;KACG,EAAE;KAAK;KAAG,EAAE,OAAO,aAAa;KAAC;KAC3B;MAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAoB,CAAG;;;;;;;;;;;;AAiB7B,SAAgB,EAAqB,EACnC,OACA,cACA,UACA,aACA,aAAU,EAAE,IAOX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAKvB,GAAmB;EACpB,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ;IAAE,GAAI,KAAY,EAAE,QAAQ,GAAU;IAAG,GAAI,KAAa,EAAE,cAAW;IAAG;GACnF;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,oBAAoB,SAAS,EAAE,EAC7C,IAAQ,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EACpD,IAAQ,GAAM,oBAAoB,cAAc;AACtD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C;KAME,kBAAC,UAAD;MAAQ,OAAM;gBACX,KAAW,CAAC,IAAO,eAAe,EAAM,SAAS,uBAAuB;MAClE,CAAA;KAER,KAAS,CAAC,EAAM,MAAM,MAAM,EAAE,OAAO,EAAM,IAC1C,kBAAC,UAAD;MAAe;gBAAO;MAAmC,CAAA;KAE1D,EAAM,KAAK,MACV,kBAAC,UAAD;MAAmB,OAAO,EAAE;gBAA5B;OACG,EAAE;OAAI;OAAI,EAAE;OACN;QAFI,EAAE,GAEN,CACT;KACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAc,CAAG;;;;;;;;;;;AAavB,SAAgB,EAAgB,EAC9B,OACA,UACA,aACA,aAAU,EAAE,IAMX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAEvB,GAAa;EACd,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ,IAAW,EAAE,QAAQ,GAAU,GAAG,KAAA;GAC3C;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,eAAe,SAAS,EAAE,EACxC,IAAO,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EACnD,IAAQ,GAAM,eAAe,cAAc;AACjD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C,CAME,kBAAC,UAAD;KAAQ,OAAM;eACX,KAAW,CAAC,IAAO,eAAe,EAAK,SAAS,iBAAiB;KAC3D,CAAA,EACR,EAAK,KAAK,MACT,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAkB,CAAG;;;;;;;;;;;AAa3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACpF,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAoB,CAAG;;;;;;;;;;;AAa7B,SAAgB,EAAgB,EAC9B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAmB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACtF,IAAa,GAAM,qBAAqB,SAAS,EAAE;AACzD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAW,WAAW,KAAK,MAAc,KAAA,KACxC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,wBACA,IACE,mCACA;IACC,CAAA;GAEV,EAAW,KAAK,MACf,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAqB,CAAG;;;;;;;;;;;AAa9B,SAAgB,EAAuB,EACrC,OACA,UACA,aACA,aAAU,EAAE,IAMX;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,EAC1C,EAAE,SAAM,eAAY,EAEvB,GAAoB;EACrB,WAAW;GACT,aAAa,KAAe;GAC5B,QAAQ,IAAW,EAAE,QAAQ,GAAU,GAAG,KAAA;GAC3C;EACD,MAAM,CAAC;EACR,CAAC,EACI,IAAQ,GAAM,sBAAsB,SAAS,EAAE,EAC/C,IAAc,EAAM,QAAQ,MAAM,CAAC,EAAQ,SAAS,EAAE,GAAG,CAAC,EAC1D,IAAQ,GAAM,sBAAsB,cAAc;AACxD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAJ3C,CAME,kBAAC,UAAD;KAAQ,OAAM;eACX,KAAW,CAAC,IACT,eACA,EAAY,SACV,wBACA;KACC,CAAA,EACR,EAAY,KAAK,MAChB,kBAAC,UAAD;KAAmB,OAAO,EAAE;eACzB,EAAE;KACI,EAFI,EAAE,GAEN,CACT,CACK;;GACR,IAAQ,EAAM,UACb,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KAClC,EAAM;KAAO;KAAK;KAAM;KAC/B;;GAEF;;;AAIV,IAAM,IAAkB,CAAG;;;;;;;;;;;AAa3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,gBAMC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAAE,WAAW,EAAE,aAAa,KAAe,IAAI;EAAE,MAAM,CAAC;EAAa,CAAC,EACpF,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAkB,CAAG;;;;;;;;;;;AAgB3B,SAAgB,EAAc,EAC5B,OACA,UACA,aACA,cACA,gBAOC;CACD,IAAM,EAAE,mBAAgB,GAAe,EACjC,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAiB;EAClB,WAAW;GAAE,aAAa,KAAe;GAAI,QAAQ,IAAY,EAAE,cAAW,GAAG,KAAA;GAAW;EAC5F,MAAM,CAAC;EACR,CAAC,EACI,IAAW,GAAM,mBAAmB,SAAS,EAAE;AACrD,QACE,kBAAC,UAAD;EACM;EACJ,WAAW;EACJ;EACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;YAJ3C;GAMG,MAAc,KAAA,KAAa,kBAAC,UAAD;IAAQ,OAAM;cAAI;IAAmB,CAAA;GAChE,EAAS,WAAW,KAAK,MAAc,KAAA,KACtC,kBAAC,UAAD;IAAQ,OAAM;cACX,KAAW,CAAC,IACT,sBACA,IACE,iCACA;IACC,CAAA;GAEV,EAAS,KAAK,MACb,kBAAC,UAAD;IAAmB,OAAO,EAAE;cACzB,EAAE;IACI,EAFI,EAAE,GAEN,CACT;GACK;;;AAIb,IAAM,IAAwB,CAAG;;;;;;;;;GAa3B,IAA4B;AAUlC,SAAgB,EAAmB,EACjC,OACA,UACA,aACA,cACA,cAAW,MAOV;CACD,IAAM,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,IAAW,EAAiB,EAAO,MAAM,CAAC,aAAa,CAAC,EACxD,EAAE,SAAM,YAAS,aAAU,EAE9B,GAAuB,EACxB,WAAW,EAAE,YAAY,EAAE,OAAO,GAA2B,EAAE,EAChE,CAAC,EACI,IAAS,IAAQ,EAAE,GAAI,GAAM,iBAAiB,EAAE,EAChD,IAAY,IACd,EAAO,QACJ,MACC,GAAG,EAAE,UAAU,GAAG,EAAE,WAAW,aAAa,CAAC,SAAS,EAAS,IAC/D,EAAE,MAAM,aAAa,CAAC,SAAS,EAAS,CAC3C,GACD;AACJ,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACG;IACV,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;IAC1C,CAAA;GACF,kBAAC,UAAD;IACM;IACJ,WAAW;IACJ;IACG;IACV,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;cAL3C;KAOG,MAAc,KAAA,KAAa,kBAAC,UAAD;MAAQ,OAAM;gBAAI;MAAmB,CAAA;KAChE,EAAU,WAAW,KAAK,MAAc,KAAA,KACvC,kBAAC,UAAD;MAAQ,OAAM;gBACX,KAAW,CAAC,IAAO,aAAa,IAAQ,mCAAmC;MACrE,CAAA;KAEV,EAAU,KAAK,MACd,kBAAC,UAAD;MAAmB,OAAO,EAAE;gBAA5B;OACG,EAAE;OAAU;OAAE,EAAE;OAAS;OAAI,EAAE;OACzB;QAFI,EAAE,GAEN,CACT;KACK;;GACR,CAAC,KAAY,EAAO,UAAU,KAC7B,kBAAC,KAAD;IAAG,WAAU;cAAb;KAA6C;KACxB;KAA0B;KAC3C;;GAEF"}
|
|
@@ -49,10 +49,6 @@ export declare function DashboardCardShell({ drillDown, children, }: {
|
|
|
49
49
|
};
|
|
50
50
|
children: ReactNode;
|
|
51
51
|
}): import("react/jsx-runtime").JSX.Element;
|
|
52
|
-
/** "There is no skill concept in PlanMagnet" — stated once per page, never invented. */
|
|
53
|
-
export declare function NoSkillsNote({ message }: {
|
|
54
|
-
message: string;
|
|
55
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
56
52
|
/** A small number-and-label tile, the same shape `analytics/DeliveryHealthCards.tsx`'s local `StatTile` uses. */
|
|
57
53
|
export declare function StatTile({ label, value, hint }: {
|
|
58
54
|
label: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardUI.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC;;;;;;;GAOG;AAEH;4FAC4F;AAC5F,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,2CAWvF;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,YAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,kBAAkB,2CA+CpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAOA;AAED,
|
|
1
|
+
{"version":3,"file":"DashboardUI.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC;;;;;;;GAOG;AAEH;4FAC4F;AAC5F,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,2CAWvF;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,OAAO,EACP,KAAK,EACL,KAAK,EACL,YAAY,EACZ,SAAS,EACT,QAAQ,GACT,EAAE,kBAAkB,2CA+CpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IACnD,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAOA;AAED,iHAAiH;AACjH,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,2CAQ/F;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,2CAyCxF;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAExE;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjF"}
|
|
@@ -53,14 +53,7 @@ function d({ drillDown: e, children: t }) {
|
|
|
53
53
|
}) : null]
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
function f({
|
|
57
|
-
return /* @__PURE__ */ n("p", {
|
|
58
|
-
"data-testid": "dashboard-no-skills-note",
|
|
59
|
-
className: "text-xs italic text-muted-foreground",
|
|
60
|
-
children: e
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
function p({ label: e, value: t, hint: i }) {
|
|
56
|
+
function f({ label: e, value: t, hint: i }) {
|
|
64
57
|
return /* @__PURE__ */ r("div", {
|
|
65
58
|
className: "rounded-md border p-3",
|
|
66
59
|
children: [
|
|
@@ -79,7 +72,7 @@ function p({ label: e, value: t, hint: i }) {
|
|
|
79
72
|
]
|
|
80
73
|
});
|
|
81
74
|
}
|
|
82
|
-
function
|
|
75
|
+
function p({ percent: e, label: t }) {
|
|
83
76
|
if (e == null) return /* @__PURE__ */ r("div", {
|
|
84
77
|
className: "space-y-1",
|
|
85
78
|
children: [t ? /* @__PURE__ */ r("div", {
|
|
@@ -110,17 +103,17 @@ function m({ percent: e, label: t }) {
|
|
|
110
103
|
})]
|
|
111
104
|
});
|
|
112
105
|
}
|
|
113
|
-
function
|
|
106
|
+
function m(e) {
|
|
114
107
|
return e == null ? "—" : `${Math.round(e * 100)}%`;
|
|
115
108
|
}
|
|
116
|
-
function
|
|
109
|
+
function h(e) {
|
|
117
110
|
let t = e / 60;
|
|
118
111
|
return `${t < 10 ? t.toFixed(1) : Math.round(t)} h`;
|
|
119
112
|
}
|
|
120
|
-
function
|
|
113
|
+
function g(e, t, n) {
|
|
121
114
|
return t > e ? `Showing ${e} of ${t} ${n}` : `${t} ${n}`;
|
|
122
115
|
}
|
|
123
116
|
//#endregion
|
|
124
|
-
export { u as DashboardCard, d as DashboardCardShell, l as DrillDownLink,
|
|
117
|
+
export { u as DashboardCard, d as DashboardCardShell, l as DrillDownLink, p as PercentBar, f as StatTile, g as cappedSubtitle, h as formatHours, m as ratePercentLabel };
|
|
125
118
|
|
|
126
119
|
//# sourceMappingURL=DashboardUI.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardUI.js","names":[],"sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowRight } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\n\n/**\n * Shared building blocks for the four role dashboards (ANR-06/07/28, CWF-31): every card on every dashboard\n * follows the same contract — a title, a one-line definition of what the number means, and loading/empty/\n * error/forbidden states, so a viewer never has to guess whether a blank card means \"nothing to show\" or \"this\n * broke\". `isForbidden` is the same RBAC-denial detector `releases/ReleaseReadinessCard.tsx`'s local `denied()`\n * generalizes from (see `utils/graphqlErrors.ts`) — a card whose resource the caller's role cannot read says so\n * instead of leaking an unreadable error string or silently rendering empty.\n */\n\n/** A one-line link out of a card into the page that can show more. Rendered as a real button, not a link styled\n * to look clickable, since it runs client-side navigation rather than following an href. */\nexport function DrillDownLink({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline\"\n >\n {label}\n <ArrowRight className=\"h-3 w-3\" aria-hidden={true} />\n </button>\n );\n}\n\nexport interface DashboardCardProps {\n icon: ReactNode;\n title: string;\n /** One line: what the number(s) on this card mean and where they come from. */\n definition: string;\n loading: boolean;\n error?: unknown;\n /** True when the query succeeded but there is nothing to show. */\n empty?: boolean;\n emptyMessage?: string;\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}\n\n/**\n * The full card shell: title + definition + icon in the header, then one of loading / forbidden / error / empty\n * / content in the body, then an optional drill-down link. Use this for a card built fresh for the dashboards.\n * For a card that already exists as its own `<Card>` (e.g. `BudgetPerformanceCard`), wrap it in\n * `DashboardCardShell` instead so its own header isn't duplicated.\n */\nexport function DashboardCard({\n icon,\n title,\n definition,\n loading,\n error,\n empty,\n emptyMessage,\n drillDown,\n children,\n}: DashboardCardProps) {\n const { t } = useI18n();\n const forbidden = isForbidden(error);\n const testId = forbidden ? 'dashboard-card-forbidden' : 'dashboard-card';\n\n let body: ReactNode;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {forbidden\n ? t('planmagnet.dashboards.forbidden', 'You do not have access to {{title}}.', { title })\n : t('planmagnet.dashboards.error', 'Could not be loaded: {{message}}', {\n message: error instanceof Error ? error.message : 'unknown error',\n })}\n </p>\n );\n } else if (loading) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.dashboards.loading', 'Loading…')}\n </p>\n );\n } else if (empty) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {emptyMessage ?? t('planmagnet.dashboards.empty', 'Nothing to show yet.')}\n </p>\n );\n } else {\n body = children;\n }\n\n return (\n <Card data-testid={testId}>\n <CardHeader className=\"space-y-1\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n {icon}\n {title}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">{definition}</p>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {body}\n {drillDown && !loading ? <DrillDownLink {...drillDown} /> : null}\n </CardContent>\n </Card>\n );\n}\n\n/**\n * Wraps an already-complete card component (it renders its own `<Card>`, title and loading/empty/error states —\n * `BudgetPerformanceCard`, `CycleTimeCard`, `SprintBurndownCard`, `SprintVelocityCard`) so it carries the\n * `dashboard-card` testid and an optional drill-down link without duplicating its header.\n */\nexport function DashboardCardShell({\n drillDown,\n children,\n}: {\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}) {\n return (\n <div data-testid=\"dashboard-card\" className=\"space-y-2\">\n {children}\n {drillDown ? <div className=\"px-1\">{<DrillDownLink {...drillDown} />}</div> : null}\n </div>\n );\n}\n\n/** \"There is no skill concept in PlanMagnet\" — stated once per page, never invented. */\nexport function NoSkillsNote({ message }: { message: string }) {\n return (\n <p data-testid=\"dashboard-no-skills-note\" className=\"text-xs italic text-muted-foreground\">\n {message}\n </p>\n );\n}\n\n/** A small number-and-label tile, the same shape `analytics/DeliveryHealthCards.tsx`'s local `StatTile` uses. */\nexport function StatTile({ label, value, hint }: { label: string; value: string; hint?: string }) {\n return (\n <div className=\"rounded-md border p-3\">\n <p className=\"text-xs text-muted-foreground\">{label}</p>\n <p className=\"mt-1 text-2xl font-semibold\">{value}</p>\n {hint ? <p className=\"mt-0.5 text-xs text-muted-foreground\">{hint}</p> : null}\n </div>\n );\n}\n\n/**\n * A labelled 0-100% meter, using the semantic `primary` token — never a raw color. `percent: null` means\n * \"not measured\" (e.g. a rate whose denominator was 0 — nothing completed in the window to score), and\n * renders an empty dashed track with a \"—\" label instead of a fabricated 0%. A 0%-filled bar is visually\n * indistinguishable from \"measured and it's zero\" (total delivery failure), so coercing null to 0 here\n * would mislead a viewer skimming the card — the caller's null case must flow through, never `?? 0`.\n */\nexport function PercentBar({ percent, label }: { percent: number | null; label?: string }) {\n if (percent == null) {\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>—</span>\n </div>\n ) : null}\n <div\n aria-hidden=\"true\"\n className=\"h-2 w-full overflow-hidden rounded-full border border-dashed border-muted-foreground/30\"\n />\n </div>\n );\n }\n const pct = Math.max(0, Math.min(100, Math.round(percent)));\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>{pct}%</span>\n </div>\n ) : null}\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n aria-label={label ?? `${pct}%`}\n className=\"h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n}\n\n/** \"—\" for a rate that is null because its denominator was 0 — never a fabricated 0% or 100%. */\nexport function ratePercentLabel(rate: number | null | undefined): string {\n return rate == null ? '—' : `${Math.round(rate * 100)}%`;\n}\n\n/** Minutes read naturally as hours (one decimal below 10h, whole hours above). */\nexport function formatHours(minutes: number): string {\n const hours = minutes / 60;\n return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)} h`;\n}\n\n/** Cap-aware \"N of TOTAL\" wording for a list a card intentionally truncates instead of paginating in place. */\nexport function cappedSubtitle(shown: number, total: number, noun: string): string {\n return total > shown ? `Showing ${shown} of ${total} ${noun}` : `${total} ${noun}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardUI.js","names":[],"sources":["../../../src/planmagnet/dashboards/DashboardUI.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowRight } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\n\n/**\n * Shared building blocks for the four role dashboards (ANR-06/07/28, CWF-31): every card on every dashboard\n * follows the same contract — a title, a one-line definition of what the number means, and loading/empty/\n * error/forbidden states, so a viewer never has to guess whether a blank card means \"nothing to show\" or \"this\n * broke\". `isForbidden` is the same RBAC-denial detector `releases/ReleaseReadinessCard.tsx`'s local `denied()`\n * generalizes from (see `utils/graphqlErrors.ts`) — a card whose resource the caller's role cannot read says so\n * instead of leaking an unreadable error string or silently rendering empty.\n */\n\n/** A one-line link out of a card into the page that can show more. Rendered as a real button, not a link styled\n * to look clickable, since it runs client-side navigation rather than following an href. */\nexport function DrillDownLink({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline\"\n >\n {label}\n <ArrowRight className=\"h-3 w-3\" aria-hidden={true} />\n </button>\n );\n}\n\nexport interface DashboardCardProps {\n icon: ReactNode;\n title: string;\n /** One line: what the number(s) on this card mean and where they come from. */\n definition: string;\n loading: boolean;\n error?: unknown;\n /** True when the query succeeded but there is nothing to show. */\n empty?: boolean;\n emptyMessage?: string;\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}\n\n/**\n * The full card shell: title + definition + icon in the header, then one of loading / forbidden / error / empty\n * / content in the body, then an optional drill-down link. Use this for a card built fresh for the dashboards.\n * For a card that already exists as its own `<Card>` (e.g. `BudgetPerformanceCard`), wrap it in\n * `DashboardCardShell` instead so its own header isn't duplicated.\n */\nexport function DashboardCard({\n icon,\n title,\n definition,\n loading,\n error,\n empty,\n emptyMessage,\n drillDown,\n children,\n}: DashboardCardProps) {\n const { t } = useI18n();\n const forbidden = isForbidden(error);\n const testId = forbidden ? 'dashboard-card-forbidden' : 'dashboard-card';\n\n let body: ReactNode;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {forbidden\n ? t('planmagnet.dashboards.forbidden', 'You do not have access to {{title}}.', { title })\n : t('planmagnet.dashboards.error', 'Could not be loaded: {{message}}', {\n message: error instanceof Error ? error.message : 'unknown error',\n })}\n </p>\n );\n } else if (loading) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.dashboards.loading', 'Loading…')}\n </p>\n );\n } else if (empty) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {emptyMessage ?? t('planmagnet.dashboards.empty', 'Nothing to show yet.')}\n </p>\n );\n } else {\n body = children;\n }\n\n return (\n <Card data-testid={testId}>\n <CardHeader className=\"space-y-1\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n {icon}\n {title}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">{definition}</p>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {body}\n {drillDown && !loading ? <DrillDownLink {...drillDown} /> : null}\n </CardContent>\n </Card>\n );\n}\n\n/**\n * Wraps an already-complete card component (it renders its own `<Card>`, title and loading/empty/error states —\n * `BudgetPerformanceCard`, `CycleTimeCard`, `SprintBurndownCard`, `SprintVelocityCard`) so it carries the\n * `dashboard-card` testid and an optional drill-down link without duplicating its header.\n */\nexport function DashboardCardShell({\n drillDown,\n children,\n}: {\n drillDown?: { label: string; onClick: () => void };\n children: ReactNode;\n}) {\n return (\n <div data-testid=\"dashboard-card\" className=\"space-y-2\">\n {children}\n {drillDown ? <div className=\"px-1\">{<DrillDownLink {...drillDown} />}</div> : null}\n </div>\n );\n}\n\n/** A small number-and-label tile, the same shape `analytics/DeliveryHealthCards.tsx`'s local `StatTile` uses. */\nexport function StatTile({ label, value, hint }: { label: string; value: string; hint?: string }) {\n return (\n <div className=\"rounded-md border p-3\">\n <p className=\"text-xs text-muted-foreground\">{label}</p>\n <p className=\"mt-1 text-2xl font-semibold\">{value}</p>\n {hint ? <p className=\"mt-0.5 text-xs text-muted-foreground\">{hint}</p> : null}\n </div>\n );\n}\n\n/**\n * A labelled 0-100% meter, using the semantic `primary` token — never a raw color. `percent: null` means\n * \"not measured\" (e.g. a rate whose denominator was 0 — nothing completed in the window to score), and\n * renders an empty dashed track with a \"—\" label instead of a fabricated 0%. A 0%-filled bar is visually\n * indistinguishable from \"measured and it's zero\" (total delivery failure), so coercing null to 0 here\n * would mislead a viewer skimming the card — the caller's null case must flow through, never `?? 0`.\n */\nexport function PercentBar({ percent, label }: { percent: number | null; label?: string }) {\n if (percent == null) {\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>—</span>\n </div>\n ) : null}\n <div\n aria-hidden=\"true\"\n className=\"h-2 w-full overflow-hidden rounded-full border border-dashed border-muted-foreground/30\"\n />\n </div>\n );\n }\n const pct = Math.max(0, Math.min(100, Math.round(percent)));\n return (\n <div className=\"space-y-1\">\n {label ? (\n <div className=\"flex justify-between text-xs text-muted-foreground\">\n <span>{label}</span>\n <span>{pct}%</span>\n </div>\n ) : null}\n <div\n role=\"meter\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={pct}\n aria-label={label ?? `${pct}%`}\n className=\"h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div\n className=\"h-full rounded-full bg-primary transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n );\n}\n\n/** \"—\" for a rate that is null because its denominator was 0 — never a fabricated 0% or 100%. */\nexport function ratePercentLabel(rate: number | null | undefined): string {\n return rate == null ? '—' : `${Math.round(rate * 100)}%`;\n}\n\n/** Minutes read naturally as hours (one decimal below 10h, whole hours above). */\nexport function formatHours(minutes: number): string {\n const hours = minutes / 60;\n return `${hours < 10 ? hours.toFixed(1) : Math.round(hours)} h`;\n}\n\n/** Cap-aware \"N of TOTAL\" wording for a list a card intentionally truncates instead of paginating in place. */\nexport function cappedSubtitle(shown: number, total: number, noun: string): string {\n return total > shown ? `Showing ${shown} of ${total} ${noun}` : `${total} ${noun}`;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SprintCapacityCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SprintCapacityCard.d.ts","sourceRoot":"","sources":["../../../../src/planmagnet/dashboards/cards/SprintCapacityCard.tsx"],"names":[],"mappings":"AA2DA,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,2CA0HA"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { usePlanMagnetNavigate as e } from "../../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { PersonAvatar as t } from "../../people/People.js";
|
|
3
3
|
import { DashboardCard as n, DrillDownLink as r } from "../DashboardUI.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { MemberSkills as i } from "../../skills/SkillChips.js";
|
|
5
|
+
import { Users as a } from "lucide-react";
|
|
6
|
+
import { gql as o } from "@apollo/client";
|
|
7
|
+
import { useQuery as s } from "@apollo/client/react";
|
|
8
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
9
|
+
import { useI18n as u } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
9
10
|
//#region src/planmagnet/dashboards/cards/SprintCapacityCard.tsx
|
|
10
|
-
var
|
|
11
|
+
var d = o`
|
|
11
12
|
query DashboardSprintCapacity($sprintId: ID!) {
|
|
12
13
|
planmagnetSprintCapacity(sprintId: $sprintId) {
|
|
13
14
|
sprintId
|
|
@@ -15,7 +16,9 @@ var u = a`
|
|
|
15
16
|
unmatchedCount
|
|
16
17
|
members {
|
|
17
18
|
actorId
|
|
19
|
+
employeeId
|
|
18
20
|
employeeName
|
|
21
|
+
skills
|
|
19
22
|
position
|
|
20
23
|
department
|
|
21
24
|
allocatedHours
|
|
@@ -27,82 +30,90 @@ var u = a`
|
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
`;
|
|
30
|
-
function
|
|
31
|
-
let { t:
|
|
32
|
-
variables: { sprintId:
|
|
33
|
-
skip: !
|
|
34
|
-
}),
|
|
35
|
-
return /* @__PURE__ */
|
|
36
|
-
icon: /* @__PURE__ */
|
|
37
|
-
title:
|
|
38
|
-
definition:
|
|
39
|
-
loading:
|
|
40
|
-
error:
|
|
41
|
-
empty: !
|
|
42
|
-
emptyMessage:
|
|
33
|
+
function f({ sprintId: o, onDrillDown: f }) {
|
|
34
|
+
let { t: p } = u(), m = e(), { data: h, loading: g, error: _ } = s(d, {
|
|
35
|
+
variables: { sprintId: o },
|
|
36
|
+
skip: !o
|
|
37
|
+
}), v = h?.planmagnetSprintCapacity;
|
|
38
|
+
return /* @__PURE__ */ c(n, {
|
|
39
|
+
icon: /* @__PURE__ */ c(a, { className: "h-4 w-4" }),
|
|
40
|
+
title: p("planmagnet.dashboards.pm.capacity.title", "Capacity"),
|
|
41
|
+
definition: p("planmagnet.dashboards.pm.capacity.definition", "Real CrewFoundry allocation for the active sprint's assignees — hours and story points, per person."),
|
|
42
|
+
loading: g && !h,
|
|
43
|
+
error: _,
|
|
44
|
+
empty: !o || !!v && v.members.length === 0 && !v.crewfoundryUnavailable,
|
|
45
|
+
emptyMessage: o ? p("planmagnet.dashboards.pm.capacity.noMembers", "No CrewFoundry-matched assignees in this sprint.") : p("planmagnet.dashboards.pm.capacity.noSprint", "No active sprint for this project."),
|
|
43
46
|
drillDown: {
|
|
44
|
-
label:
|
|
45
|
-
onClick:
|
|
47
|
+
label: p("planmagnet.dashboards.pm.capacity.drillDown", "Open project"),
|
|
48
|
+
onClick: f
|
|
46
49
|
},
|
|
47
|
-
children:
|
|
50
|
+
children: v ? v.crewfoundryUnavailable ? /* @__PURE__ */ c("p", {
|
|
48
51
|
className: "text-sm text-muted-foreground",
|
|
49
|
-
children:
|
|
50
|
-
}) : /* @__PURE__ */
|
|
52
|
+
children: p("planmagnet.dashboards.pm.capacity.crewfoundryUnavailable", "CrewFoundry is temporarily unavailable, so capacity and skills can’t be shown right now. Try again shortly.")
|
|
53
|
+
}) : /* @__PURE__ */ l("div", {
|
|
51
54
|
className: "space-y-2",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
55
|
+
children: [/* @__PURE__ */ l("table", {
|
|
53
56
|
className: "w-full text-sm",
|
|
54
|
-
children: [/* @__PURE__ */
|
|
57
|
+
children: [/* @__PURE__ */ c("thead", { children: /* @__PURE__ */ l("tr", {
|
|
55
58
|
className: "text-left text-xs text-muted-foreground",
|
|
56
59
|
children: [
|
|
57
|
-
/* @__PURE__ */
|
|
60
|
+
/* @__PURE__ */ c("th", {
|
|
58
61
|
className: "py-1 pr-2 font-normal",
|
|
59
|
-
children:
|
|
62
|
+
children: p("planmagnet.dashboards.pm.capacity.person", "Person")
|
|
60
63
|
}),
|
|
61
|
-
/* @__PURE__ */
|
|
64
|
+
/* @__PURE__ */ c("th", {
|
|
62
65
|
className: "py-1 pr-2 text-right font-normal",
|
|
63
|
-
children:
|
|
66
|
+
children: p("planmagnet.dashboards.pm.capacity.allocated", "Allocated h")
|
|
64
67
|
}),
|
|
65
|
-
/* @__PURE__ */
|
|
68
|
+
/* @__PURE__ */ c("th", {
|
|
66
69
|
className: "py-1 pr-2 text-right font-normal",
|
|
67
|
-
children:
|
|
70
|
+
children: p("planmagnet.dashboards.pm.capacity.utilization", "Utilization")
|
|
68
71
|
}),
|
|
69
|
-
/* @__PURE__ */
|
|
72
|
+
/* @__PURE__ */ c("th", {
|
|
70
73
|
className: "py-1 text-right font-normal",
|
|
71
|
-
children:
|
|
74
|
+
children: p("planmagnet.dashboards.pm.capacity.items", "Items (pts)")
|
|
72
75
|
})
|
|
73
76
|
]
|
|
74
|
-
}) }), /* @__PURE__ */
|
|
77
|
+
}) }), /* @__PURE__ */ c("tbody", { children: v.members.map((e) => /* @__PURE__ */ l("tr", {
|
|
75
78
|
className: "border-t",
|
|
76
79
|
children: [
|
|
77
|
-
/* @__PURE__ */
|
|
80
|
+
/* @__PURE__ */ c("td", {
|
|
78
81
|
className: "py-1.5 pr-2",
|
|
79
|
-
children: /* @__PURE__ */
|
|
82
|
+
children: /* @__PURE__ */ l("div", {
|
|
80
83
|
className: "flex items-center gap-2",
|
|
81
|
-
children: [/* @__PURE__ */
|
|
84
|
+
children: [/* @__PURE__ */ c(t, {
|
|
82
85
|
id: e.actorId,
|
|
83
86
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"
|
|
84
|
-
}), /* @__PURE__ */
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
}), /* @__PURE__ */ l("div", { children: [
|
|
88
|
+
/* @__PURE__ */ c("div", {
|
|
89
|
+
className: "font-medium",
|
|
90
|
+
children: e.employeeName
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ l("div", {
|
|
93
|
+
className: "text-xs text-muted-foreground",
|
|
94
|
+
children: [
|
|
95
|
+
e.position,
|
|
96
|
+
" · ",
|
|
97
|
+
e.department
|
|
98
|
+
]
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ c(i, {
|
|
101
|
+
skills: e.skills,
|
|
102
|
+
employeeId: e.employeeId,
|
|
103
|
+
employeeName: e.employeeName
|
|
104
|
+
})
|
|
105
|
+
] })]
|
|
95
106
|
})
|
|
96
107
|
}),
|
|
97
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ c("td", {
|
|
98
109
|
className: "py-1.5 pr-2 text-right tabular-nums",
|
|
99
110
|
children: e.allocatedHours.toFixed(1)
|
|
100
111
|
}),
|
|
101
|
-
/* @__PURE__ */
|
|
112
|
+
/* @__PURE__ */ c("td", {
|
|
102
113
|
className: "py-1.5 pr-2 text-right tabular-nums",
|
|
103
114
|
children: e.utilizationPercent == null ? "—" : `${e.utilizationPercent}%`
|
|
104
115
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
116
|
+
/* @__PURE__ */ l("td", {
|
|
106
117
|
className: "py-1.5 text-right tabular-nums",
|
|
107
118
|
children: [
|
|
108
119
|
e.assignedWorkItemCount,
|
|
@@ -113,21 +124,21 @@ function d({ sprintId: a, onDrillDown: d }) {
|
|
|
113
124
|
})
|
|
114
125
|
]
|
|
115
126
|
}, e.actorId)) })]
|
|
116
|
-
}),
|
|
127
|
+
}), v.unmatchedCount > 0 ? /* @__PURE__ */ l("div", {
|
|
117
128
|
className: "flex items-center justify-between gap-2",
|
|
118
129
|
"data-testid": "sprint-capacity-card-unmatched",
|
|
119
|
-
children: [/* @__PURE__ */
|
|
130
|
+
children: [/* @__PURE__ */ c("p", {
|
|
120
131
|
className: "text-xs text-muted-foreground",
|
|
121
|
-
children:
|
|
122
|
-
}), /* @__PURE__ */
|
|
123
|
-
label:
|
|
124
|
-
onClick: () =>
|
|
132
|
+
children: p("planmagnet.dashboards.pm.capacity.unmatched", "{{count}} sprint assignee(s) could not be matched to a CrewFoundry employee record.", { count: v.unmatchedCount })
|
|
133
|
+
}), /* @__PURE__ */ c(r, {
|
|
134
|
+
label: p("planmagnet.dashboards.pm.capacity.fixUnmatched", "Fix"),
|
|
135
|
+
onClick: () => m("/team/team-page?focus=unmatched-crew-identities")
|
|
125
136
|
})]
|
|
126
137
|
}) : null]
|
|
127
138
|
}) : null
|
|
128
139
|
});
|
|
129
140
|
}
|
|
130
141
|
//#endregion
|
|
131
|
-
export {
|
|
142
|
+
export { f as SprintCapacityCard };
|
|
132
143
|
|
|
133
144
|
//# sourceMappingURL=SprintCapacityCard.js.map
|