@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { DashboardCard as t } from "../dashboards/DashboardUI.js";
|
|
3
|
+
import { coverageBadgeVariant as n, coverageCounts as r, coverageFilter as i, crewFoundryEmployeesUrl as a, orderCoverage as o } from "./skillTags.js";
|
|
4
|
+
import { CrewFoundryProfileLink as s } from "./SkillChips.js";
|
|
5
|
+
import { SKILL_COVERAGE as c } from "./skillOperations.js";
|
|
6
|
+
import { useState as l } from "react";
|
|
7
|
+
import { ExternalLink as u, GraduationCap as d } from "lucide-react";
|
|
8
|
+
import { useQuery as f } from "@apollo/client/react";
|
|
9
|
+
import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
10
|
+
import { Badge as g, Button as _ } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/skills/SkillCoverageCard.tsx
|
|
12
|
+
var v = 5;
|
|
13
|
+
function y({ workspaceId: n, projectId: s, teamId: g, sprintId: v, notReadyMessage: y }) {
|
|
14
|
+
let x = e(), S = i({
|
|
15
|
+
projectId: s,
|
|
16
|
+
teamId: g,
|
|
17
|
+
sprintId: v
|
|
18
|
+
}), C = Object.keys(S).length > 0, { data: w, loading: E, error: D } = f(c, {
|
|
19
|
+
variables: {
|
|
20
|
+
workspaceId: n ?? "",
|
|
21
|
+
filter: S
|
|
22
|
+
},
|
|
23
|
+
skip: !n || !C
|
|
24
|
+
}), [O, k] = l(!1), A = w?.planmagnetSkillCoverage ?? null, j = o(A?.skills ?? []), M = j.filter((e) => e.status !== "NO_DEMAND"), N = j.filter((e) => e.status === "NO_DEMAND"), P = r(j), F = !!A && (A.crewfoundryUnavailable || A.membersUnavailable), I = O ? [...M, ...N] : M, L = x("planmagnet.skills.coverage.title", "Skills coverage");
|
|
25
|
+
return /* @__PURE__ */ m(t, {
|
|
26
|
+
icon: /* @__PURE__ */ m(d, { className: "h-4 w-4" }),
|
|
27
|
+
title: L,
|
|
28
|
+
definition: x("planmagnet.skills.coverage.definition", "Open work items that require each skill, and the people whose CrewFoundry skills match it by name (ignoring case; no proficiency levels)."),
|
|
29
|
+
loading: E && !w,
|
|
30
|
+
error: D,
|
|
31
|
+
empty: !C,
|
|
32
|
+
emptyMessage: y,
|
|
33
|
+
children: A ? /* @__PURE__ */ h("div", {
|
|
34
|
+
className: "space-y-3",
|
|
35
|
+
"data-testid": "skill-coverage",
|
|
36
|
+
children: [
|
|
37
|
+
A.crewfoundryUnavailable && /* @__PURE__ */ m(b, {
|
|
38
|
+
testId: "skill-coverage-crewfoundry-unavailable",
|
|
39
|
+
children: x("planmagnet.skills.coverage.crewfoundryUnavailable", "CrewFoundry is temporarily unavailable, so people’s skills can’t be read right now. Skills that open work requires show as Unknown until it is back — try again shortly.")
|
|
40
|
+
}),
|
|
41
|
+
A.membersUnavailable && /* @__PURE__ */ m(b, {
|
|
42
|
+
testId: "skill-coverage-members-unavailable",
|
|
43
|
+
children: x("planmagnet.skills.coverage.membersUnavailable", "The workspace member list could not be read in full, so qualified people can’t be counted right now. Skills that open work requires show as Unknown.")
|
|
44
|
+
}),
|
|
45
|
+
A.truncated && /* @__PURE__ */ m(b, {
|
|
46
|
+
testId: "skill-coverage-truncated",
|
|
47
|
+
children: x("planmagnet.skills.coverage.truncated", "More than 5,000 open work items here require a skill: the demand figures cover the oldest 5,000 only.")
|
|
48
|
+
}),
|
|
49
|
+
j.length === 0 ? /* @__PURE__ */ m("p", {
|
|
50
|
+
className: "text-sm text-muted-foreground",
|
|
51
|
+
"data-testid": "skill-coverage-empty",
|
|
52
|
+
children: F ? x("planmagnet.skills.coverage.emptyPeopleUnreadable", "No open work here requires a skill. People’s skills could not be read, so none are listed.") : x("planmagnet.skills.coverage.empty", "No open work here requires a skill, and no one in scope lists a skill in CrewFoundry. Add required skills on a task’s edit page to see coverage.")
|
|
53
|
+
}) : /* @__PURE__ */ h(p, { children: [
|
|
54
|
+
/* @__PURE__ */ m("p", {
|
|
55
|
+
className: "text-xs text-muted-foreground",
|
|
56
|
+
"data-testid": "skill-coverage-summary",
|
|
57
|
+
children: x("planmagnet.skills.coverage.summary", "{{uncovered}} uncovered · {{unknown}} unknown · {{covered}} covered · {{noDemand}} with no open work", {
|
|
58
|
+
uncovered: P.UNCOVERED,
|
|
59
|
+
unknown: P.UNKNOWN,
|
|
60
|
+
covered: P.COVERED,
|
|
61
|
+
noDemand: P.NO_DEMAND
|
|
62
|
+
})
|
|
63
|
+
}),
|
|
64
|
+
M.length === 0 && /* @__PURE__ */ m("p", {
|
|
65
|
+
className: "text-sm text-muted-foreground",
|
|
66
|
+
"data-testid": "skill-coverage-no-demand",
|
|
67
|
+
children: x("planmagnet.skills.coverage.noDemand", "No open work here requires a skill yet. Add required skills on a task’s edit page to see coverage.")
|
|
68
|
+
}),
|
|
69
|
+
I.length > 0 && /* @__PURE__ */ m("div", {
|
|
70
|
+
className: "overflow-x-auto",
|
|
71
|
+
children: /* @__PURE__ */ h("table", {
|
|
72
|
+
className: "w-full text-sm",
|
|
73
|
+
"data-testid": "skill-coverage-table",
|
|
74
|
+
children: [/* @__PURE__ */ m("thead", { children: /* @__PURE__ */ h("tr", {
|
|
75
|
+
className: "text-left text-xs text-muted-foreground",
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ m("th", {
|
|
78
|
+
className: "py-1 pr-2 font-normal",
|
|
79
|
+
children: x("planmagnet.skills.coverage.col.skill", "Skill")
|
|
80
|
+
}),
|
|
81
|
+
/* @__PURE__ */ m("th", {
|
|
82
|
+
className: "py-1 pr-2 font-normal",
|
|
83
|
+
children: x("planmagnet.skills.coverage.col.status", "Status")
|
|
84
|
+
}),
|
|
85
|
+
/* @__PURE__ */ m("th", {
|
|
86
|
+
className: "py-1 pr-2 text-right font-normal",
|
|
87
|
+
children: x("planmagnet.skills.coverage.col.open", "Open work")
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ m("th", {
|
|
90
|
+
className: "py-1 pr-2 text-right font-normal",
|
|
91
|
+
children: x("planmagnet.skills.coverage.col.assigned", "Assigned")
|
|
92
|
+
}),
|
|
93
|
+
/* @__PURE__ */ m("th", {
|
|
94
|
+
className: "py-1 font-normal",
|
|
95
|
+
children: x("planmagnet.skills.coverage.col.people", "Qualified people")
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
}) }), /* @__PURE__ */ m("tbody", { children: I.map((e) => /* @__PURE__ */ m(T, {
|
|
99
|
+
row: e,
|
|
100
|
+
tr: x
|
|
101
|
+
}, e.skill)) })]
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
N.length > 0 && /* @__PURE__ */ m(_, {
|
|
105
|
+
type: "button",
|
|
106
|
+
variant: "ghost",
|
|
107
|
+
size: "sm",
|
|
108
|
+
"aria-expanded": O,
|
|
109
|
+
onClick: () => k((e) => !e),
|
|
110
|
+
"data-testid": "skill-coverage-toggle-no-demand",
|
|
111
|
+
children: O ? x("planmagnet.skills.coverage.hideNoDemand", "Hide skills no open work requires") : N.length === 1 ? x("planmagnet.skills.coverage.showNoDemand.one", "Show 1 skill people have that no open work requires") : x("planmagnet.skills.coverage.showNoDemand.other", "Show {{count}} skills people have that no open work requires", { count: N.length })
|
|
112
|
+
})
|
|
113
|
+
] }),
|
|
114
|
+
/* @__PURE__ */ h("div", {
|
|
115
|
+
className: "space-y-1 border-t pt-2 text-xs text-muted-foreground",
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ m("p", {
|
|
118
|
+
"data-testid": "skill-coverage-utilization-note",
|
|
119
|
+
children: x("planmagnet.skills.coverage.utilizationNote", "Counts and story points only (an item with no estimate counts as 0 points). Hours-based skill utilization is not available yet: it needs per-person capacity in CrewFoundry.")
|
|
120
|
+
}),
|
|
121
|
+
/* @__PURE__ */ m("p", { children: x("planmagnet.skills.coverage.coveredNote", "Covered means at least one qualified person exists, not that they have time for the work.") }),
|
|
122
|
+
/* @__PURE__ */ h("p", {
|
|
123
|
+
className: "flex flex-wrap items-center gap-1",
|
|
124
|
+
children: [x("planmagnet.skills.coverage.crewfoundryNote", "People’s skills come from CrewFoundry and are read-only here."), /* @__PURE__ */ h("a", {
|
|
125
|
+
href: a(typeof window > "u" ? "" : window.location.hostname),
|
|
126
|
+
target: "_blank",
|
|
127
|
+
rel: "noopener noreferrer",
|
|
128
|
+
className: "inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline",
|
|
129
|
+
children: [x("planmagnet.skills.coverage.openCrewfoundry", "Edit them in CrewFoundry"), /* @__PURE__ */ m(u, {
|
|
130
|
+
className: "h-3 w-3",
|
|
131
|
+
"aria-hidden": !0
|
|
132
|
+
})]
|
|
133
|
+
})]
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}) : null
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function b({ testId: e, children: t }) {
|
|
142
|
+
return /* @__PURE__ */ m("p", {
|
|
143
|
+
role: "status",
|
|
144
|
+
"data-testid": e,
|
|
145
|
+
className: "rounded-md border border-status-warning-border bg-status-warning-subtle p-2 text-xs text-status-warning-text",
|
|
146
|
+
children: t
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function x(e, t) {
|
|
150
|
+
switch (e) {
|
|
151
|
+
case "UNCOVERED": return t("planmagnet.skills.coverage.status.uncovered", "Uncovered");
|
|
152
|
+
case "UNKNOWN": return t("planmagnet.skills.coverage.status.unknown", "Unknown");
|
|
153
|
+
case "COVERED": return t("planmagnet.skills.coverage.status.covered", "Covered");
|
|
154
|
+
case "NO_DEMAND": return t("planmagnet.skills.coverage.status.noDemand", "No open work");
|
|
155
|
+
default: return e;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function S(e, t) {
|
|
159
|
+
switch (e) {
|
|
160
|
+
case "UNCOVERED": return t("planmagnet.skills.coverage.statusHelp.uncovered", "Open work requires this skill and no qualified person has it.");
|
|
161
|
+
case "UNKNOWN": return t("planmagnet.skills.coverage.statusHelp.unknown", "Open work requires this skill, but qualified people can’t be counted right now.");
|
|
162
|
+
case "COVERED": return t("planmagnet.skills.coverage.statusHelp.covered", "Open work requires this skill and at least one qualified person has it.");
|
|
163
|
+
case "NO_DEMAND": return t("planmagnet.skills.coverage.statusHelp.noDemand", "A qualified person has this skill; no open work here requires it.");
|
|
164
|
+
default: return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function C(e, t) {
|
|
168
|
+
return e === 1 ? t("planmagnet.skills.coverage.items.one", "1 item") : t("planmagnet.skills.coverage.items.other", "{{count}} items", { count: e });
|
|
169
|
+
}
|
|
170
|
+
function w(e, t) {
|
|
171
|
+
return t("planmagnet.skills.coverage.demand", "{{items}} · {{points}} pts", {
|
|
172
|
+
items: C(e.workItemCount, t),
|
|
173
|
+
points: e.storyPoints
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function T({ row: e, tr: t }) {
|
|
177
|
+
let r = e.status === "NO_DEMAND";
|
|
178
|
+
return /* @__PURE__ */ h("tr", {
|
|
179
|
+
className: "border-t align-top",
|
|
180
|
+
"data-testid": "skill-coverage-row",
|
|
181
|
+
"data-status": e.status,
|
|
182
|
+
children: [
|
|
183
|
+
/* @__PURE__ */ m("td", {
|
|
184
|
+
className: "py-1.5 pr-2 font-medium",
|
|
185
|
+
children: e.skill
|
|
186
|
+
}),
|
|
187
|
+
/* @__PURE__ */ m("td", {
|
|
188
|
+
className: "py-1.5 pr-2",
|
|
189
|
+
children: /* @__PURE__ */ m(g, {
|
|
190
|
+
variant: n(e.status),
|
|
191
|
+
title: S(e.status, t),
|
|
192
|
+
children: x(e.status, t)
|
|
193
|
+
})
|
|
194
|
+
}),
|
|
195
|
+
/* @__PURE__ */ m("td", {
|
|
196
|
+
className: "whitespace-nowrap py-1.5 pr-2 text-right tabular-nums",
|
|
197
|
+
children: r ? "—" : w(e.openDemand, t)
|
|
198
|
+
}),
|
|
199
|
+
/* @__PURE__ */ m("td", {
|
|
200
|
+
className: "whitespace-nowrap py-1.5 pr-2 text-right tabular-nums",
|
|
201
|
+
children: r ? "—" : w(e.assignedDemand, t)
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ m("td", {
|
|
204
|
+
className: "py-1.5",
|
|
205
|
+
children: /* @__PURE__ */ m(E, {
|
|
206
|
+
row: e,
|
|
207
|
+
tr: t
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function E({ row: e, tr: t }) {
|
|
214
|
+
let [n, r] = l(!1);
|
|
215
|
+
if (e.status === "UNKNOWN") return /* @__PURE__ */ m("span", {
|
|
216
|
+
className: "text-xs text-muted-foreground",
|
|
217
|
+
children: t("planmagnet.skills.coverage.peopleUnknown", "Can’t be counted right now")
|
|
218
|
+
});
|
|
219
|
+
if (e.qualifiedMembers.length === 0) return /* @__PURE__ */ m("span", {
|
|
220
|
+
className: "text-xs text-muted-foreground",
|
|
221
|
+
children: t("planmagnet.skills.coverage.peopleNone", "No one qualified")
|
|
222
|
+
});
|
|
223
|
+
let i = n ? e.qualifiedMembers : e.qualifiedMembers.slice(0, v), a = e.qualifiedMembers.length - i.length;
|
|
224
|
+
return /* @__PURE__ */ h("div", {
|
|
225
|
+
className: "space-y-1",
|
|
226
|
+
children: [/* @__PURE__ */ h("ul", {
|
|
227
|
+
className: "flex flex-wrap gap-x-3 gap-y-1",
|
|
228
|
+
children: [i.map((n) => /* @__PURE__ */ h("li", {
|
|
229
|
+
className: "text-xs",
|
|
230
|
+
children: [/* @__PURE__ */ m(s, {
|
|
231
|
+
employeeId: n.employeeId,
|
|
232
|
+
name: n.employeeName,
|
|
233
|
+
children: n.employeeName
|
|
234
|
+
}), e.status !== "NO_DEMAND" && /* @__PURE__ */ m("span", {
|
|
235
|
+
className: "ml-1 text-muted-foreground",
|
|
236
|
+
children: t("planmagnet.skills.coverage.memberAssigned", "({{count}} assigned)", { count: n.assignedDemandCount })
|
|
237
|
+
})]
|
|
238
|
+
}, n.employeeId)), a > 0 && /* @__PURE__ */ m("li", { children: /* @__PURE__ */ m("button", {
|
|
239
|
+
type: "button",
|
|
240
|
+
className: "text-xs text-primary underline-offset-2 hover:underline",
|
|
241
|
+
onClick: () => r(!0),
|
|
242
|
+
children: t("planmagnet.skills.coverage.showAllPeople", "Show all {{count}}", { count: e.qualifiedMembers.length })
|
|
243
|
+
}) })]
|
|
244
|
+
}), e.status !== "NO_DEMAND" && e.unassignedQualifiedCount > 0 && /* @__PURE__ */ m("p", {
|
|
245
|
+
className: "text-xs text-muted-foreground",
|
|
246
|
+
children: t("planmagnet.skills.coverage.unassignedQualified", "{{count}} of {{total}} not assigned any of this work", {
|
|
247
|
+
count: e.unassignedQualifiedCount,
|
|
248
|
+
total: e.qualifiedMemberCount
|
|
249
|
+
})
|
|
250
|
+
})]
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
export { y as SkillCoverageCard };
|
|
255
|
+
|
|
256
|
+
//# sourceMappingURL=SkillCoverageCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SkillCoverageCard.js","names":[],"sources":["../../../src/planmagnet/skills/SkillCoverageCard.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Button } from '@burdenoff/fe-libs/ui';\nimport { ExternalLink, GraduationCap } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { DashboardCard } from '../dashboards/DashboardUI';\nimport { CrewFoundryProfileLink } from './SkillChips';\nimport {\n SKILL_COVERAGE,\n type SkillCoverageData,\n type SkillCoverageRow,\n type SkillDemand,\n} from './skillOperations';\nimport {\n coverageBadgeVariant,\n coverageCounts,\n coverageFilter,\n crewFoundryEmployeesUrl,\n orderCoverage,\n} from './skillTags';\n\n/** Qualified people shown per skill before \"Show all\". */\nconst MEMBERS_SHOWN = 5;\n\ntype Tr = (key: string, fallback: string, params?: Record<string, string | number>) => string;\n\n/**\n * Skills coverage (ANR-07 skill view on the project manager dashboard, ANR-28 skill utilization on the team\n * dashboard): per skill, the open work items that require it (`WorkItem.requiredSkills`), how much of that work is\n * assigned, and which members are qualified by their CrewFoundry skills. UNCOVERED rows come first.\n *\n * Honest by construction: when CrewFoundry or the workspace member list cannot be read, the service reports every\n * skill with demand as UNKNOWN (never UNCOVERED) and this card says why above the list; a truncated scan says the\n * demand figures are partial; an empty result says what \"nothing\" means here. Nothing renders as an \"all good\".\n * Utilization is counts and story points only — hours-based skill utilization waits for a per-person capacity\n * baseline in CrewFoundry (CWF-28), and the card says so. People's skills are read-only here (CrewFoundry owns\n * them); each name links to the person's CrewFoundry profile.\n *\n * Reads are gated like the dashboards' other cards: the gateway enforces `planmagnet-project:read` and a refusal\n * shows as \"You do not have access\" (DashboardCard).\n */\nexport function SkillCoverageCard({\n workspaceId,\n projectId,\n teamId,\n sprintId,\n notReadyMessage,\n}: {\n workspaceId?: string | null;\n projectId?: string | null;\n teamId?: string | null;\n sprintId?: string | null;\n /** Shown until the page's project / team is picked. */\n notReadyMessage: string;\n}) {\n const tr = useTr();\n const filter = coverageFilter({ projectId, teamId, sprintId });\n const ready = Object.keys(filter).length > 0;\n const { data, loading, error } = useQuery<SkillCoverageData>(SKILL_COVERAGE, {\n variables: { workspaceId: workspaceId ?? '', filter },\n skip: !workspaceId || !ready,\n });\n const [showNoDemand, setShowNoDemand] = useState(false);\n\n const result = data?.planmagnetSkillCoverage ?? null;\n const rows = orderCoverage(result?.skills ?? []);\n const withDemand = rows.filter((row) => row.status !== 'NO_DEMAND');\n const noDemand = rows.filter((row) => row.status === 'NO_DEMAND');\n const counts = coverageCounts(rows);\n const peopleUnreadable = !!result && (result.crewfoundryUnavailable || result.membersUnavailable);\n const shownRows = showNoDemand ? [...withDemand, ...noDemand] : withDemand;\n const title = tr('planmagnet.skills.coverage.title', 'Skills coverage');\n\n return (\n <DashboardCard\n icon={<GraduationCap className=\"h-4 w-4\" />}\n title={title}\n definition={tr(\n 'planmagnet.skills.coverage.definition',\n 'Open work items that require each skill, and the people whose CrewFoundry skills match it by name (ignoring case; no proficiency levels).'\n )}\n loading={loading && !data}\n error={error}\n empty={!ready}\n emptyMessage={notReadyMessage}\n >\n {result ? (\n <div className=\"space-y-3\" data-testid=\"skill-coverage\">\n {result.crewfoundryUnavailable && (\n <Notice testId=\"skill-coverage-crewfoundry-unavailable\">\n {tr(\n 'planmagnet.skills.coverage.crewfoundryUnavailable',\n 'CrewFoundry is temporarily unavailable, so people’s skills can’t be read right now. Skills that open work requires show as Unknown until it is back — try again shortly.'\n )}\n </Notice>\n )}\n {result.membersUnavailable && (\n <Notice testId=\"skill-coverage-members-unavailable\">\n {tr(\n 'planmagnet.skills.coverage.membersUnavailable',\n 'The workspace member list could not be read in full, so qualified people can’t be counted right now. Skills that open work requires show as Unknown.'\n )}\n </Notice>\n )}\n {result.truncated && (\n <Notice testId=\"skill-coverage-truncated\">\n {tr(\n 'planmagnet.skills.coverage.truncated',\n 'More than 5,000 open work items here require a skill: the demand figures cover the oldest 5,000 only.'\n )}\n </Notice>\n )}\n\n {rows.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\" data-testid=\"skill-coverage-empty\">\n {peopleUnreadable\n ? tr(\n 'planmagnet.skills.coverage.emptyPeopleUnreadable',\n 'No open work here requires a skill. People’s skills could not be read, so none are listed.'\n )\n : tr(\n 'planmagnet.skills.coverage.empty',\n 'No open work here requires a skill, and no one in scope lists a skill in CrewFoundry. Add required skills on a task’s edit page to see coverage.'\n )}\n </p>\n ) : (\n <>\n <p className=\"text-xs text-muted-foreground\" data-testid=\"skill-coverage-summary\">\n {tr(\n 'planmagnet.skills.coverage.summary',\n '{{uncovered}} uncovered · {{unknown}} unknown · {{covered}} covered · {{noDemand}} with no open work',\n {\n uncovered: counts.UNCOVERED,\n unknown: counts.UNKNOWN,\n covered: counts.COVERED,\n noDemand: counts.NO_DEMAND,\n }\n )}\n </p>\n {withDemand.length === 0 && (\n <p className=\"text-sm text-muted-foreground\" data-testid=\"skill-coverage-no-demand\">\n {tr(\n 'planmagnet.skills.coverage.noDemand',\n 'No open work here requires a skill yet. Add required skills on a task’s edit page to see coverage.'\n )}\n </p>\n )}\n {shownRows.length > 0 && (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\" data-testid=\"skill-coverage-table\">\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {tr('planmagnet.skills.coverage.col.skill', 'Skill')}\n </th>\n <th className=\"py-1 pr-2 font-normal\">\n {tr('planmagnet.skills.coverage.col.status', 'Status')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {tr('planmagnet.skills.coverage.col.open', 'Open work')}\n </th>\n <th className=\"py-1 pr-2 text-right font-normal\">\n {tr('planmagnet.skills.coverage.col.assigned', 'Assigned')}\n </th>\n <th className=\"py-1 font-normal\">\n {tr('planmagnet.skills.coverage.col.people', 'Qualified people')}\n </th>\n </tr>\n </thead>\n <tbody>\n {shownRows.map((row) => (\n <CoverageRow key={row.skill} row={row} tr={tr} />\n ))}\n </tbody>\n </table>\n </div>\n )}\n {noDemand.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-expanded={showNoDemand}\n onClick={() => setShowNoDemand((v) => !v)}\n data-testid=\"skill-coverage-toggle-no-demand\"\n >\n {showNoDemand\n ? tr(\n 'planmagnet.skills.coverage.hideNoDemand',\n 'Hide skills no open work requires'\n )\n : noDemand.length === 1\n ? tr(\n 'planmagnet.skills.coverage.showNoDemand.one',\n 'Show 1 skill people have that no open work requires'\n )\n : tr(\n 'planmagnet.skills.coverage.showNoDemand.other',\n 'Show {{count}} skills people have that no open work requires',\n { count: noDemand.length }\n )}\n </Button>\n )}\n </>\n )}\n\n <div className=\"space-y-1 border-t pt-2 text-xs text-muted-foreground\">\n <p data-testid=\"skill-coverage-utilization-note\">\n {tr(\n 'planmagnet.skills.coverage.utilizationNote',\n 'Counts and story points only (an item with no estimate counts as 0 points). Hours-based skill utilization is not available yet: it needs per-person capacity in CrewFoundry.'\n )}\n </p>\n <p>\n {tr(\n 'planmagnet.skills.coverage.coveredNote',\n 'Covered means at least one qualified person exists, not that they have time for the work.'\n )}\n </p>\n <p className=\"flex flex-wrap items-center gap-1\">\n {tr(\n 'planmagnet.skills.coverage.crewfoundryNote',\n 'People’s skills come from CrewFoundry and are read-only here.'\n )}\n <a\n href={crewFoundryEmployeesUrl(\n typeof window === 'undefined' ? '' : window.location.hostname\n )}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline\"\n >\n {tr('planmagnet.skills.coverage.openCrewfoundry', 'Edit them in CrewFoundry')}\n <ExternalLink className=\"h-3 w-3\" aria-hidden={true} />\n </a>\n </p>\n </div>\n </div>\n ) : null}\n </DashboardCard>\n );\n}\n\nfunction Notice({ testId, children }: { testId: string; children: ReactNode }) {\n return (\n <p\n role=\"status\"\n data-testid={testId}\n className=\"rounded-md border border-status-warning-border bg-status-warning-subtle p-2 text-xs text-status-warning-text\"\n >\n {children}\n </p>\n );\n}\n\nfunction statusLabel(status: string, tr: Tr): string {\n switch (status) {\n case 'UNCOVERED':\n return tr('planmagnet.skills.coverage.status.uncovered', 'Uncovered');\n case 'UNKNOWN':\n return tr('planmagnet.skills.coverage.status.unknown', 'Unknown');\n case 'COVERED':\n return tr('planmagnet.skills.coverage.status.covered', 'Covered');\n case 'NO_DEMAND':\n return tr('planmagnet.skills.coverage.status.noDemand', 'No open work');\n default:\n return status;\n }\n}\n\nfunction statusHelp(status: string, tr: Tr): string | undefined {\n switch (status) {\n case 'UNCOVERED':\n return tr(\n 'planmagnet.skills.coverage.statusHelp.uncovered',\n 'Open work requires this skill and no qualified person has it.'\n );\n case 'UNKNOWN':\n return tr(\n 'planmagnet.skills.coverage.statusHelp.unknown',\n 'Open work requires this skill, but qualified people can’t be counted right now.'\n );\n case 'COVERED':\n return tr(\n 'planmagnet.skills.coverage.statusHelp.covered',\n 'Open work requires this skill and at least one qualified person has it.'\n );\n case 'NO_DEMAND':\n return tr(\n 'planmagnet.skills.coverage.statusHelp.noDemand',\n 'A qualified person has this skill; no open work here requires it.'\n );\n default:\n return undefined;\n }\n}\n\nfunction itemsText(count: number, tr: Tr): string {\n return count === 1\n ? tr('planmagnet.skills.coverage.items.one', '1 item')\n : tr('planmagnet.skills.coverage.items.other', '{{count}} items', { count });\n}\n\nfunction demandText(demand: SkillDemand, tr: Tr): string {\n return tr('planmagnet.skills.coverage.demand', '{{items}} · {{points}} pts', {\n items: itemsText(demand.workItemCount, tr),\n points: demand.storyPoints,\n });\n}\n\nfunction CoverageRow({ row, tr }: { row: SkillCoverageRow; tr: Tr }) {\n const noDemand = row.status === 'NO_DEMAND';\n return (\n <tr className=\"border-t align-top\" data-testid=\"skill-coverage-row\" data-status={row.status}>\n <td className=\"py-1.5 pr-2 font-medium\">{row.skill}</td>\n <td className=\"py-1.5 pr-2\">\n <Badge variant={coverageBadgeVariant(row.status)} title={statusHelp(row.status, tr)}>\n {statusLabel(row.status, tr)}\n </Badge>\n </td>\n <td className=\"whitespace-nowrap py-1.5 pr-2 text-right tabular-nums\">\n {noDemand ? '—' : demandText(row.openDemand, tr)}\n </td>\n <td className=\"whitespace-nowrap py-1.5 pr-2 text-right tabular-nums\">\n {noDemand ? '—' : demandText(row.assignedDemand, tr)}\n </td>\n <td className=\"py-1.5\">\n <QualifiedPeople row={row} tr={tr} />\n </td>\n </tr>\n );\n}\n\nfunction QualifiedPeople({ row, tr }: { row: SkillCoverageRow; tr: Tr }) {\n const [showAll, setShowAll] = useState(false);\n if (row.status === 'UNKNOWN') {\n return (\n <span className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.skills.coverage.peopleUnknown', 'Can’t be counted right now')}\n </span>\n );\n }\n if (row.qualifiedMembers.length === 0) {\n return (\n <span className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.skills.coverage.peopleNone', 'No one qualified')}\n </span>\n );\n }\n const members = showAll ? row.qualifiedMembers : row.qualifiedMembers.slice(0, MEMBERS_SHOWN);\n const hidden = row.qualifiedMembers.length - members.length;\n return (\n <div className=\"space-y-1\">\n <ul className=\"flex flex-wrap gap-x-3 gap-y-1\">\n {members.map((member) => (\n <li key={member.employeeId} className=\"text-xs\">\n <CrewFoundryProfileLink employeeId={member.employeeId} name={member.employeeName}>\n {member.employeeName}\n </CrewFoundryProfileLink>\n {row.status !== 'NO_DEMAND' && (\n <span className=\"ml-1 text-muted-foreground\">\n {tr('planmagnet.skills.coverage.memberAssigned', '({{count}} assigned)', {\n count: member.assignedDemandCount,\n })}\n </span>\n )}\n </li>\n ))}\n {hidden > 0 && (\n <li>\n <button\n type=\"button\"\n className=\"text-xs text-primary underline-offset-2 hover:underline\"\n onClick={() => setShowAll(true)}\n >\n {tr('planmagnet.skills.coverage.showAllPeople', 'Show all {{count}}', {\n count: row.qualifiedMembers.length,\n })}\n </button>\n </li>\n )}\n </ul>\n {row.status !== 'NO_DEMAND' && row.unassignedQualifiedCount > 0 && (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.skills.coverage.unassignedQualified',\n '{{count}} of {{total}} not assigned any of this work',\n { count: row.unassignedQualifiedCount, total: row.qualifiedMemberCount }\n )}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Required skills on a work item's page (ANR-07, ANR-28): read-only chips — they are edited on the task's edit page.
|
|
3
|
+
* An EPIC also shows its aggregated skills: its own plus those of its live child items, computed by the service on
|
|
4
|
+
* every read (`aggregatedRequiredSkills`), so they are never edited here. Its own query: a failure says so in this
|
|
5
|
+
* block and never blanks the task page.
|
|
6
|
+
*/
|
|
7
|
+
export declare function WorkItemRequiredSkills({ workItemId }: {
|
|
8
|
+
workItemId: string;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=WorkItemRequiredSkills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkItemRequiredSkills.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/skills/WorkItemRequiredSkills.tsx"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,kDAoF5E"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { SkillChipList as t } from "./SkillChips.js";
|
|
3
|
+
import { WORK_ITEM_REQUIRED_SKILLS as n } from "./skillOperations.js";
|
|
4
|
+
import { useQuery as r } from "@apollo/client/react";
|
|
5
|
+
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
6
|
+
//#region src/planmagnet/skills/WorkItemRequiredSkills.tsx
|
|
7
|
+
function s({ workItemId: s }) {
|
|
8
|
+
let c = e(), { data: l, loading: u, error: d } = r(n, {
|
|
9
|
+
variables: { id: s },
|
|
10
|
+
skip: !s
|
|
11
|
+
}), f = l?.planmagnetWorkItem ?? null, p;
|
|
12
|
+
if (d && !f) p = /* @__PURE__ */ a("p", {
|
|
13
|
+
className: "text-xs text-muted-foreground",
|
|
14
|
+
children: c("planmagnet.skills.detail.loadError", "Required skills could not be loaded: {{message}}", { message: d.message })
|
|
15
|
+
});
|
|
16
|
+
else if (u && !l) p = /* @__PURE__ */ a("p", {
|
|
17
|
+
className: "text-xs text-muted-foreground",
|
|
18
|
+
children: c("planmagnet.skills.detail.loading", "Loading…")
|
|
19
|
+
});
|
|
20
|
+
else if (f) {
|
|
21
|
+
let e = f.type === "EPIC";
|
|
22
|
+
p = /* @__PURE__ */ o(i, { children: [f.requiredSkills.length > 0 ? /* @__PURE__ */ a(t, {
|
|
23
|
+
skills: f.requiredSkills,
|
|
24
|
+
label: c("planmagnet.skills.detail.label", "Required skills"),
|
|
25
|
+
testId: "work-item-required-skills-own"
|
|
26
|
+
}) : /* @__PURE__ */ a("p", {
|
|
27
|
+
className: "text-xs text-muted-foreground",
|
|
28
|
+
children: e ? c("planmagnet.skills.detail.noneOnEpic", "None on the epic itself.") : c("planmagnet.skills.detail.none", "None.")
|
|
29
|
+
}), e && /* @__PURE__ */ o("div", {
|
|
30
|
+
className: "space-y-1 pt-1",
|
|
31
|
+
"data-testid": "work-item-required-skills-aggregated",
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ a("span", {
|
|
34
|
+
className: "text-xs text-muted-foreground",
|
|
35
|
+
children: c("planmagnet.skills.detail.aggregatedLabel", "Across the epic")
|
|
36
|
+
}),
|
|
37
|
+
f.aggregatedRequiredSkills.length > 0 ? /* @__PURE__ */ a(t, {
|
|
38
|
+
skills: f.aggregatedRequiredSkills,
|
|
39
|
+
label: c("planmagnet.skills.detail.aggregatedLabel", "Across the epic")
|
|
40
|
+
}) : /* @__PURE__ */ a("p", {
|
|
41
|
+
className: "text-xs text-muted-foreground",
|
|
42
|
+
children: c("planmagnet.skills.detail.aggregatedNone", "Neither the epic nor its child items require a skill.")
|
|
43
|
+
}),
|
|
44
|
+
/* @__PURE__ */ a("p", {
|
|
45
|
+
className: "text-xs text-muted-foreground",
|
|
46
|
+
children: c("planmagnet.skills.detail.aggregatedNote", "The epic’s own skills plus those of its child items. Read-only: change them on each item.")
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
})] });
|
|
50
|
+
} else return null;
|
|
51
|
+
return /* @__PURE__ */ o("div", {
|
|
52
|
+
"data-testid": "work-item-required-skills",
|
|
53
|
+
children: [/* @__PURE__ */ a("span", {
|
|
54
|
+
className: "text-muted-foreground text-xs",
|
|
55
|
+
children: c("planmagnet.skills.detail.label", "Required skills")
|
|
56
|
+
}), /* @__PURE__ */ a("div", {
|
|
57
|
+
className: "mt-1 space-y-1",
|
|
58
|
+
children: p
|
|
59
|
+
})]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { s as WorkItemRequiredSkills };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=WorkItemRequiredSkills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkItemRequiredSkills.js","names":[],"sources":["../../../src/planmagnet/skills/WorkItemRequiredSkills.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { SkillChipList } from './SkillChips';\nimport { WORK_ITEM_REQUIRED_SKILLS, type WorkItemRequiredSkillsData } from './skillOperations';\n\n/**\n * Required skills on a work item's page (ANR-07, ANR-28): read-only chips — they are edited on the task's edit page.\n * An EPIC also shows its aggregated skills: its own plus those of its live child items, computed by the service on\n * every read (`aggregatedRequiredSkills`), so they are never edited here. Its own query: a failure says so in this\n * block and never blanks the task page.\n */\nexport function WorkItemRequiredSkills({ workItemId }: { workItemId: string }) {\n const tr = useTr();\n const { data, loading, error } = useQuery<WorkItemRequiredSkillsData>(WORK_ITEM_REQUIRED_SKILLS, {\n variables: { id: workItemId },\n skip: !workItemId,\n });\n const item = data?.planmagnetWorkItem ?? null;\n\n let body: ReactNode;\n if (error && !item) {\n body = (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.skills.detail.loadError',\n 'Required skills could not be loaded: {{message}}',\n {\n message: error.message,\n }\n )}\n </p>\n );\n } else if (loading && !data) {\n body = (\n <p className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.skills.detail.loading', 'Loading…')}\n </p>\n );\n } else if (item) {\n const isEpic = item.type === 'EPIC';\n body = (\n <>\n {item.requiredSkills.length > 0 ? (\n <SkillChipList\n skills={item.requiredSkills}\n label={tr('planmagnet.skills.detail.label', 'Required skills')}\n testId=\"work-item-required-skills-own\"\n />\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {isEpic\n ? tr('planmagnet.skills.detail.noneOnEpic', 'None on the epic itself.')\n : tr('planmagnet.skills.detail.none', 'None.')}\n </p>\n )}\n {isEpic && (\n <div className=\"space-y-1 pt-1\" data-testid=\"work-item-required-skills-aggregated\">\n <span className=\"text-xs text-muted-foreground\">\n {tr('planmagnet.skills.detail.aggregatedLabel', 'Across the epic')}\n </span>\n {item.aggregatedRequiredSkills.length > 0 ? (\n <SkillChipList\n skills={item.aggregatedRequiredSkills}\n label={tr('planmagnet.skills.detail.aggregatedLabel', 'Across the epic')}\n />\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.skills.detail.aggregatedNone',\n 'Neither the epic nor its child items require a skill.'\n )}\n </p>\n )}\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'planmagnet.skills.detail.aggregatedNote',\n 'The epic’s own skills plus those of its child items. Read-only: change them on each item.'\n )}\n </p>\n </div>\n )}\n </>\n );\n } else {\n return null;\n }\n\n return (\n <div data-testid=\"work-item-required-skills\">\n <span className=\"text-muted-foreground text-xs\">\n {tr('planmagnet.skills.detail.label', 'Required skills')}\n </span>\n <div className=\"mt-1 space-y-1\">{body}</div>\n </div>\n );\n}\n"],"mappings":";;;;;;AAYA,SAAgB,EAAuB,EAAE,iBAAsC;CAC7E,IAAM,IAAK,GAAO,EACZ,EAAE,SAAM,YAAS,aAAU,EAAqC,GAA2B;EAC/F,WAAW,EAAE,IAAI,GAAY;EAC7B,MAAM,CAAC;EACR,CAAC,EACI,IAAO,GAAM,sBAAsB,MAErC;AACJ,KAAI,KAAS,CAAC,EACZ,KACE,kBAAC,KAAD;EAAG,WAAU;YACV,EACC,sCACA,oDACA,EACE,SAAS,EAAM,SAChB,CACF;EACC,CAAA;UAEG,KAAW,CAAC,EACrB,KACE,kBAAC,KAAD;EAAG,WAAU;YACV,EAAG,oCAAoC,WAAW;EACjD,CAAA;UAEG,GAAM;EACf,IAAM,IAAS,EAAK,SAAS;AAC7B,MACE,kBAAA,GAAA,EAAA,UAAA,CACG,EAAK,eAAe,SAAS,IAC5B,kBAAC,GAAD;GACE,QAAQ,EAAK;GACb,OAAO,EAAG,kCAAkC,kBAAkB;GAC9D,QAAO;GACP,CAAA,GAEF,kBAAC,KAAD;GAAG,WAAU;aACV,IACG,EAAG,uCAAuC,2BAA2B,GACrE,EAAG,iCAAiC,QAAQ;GAC9C,CAAA,EAEL,KACC,kBAAC,OAAD;GAAK,WAAU;GAAiB,eAAY;aAA5C;IACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAG,4CAA4C,kBAAkB;KAC7D,CAAA;IACN,EAAK,yBAAyB,SAAS,IACtC,kBAAC,GAAD;KACE,QAAQ,EAAK;KACb,OAAO,EAAG,4CAA4C,kBAAkB;KACxE,CAAA,GAEF,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,2CACA,wDACD;KACC,CAAA;IAEN,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,2CACA,4FACD;KACC,CAAA;IACA;KAEP,EAAA,CAAA;OAGL,QAAO;AAGT,QACE,kBAAC,OAAD;EAAK,eAAY;YAAjB,CACE,kBAAC,QAAD;GAAM,WAAU;aACb,EAAG,kCAAkC,kBAAkB;GACnD,CAAA,EACP,kBAAC,OAAD;GAAK,WAAU;aAAkB;GAAW,CAAA,CACxC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANR-07 / ANR-28 GraphQL documents: a work item's required skills, and skill coverage. Every document here has a
|
|
3
|
+
* twin in src/operations/wspace/planmagnet/queries/skills.graphql, which `bun run codegen` validates against the
|
|
4
|
+
* planmagnet schema and CI's live-supergraph drift job validates against what production serves;
|
|
5
|
+
* tests/skillOperations.test.ts fails when the two copies differ.
|
|
6
|
+
*
|
|
7
|
+
* Each section that reads a skill field has a query of its own rather than a field added to an existing page query
|
|
8
|
+
* (the PRM-01 rule): a field the gateway does not serve fails the WHOLE query, so a shared query would blank the
|
|
9
|
+
* task page instead of one block. The writes need no document of their own — `requiredSkills` rides in the input
|
|
10
|
+
* of the create / update / bulk-update mutations the forms already send.
|
|
11
|
+
*/
|
|
12
|
+
/** A work item's own required skills, and for an EPIC the union with its live children's (read-only). */
|
|
13
|
+
export declare const WORK_ITEM_REQUIRED_SKILLS: import('graphql').DocumentNode;
|
|
14
|
+
/** Per skill: open demand, how much of it is assigned, and who is qualified (CrewFoundry skills, read-only). */
|
|
15
|
+
export declare const SKILL_COVERAGE: import('graphql').DocumentNode;
|
|
16
|
+
export interface WorkItemRequiredSkillsData {
|
|
17
|
+
planmagnetWorkItem: {
|
|
18
|
+
id: string;
|
|
19
|
+
type: string;
|
|
20
|
+
requiredSkills: string[];
|
|
21
|
+
aggregatedRequiredSkills: string[];
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
export interface SkillDemand {
|
|
25
|
+
workItemCount: number;
|
|
26
|
+
storyPoints: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SkillQualifiedMember {
|
|
29
|
+
actorId: string;
|
|
30
|
+
employeeId: string;
|
|
31
|
+
employeeName: string;
|
|
32
|
+
assignedDemandCount: number;
|
|
33
|
+
}
|
|
34
|
+
export interface SkillCoverageRow {
|
|
35
|
+
skill: string;
|
|
36
|
+
/** UNCOVERED | UNKNOWN | COVERED | NO_DEMAND — kept a string so a value added later still renders. */
|
|
37
|
+
status: string;
|
|
38
|
+
openDemand: SkillDemand;
|
|
39
|
+
assignedDemand: SkillDemand;
|
|
40
|
+
qualifiedMemberCount: number;
|
|
41
|
+
unassignedQualifiedCount: number;
|
|
42
|
+
qualifiedMembers: SkillQualifiedMember[];
|
|
43
|
+
}
|
|
44
|
+
export interface SkillCoverageData {
|
|
45
|
+
planmagnetSkillCoverage: {
|
|
46
|
+
crewfoundryUnavailable: boolean;
|
|
47
|
+
membersUnavailable: boolean;
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
skills: SkillCoverageRow[];
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=skillOperations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillOperations.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/skills/skillOperations.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,yGAAyG;AACzG,eAAO,MAAM,yBAAyB,gCASrC,CAAC;AAEF,gHAAgH;AAChH,eAAO,MAAM,cAAc,gCA4B1B,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,kBAAkB,EAAE;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,wBAAwB,EAAE,MAAM,EAAE,CAAC;KACpC,GAAG,IAAI,CAAC;CACV;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,WAAW,CAAC;IACxB,cAAc,EAAE,WAAW,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wBAAwB,EAAE,MAAM,CAAC;IACjC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAChC,uBAAuB,EAAE;QACvB,sBAAsB,EAAE,OAAO,CAAC;QAChC,kBAAkB,EAAE,OAAO,CAAC;QAC5B,SAAS,EAAE,OAAO,CAAC;QACnB,MAAM,EAAE,gBAAgB,EAAE,CAAC;KAC5B,CAAC;CACH"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { gql as e } from "@apollo/client";
|
|
2
|
+
//#region src/planmagnet/skills/skillOperations.ts
|
|
3
|
+
var t = e`
|
|
4
|
+
query WorkItemRequiredSkills($id: ID!) {
|
|
5
|
+
planmagnetWorkItem(id: $id) {
|
|
6
|
+
id
|
|
7
|
+
type
|
|
8
|
+
requiredSkills
|
|
9
|
+
aggregatedRequiredSkills
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
`, n = e`
|
|
13
|
+
query SkillCoverage($workspaceId: ID!, $filter: PlanmagnetSkillCoverageFilter) {
|
|
14
|
+
planmagnetSkillCoverage(workspaceId: $workspaceId, filter: $filter) {
|
|
15
|
+
crewfoundryUnavailable
|
|
16
|
+
membersUnavailable
|
|
17
|
+
truncated
|
|
18
|
+
skills {
|
|
19
|
+
skill
|
|
20
|
+
status
|
|
21
|
+
openDemand {
|
|
22
|
+
workItemCount
|
|
23
|
+
storyPoints
|
|
24
|
+
}
|
|
25
|
+
assignedDemand {
|
|
26
|
+
workItemCount
|
|
27
|
+
storyPoints
|
|
28
|
+
}
|
|
29
|
+
qualifiedMemberCount
|
|
30
|
+
unassignedQualifiedCount
|
|
31
|
+
qualifiedMembers {
|
|
32
|
+
actorId
|
|
33
|
+
employeeId
|
|
34
|
+
employeeName
|
|
35
|
+
assignedDemandCount
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { n as SKILL_COVERAGE, t as WORK_ITEM_REQUIRED_SKILLS };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=skillOperations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillOperations.js","names":[],"sources":["../../../src/planmagnet/skills/skillOperations.ts"],"sourcesContent":["import { gql } from '@apollo/client';\n\n/**\n * ANR-07 / ANR-28 GraphQL documents: a work item's required skills, and skill coverage. Every document here has a\n * twin in src/operations/wspace/planmagnet/queries/skills.graphql, which `bun run codegen` validates against the\n * planmagnet schema and CI's live-supergraph drift job validates against what production serves;\n * tests/skillOperations.test.ts fails when the two copies differ.\n *\n * Each section that reads a skill field has a query of its own rather than a field added to an existing page query\n * (the PRM-01 rule): a field the gateway does not serve fails the WHOLE query, so a shared query would blank the\n * task page instead of one block. The writes need no document of their own — `requiredSkills` rides in the input\n * of the create / update / bulk-update mutations the forms already send.\n */\n\n/** A work item's own required skills, and for an EPIC the union with its live children's (read-only). */\nexport const WORK_ITEM_REQUIRED_SKILLS = gql`\n query WorkItemRequiredSkills($id: ID!) {\n planmagnetWorkItem(id: $id) {\n id\n type\n requiredSkills\n aggregatedRequiredSkills\n }\n }\n`;\n\n/** Per skill: open demand, how much of it is assigned, and who is qualified (CrewFoundry skills, read-only). */\nexport const SKILL_COVERAGE = gql`\n query SkillCoverage($workspaceId: ID!, $filter: PlanmagnetSkillCoverageFilter) {\n planmagnetSkillCoverage(workspaceId: $workspaceId, filter: $filter) {\n crewfoundryUnavailable\n membersUnavailable\n truncated\n skills {\n skill\n status\n openDemand {\n workItemCount\n storyPoints\n }\n assignedDemand {\n workItemCount\n storyPoints\n }\n qualifiedMemberCount\n unassignedQualifiedCount\n qualifiedMembers {\n actorId\n employeeId\n employeeName\n assignedDemandCount\n }\n }\n }\n }\n`;\n\nexport interface WorkItemRequiredSkillsData {\n planmagnetWorkItem: {\n id: string;\n type: string;\n requiredSkills: string[];\n aggregatedRequiredSkills: string[];\n } | null;\n}\n\nexport interface SkillDemand {\n workItemCount: number;\n storyPoints: number;\n}\n\nexport interface SkillQualifiedMember {\n actorId: string;\n employeeId: string;\n employeeName: string;\n assignedDemandCount: number;\n}\n\nexport interface SkillCoverageRow {\n skill: string;\n /** UNCOVERED | UNKNOWN | COVERED | NO_DEMAND — kept a string so a value added later still renders. */\n status: string;\n openDemand: SkillDemand;\n assignedDemand: SkillDemand;\n qualifiedMemberCount: number;\n unassignedQualifiedCount: number;\n qualifiedMembers: SkillQualifiedMember[];\n}\n\nexport interface SkillCoverageData {\n planmagnetSkillCoverage: {\n crewfoundryUnavailable: boolean;\n membersUnavailable: boolean;\n truncated: boolean;\n skills: SkillCoverageRow[];\n };\n}\n"],"mappings":";;AAeA,IAAa,IAA4B,CAAG;;;;;;;;;GAY/B,IAAiB,CAAG"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill tags (ANR-07 skill view, ANR-28 skill utilization). PlanMagnet owns the skills a WORK ITEM requires
|
|
3
|
+
* (`WorkItem.requiredSkills`); CrewFoundry owns PEOPLE's skills, which PlanMagnet only reads (owner decision
|
|
4
|
+
* 2026-10-02). The two meet by exact tag matching on a key: the tag trimmed, whitespace runs collapsed, lower-cased.
|
|
5
|
+
* No proficiency levels, no synonyms.
|
|
6
|
+
*
|
|
7
|
+
* The service is the authority on the write rules (wspace-planmagnet-svc `src/utils/required-skills.ts`: tidy,
|
|
8
|
+
* case-insensitive duplicates dropped keeping the first spelling, at most 20 tags of at most 64 characters, an empty
|
|
9
|
+
* or control-character tag refused). The editor applies the same rules as the user types, so the common mistakes
|
|
10
|
+
* are explained before anything is sent; a refusal that still comes back is shown on the field
|
|
11
|
+
* (`requiredSkillsRefusal`).
|
|
12
|
+
*/
|
|
13
|
+
/** The most skills one work item may require (the service's MAX_REQUIRED_SKILLS). */
|
|
14
|
+
export declare const MAX_REQUIRED_SKILLS = 20;
|
|
15
|
+
/** The longest one skill may be, in characters (Unicode code points) after tidying (the service's MAX_SKILL_LENGTH). */
|
|
16
|
+
export declare const MAX_SKILL_LENGTH = 64;
|
|
17
|
+
/** Trim and collapse every run of whitespace to one space: the stored and displayed form of a tag. */
|
|
18
|
+
export declare function tidySkill(value: string): string;
|
|
19
|
+
/** The matching key of a tag. Two tags are the same skill exactly when their keys are equal. */
|
|
20
|
+
export declare function skillKey(value: string): string;
|
|
21
|
+
/** Why something the user typed was not added to the list. */
|
|
22
|
+
export type SkillTagProblem = {
|
|
23
|
+
kind: 'duplicate';
|
|
24
|
+
skill: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'tooLong';
|
|
27
|
+
max: number;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'tooMany';
|
|
30
|
+
max: number;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'invalid';
|
|
33
|
+
};
|
|
34
|
+
export interface AddSkillTagsResult {
|
|
35
|
+
/** The list with every acceptable new tag appended, in the order typed. */
|
|
36
|
+
skills: string[];
|
|
37
|
+
/** What could not be added, to leave in the input: a refusal never silently drops what was typed. */
|
|
38
|
+
rest: string;
|
|
39
|
+
/** The first reason something was not added, or null when everything typed was added. */
|
|
40
|
+
problem: SkillTagProblem | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Add what the user typed — one skill, or several separated by commas or line breaks — to `current`. Each tag is
|
|
44
|
+
* tidied; blank parts are ignored; a tag already on the list (ignoring case) is not added twice, and the list keeps
|
|
45
|
+
* its first spelling, as the service does. A tag that is too long, has a control character, or would go past
|
|
46
|
+
* MAX_REQUIRED_SKILLS stays in `rest` with the reason in `problem`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function addSkillTags(current: readonly string[], typed: string): AddSkillTagsResult;
|
|
49
|
+
/** `current` without the tag at `index`. */
|
|
50
|
+
export declare function removeSkillAt(current: readonly string[], index: number): string[];
|
|
51
|
+
/** Same tags in the same order and spelling: what "unchanged" means for a save that sends only changed fields. */
|
|
52
|
+
export declare function sameSkillList(a: readonly string[], b: readonly string[]): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The service's own reason when a write was refused because of `requiredSkills` (BAD_USER_INPUT tagged
|
|
55
|
+
* `extensions.field: requiredSkills`, e.g. "A work item can require at most 20 skills"), or null for any other
|
|
56
|
+
* failure — which the caller shows the way it shows every other refusal.
|
|
57
|
+
*/
|
|
58
|
+
export declare function requiredSkillsRefusal(error: unknown): string | null;
|
|
59
|
+
export type SkillCoverageStatus = 'UNCOVERED' | 'UNKNOWN' | 'COVERED' | 'NO_DEMAND';
|
|
60
|
+
/**
|
|
61
|
+
* Rows in status order (UNCOVERED first), keeping the service's order within a status (most open work first, then
|
|
62
|
+
* by skill). The service already sends them this way; sorting here keeps "uncovered first" true on screen even if
|
|
63
|
+
* that changes. A status this client does not know yet goes last rather than disappearing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function orderCoverage<T extends {
|
|
66
|
+
status: string;
|
|
67
|
+
}>(rows: readonly T[]): T[];
|
|
68
|
+
/** How many rows have each status. */
|
|
69
|
+
export declare function coverageCounts(rows: readonly {
|
|
70
|
+
status: string;
|
|
71
|
+
}[]): Record<SkillCoverageStatus, number>;
|
|
72
|
+
export type CoverageBadgeVariant = 'destructive' | 'warning' | 'success' | 'secondary' | 'outline';
|
|
73
|
+
/**
|
|
74
|
+
* Severity, not identity: uncovered demand is the problem, unknown needs a look, covered is fine, no demand is
|
|
75
|
+
* neutral.
|
|
76
|
+
*/
|
|
77
|
+
export declare function coverageBadgeVariant(status: string): CoverageBadgeVariant;
|
|
78
|
+
/**
|
|
79
|
+
* The filter for planmagnetSkillCoverage with only the ids actually chosen: an empty id is left out (the service
|
|
80
|
+
* answers NOT_FOUND for an id that is not a live project / team / sprint of the workspace).
|
|
81
|
+
*/
|
|
82
|
+
export declare function coverageFilter(filter: {
|
|
83
|
+
projectId?: string | null;
|
|
84
|
+
teamId?: string | null;
|
|
85
|
+
sprintId?: string | null;
|
|
86
|
+
}): {
|
|
87
|
+
projectId?: string;
|
|
88
|
+
teamId?: string;
|
|
89
|
+
sprintId?: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* CrewFoundry's app in the same environment as this page: alpha shells (`alphaapp.*`) link to alpha, the local dev
|
|
93
|
+
* proxy (`*.local.burdenoff.com`, localhost) to the local CrewFoundry shell, everything else to production.
|
|
94
|
+
*/
|
|
95
|
+
export declare function crewFoundryOrigin(hostname: string): string;
|
|
96
|
+
/**
|
|
97
|
+
* A CrewFoundry employee's profile, where their skills are edited (CrewFoundry mounts its pages under
|
|
98
|
+
* /crewfoundry).
|
|
99
|
+
*/
|
|
100
|
+
export declare function crewFoundryEmployeeUrl(employeeId: string, hostname: string): string;
|
|
101
|
+
/** CrewFoundry's employee list. */
|
|
102
|
+
export declare function crewFoundryEmployeesUrl(hostname: string): string;
|
|
103
|
+
//# sourceMappingURL=skillTags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillTags.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/skills/skillTags.ts"],"names":[],"mappings":"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"}
|