@burdenoff/microfe-planmagnet 2026.1001.1 → 2026.1001.2
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/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +156 -150
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +19 -15
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/comments/CommentThread.js +36 -36
- package/dist/planmagnet/comments/RoadmapComments.js +21 -21
- package/dist/planmagnet/controls/AutomationRuleControls.js +24 -24
- package/dist/planmagnet/controls/FeatureControls.js +60 -60
- package/dist/planmagnet/controls/SyncControls.js +59 -59
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +15 -15
- package/dist/planmagnet/docs/DocSuggestionsPanel.js +12 -12
- package/dist/planmagnet/docs/DocVersionCompare.js +3 -3
- package/dist/planmagnet/forecast/DeliveryForecastCard.d.ts +12 -0
- package/dist/planmagnet/forecast/DeliveryForecastCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/DeliveryForecastCard.js +208 -0
- package/dist/planmagnet/forecast/DeliveryForecastCard.js.map +1 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.d.ts +5 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.js +190 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.js.map +1 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.d.ts +5 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +99 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js.map +1 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.d.ts +8 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.js +185 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -0
- package/dist/planmagnet/forecast/riskFlags.d.ts +35 -0
- package/dist/planmagnet/forecast/riskFlags.d.ts.map +1 -0
- package/dist/planmagnet/forecast/riskFlags.js +37 -0
- package/dist/planmagnet/forecast/riskFlags.js.map +1 -0
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +216 -182
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js +11 -11
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +4 -4
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +163 -163
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js +165 -165
- package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +234 -0
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +206 -182
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportPagePage.js +22 -22
- package/dist/planmagnet/pages/ReportsReportPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +21 -21
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js +88 -83
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +139 -112
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js.map +1 -1
- package/dist/planmagnet/pages/analytics/StatusFlowCard.js +21 -21
- package/dist/planmagnet/product/ProductControls.js +91 -91
- package/dist/planmagnet/reports/SchedulesPanel.js +33 -33
- package/dist/planmagnet/reports/ShareDialog.js +20 -20
- package/dist/planmagnet/reports/ShareDialog.js.map +1 -1
- package/dist/planmagnet/reports/sharedReportRoute.d.ts +3 -2
- package/dist/planmagnet/reports/sharedReportRoute.d.ts.map +1 -1
- package/dist/planmagnet/reports/sharedReportRoute.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +60 -60
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +30 -30
- package/dist/planmagnet/roadmaps/RoadmapReviewPanel.js +21 -21
- package/dist/planmagnet/roadmaps/RoadmapVersionsPanel.js +22 -22
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { isForbidden as e } from "../utils/graphqlErrors.js";
|
|
2
|
+
import { useState as t } from "react";
|
|
3
|
+
import { CalendarRange as n } from "lucide-react";
|
|
4
|
+
import { gql as r } from "@apollo/client";
|
|
5
|
+
import { useQuery as i } from "@apollo/client/react";
|
|
6
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
7
|
+
import { useI18n as s } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
8
|
+
import { Card as c, CardContent as l, CardHeader as u, CardTitle as d, Checkbox as f } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/planmagnet/forecast/DeliveryForecastCard.tsx
|
|
10
|
+
var p = r`
|
|
11
|
+
query PlanmagnetDeliveryForecast(
|
|
12
|
+
$workspaceId: ID!
|
|
13
|
+
$projectId: ID!
|
|
14
|
+
$scope: PlanmagnetForecastScopeInput
|
|
15
|
+
) {
|
|
16
|
+
planmagnetForecastDelivery(workspaceId: $workspaceId, projectId: $projectId, scope: $scope) {
|
|
17
|
+
method
|
|
18
|
+
insufficientHistory
|
|
19
|
+
reason
|
|
20
|
+
remainingPoints
|
|
21
|
+
unestimatedItems
|
|
22
|
+
scopeTruncated
|
|
23
|
+
confidenceNote
|
|
24
|
+
sprintsNeeded {
|
|
25
|
+
p50
|
|
26
|
+
p85
|
|
27
|
+
p95
|
|
28
|
+
}
|
|
29
|
+
dates {
|
|
30
|
+
p50
|
|
31
|
+
p85
|
|
32
|
+
p95
|
|
33
|
+
}
|
|
34
|
+
inputRefs {
|
|
35
|
+
sprintId
|
|
36
|
+
name
|
|
37
|
+
completedPoints
|
|
38
|
+
endDate
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
function m(e) {
|
|
44
|
+
return new Date(e).toLocaleDateString(void 0, {
|
|
45
|
+
timeZone: "UTC",
|
|
46
|
+
year: "numeric",
|
|
47
|
+
month: "short",
|
|
48
|
+
day: "numeric"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function h({ workspaceId: r, projectId: h, sprintId: g, sprintName: _ }) {
|
|
52
|
+
let { t: v } = s(), [y, b] = t(!1), x = y && !!g, { data: S, loading: C, error: w } = i(p, {
|
|
53
|
+
variables: {
|
|
54
|
+
workspaceId: r,
|
|
55
|
+
projectId: h,
|
|
56
|
+
scope: x && g ? { sprintId: g } : void 0
|
|
57
|
+
},
|
|
58
|
+
skip: !r || !h
|
|
59
|
+
}), T = S?.planmagnetForecastDelivery;
|
|
60
|
+
return /* @__PURE__ */ o(c, {
|
|
61
|
+
"data-testid": "forecast-card",
|
|
62
|
+
children: [/* @__PURE__ */ o(u, { children: [
|
|
63
|
+
/* @__PURE__ */ o(d, {
|
|
64
|
+
className: "flex items-center gap-2",
|
|
65
|
+
children: [/* @__PURE__ */ a(n, { className: "h-4 w-4" }), v("planmagnet.forecast.delivery.title", "Delivery forecast")]
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ a("p", {
|
|
68
|
+
className: "text-xs text-muted-foreground",
|
|
69
|
+
children: x ? v("planmagnet.forecast.delivery.subtitleSprint", "For the open items of {{sprint}} only.", { sprint: _ ?? v("planmagnet.forecast.delivery.thisSprint", "this sprint") }) : v("planmagnet.forecast.delivery.subtitle", "For this project's own open, estimated work items.")
|
|
70
|
+
}),
|
|
71
|
+
g && /* @__PURE__ */ o("label", {
|
|
72
|
+
htmlFor: `forecast-scope-sprint-${h}`,
|
|
73
|
+
className: "flex items-center gap-2 pt-1 text-xs font-normal text-muted-foreground",
|
|
74
|
+
children: [/* @__PURE__ */ a(f, {
|
|
75
|
+
id: `forecast-scope-sprint-${h}`,
|
|
76
|
+
checked: y,
|
|
77
|
+
onCheckedChange: (e) => b(e === !0)
|
|
78
|
+
}), v("planmagnet.forecast.delivery.scopeToggle", "Only this sprint's items ({{sprint}})", { sprint: _ ?? v("planmagnet.forecast.delivery.thisSprint", "this sprint") })]
|
|
79
|
+
})
|
|
80
|
+
] }), /* @__PURE__ */ a(l, { children: h ? C && !S ? /* @__PURE__ */ a("div", {
|
|
81
|
+
className: "flex justify-center py-8",
|
|
82
|
+
children: /* @__PURE__ */ a("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
83
|
+
}) : w ? /* @__PURE__ */ a("p", {
|
|
84
|
+
className: "text-sm text-status-error-text",
|
|
85
|
+
children: e(w) ? v("planmagnet.forecast.delivery.forbidden", "You do not have access to the delivery forecast for this project.") : `${v("planmagnet.forecast.delivery.error", "Delivery forecast could not be loaded")}: ${w.message}`
|
|
86
|
+
}) : T ? T.insufficientHistory ? /* @__PURE__ */ o("div", {
|
|
87
|
+
className: "space-y-2",
|
|
88
|
+
"data-testid": "forecast-insufficient",
|
|
89
|
+
children: [/* @__PURE__ */ a("p", {
|
|
90
|
+
className: "text-sm text-muted-foreground",
|
|
91
|
+
children: T.reason ?? v("planmagnet.forecast.delivery.insufficientFallback", "Not enough completed-sprint history yet to forecast a delivery date.")
|
|
92
|
+
}), /* @__PURE__ */ a("p", {
|
|
93
|
+
className: "text-xs text-muted-foreground",
|
|
94
|
+
children: T.confidenceNote
|
|
95
|
+
})]
|
|
96
|
+
}) : /* @__PURE__ */ o("div", {
|
|
97
|
+
className: "space-y-4",
|
|
98
|
+
children: [
|
|
99
|
+
/* @__PURE__ */ o("dl", {
|
|
100
|
+
className: "grid grid-cols-3 gap-3 text-center",
|
|
101
|
+
"data-testid": "forecast-dates",
|
|
102
|
+
children: [
|
|
103
|
+
/* @__PURE__ */ o("div", {
|
|
104
|
+
className: "rounded-md border p-2.5",
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ a("dt", {
|
|
107
|
+
className: "text-xs text-muted-foreground",
|
|
108
|
+
children: v("planmagnet.forecast.delivery.p50Label", "Likely by (p50)")
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ a("dd", {
|
|
111
|
+
className: "mt-1 text-sm font-semibold",
|
|
112
|
+
children: T.dates ? m(T.dates.p50) : "—"
|
|
113
|
+
}),
|
|
114
|
+
/* @__PURE__ */ a("dd", {
|
|
115
|
+
className: "text-xs text-muted-foreground",
|
|
116
|
+
children: T.sprintsNeeded ? v("planmagnet.forecast.delivery.sprints", "{{count}} sprint(s)", { count: T.sprintsNeeded.p50 }) : "—"
|
|
117
|
+
})
|
|
118
|
+
]
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ o("div", {
|
|
121
|
+
className: "rounded-md border p-2.5",
|
|
122
|
+
children: [
|
|
123
|
+
/* @__PURE__ */ a("dt", {
|
|
124
|
+
className: "text-xs text-muted-foreground",
|
|
125
|
+
children: v("planmagnet.forecast.delivery.p85Label", "Confident by (p85)")
|
|
126
|
+
}),
|
|
127
|
+
/* @__PURE__ */ a("dd", {
|
|
128
|
+
className: "mt-1 text-sm font-semibold",
|
|
129
|
+
children: T.dates ? m(T.dates.p85) : "—"
|
|
130
|
+
}),
|
|
131
|
+
/* @__PURE__ */ a("dd", {
|
|
132
|
+
className: "text-xs text-muted-foreground",
|
|
133
|
+
children: T.sprintsNeeded ? v("planmagnet.forecast.delivery.sprints", "{{count}} sprint(s)", { count: T.sprintsNeeded.p85 }) : "—"
|
|
134
|
+
})
|
|
135
|
+
]
|
|
136
|
+
}),
|
|
137
|
+
/* @__PURE__ */ o("div", {
|
|
138
|
+
className: "rounded-md border p-2.5",
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ a("dt", {
|
|
141
|
+
className: "text-xs text-muted-foreground",
|
|
142
|
+
children: v("planmagnet.forecast.delivery.p95Label", "Very confident by (p95)")
|
|
143
|
+
}),
|
|
144
|
+
/* @__PURE__ */ a("dd", {
|
|
145
|
+
className: "mt-1 text-sm font-semibold",
|
|
146
|
+
children: T.dates ? m(T.dates.p95) : "—"
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ a("dd", {
|
|
149
|
+
className: "text-xs text-muted-foreground",
|
|
150
|
+
children: T.sprintsNeeded ? v("planmagnet.forecast.delivery.sprints", "{{count}} sprint(s)", { count: T.sprintsNeeded.p95 }) : "—"
|
|
151
|
+
})
|
|
152
|
+
]
|
|
153
|
+
})
|
|
154
|
+
]
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ o("div", {
|
|
157
|
+
className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground",
|
|
158
|
+
children: [/* @__PURE__ */ a("span", { children: v("planmagnet.forecast.delivery.remaining", "Remaining: {{points}} pts", { points: T.remainingPoints }) }), T.unestimatedItems > 0 && /* @__PURE__ */ a("span", {
|
|
159
|
+
className: "text-status-warning-text",
|
|
160
|
+
children: v("planmagnet.forecast.delivery.unestimated", "{{count}} open item(s) have no estimate (counted as 0 pts)", { count: T.unestimatedItems })
|
|
161
|
+
})]
|
|
162
|
+
}),
|
|
163
|
+
T.scopeTruncated && /* @__PURE__ */ a("p", {
|
|
164
|
+
className: "text-xs text-status-warning-text",
|
|
165
|
+
children: v("planmagnet.forecast.delivery.truncated", "This scope is large enough that the scan stopped early — remaining points and unestimated items reflect only the first items found, not the whole project.")
|
|
166
|
+
}),
|
|
167
|
+
/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h3", {
|
|
168
|
+
className: "mb-1 text-sm font-medium",
|
|
169
|
+
children: v("planmagnet.forecast.delivery.evidence", "Based on")
|
|
170
|
+
}), /* @__PURE__ */ o("ul", {
|
|
171
|
+
className: "space-y-1 text-xs text-muted-foreground",
|
|
172
|
+
"data-testid": "forecast-inputs",
|
|
173
|
+
children: [T.inputRefs.map((e) => /* @__PURE__ */ o("li", {
|
|
174
|
+
className: "flex items-center justify-between gap-2",
|
|
175
|
+
children: [/* @__PURE__ */ a("span", {
|
|
176
|
+
className: "truncate",
|
|
177
|
+
children: e.name
|
|
178
|
+
}), /* @__PURE__ */ o("span", {
|
|
179
|
+
className: "shrink-0 tabular-nums",
|
|
180
|
+
children: [v("planmagnet.forecast.delivery.evidenceRow", "{{points}} pts", { points: e.completedPoints }), e.endDate ? ` · ${m(e.endDate)}` : ""]
|
|
181
|
+
})]
|
|
182
|
+
}, e.sprintId)), T.inputRefs.length === 0 && /* @__PURE__ */ a("li", { children: v("planmagnet.forecast.delivery.noInputs", "No sprint history available.") })]
|
|
183
|
+
})] }),
|
|
184
|
+
/* @__PURE__ */ a("p", {
|
|
185
|
+
className: "text-xs text-muted-foreground",
|
|
186
|
+
children: T.confidenceNote
|
|
187
|
+
}),
|
|
188
|
+
/* @__PURE__ */ o("details", {
|
|
189
|
+
className: "text-xs text-muted-foreground",
|
|
190
|
+
children: [/* @__PURE__ */ a("summary", {
|
|
191
|
+
className: "cursor-pointer select-none font-medium text-foreground",
|
|
192
|
+
children: v("planmagnet.forecast.whatIsThis", "What is this?")
|
|
193
|
+
}), /* @__PURE__ */ a("p", {
|
|
194
|
+
className: "mt-1",
|
|
195
|
+
children: v("planmagnet.forecast.delivery.explainer", "Statistical forecast (Monte Carlo over past sprints): a simulation that resamples this project’s trailing completed sprints’ real velocities thousands of times to see how many more sprints the remaining work is likely to take. It is never an AI/LLM call, and it never invents a date from a sample it doesn’t trust — when history is too thin, it says so instead of guessing.")
|
|
196
|
+
})]
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
}) : null : /* @__PURE__ */ a("p", {
|
|
200
|
+
className: "text-sm text-muted-foreground",
|
|
201
|
+
children: v("planmagnet.forecast.delivery.noProject", "Select a project to see its forecast.")
|
|
202
|
+
}) })]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
export { h as DeliveryForecastCard };
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=DeliveryForecastCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeliveryForecastCard.js","names":[],"sources":["../../../src/planmagnet/forecast/DeliveryForecastCard.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Checkbox } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CalendarRange } from 'lucide-react';\nimport { isForbidden } from '../utils/graphqlErrors';\n\n/**\n * ANR-02: the Monte Carlo delivery forecast (`planmagnetForecastDelivery`) — how many more\n * sprints a project's (or one sprint's) remaining scope is likely to take, and the calendar\n * dates those sprint counts land on. Shared by the project detail page and the Analytics page\n * (both project-scoped) so the wording and the insufficient-history handling can never drift\n * between the two.\n *\n * NEVER an AI/LLM call — the server resamples the trailing completed sprints' real velocities\n * (2,000 seeded trials, deterministic). Labelled \"Statistical forecast (Monte Carlo over past\n * sprints)\" everywhere it appears, never presented as AI.\n */\n\nconst GET_DELIVERY_FORECAST = gql`\n query PlanmagnetDeliveryForecast(\n $workspaceId: ID!\n $projectId: ID!\n $scope: PlanmagnetForecastScopeInput\n ) {\n planmagnetForecastDelivery(workspaceId: $workspaceId, projectId: $projectId, scope: $scope) {\n method\n insufficientHistory\n reason\n remainingPoints\n unestimatedItems\n scopeTruncated\n confidenceNote\n sprintsNeeded {\n p50\n p85\n p95\n }\n dates {\n p50\n p85\n p95\n }\n inputRefs {\n sprintId\n name\n completedPoints\n endDate\n }\n }\n }\n`;\n\ninterface DeliveryForecast {\n method: 'MONTE_CARLO_VELOCITY';\n insufficientHistory: boolean;\n reason: string | null;\n remainingPoints: number;\n unestimatedItems: number;\n scopeTruncated: boolean;\n confidenceNote: string;\n sprintsNeeded: { p50: number; p85: number; p95: number } | null;\n dates: { p50: string; p85: string; p95: string } | null;\n inputRefs: { sprintId: string; name: string; completedPoints: number; endDate: string | null }[];\n}\n\ninterface DeliveryForecastData {\n planmagnetForecastDelivery: DeliveryForecast;\n}\n\n/** Forecast dates are calendar days (UTC midnight, derived from Sprint.endDate); show them\n * without a time-zone shift — same convention as SprintDates.tsx's `day()` helper. */\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString(undefined, {\n timeZone: 'UTC',\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n}\n\nexport function DeliveryForecastCard({\n workspaceId,\n projectId,\n /** When set, the card offers an \"only this sprint's items\" toggle that narrows\n * `scope.sprintId` to this sprint instead of the project's full remaining scope. Pass the\n * active/current sprint of whatever page hosts this card — omit it where no single sprint\n * is in context (the card then always forecasts the project's full open scope). */\n sprintId,\n sprintName,\n}: {\n workspaceId?: string | null;\n projectId: string;\n sprintId?: string | null;\n sprintName?: string | null;\n}) {\n const { t } = useI18n();\n const [scopeToSprint, setScopeToSprint] = useState(false);\n const effectiveScopeToSprint = scopeToSprint && Boolean(sprintId);\n const { data, loading, error } = useQuery<DeliveryForecastData>(GET_DELIVERY_FORECAST, {\n variables: {\n workspaceId,\n projectId,\n scope: effectiveScopeToSprint && sprintId ? { sprintId } : undefined,\n },\n skip: !workspaceId || !projectId,\n });\n const forecast = data?.planmagnetForecastDelivery;\n\n return (\n <Card data-testid=\"forecast-card\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <CalendarRange className=\"h-4 w-4\" />\n {t('planmagnet.forecast.delivery.title', 'Delivery forecast')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {effectiveScopeToSprint\n ? t(\n 'planmagnet.forecast.delivery.subtitleSprint',\n 'For the open items of {{sprint}} only.',\n {\n sprint: sprintName ?? t('planmagnet.forecast.delivery.thisSprint', 'this sprint'),\n }\n )\n : t(\n 'planmagnet.forecast.delivery.subtitle',\n \"For this project's own open, estimated work items.\"\n )}\n </p>\n {sprintId && (\n <label\n htmlFor={`forecast-scope-sprint-${projectId}`}\n className=\"flex items-center gap-2 pt-1 text-xs font-normal text-muted-foreground\"\n >\n <Checkbox\n id={`forecast-scope-sprint-${projectId}`}\n checked={scopeToSprint}\n onCheckedChange={(checked) => setScopeToSprint(checked === true)}\n />\n {t(\n 'planmagnet.forecast.delivery.scopeToggle',\n \"Only this sprint's items ({{sprint}})\",\n { sprint: sprintName ?? t('planmagnet.forecast.delivery.thisSprint', 'this sprint') }\n )}\n </label>\n )}\n </CardHeader>\n <CardContent>\n {!projectId ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.forecast.delivery.noProject', 'Select a project to see its forecast.')}\n </p>\n ) : loading && !data ? (\n <div className=\"flex justify-center py-8\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.forecast.delivery.forbidden',\n 'You do not have access to the delivery forecast for this project.'\n )\n : `${t('planmagnet.forecast.delivery.error', 'Delivery forecast could not be loaded')}: ${error.message}`}\n </p>\n ) : !forecast ? null : forecast.insufficientHistory ? (\n <div className=\"space-y-2\" data-testid=\"forecast-insufficient\">\n <p className=\"text-sm text-muted-foreground\">\n {forecast.reason ??\n t(\n 'planmagnet.forecast.delivery.insufficientFallback',\n 'Not enough completed-sprint history yet to forecast a delivery date.'\n )}\n </p>\n <p className=\"text-xs text-muted-foreground\">{forecast.confidenceNote}</p>\n </div>\n ) : (\n <div className=\"space-y-4\">\n <dl className=\"grid grid-cols-3 gap-3 text-center\" data-testid=\"forecast-dates\">\n <div className=\"rounded-md border p-2.5\">\n <dt className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.delivery.p50Label', 'Likely by (p50)')}\n </dt>\n <dd className=\"mt-1 text-sm font-semibold\">\n {forecast.dates ? formatDate(forecast.dates.p50) : '—'}\n </dd>\n <dd className=\"text-xs text-muted-foreground\">\n {forecast.sprintsNeeded\n ? t('planmagnet.forecast.delivery.sprints', '{{count}} sprint(s)', {\n count: forecast.sprintsNeeded.p50,\n })\n : '—'}\n </dd>\n </div>\n <div className=\"rounded-md border p-2.5\">\n <dt className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.delivery.p85Label', 'Confident by (p85)')}\n </dt>\n <dd className=\"mt-1 text-sm font-semibold\">\n {forecast.dates ? formatDate(forecast.dates.p85) : '—'}\n </dd>\n <dd className=\"text-xs text-muted-foreground\">\n {forecast.sprintsNeeded\n ? t('planmagnet.forecast.delivery.sprints', '{{count}} sprint(s)', {\n count: forecast.sprintsNeeded.p85,\n })\n : '—'}\n </dd>\n </div>\n <div className=\"rounded-md border p-2.5\">\n <dt className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.delivery.p95Label', 'Very confident by (p95)')}\n </dt>\n <dd className=\"mt-1 text-sm font-semibold\">\n {forecast.dates ? formatDate(forecast.dates.p95) : '—'}\n </dd>\n <dd className=\"text-xs text-muted-foreground\">\n {forecast.sprintsNeeded\n ? t('planmagnet.forecast.delivery.sprints', '{{count}} sprint(s)', {\n count: forecast.sprintsNeeded.p95,\n })\n : '—'}\n </dd>\n </div>\n </dl>\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n <span>\n {t('planmagnet.forecast.delivery.remaining', 'Remaining: {{points}} pts', {\n points: forecast.remainingPoints,\n })}\n </span>\n {forecast.unestimatedItems > 0 && (\n <span className=\"text-status-warning-text\">\n {t(\n 'planmagnet.forecast.delivery.unestimated',\n '{{count}} open item(s) have no estimate (counted as 0 pts)',\n { count: forecast.unestimatedItems }\n )}\n </span>\n )}\n </div>\n\n {forecast.scopeTruncated && (\n <p className=\"text-xs text-status-warning-text\">\n {t(\n 'planmagnet.forecast.delivery.truncated',\n 'This scope is large enough that the scan stopped early — remaining points and unestimated items reflect only the first items found, not the whole project.'\n )}\n </p>\n )}\n\n <div>\n <h3 className=\"mb-1 text-sm font-medium\">\n {t('planmagnet.forecast.delivery.evidence', 'Based on')}\n </h3>\n <ul className=\"space-y-1 text-xs text-muted-foreground\" data-testid=\"forecast-inputs\">\n {forecast.inputRefs.map((ref) => (\n <li key={ref.sprintId} className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate\">{ref.name}</span>\n <span className=\"shrink-0 tabular-nums\">\n {t('planmagnet.forecast.delivery.evidenceRow', '{{points}} pts', {\n points: ref.completedPoints,\n })}\n {ref.endDate ? ` · ${formatDate(ref.endDate)}` : ''}\n </span>\n </li>\n ))}\n {forecast.inputRefs.length === 0 && (\n <li>\n {t('planmagnet.forecast.delivery.noInputs', 'No sprint history available.')}\n </li>\n )}\n </ul>\n </div>\n\n <p className=\"text-xs text-muted-foreground\">{forecast.confidenceNote}</p>\n\n <details className=\"text-xs text-muted-foreground\">\n <summary className=\"cursor-pointer select-none font-medium text-foreground\">\n {t('planmagnet.forecast.whatIsThis', 'What is this?')}\n </summary>\n <p className=\"mt-1\">\n {t(\n 'planmagnet.forecast.delivery.explainer',\n 'Statistical forecast (Monte Carlo over past sprints): a simulation that resamples this project’s trailing completed sprints’ real velocities thousands of times to see how many more sprints the remaining work is likely to take. It is never an AI/LLM call, and it never invents a date from a sample it doesn’t trust — when history is too thin, it says so instead of guessing.'\n )}\n </p>\n </details>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectOutlookCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/forecast/ProjectOutlookCard.tsx"],"names":[],"mappings":"AAmHA,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB,2CAoMA"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { isForbidden as t } from "../utils/graphqlErrors.js";
|
|
3
|
+
import { detailPath as n } from "../utils/rowNavigation.js";
|
|
4
|
+
import { Compass as r } from "lucide-react";
|
|
5
|
+
import { gql as i } from "@apollo/client";
|
|
6
|
+
import { useQuery as a } from "@apollo/client/react";
|
|
7
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
8
|
+
import { useI18n as c } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
9
|
+
import { Badge as l, Card as u, CardContent as d, CardHeader as f, CardTitle as p } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
//#region src/planmagnet/forecast/ProjectOutlookCard.tsx
|
|
11
|
+
var m = i`
|
|
12
|
+
query PlanmagnetProjectOutlook($workspaceId: ID!, $projectId: ID!) {
|
|
13
|
+
planmagnetProjectOutlook(workspaceId: $workspaceId, projectId: $projectId) {
|
|
14
|
+
projectId
|
|
15
|
+
method
|
|
16
|
+
successLikelihood
|
|
17
|
+
successLikelihoodRule
|
|
18
|
+
timelineDeviationDays
|
|
19
|
+
qualityTrend {
|
|
20
|
+
recentDefectRatio
|
|
21
|
+
priorDefectRatio
|
|
22
|
+
trend
|
|
23
|
+
}
|
|
24
|
+
riskExposure {
|
|
25
|
+
totalExposure
|
|
26
|
+
topRisks {
|
|
27
|
+
riskId
|
|
28
|
+
title
|
|
29
|
+
likelihood
|
|
30
|
+
impact
|
|
31
|
+
exposure
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
budgetProjection {
|
|
35
|
+
budgetId
|
|
36
|
+
name
|
|
37
|
+
spentAmount
|
|
38
|
+
totalAmount
|
|
39
|
+
burnRatePerDay
|
|
40
|
+
projectedSpendAtForecastDate
|
|
41
|
+
projectedOverage
|
|
42
|
+
}
|
|
43
|
+
inputs
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`, h = {
|
|
47
|
+
HIGH: "success",
|
|
48
|
+
MEDIUM: "warning",
|
|
49
|
+
LOW: "destructive"
|
|
50
|
+
}, g = {
|
|
51
|
+
IMPROVING: "success",
|
|
52
|
+
STABLE: "secondary",
|
|
53
|
+
WORSENING: "warning",
|
|
54
|
+
UNKNOWN: "outline"
|
|
55
|
+
};
|
|
56
|
+
function _(e) {
|
|
57
|
+
return e === null ? "—" : `${Math.round(e * 100)}%`;
|
|
58
|
+
}
|
|
59
|
+
function v({ workspaceId: i, projectId: v }) {
|
|
60
|
+
let { t: y } = c(), b = e(), { data: x, loading: S, error: C } = a(m, {
|
|
61
|
+
variables: {
|
|
62
|
+
workspaceId: i,
|
|
63
|
+
projectId: v
|
|
64
|
+
},
|
|
65
|
+
skip: !i || !v
|
|
66
|
+
}), w = x?.planmagnetProjectOutlook;
|
|
67
|
+
return /* @__PURE__ */ s(u, {
|
|
68
|
+
"data-testid": "outlook-card",
|
|
69
|
+
children: [/* @__PURE__ */ s(f, {
|
|
70
|
+
className: "flex flex-row items-start justify-between space-y-0",
|
|
71
|
+
children: [/* @__PURE__ */ s("div", { children: [/* @__PURE__ */ s(p, {
|
|
72
|
+
className: "flex items-center gap-2",
|
|
73
|
+
children: [/* @__PURE__ */ o(r, { className: "h-4 w-4" }), y("planmagnet.forecast.outlook.title", "Project outlook")]
|
|
74
|
+
}), /* @__PURE__ */ o("p", {
|
|
75
|
+
className: "text-xs text-muted-foreground",
|
|
76
|
+
children: y("planmagnet.forecast.outlook.subtitle", "Explicit rule-based heuristics over real data — never AI.")
|
|
77
|
+
})] }), /* @__PURE__ */ o(l, {
|
|
78
|
+
variant: "outline",
|
|
79
|
+
children: y("planmagnet.forecast.outlook.method", "HEURISTIC")
|
|
80
|
+
})]
|
|
81
|
+
}), /* @__PURE__ */ o(d, { children: S && !x ? /* @__PURE__ */ o("div", {
|
|
82
|
+
className: "flex justify-center py-8",
|
|
83
|
+
children: /* @__PURE__ */ o("div", { className: "animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" })
|
|
84
|
+
}) : C ? /* @__PURE__ */ o("p", {
|
|
85
|
+
className: "text-sm text-status-error-text",
|
|
86
|
+
children: t(C) ? y("planmagnet.forecast.outlook.forbidden", "You do not have access to the outlook for this project.") : `${y("planmagnet.forecast.outlook.error", "Project outlook could not be loaded")}: ${C.message}`
|
|
87
|
+
}) : w ? /* @__PURE__ */ s("div", {
|
|
88
|
+
className: "space-y-4",
|
|
89
|
+
children: [
|
|
90
|
+
/* @__PURE__ */ s("div", {
|
|
91
|
+
className: "flex flex-wrap items-center gap-2",
|
|
92
|
+
children: [/* @__PURE__ */ o(l, {
|
|
93
|
+
variant: h[w.successLikelihood],
|
|
94
|
+
children: y("planmagnet.forecast.outlook.successLikelihood", "Success likelihood: {{level}}", { level: w.successLikelihood })
|
|
95
|
+
}), /* @__PURE__ */ o("span", {
|
|
96
|
+
className: "text-xs text-muted-foreground",
|
|
97
|
+
children: y("planmagnet.forecast.outlook.timelineDeviation", "Timeline: {{status}}", { status: w.timelineDeviationDays === null ? y("planmagnet.forecast.outlook.timelineUnknown", "no forecast/target to compare") : w.timelineDeviationDays > 0 ? y("planmagnet.forecast.outlook.timelineLate", "{{days}} day(s) late", { days: w.timelineDeviationDays }) : w.timelineDeviationDays < 0 ? y("planmagnet.forecast.outlook.timelineEarly", "{{days}} day(s) early", { days: Math.abs(w.timelineDeviationDays) }) : y("planmagnet.forecast.outlook.timelineOnTarget", "on target") })
|
|
98
|
+
})]
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ o("p", {
|
|
101
|
+
className: "text-xs text-muted-foreground",
|
|
102
|
+
children: w.successLikelihoodRule
|
|
103
|
+
}),
|
|
104
|
+
/* @__PURE__ */ s("div", {
|
|
105
|
+
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
106
|
+
children: [/* @__PURE__ */ s("div", {
|
|
107
|
+
className: "rounded-md border p-3",
|
|
108
|
+
children: [/* @__PURE__ */ s("h3", {
|
|
109
|
+
className: "mb-1 flex items-center gap-2 text-sm font-medium",
|
|
110
|
+
children: [y("planmagnet.forecast.outlook.quality", "Quality trend"), /* @__PURE__ */ o(l, {
|
|
111
|
+
variant: g[w.qualityTrend.trend],
|
|
112
|
+
children: w.qualityTrend.trend
|
|
113
|
+
})]
|
|
114
|
+
}), /* @__PURE__ */ o("p", {
|
|
115
|
+
className: "text-xs text-muted-foreground",
|
|
116
|
+
children: y("planmagnet.forecast.outlook.qualityRatios", "Recent 4wk defect ratio {{recent}}, prior 4wk {{prior}}", {
|
|
117
|
+
recent: _(w.qualityTrend.recentDefectRatio),
|
|
118
|
+
prior: _(w.qualityTrend.priorDefectRatio)
|
|
119
|
+
})
|
|
120
|
+
})]
|
|
121
|
+
}), /* @__PURE__ */ s("div", {
|
|
122
|
+
className: "rounded-md border p-3",
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ o("h3", {
|
|
125
|
+
className: "mb-1 text-sm font-medium",
|
|
126
|
+
children: y("planmagnet.forecast.outlook.riskExposure", "Risk exposure")
|
|
127
|
+
}),
|
|
128
|
+
/* @__PURE__ */ o("p", {
|
|
129
|
+
className: "text-xs text-muted-foreground mb-1",
|
|
130
|
+
children: y("planmagnet.forecast.outlook.totalExposure", "Total (likelihood × impact): {{value}}", { value: w.riskExposure.totalExposure })
|
|
131
|
+
}),
|
|
132
|
+
/* @__PURE__ */ s("ul", {
|
|
133
|
+
className: "space-y-0.5",
|
|
134
|
+
children: [w.riskExposure.topRisks.map((e) => /* @__PURE__ */ o("li", { children: /* @__PURE__ */ s("button", {
|
|
135
|
+
type: "button",
|
|
136
|
+
className: "truncate text-left text-xs text-primary hover:underline",
|
|
137
|
+
onClick: () => b(n("/risks/risk-detail-page", e.riskId)),
|
|
138
|
+
children: [
|
|
139
|
+
e.title,
|
|
140
|
+
" (",
|
|
141
|
+
e.exposure,
|
|
142
|
+
")"
|
|
143
|
+
]
|
|
144
|
+
}) }, e.riskId)), w.riskExposure.topRisks.length === 0 && /* @__PURE__ */ o("li", {
|
|
145
|
+
className: "text-xs text-muted-foreground",
|
|
146
|
+
children: y("planmagnet.forecast.outlook.noOpenRisks", "No open risks.")
|
|
147
|
+
})]
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
})]
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ s("div", { children: [/* @__PURE__ */ o("h3", {
|
|
153
|
+
className: "mb-1 text-sm font-medium",
|
|
154
|
+
children: y("planmagnet.forecast.outlook.budget", "Budget projection")
|
|
155
|
+
}), w.budgetProjection === null ? /* @__PURE__ */ o("p", {
|
|
156
|
+
className: "text-xs text-muted-foreground",
|
|
157
|
+
children: y("planmagnet.forecast.outlook.noBudgets", "This project has no budgets.")
|
|
158
|
+
}) : /* @__PURE__ */ o("ul", {
|
|
159
|
+
className: "space-y-1.5",
|
|
160
|
+
children: w.budgetProjection.map((e) => /* @__PURE__ */ s("li", {
|
|
161
|
+
className: "flex items-center justify-between gap-2 text-xs",
|
|
162
|
+
children: [/* @__PURE__ */ o("span", {
|
|
163
|
+
className: "truncate font-medium text-foreground",
|
|
164
|
+
children: e.name
|
|
165
|
+
}), /* @__PURE__ */ s("span", {
|
|
166
|
+
className: `shrink-0 tabular-nums ${e.projectedOverage !== null && e.projectedOverage > 0 ? "text-status-error-text" : "text-muted-foreground"}`,
|
|
167
|
+
children: [
|
|
168
|
+
e.spentAmount.toLocaleString(),
|
|
169
|
+
" / ",
|
|
170
|
+
e.totalAmount.toLocaleString(),
|
|
171
|
+
e.projectedOverage !== null && ` · ${e.projectedOverage > 0 ? y("planmagnet.forecast.outlook.projectedOver", "projected +{{amount}} over", { amount: Math.round(e.projectedOverage).toLocaleString() }) : y("planmagnet.forecast.outlook.projectedUnder", "projected within plan")}`
|
|
172
|
+
]
|
|
173
|
+
})]
|
|
174
|
+
}, e.budgetId))
|
|
175
|
+
})] }),
|
|
176
|
+
w.inputs.length > 0 && /* @__PURE__ */ s("div", { children: [/* @__PURE__ */ o("h3", {
|
|
177
|
+
className: "mb-1 text-sm font-medium",
|
|
178
|
+
children: y("planmagnet.forecast.outlook.inputs", "What this is based on")
|
|
179
|
+
}), /* @__PURE__ */ o("ul", {
|
|
180
|
+
className: "list-inside list-disc space-y-0.5 text-xs text-muted-foreground",
|
|
181
|
+
children: w.inputs.map((e, t) => /* @__PURE__ */ o("li", { children: e }, `${t}-${e.slice(0, 24)}`))
|
|
182
|
+
})] })
|
|
183
|
+
]
|
|
184
|
+
}) : null })]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { v as ProjectOutlookCard };
|
|
189
|
+
|
|
190
|
+
//# sourceMappingURL=ProjectOutlookCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectOutlookCard.js","names":[],"sources":["../../../src/planmagnet/forecast/ProjectOutlookCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Compass } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { isForbidden } from '../utils/graphqlErrors';\n\n/**\n * ANR-10: `planmagnetProjectOutlook` — a heuristic project health summary. Never an AI/LLM\n * call and never a made-up percentage: `successLikelihood` is HIGH/MEDIUM/LOW by the EXACT\n * rule in `successLikelihoodRule`, rendered verbatim so the badge is never unexplained.\n * Labelled HEURISTIC throughout, never presented as AI.\n */\n\nconst GET_PROJECT_OUTLOOK = gql`\n query PlanmagnetProjectOutlook($workspaceId: ID!, $projectId: ID!) {\n planmagnetProjectOutlook(workspaceId: $workspaceId, projectId: $projectId) {\n projectId\n method\n successLikelihood\n successLikelihoodRule\n timelineDeviationDays\n qualityTrend {\n recentDefectRatio\n priorDefectRatio\n trend\n }\n riskExposure {\n totalExposure\n topRisks {\n riskId\n title\n likelihood\n impact\n exposure\n }\n }\n budgetProjection {\n budgetId\n name\n spentAmount\n totalAmount\n burnRatePerDay\n projectedSpendAtForecastDate\n projectedOverage\n }\n inputs\n }\n }\n`;\n\ninterface ProjectOutlook {\n projectId: string;\n method: 'HEURISTIC';\n successLikelihood: 'HIGH' | 'MEDIUM' | 'LOW';\n successLikelihoodRule: string;\n timelineDeviationDays: number | null;\n qualityTrend: {\n recentDefectRatio: number | null;\n priorDefectRatio: number | null;\n trend: 'IMPROVING' | 'STABLE' | 'WORSENING' | 'UNKNOWN';\n };\n riskExposure: {\n totalExposure: number;\n topRisks: {\n riskId: string;\n title: string;\n likelihood: number | null;\n impact: number | null;\n exposure: number;\n }[];\n };\n budgetProjection:\n | {\n budgetId: string;\n name: string;\n spentAmount: number;\n totalAmount: number;\n burnRatePerDay: number;\n projectedSpendAtForecastDate: number | null;\n projectedOverage: number | null;\n }[]\n | null;\n inputs: string[];\n}\n\ninterface ProjectOutlookData {\n planmagnetProjectOutlook: ProjectOutlook;\n}\n\nconst LIKELIHOOD_BADGE: Record<\n ProjectOutlook['successLikelihood'],\n 'success' | 'warning' | 'destructive'\n> = {\n HIGH: 'success',\n MEDIUM: 'warning',\n LOW: 'destructive',\n};\n\nconst TREND_BADGE: Record<\n ProjectOutlook['qualityTrend']['trend'],\n 'success' | 'warning' | 'secondary' | 'outline'\n> = {\n IMPROVING: 'success',\n STABLE: 'secondary',\n WORSENING: 'warning',\n UNKNOWN: 'outline',\n};\n\nfunction pct(ratio: number | null): string {\n return ratio === null ? '—' : `${Math.round(ratio * 100)}%`;\n}\n\nexport function ProjectOutlookCard({\n workspaceId,\n projectId,\n}: {\n workspaceId?: string | null;\n projectId: string;\n}) {\n const { t } = useI18n();\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<ProjectOutlookData>(GET_PROJECT_OUTLOOK, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n const outlook = data?.planmagnetProjectOutlook;\n\n return (\n <Card data-testid=\"outlook-card\">\n <CardHeader className=\"flex flex-row items-start justify-between space-y-0\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <Compass className=\"h-4 w-4\" />\n {t('planmagnet.forecast.outlook.title', 'Project outlook')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.forecast.outlook.subtitle',\n 'Explicit rule-based heuristics over real data — never AI.'\n )}\n </p>\n </div>\n <Badge variant=\"outline\">{t('planmagnet.forecast.outlook.method', 'HEURISTIC')}</Badge>\n </CardHeader>\n <CardContent>\n {loading && !data ? (\n <div className=\"flex justify-center py-8\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.forecast.outlook.forbidden',\n 'You do not have access to the outlook for this project.'\n )\n : `${t('planmagnet.forecast.outlook.error', 'Project outlook could not be loaded')}: ${error.message}`}\n </p>\n ) : !outlook ? null : (\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge variant={LIKELIHOOD_BADGE[outlook.successLikelihood]}>\n {t(\n 'planmagnet.forecast.outlook.successLikelihood',\n 'Success likelihood: {{level}}',\n {\n level: outlook.successLikelihood,\n }\n )}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.outlook.timelineDeviation', 'Timeline: {{status}}', {\n status:\n outlook.timelineDeviationDays === null\n ? t(\n 'planmagnet.forecast.outlook.timelineUnknown',\n 'no forecast/target to compare'\n )\n : outlook.timelineDeviationDays > 0\n ? t('planmagnet.forecast.outlook.timelineLate', '{{days}} day(s) late', {\n days: outlook.timelineDeviationDays,\n })\n : outlook.timelineDeviationDays < 0\n ? t(\n 'planmagnet.forecast.outlook.timelineEarly',\n '{{days}} day(s) early',\n {\n days: Math.abs(outlook.timelineDeviationDays),\n }\n )\n : t('planmagnet.forecast.outlook.timelineOnTarget', 'on target'),\n })}\n </span>\n </div>\n {/* The rule text verbatim — the badge above is never unexplained. */}\n <p className=\"text-xs text-muted-foreground\">{outlook.successLikelihoodRule}</p>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div className=\"rounded-md border p-3\">\n <h3 className=\"mb-1 flex items-center gap-2 text-sm font-medium\">\n {t('planmagnet.forecast.outlook.quality', 'Quality trend')}\n <Badge variant={TREND_BADGE[outlook.qualityTrend.trend]}>\n {outlook.qualityTrend.trend}\n </Badge>\n </h3>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.forecast.outlook.qualityRatios',\n 'Recent 4wk defect ratio {{recent}}, prior 4wk {{prior}}',\n {\n recent: pct(outlook.qualityTrend.recentDefectRatio),\n prior: pct(outlook.qualityTrend.priorDefectRatio),\n }\n )}\n </p>\n </div>\n <div className=\"rounded-md border p-3\">\n <h3 className=\"mb-1 text-sm font-medium\">\n {t('planmagnet.forecast.outlook.riskExposure', 'Risk exposure')}\n </h3>\n <p className=\"text-xs text-muted-foreground mb-1\">\n {t(\n 'planmagnet.forecast.outlook.totalExposure',\n 'Total (likelihood × impact): {{value}}',\n {\n value: outlook.riskExposure.totalExposure,\n }\n )}\n </p>\n <ul className=\"space-y-0.5\">\n {outlook.riskExposure.topRisks.map((risk) => (\n <li key={risk.riskId}>\n <button\n type=\"button\"\n className=\"truncate text-left text-xs text-primary hover:underline\"\n onClick={() => navigate(detailPath('/risks/risk-detail-page', risk.riskId))}\n >\n {risk.title} ({risk.exposure})\n </button>\n </li>\n ))}\n {outlook.riskExposure.topRisks.length === 0 && (\n <li className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.outlook.noOpenRisks', 'No open risks.')}\n </li>\n )}\n </ul>\n </div>\n </div>\n\n <div>\n <h3 className=\"mb-1 text-sm font-medium\">\n {t('planmagnet.forecast.outlook.budget', 'Budget projection')}\n </h3>\n {outlook.budgetProjection === null ? (\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.outlook.noBudgets', 'This project has no budgets.')}\n </p>\n ) : (\n <ul className=\"space-y-1.5\">\n {outlook.budgetProjection.map((b) => (\n <li\n key={b.budgetId}\n className=\"flex items-center justify-between gap-2 text-xs\"\n >\n <span className=\"truncate font-medium text-foreground\">{b.name}</span>\n <span\n className={`shrink-0 tabular-nums ${\n b.projectedOverage !== null && b.projectedOverage > 0\n ? 'text-status-error-text'\n : 'text-muted-foreground'\n }`}\n >\n {b.spentAmount.toLocaleString()} / {b.totalAmount.toLocaleString()}\n {b.projectedOverage !== null &&\n ` · ${\n b.projectedOverage > 0\n ? t(\n 'planmagnet.forecast.outlook.projectedOver',\n 'projected +{{amount}} over',\n {\n amount: Math.round(b.projectedOverage).toLocaleString(),\n }\n )\n : t(\n 'planmagnet.forecast.outlook.projectedUnder',\n 'projected within plan'\n )\n }`}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {outlook.inputs.length > 0 && (\n <div>\n <h3 className=\"mb-1 text-sm font-medium\">\n {t('planmagnet.forecast.outlook.inputs', 'What this is based on')}\n </h3>\n <ul className=\"list-inside list-disc space-y-0.5 text-xs text-muted-foreground\">\n {outlook.inputs.map((note, i) => (\n // Plain-language notes carry no stable id of their own; the list is short and never reordered.\n <li key={`${i}-${note.slice(0, 24)}`}>{note}</li>\n ))}\n </ul>\n </div>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRiskSummaryCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/forecast/ProjectRiskSummaryCard.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,sBAAsB,CAAC,EACrC,WAAW,EACX,SAAS,GACV,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB,2CAkFA"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
+
import { isForbidden as t } from "../utils/graphqlErrors.js";
|
|
3
|
+
import { enumLabel as n } from "../controls/WorkItemControls.js";
|
|
4
|
+
import { RISK_FLAG_KIND_ORDER as r, RISK_FLAG_SEVERITY_ORDER as i, riskFlagKindIcon as a, riskFlagSeverityBadgeVariant as o } from "./riskFlags.js";
|
|
5
|
+
import { ShieldAlert as s } from "lucide-react";
|
|
6
|
+
import { gql as c } from "@apollo/client";
|
|
7
|
+
import { useQuery as l } from "@apollo/client/react";
|
|
8
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
9
|
+
import { useI18n as f } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { Badge as p, Card as m, CardContent as h, CardHeader as g, CardTitle as _ } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/planmagnet/forecast/ProjectRiskSummaryCard.tsx
|
|
12
|
+
var v = c`
|
|
13
|
+
query PlanmagnetProjectRiskSummary($workspaceId: ID!, $projectId: ID!) {
|
|
14
|
+
planmagnetProjectRiskSummary(workspaceId: $workspaceId, projectId: $projectId) {
|
|
15
|
+
projectId
|
|
16
|
+
totalFlaggedItems
|
|
17
|
+
byKind {
|
|
18
|
+
kind
|
|
19
|
+
count
|
|
20
|
+
}
|
|
21
|
+
bySeverity {
|
|
22
|
+
severity
|
|
23
|
+
count
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
function y({ workspaceId: c, projectId: y }) {
|
|
29
|
+
let { t: b } = f(), x = e(), { data: S, loading: C, error: w } = l(v, {
|
|
30
|
+
variables: {
|
|
31
|
+
workspaceId: c,
|
|
32
|
+
projectId: y
|
|
33
|
+
},
|
|
34
|
+
skip: !c || !y
|
|
35
|
+
}), T = S?.planmagnetProjectRiskSummary, E = new Map(T?.byKind.map((e) => [e.kind, e.count]) ?? []), D = new Map(T?.bySeverity.map((e) => [e.severity, e.count]) ?? []);
|
|
36
|
+
return /* @__PURE__ */ d(m, {
|
|
37
|
+
"data-testid": "risk-summary",
|
|
38
|
+
children: [/* @__PURE__ */ d(g, {
|
|
39
|
+
className: "flex flex-row items-start justify-between space-y-0",
|
|
40
|
+
children: [/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ d(_, {
|
|
41
|
+
className: "flex items-center gap-2",
|
|
42
|
+
children: [/* @__PURE__ */ u(s, { className: "h-4 w-4" }), b("planmagnet.forecast.riskSummary.title", "At-risk work")]
|
|
43
|
+
}), /* @__PURE__ */ u("p", {
|
|
44
|
+
className: "text-xs text-muted-foreground",
|
|
45
|
+
children: b("planmagnet.forecast.riskSummary.subtitle", "Explicit threshold rules — never AI.")
|
|
46
|
+
})] }), /* @__PURE__ */ u(p, {
|
|
47
|
+
variant: "outline",
|
|
48
|
+
children: b("planmagnet.forecast.riskSummary.method", "Rule-based")
|
|
49
|
+
})]
|
|
50
|
+
}), /* @__PURE__ */ u(h, { children: C && !S ? /* @__PURE__ */ u("p", {
|
|
51
|
+
className: "text-sm text-muted-foreground",
|
|
52
|
+
children: b("planmagnet.forecast.loading", "Loading…")
|
|
53
|
+
}) : w ? /* @__PURE__ */ u("p", {
|
|
54
|
+
className: "text-sm text-status-error-text",
|
|
55
|
+
children: t(w) ? b("planmagnet.forecast.riskSummary.forbidden", "You do not have access to at-risk work for this project.") : `${b("planmagnet.forecast.riskSummary.error", "At-risk summary could not be loaded")}: ${w.message}`
|
|
56
|
+
}) : T ? T.totalFlaggedItems === 0 ? /* @__PURE__ */ u("p", {
|
|
57
|
+
className: "text-sm text-muted-foreground",
|
|
58
|
+
children: b("planmagnet.forecast.riskSummary.empty", "No flagged work items right now.")
|
|
59
|
+
}) : /* @__PURE__ */ d("div", {
|
|
60
|
+
className: "space-y-3",
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ u("div", {
|
|
63
|
+
className: "flex flex-wrap items-center gap-2",
|
|
64
|
+
children: i.filter((e) => (D.get(e) ?? 0) > 0).map((e) => /* @__PURE__ */ d(p, {
|
|
65
|
+
variant: o(e),
|
|
66
|
+
children: [
|
|
67
|
+
n(e),
|
|
68
|
+
": ",
|
|
69
|
+
D.get(e)
|
|
70
|
+
]
|
|
71
|
+
}, e))
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ u("ul", {
|
|
74
|
+
className: "space-y-1 text-xs",
|
|
75
|
+
children: r.filter((e) => (E.get(e) ?? 0) > 0).map((e) => /* @__PURE__ */ d("li", {
|
|
76
|
+
className: "flex items-center justify-between text-muted-foreground",
|
|
77
|
+
children: [/* @__PURE__ */ d("span", {
|
|
78
|
+
className: "flex items-center gap-1.5",
|
|
79
|
+
children: [/* @__PURE__ */ u(a(e), { className: "h-3.5 w-3.5" }), n(e)]
|
|
80
|
+
}), /* @__PURE__ */ u("span", {
|
|
81
|
+
className: "tabular-nums",
|
|
82
|
+
children: E.get(e)
|
|
83
|
+
})]
|
|
84
|
+
}, e))
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ u("button", {
|
|
87
|
+
type: "button",
|
|
88
|
+
className: "text-xs text-primary hover:underline",
|
|
89
|
+
onClick: () => x(`/projects/at-risk-page?projectId=${encodeURIComponent(y)}`),
|
|
90
|
+
children: b("planmagnet.forecast.riskSummary.viewAll", "View at-risk work ({{count}}) →", { count: T.totalFlaggedItems })
|
|
91
|
+
})
|
|
92
|
+
]
|
|
93
|
+
}) : null })]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { y as ProjectRiskSummaryCard };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=ProjectRiskSummaryCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectRiskSummaryCard.js","names":[],"sources":["../../../src/planmagnet/forecast/ProjectRiskSummaryCard.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Badge, Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ShieldAlert } from 'lucide-react';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport {\n RISK_FLAG_KIND_ORDER,\n RISK_FLAG_SEVERITY_ORDER,\n riskFlagKindIcon,\n riskFlagSeverityBadgeVariant,\n type RiskFlagKindValue,\n type RiskFlagSeverityValue,\n} from './riskFlags';\n\n/**\n * AI-10 at-risk counts (`planmagnetProjectRiskSummary`), compact — a project-page tile that\n * links through to the full At-risk work list (`ProjectsAtRiskPagePage`). Rule-based, never\n * AI/LLM: every count here is a deterministic threshold rule, same as `planmagnetAtRiskWorkItems`.\n */\n\nconst GET_PROJECT_RISK_SUMMARY = gql`\n query PlanmagnetProjectRiskSummary($workspaceId: ID!, $projectId: ID!) {\n planmagnetProjectRiskSummary(workspaceId: $workspaceId, projectId: $projectId) {\n projectId\n totalFlaggedItems\n byKind {\n kind\n count\n }\n bySeverity {\n severity\n count\n }\n }\n }\n`;\n\ninterface ProjectRiskSummary {\n projectId: string;\n totalFlaggedItems: number;\n byKind: { kind: RiskFlagKindValue; count: number }[];\n bySeverity: { severity: RiskFlagSeverityValue; count: number }[];\n}\n\ninterface ProjectRiskSummaryData {\n planmagnetProjectRiskSummary: ProjectRiskSummary;\n}\n\nexport function ProjectRiskSummaryCard({\n workspaceId,\n projectId,\n}: {\n workspaceId?: string | null;\n projectId: string;\n}) {\n const { t } = useI18n();\n const navigate = usePlanMagnetNavigate();\n const { data, loading, error } = useQuery<ProjectRiskSummaryData>(GET_PROJECT_RISK_SUMMARY, {\n variables: { workspaceId, projectId },\n skip: !workspaceId || !projectId,\n });\n const summary = data?.planmagnetProjectRiskSummary;\n const byKind = new Map(summary?.byKind.map((k) => [k.kind, k.count]) ?? []);\n const bySeverity = new Map(summary?.bySeverity.map((s) => [s.severity, s.count]) ?? []);\n\n return (\n <Card data-testid=\"risk-summary\">\n <CardHeader className=\"flex flex-row items-start justify-between space-y-0\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldAlert className=\"h-4 w-4\" />\n {t('planmagnet.forecast.riskSummary.title', 'At-risk work')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.forecast.riskSummary.subtitle', 'Explicit threshold rules — never AI.')}\n </p>\n </div>\n <Badge variant=\"outline\">{t('planmagnet.forecast.riskSummary.method', 'Rule-based')}</Badge>\n </CardHeader>\n <CardContent>\n {loading && !data ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.forecast.loading', 'Loading…')}\n </p>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.forecast.riskSummary.forbidden',\n 'You do not have access to at-risk work for this project.'\n )\n : `${t('planmagnet.forecast.riskSummary.error', 'At-risk summary could not be loaded')}: ${error.message}`}\n </p>\n ) : !summary ? null : summary.totalFlaggedItems === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.forecast.riskSummary.empty', 'No flagged work items right now.')}\n </p>\n ) : (\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {RISK_FLAG_SEVERITY_ORDER.filter((s) => (bySeverity.get(s) ?? 0) > 0).map((s) => (\n <Badge key={s} variant={riskFlagSeverityBadgeVariant(s)}>\n {enumLabel(s)}: {bySeverity.get(s)}\n </Badge>\n ))}\n </div>\n <ul className=\"space-y-1 text-xs\">\n {RISK_FLAG_KIND_ORDER.filter((k) => (byKind.get(k) ?? 0) > 0).map((k) => {\n const Icon = riskFlagKindIcon(k);\n return (\n <li key={k} className=\"flex items-center justify-between text-muted-foreground\">\n <span className=\"flex items-center gap-1.5\">\n <Icon className=\"h-3.5 w-3.5\" />\n {enumLabel(k)}\n </span>\n <span className=\"tabular-nums\">{byKind.get(k)}</span>\n </li>\n );\n })}\n </ul>\n <button\n type=\"button\"\n className=\"text-xs text-primary hover:underline\"\n onClick={() =>\n navigate(`/projects/at-risk-page?projectId=${encodeURIComponent(projectId)}`)\n }\n >\n {t('planmagnet.forecast.riskSummary.viewAll', 'View at-risk work ({{count}}) →', {\n count: summary.totalFlaggedItems,\n })}\n </button>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;;;AAuBA,IAAM,IAA2B,CAAG;;;;;;;;;;;;;;;;AA4BpC,SAAgB,EAAuB,EACrC,gBACA,gBAIC;CACD,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAuB,EAClC,EAAE,SAAM,YAAS,aAAU,EAAiC,GAA0B;EAC1F,WAAW;GAAE;GAAa;GAAW;EACrC,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EACI,IAAU,GAAM,8BAChB,IAAS,IAAI,IAAI,GAAS,OAAO,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,EACrE,IAAa,IAAI,IAAI,GAAS,WAAW,KAAK,MAAM,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAEvF,QACE,kBAAC,GAAD;EAAM,eAAY;YAAlB,CACE,kBAAC,GAAD;GAAY,WAAU;aAAtB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD;IAAW,WAAU;cAArB,CACE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAClC,EAAE,yCAAyC,eAAe,CACjD;OACZ,kBAAC,KAAD;IAAG,WAAU;cACV,EAAE,4CAA4C,uCAAuC;IACpF,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD;IAAO,SAAQ;cAAW,EAAE,0CAA0C,aAAa;IAAS,CAAA,CACjF;MACb,kBAAC,GAAD,EAAA,UACG,KAAW,CAAC,IACX,kBAAC,KAAD;GAAG,WAAU;aACV,EAAE,+BAA+B,WAAW;GAC3C,CAAA,GACF,IACF,kBAAC,KAAD;GAAG,WAAU;aACV,EAAY,EAAM,GACf,EACE,6CACA,2DACD,GACD,GAAG,EAAE,yCAAyC,sCAAsC,CAAC,IAAI,EAAM;GACjG,CAAA,GACD,IAAiB,EAAQ,sBAAsB,IAClD,kBAAC,KAAD;GAAG,WAAU;aACV,EAAE,yCAAyC,mCAAmC;GAC7E,CAAA,GAEJ,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAyB,QAAQ,OAAO,EAAW,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC,KAAK,MACzE,kBAAC,GAAD;MAAe,SAAS,EAA6B,EAAE;gBAAvD;OACG,EAAU,EAAE;OAAC;OAAG,EAAW,IAAI,EAAE;OAC5B;QAFI,EAEJ,CACR;KACE,CAAA;IACN,kBAAC,MAAD;KAAI,WAAU;eACX,EAAqB,QAAQ,OAAO,EAAO,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC,KAAK,MAG/D,kBAAC,MAAD;MAAY,WAAU;gBAAtB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAJO,EAAiB,EAAE,EAI1B,EAAM,WAAU,eAAgB,CAAA,EAC/B,EAAU,EAAE,CACR;UACP,kBAAC,QAAD;OAAM,WAAU;iBAAgB,EAAO,IAAI,EAAE;OAAQ,CAAA,CAClD;QANI,EAMJ,CAEP;KACC,CAAA;IACL,kBAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,eACE,EAAS,oCAAoC,mBAAmB,EAAU,GAAG;eAG9E,EAAE,2CAA2C,mCAAmC,EAC/E,OAAO,EAAQ,mBAChB,CAAC;KACK,CAAA;IACL;OAtCO,MAwCH,CAAA,CACT"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function ResourceForecastCard({ workspaceId, teamId, projectId, horizonSprints, }: {
|
|
2
|
+
workspaceId?: string | null;
|
|
3
|
+
/** Exactly one of teamId/projectId — the server requires it (`horizonSprints` schema doc). */
|
|
4
|
+
teamId?: string;
|
|
5
|
+
projectId?: string;
|
|
6
|
+
horizonSprints?: number;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=ResourceForecastCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceForecastCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/forecast/ResourceForecastCard.tsx"],"names":[],"mappings":"AAyEA,wBAAgB,oBAAoB,CAAC,EACnC,WAAW,EACX,MAAM,EACN,SAAS,EACT,cAAkB,GACnB,EAAE;IACD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,2CA6KA"}
|