@burdenoff/microfe-planmagnet 2026.1001.4 → 2026.1001.6
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/planmagnet/controls/CompetitorControls.d.ts +71 -0
- package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/CompetitorControls.js +346 -12
- package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts +5 -0
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +363 -218
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/Pickers.d.ts +15 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +61 -1
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
- package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
- package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
- package/dist/planmagnet/dashboards/DashboardUI.js +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts.map +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js +1 -1
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +289 -282
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +162 -122
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +135 -109
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../../src/planmagnet/pages/analytics/MetricsCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Activity, Info } from 'lucide-react';\nimport { formatDuration, StatTile } from './DeliveryHealthCards';\n\n/**\n * ANR-18 / ANR-19: the generic Metric/Analytics framework. `planmagnetMetrics` (workspace-wide) and\n * `planmagnetAnalytics.projectMetrics`/`.teamMetrics` (one project) are both a THIN composition over the\n * narrow analytics queries the rest of this page already renders (velocity, cycle time, throughput,\n * cumulative flow, delivery KPIs) into one generic `{category, value, unit, trend, target}` row shape — never\n * a second computation engine. Both are TTL-cached (60s) server-side (see analytics.gql's own doc comment),\n * so a reading here can be up to a minute stale — said plainly below rather than implied as live.\n * `trend`/`target` are only ever rendered when the server actually sets them: no metric sets them today (see\n * PlanmagnetMetric's own field doc), so they never show as a fabricated value.\n */\n\nconst GET_WORKSPACE_METRICS = gql`\n query GetWorkspaceMetrics($workspaceId: ID!, $timeRange: PlanmagnetTimeRangeInput) {\n planmagnetMetrics(workspaceId: $workspaceId, timeRange: $timeRange) {\n category\n value\n unit\n trend\n target\n calculatedAt\n }\n }\n`;\n\nconst GET_PROJECT_ANALYTICS_METRICS = gql`\n query GetProjectAnalyticsMetrics(\n $workspaceId: ID!\n $projectId: ID\n $timeRange: PlanmagnetTimeRangeInput\n ) {\n planmagnetAnalytics(workspaceId: $workspaceId, projectId: $projectId, timeRange: $timeRange) {\n projectMetrics {\n category\n value\n unit\n trend\n target\n calculatedAt\n }\n teamMetrics {\n category\n value\n unit\n trend\n target\n calculatedAt\n }\n }\n }\n`;\n\ninterface MetricRow {\n category: string;\n value: number;\n unit: string;\n trend: number | null;\n target: number | null;\n calculatedAt: string;\n}\n\ninterface WorkspaceMetricsData {\n planmagnetMetrics: MetricRow[];\n}\n\ninterface ProjectAnalyticsData {\n planmagnetAnalytics: {\n projectMetrics: MetricRow[];\n teamMetrics: MetricRow[];\n };\n}\n\n// Every category AnalyticsService.metrics()/composeProjectMetrics()/composeTeamMetrics() can actually emit\n// today (see wspace-planmagnet-svc analytics.service.ts) — kept as a lookup, not re-derived, so a category the\n// server adds later still renders (falls back to itself) instead of disappearing.\nconst METRIC_LABELS: Record<string, string> = {\n sprintOnTimeRate: 'Sprint on-time rate',\n releaseOnTimeRate: 'Release on-time rate',\n milestoneOnTimeRate: 'Milestone on-time rate',\n scopeAdherenceRate: 'Scope adherence rate',\n velocity: 'Velocity',\n throughput: 'Throughput',\n cycleTime: 'Cycle time',\n wip: 'Work in progress',\n sprintRemainingPoints: 'Sprint remaining points',\n teamVelocity: 'Team velocity',\n};\n\nfunction metricLabel(category: string): string {\n return METRIC_LABELS[category] ?? category;\n}\n\nfunction formatMetricValue(m: MetricRow): string {\n if (m.unit === '%') return `${Math.round(m.value)}%`;\n if (m.unit === 'hours') return formatDuration(m.value);\n const rounded = Math.round(m.value * 100) / 100;\n return `${rounded} ${m.unit}`;\n}\n\nfunction MetricTile({ metric }: { metric: MetricRow }) {\n const hints: string[] = [];\n // Neither is set by the server today (see the module doc above) — kept so a future source for either shows\n // up here with no UI change needed.\n if (metric.trend != null) hints.push(`Trend ${metric.trend > 0 ? '+' : ''}${metric.trend}`);\n if (metric.target != null) {\n hints.push(`Target ${metric.target}${metric.unit === '%' ? '%' : ` ${metric.unit}`}`);\n }\n return (\n <StatTile\n label={metricLabel(metric.category)}\n value={formatMetricValue(metric)}\n hint={hints.length > 0 ? hints.join(' · ') : undefined}\n />\n );\n}\n\nexport function MetricsSection({\n workspaceId,\n projectId,\n startDate,\n endDate,\n}: {\n workspaceId?: string | null;\n /** Omitted = no project selected (ANR-09 scope set to \"All projects\") — nothing project-scoped to show. */\n projectId?: string;\n startDate: string;\n endDate: string;\n}) {\n const { t } = useI18n();\n const timeRange = { startDate, endDate };\n const { data, loading, error } = useQuery<WorkspaceMetricsData>(GET_WORKSPACE_METRICS, {\n variables: { workspaceId, timeRange },\n skip: !workspaceId,\n });\n const {\n data: projectData,\n loading: projectLoading,\n error: projectError,\n } = useQuery<ProjectAnalyticsData>(GET_PROJECT_ANALYTICS_METRICS, {\n variables: { workspaceId, projectId, timeRange },\n skip: !workspaceId || !projectId,\n });\n\n const metrics = data?.planmagnetMetrics ?? [];\n const projectMetrics = projectData?.planmagnetAnalytics.projectMetrics ?? [];\n const teamMetrics = projectData?.planmagnetAnalytics.teamMetrics ?? [];\n const projectRows = [...projectMetrics, ...teamMetrics];\n\n return (\n <Card data-testid=\"metrics-card\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Activity className=\"h-4 w-4\" />\n {t('planmagnet.analytics.metrics.title', 'Metrics')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.metrics.subtitle',\n \"Workspace-wide KPIs, plus the selected project's own metrics, composed from the queries above into one generic shape.\"\n )}\n </p>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <h3 className=\"text-sm font-medium mb-1\">\n {t('planmagnet.analytics.metrics.workspaceTitle', 'Workspace')}\n </h3>\n {loading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {t('planmagnet.analytics.metrics.error', 'Metrics could not be loaded')}:{' '}\n {error.message}\n </p>\n ) : metrics.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-2\">\n {t(\n 'planmagnet.analytics.metrics.workspaceEmpty',\n 'Nothing to show in this range yet.'\n )}\n </p>\n ) : (\n <div\n className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\"\n data-testid=\"metrics-workspace-tiles\"\n >\n {metrics.map((m) => (\n <MetricTile key={m.category} metric={m} />\n ))}\n </div>\n )}\n </div>\n\n <div className=\"border-t pt-3\">\n <h3 className=\"text-sm font-medium mb-1\">\n {t('planmagnet.analytics.metrics.projectTitle', 'Selected project')}\n </h3>\n {!projectId ? (\n <p className=\"text-sm text-muted-foreground py-2\">\n {t(\n 'planmagnet.analytics.metrics.noProject',\n 'Pick a project above to see its own metrics.'\n )}\n </p>\n ) : projectLoading ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.loading', 'Loading…')}\n </p>\n ) : projectError ? (\n <p className=\"text-sm text-status-error-text\">\n {t(\n 'planmagnet.analytics.metrics.projectError',\n 'Project metrics could not be loaded'\n )}\n : {projectError.message}\n </p>\n ) : projectRows.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-2\">\n {t(\n 'planmagnet.analytics.metrics.projectEmpty',\n 'Nothing to show for this project in this range yet.'\n )}\n </p>\n ) : (\n <div\n className=\"grid grid-cols-2 gap-3 sm:grid-cols-4\"\n data-testid=\"metrics-project-tiles\"\n >\n {projectRows.map((m) => (\n <MetricTile key={m.category} metric={m} />\n ))}\n </div>\n )}\n </div>\n\n <p\n className=\"flex items-start gap-1.5 text-xs text-muted-foreground border-t pt-2\"\n data-testid=\"metrics-cache-note\"\n >\n <Info className=\"h-3.5 w-3.5 shrink-0 mt-0.5\" aria-hidden={true} />\n {t(\n 'planmagnet.analytics.metrics.cacheNote',\n 'Updated within the last minute — these metrics are cached for 60 seconds.'\n )}\n </p>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UnmatchedCrewIdentities.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/team/UnmatchedCrewIdentities.tsx"],"names":[],"mappings":"AAyKA,wBAAgB,4BAA4B,4CAqD3C"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { mutationErrorMessage as t } from "../utils/graphqlErrors.js";
|
|
3
|
+
import { LEAVES_PAGE_CONTEXT as n, throwOnError as r } from "../utils/mutationResult.js";
|
|
4
|
+
import { ErrorLine as i } from "../controls/ConfirmButton.js";
|
|
5
|
+
import { CrewEmployeeSelect as a } from "../controls/Pickers.js";
|
|
6
|
+
import { useState as o } from "react";
|
|
7
|
+
import { AlertTriangle as s } from "lucide-react";
|
|
8
|
+
import { gql as c } from "@apollo/client";
|
|
9
|
+
import { useMutation as l, useQuery as u } from "@apollo/client/react";
|
|
10
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
11
|
+
import { Badge as p, Button as m, Card as h, CardContent as g, CardHeader as _, CardTitle as v } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
//#region src/planmagnet/team/UnmatchedCrewIdentities.tsx
|
|
13
|
+
var y = c`
|
|
14
|
+
query GetUnmatchedCrewIdentities($workspaceId: ID!) {
|
|
15
|
+
planmagnetUnmatchedCrewIdentities(workspaceId: $workspaceId) {
|
|
16
|
+
platformUserId
|
|
17
|
+
displayName
|
|
18
|
+
email
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`, b = c`
|
|
22
|
+
mutation SetMemberIdentityOverride(
|
|
23
|
+
$workspaceId: ID!
|
|
24
|
+
$platformUserId: String!
|
|
25
|
+
$crewEmployeeId: String!
|
|
26
|
+
) {
|
|
27
|
+
planmagnetSetMemberIdentityOverride(
|
|
28
|
+
workspaceId: $workspaceId
|
|
29
|
+
platformUserId: $platformUserId
|
|
30
|
+
crewEmployeeId: $crewEmployeeId
|
|
31
|
+
) {
|
|
32
|
+
id
|
|
33
|
+
crewEmployeeId
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
`, x = c`
|
|
37
|
+
mutation ClearMemberIdentityOverride($workspaceId: ID!, $platformUserId: String!) {
|
|
38
|
+
planmagnetClearMemberIdentityOverride(
|
|
39
|
+
workspaceId: $workspaceId
|
|
40
|
+
platformUserId: $platformUserId
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
function S({ identity: e, workspaceId: s }) {
|
|
45
|
+
let [c, u] = o(""), [h, g] = o(!1), [_, v] = o(null), [y, S] = l(b, { context: n }), [C, w] = l(x, { context: n }), T = async () => {
|
|
46
|
+
if (c) {
|
|
47
|
+
v(null);
|
|
48
|
+
try {
|
|
49
|
+
await r(y({ variables: {
|
|
50
|
+
workspaceId: s,
|
|
51
|
+
platformUserId: e.platformUserId,
|
|
52
|
+
crewEmployeeId: c
|
|
53
|
+
} })), g(!0);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
v(t(e, "The override could not be saved."));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, E = async () => {
|
|
59
|
+
v(null);
|
|
60
|
+
try {
|
|
61
|
+
await r(C({ variables: {
|
|
62
|
+
workspaceId: s,
|
|
63
|
+
platformUserId: e.platformUserId
|
|
64
|
+
} })), g(!1), u("");
|
|
65
|
+
} catch (e) {
|
|
66
|
+
v(t(e, "The override could not be cleared."));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return /* @__PURE__ */ f("li", {
|
|
70
|
+
className: "flex flex-col gap-2 border rounded-md p-3 sm:flex-row sm:items-center sm:justify-between",
|
|
71
|
+
"data-testid": "unmatched-identity-row",
|
|
72
|
+
children: [/* @__PURE__ */ f("div", {
|
|
73
|
+
className: "min-w-0",
|
|
74
|
+
children: [/* @__PURE__ */ d("p", {
|
|
75
|
+
className: "text-sm font-medium truncate",
|
|
76
|
+
children: e.displayName ?? e.platformUserId
|
|
77
|
+
}), /* @__PURE__ */ d("p", {
|
|
78
|
+
className: "text-xs text-muted-foreground truncate",
|
|
79
|
+
children: e.email ?? "No workspace e-mail on file"
|
|
80
|
+
})]
|
|
81
|
+
}), /* @__PURE__ */ f("div", {
|
|
82
|
+
className: "flex flex-col items-start gap-2 sm:items-end",
|
|
83
|
+
children: [h ? /* @__PURE__ */ f("div", {
|
|
84
|
+
className: "flex items-center gap-2",
|
|
85
|
+
children: [/* @__PURE__ */ d(p, {
|
|
86
|
+
variant: "outline",
|
|
87
|
+
className: "text-xs",
|
|
88
|
+
"data-testid": "unmatched-identity-overridden",
|
|
89
|
+
children: "Override set"
|
|
90
|
+
}), /* @__PURE__ */ d(m, {
|
|
91
|
+
size: "sm",
|
|
92
|
+
variant: "ghost",
|
|
93
|
+
disabled: w.loading,
|
|
94
|
+
"data-testid": "unmatched-identity-clear",
|
|
95
|
+
onClick: () => void E(),
|
|
96
|
+
children: w.loading ? "…" : "Clear override"
|
|
97
|
+
})]
|
|
98
|
+
}) : /* @__PURE__ */ f("div", {
|
|
99
|
+
className: "flex items-center gap-2",
|
|
100
|
+
children: [/* @__PURE__ */ d("div", {
|
|
101
|
+
className: "w-64",
|
|
102
|
+
children: /* @__PURE__ */ d(a, {
|
|
103
|
+
id: `unmatched-${e.platformUserId}`,
|
|
104
|
+
value: c,
|
|
105
|
+
onChange: u,
|
|
106
|
+
noneLabel: "Choose a CrewFoundry employee"
|
|
107
|
+
})
|
|
108
|
+
}), /* @__PURE__ */ d(m, {
|
|
109
|
+
size: "sm",
|
|
110
|
+
disabled: !c || S.loading,
|
|
111
|
+
"data-testid": "unmatched-identity-set",
|
|
112
|
+
onClick: () => void T(),
|
|
113
|
+
children: S.loading ? "Saving…" : "Set override"
|
|
114
|
+
})]
|
|
115
|
+
}), _ && /* @__PURE__ */ d(i, { message: _ })]
|
|
116
|
+
})]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function C() {
|
|
120
|
+
let { workspaceId: t } = e(), { data: n, loading: r, error: i } = u(y, {
|
|
121
|
+
variables: { workspaceId: t ?? "" },
|
|
122
|
+
skip: !t
|
|
123
|
+
}), a = n?.planmagnetUnmatchedCrewIdentities ?? [];
|
|
124
|
+
return /* @__PURE__ */ f(h, {
|
|
125
|
+
id: "unmatched-crew-identities",
|
|
126
|
+
"data-testid": "unmatched-crew-identities-panel",
|
|
127
|
+
children: [/* @__PURE__ */ f(_, { children: [/* @__PURE__ */ f(v, {
|
|
128
|
+
className: "flex items-center gap-2",
|
|
129
|
+
children: [/* @__PURE__ */ d(s, { className: "h-4 w-4 text-status-warning-text" }), "Unmatched CrewFoundry identities"]
|
|
130
|
+
}), /* @__PURE__ */ d("p", {
|
|
131
|
+
className: "text-xs text-muted-foreground",
|
|
132
|
+
children: "An override pins one workspace member directly to one CrewFoundry employee record, for when the default best-effort e-mail match can't find — or finds the wrong — employee for them."
|
|
133
|
+
})] }), /* @__PURE__ */ d(g, { children: r && !n ? /* @__PURE__ */ d("p", {
|
|
134
|
+
className: "text-sm text-muted-foreground",
|
|
135
|
+
children: "Loading…"
|
|
136
|
+
}) : i ? /* @__PURE__ */ f("p", {
|
|
137
|
+
className: "text-sm text-status-error-text",
|
|
138
|
+
children: ["Unmatched identities could not be loaded: ", i.message]
|
|
139
|
+
}) : a.length === 0 ? /* @__PURE__ */ d("p", {
|
|
140
|
+
className: "text-sm text-muted-foreground py-2",
|
|
141
|
+
"data-testid": "unmatched-identities-empty",
|
|
142
|
+
children: "Every assigned member currently matches a CrewFoundry employee — nothing to fix."
|
|
143
|
+
}) : /* @__PURE__ */ d("ul", {
|
|
144
|
+
className: "space-y-2",
|
|
145
|
+
children: a.map((e) => /* @__PURE__ */ d(S, {
|
|
146
|
+
identity: e,
|
|
147
|
+
workspaceId: t ?? ""
|
|
148
|
+
}, e.platformUserId))
|
|
149
|
+
}) })]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { C as UnmatchedCrewIdentitiesPanel };
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=UnmatchedCrewIdentities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UnmatchedCrewIdentities.js","names":[],"sources":["../../../src/planmagnet/team/UnmatchedCrewIdentities.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Badge, Button, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { AlertTriangle } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { throwOnError, LEAVES_PAGE_CONTEXT } from '../utils/mutationResult';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { CrewEmployeeSelect } from '../controls/Pickers';\n\n/**\n * CWF-30 admin remediation panel, on the workspace Members page (`/team/team-page`):\n * `planmagnetUnmatchedCrewIdentities` lists workspace members with a live work-item assignment whose identity\n * could not be resolved to a CrewFoundry employee by the default best-effort e-mail match (see\n * `planmagnetSprintCapacity`). Setting an override here pins one member directly to one CrewFoundry employee\n * record so sprint capacity can find them; clearing it returns that member to the e-mail match.\n *\n * Setting/clearing an override is deliberately published with `LEAVES_PAGE_CONTEXT` (skips fe-libs' global\n * post-mutation auto-refresh): the row that was just fixed would otherwise vanish from this list on its own\n * next refetch mid-interaction, taking the \"Clear override\" affordance with it before the admin could use it.\n * The row's own local state (not a refetch) is what shows the result of the write it just made; the row drops\n * off this list for real only the next time the page itself reloads and finds the member matched.\n */\n\nconst GET_UNMATCHED = gql`\n query GetUnmatchedCrewIdentities($workspaceId: ID!) {\n planmagnetUnmatchedCrewIdentities(workspaceId: $workspaceId) {\n platformUserId\n displayName\n email\n }\n }\n`;\n\nconst SET_OVERRIDE = gql`\n mutation SetMemberIdentityOverride(\n $workspaceId: ID!\n $platformUserId: String!\n $crewEmployeeId: String!\n ) {\n planmagnetSetMemberIdentityOverride(\n workspaceId: $workspaceId\n platformUserId: $platformUserId\n crewEmployeeId: $crewEmployeeId\n ) {\n id\n crewEmployeeId\n }\n }\n`;\n\nconst CLEAR_OVERRIDE = gql`\n mutation ClearMemberIdentityOverride($workspaceId: ID!, $platformUserId: String!) {\n planmagnetClearMemberIdentityOverride(\n workspaceId: $workspaceId\n platformUserId: $platformUserId\n )\n }\n`;\n\ninterface UnmatchedIdentity {\n platformUserId: string;\n displayName: string | null;\n email: string | null;\n}\n\nfunction UnmatchedIdentityRow({\n identity,\n workspaceId,\n}: {\n identity: UnmatchedIdentity;\n workspaceId: string;\n}) {\n const [crewEmployeeId, setCrewEmployeeId] = useState('');\n const [overridden, setOverridden] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [setOverride, setState] = useMutation(SET_OVERRIDE, { context: LEAVES_PAGE_CONTEXT });\n const [clearOverride, clearState] = useMutation(CLEAR_OVERRIDE, {\n context: LEAVES_PAGE_CONTEXT,\n });\n\n const onSet = async () => {\n if (!crewEmployeeId) return;\n setError(null);\n try {\n await throwOnError(\n setOverride({\n variables: { workspaceId, platformUserId: identity.platformUserId, crewEmployeeId },\n })\n );\n setOverridden(true);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The override could not be saved.'));\n }\n };\n\n const onClear = async () => {\n setError(null);\n try {\n await throwOnError(\n clearOverride({ variables: { workspaceId, platformUserId: identity.platformUserId } })\n );\n setOverridden(false);\n setCrewEmployeeId('');\n } catch (e) {\n setError(mutationErrorMessage(e, 'The override could not be cleared.'));\n }\n };\n\n return (\n <li\n className=\"flex flex-col gap-2 border rounded-md p-3 sm:flex-row sm:items-center sm:justify-between\"\n data-testid=\"unmatched-identity-row\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium truncate\">\n {identity.displayName ?? identity.platformUserId}\n </p>\n <p className=\"text-xs text-muted-foreground truncate\">\n {identity.email ?? 'No workspace e-mail on file'}\n </p>\n </div>\n <div className=\"flex flex-col items-start gap-2 sm:items-end\">\n {overridden ? (\n <div className=\"flex items-center gap-2\">\n <Badge\n variant=\"outline\"\n className=\"text-xs\"\n data-testid=\"unmatched-identity-overridden\"\n >\n Override set\n </Badge>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n disabled={clearState.loading}\n data-testid=\"unmatched-identity-clear\"\n onClick={() => void onClear()}\n >\n {clearState.loading ? '…' : 'Clear override'}\n </Button>\n </div>\n ) : (\n <div className=\"flex items-center gap-2\">\n <div className=\"w-64\">\n <CrewEmployeeSelect\n id={`unmatched-${identity.platformUserId}`}\n value={crewEmployeeId}\n onChange={setCrewEmployeeId}\n noneLabel=\"Choose a CrewFoundry employee\"\n />\n </div>\n <Button\n size=\"sm\"\n disabled={!crewEmployeeId || setState.loading}\n data-testid=\"unmatched-identity-set\"\n onClick={() => void onSet()}\n >\n {setState.loading ? 'Saving…' : 'Set override'}\n </Button>\n </div>\n )}\n {error && <ErrorLine message={error} />}\n </div>\n </li>\n );\n}\n\nexport function UnmatchedCrewIdentitiesPanel() {\n const { workspaceId } = usePlanMagnet();\n const { data, loading, error } = useQuery<{\n planmagnetUnmatchedCrewIdentities: UnmatchedIdentity[];\n }>(GET_UNMATCHED, {\n variables: { workspaceId: workspaceId ?? '' },\n skip: !workspaceId,\n });\n const identities = data?.planmagnetUnmatchedCrewIdentities ?? [];\n\n return (\n <Card id=\"unmatched-crew-identities\" data-testid=\"unmatched-crew-identities-panel\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <AlertTriangle className=\"h-4 w-4 text-status-warning-text\" />\n Unmatched CrewFoundry identities\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n An override pins one workspace member directly to one CrewFoundry employee record, for\n when the default best-effort e-mail match can't find — or finds the wrong — employee\n for them.\n </p>\n </CardHeader>\n <CardContent>\n {/* Only before the first load: a refetch must not blank the panel (it would unmount every\n row's own in-progress edit/local override state). */}\n {loading && !data ? (\n <p className=\"text-sm text-muted-foreground\">Loading…</p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n Unmatched identities could not be loaded: {error.message}\n </p>\n ) : identities.length === 0 ? (\n <p\n className=\"text-sm text-muted-foreground py-2\"\n data-testid=\"unmatched-identities-empty\"\n >\n Every assigned member currently matches a CrewFoundry employee — nothing to fix.\n </p>\n ) : (\n <ul className=\"space-y-2\">\n {identities.map((identity) => (\n <UnmatchedIdentityRow\n key={identity.platformUserId}\n identity={identity}\n workspaceId={workspaceId ?? ''}\n />\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|