@burdenoff/microfe-planmagnet 2026.925.17 → 2026.926.1

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.
Files changed (59) hide show
  1. package/dist/generated/wspace-types.d.ts +76 -2
  2. package/dist/generated/wspace-types.d.ts.map +1 -1
  3. package/dist/node_modules/@apollo/client/errors/ServerError.js +17 -0
  4. package/dist/node_modules/@apollo/client/errors/ServerError.js.map +1 -0
  5. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  6. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +128 -127
  7. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  8. package/dist/planmagnet/pages/ProductsCreateProductPagePage.d.ts.map +1 -1
  9. package/dist/planmagnet/pages/ProductsCreateProductPagePage.js +93 -98
  10. package/dist/planmagnet/pages/ProductsCreateProductPagePage.js.map +1 -1
  11. package/dist/planmagnet/pages/ProductsCreateRoadmapPagePage.d.ts.map +1 -1
  12. package/dist/planmagnet/pages/ProductsCreateRoadmapPagePage.js +72 -79
  13. package/dist/planmagnet/pages/ProductsCreateRoadmapPagePage.js.map +1 -1
  14. package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
  15. package/dist/planmagnet/pages/ProductsEditProductPagePage.js +112 -123
  16. package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
  17. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  18. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +92 -98
  19. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  20. package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
  21. package/dist/planmagnet/pages/ProductsProductsPagePage.js +88 -96
  22. package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
  23. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
  24. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +102 -122
  25. package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
  26. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  27. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +116 -114
  28. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  29. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +82 -88
  31. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  32. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
  33. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +79 -79
  34. package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
  35. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  36. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +150 -154
  37. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  38. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.d.ts.map +1 -1
  39. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js +120 -109
  40. package/dist/planmagnet/pages/ProjectsPlanningEditPagePage.js.map +1 -1
  41. package/dist/planmagnet/pages/SprintsStartSprintPagePage.d.ts.map +1 -1
  42. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +75 -31
  43. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js.map +1 -1
  44. package/dist/planmagnet/releases/ReleaseReadinessCard.d.ts.map +1 -1
  45. package/dist/planmagnet/releases/ReleaseReadinessCard.js +90 -89
  46. package/dist/planmagnet/releases/ReleaseReadinessCard.js.map +1 -1
  47. package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts +13 -0
  48. package/dist/planmagnet/releases/ReleaseWorkItemsCard.d.ts.map +1 -0
  49. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js +189 -0
  50. package/dist/planmagnet/releases/ReleaseWorkItemsCard.js.map +1 -0
  51. package/dist/planmagnet/roadmaps/planningFocus.d.ts +23 -0
  52. package/dist/planmagnet/roadmaps/planningFocus.d.ts.map +1 -0
  53. package/dist/planmagnet/roadmaps/planningFocus.js +76 -0
  54. package/dist/planmagnet/roadmaps/planningFocus.js.map +1 -0
  55. package/dist/planmagnet/sprints/SprintDates.d.ts +37 -0
  56. package/dist/planmagnet/sprints/SprintDates.d.ts.map +1 -0
  57. package/dist/planmagnet/sprints/SprintDates.js +114 -0
  58. package/dist/planmagnet/sprints/SprintDates.js.map +1 -0
  59. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ReleaseReadinessCard.js","names":[],"sources":["../../../src/planmagnet/releases/ReleaseReadinessCard.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { CombinedGraphQLErrors } from '@apollo/client/errors';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n CheckCircle2,\n Link2,\n Wallet,\n Users,\n ShieldAlert,\n Clock,\n} from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { RELEASE_GATES } from './ReleaseGatesCard';\n\n/**\n * Release overview (P1.4): what stands between this release and shipping, in\n * one place: unfinished and dependency-blocked work, gates not yet passed,\n * the budget position of its project, its open risks, and the customer\n * requests that ride on its unfinished work.\n *\n * Each part is read through its OWN gated query. Budgets, risks and feature\n * requests are separate RBAC resources (a product manager can read releases\n * but not budgets), so a part the viewer may not see says so, instead of the\n * release leaking it or the whole card failing.\n */\n\nconst READINESS = gql`\n query ReleaseReadiness($releaseId: ID!) {\n planmagnetReleaseReadiness(releaseId: $releaseId) {\n scope\n totalItems\n doneItems\n openWorkItemIds\n blockers {\n workItem {\n id\n key\n title\n status\n }\n waitingOn {\n id\n key\n title\n status\n projectId\n }\n }\n }\n }\n`;\nconst BUDGETS = gql`\n query ReleaseBudgets($workspaceId: ID!, $projectId: String!) {\n planmagnetBudgets(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 50 }\n ) {\n items {\n id\n name\n status\n currency\n plannedAmount\n spentAmount\n remainingAmount\n approvedAt\n }\n }\n }\n`;\nconst RISKS = gql`\n query ReleaseRisks($workspaceId: ID!, $projectId: String!) {\n planmagnetRisks(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n title\n severity\n status\n riskScore\n }\n }\n }\n`;\nconst REQUESTS = gql`\n query ReleaseCustomerRequests($workspaceId: ID!, $projectId: String!) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n title\n status\n voteCount\n workItemId\n }\n }\n }\n`;\n\ninterface ItemRef {\n id: string;\n key: string;\n title: string;\n status: string;\n projectId?: string;\n}\ninterface Readiness {\n scope: 'WORK_ITEMS' | 'PROJECTS' | 'NONE';\n totalItems: number;\n doneItems: number;\n openWorkItemIds: string[];\n blockers: { workItem: ItemRef; waitingOn: ItemRef[] }[];\n}\ninterface Budget {\n id: string;\n name: string;\n status: string;\n currency: string;\n plannedAmount: number;\n spentAmount: number;\n remainingAmount: number;\n approvedAt: string | null;\n}\ninterface Risk {\n id: string;\n title: string;\n severity: string;\n status: string;\n riskScore: number;\n}\ninterface Request {\n id: string;\n title: string;\n status: string;\n voteCount: number;\n workItemId: string | null;\n}\n\nconst CLOSED_RISK = new Set(['RESOLVED', 'ACCEPTED', 'CLOSED']);\nconst CLOSED_REQUEST = new Set(['COMPLETED', 'DECLINED', 'DUPLICATE']);\nconst DAY = 24 * 60 * 60 * 1000;\n\nfunction denied(error: unknown): boolean {\n return (\n CombinedGraphQLErrors.is(error) && error.errors.some((e) => e.extensions?.code === 'FORBIDDEN')\n );\n}\n\n/** One part of the overview: its own title, and its own loading/denied/error state. */\nfunction Part({\n icon,\n title,\n testId,\n loading,\n error,\n children,\n}: {\n icon: ReactNode;\n title: string;\n testId: string;\n loading: boolean;\n error: unknown;\n children: ReactNode;\n}) {\n let body = children;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {denied(error)\n ? 'Your role cannot see this.'\n : `Could not be loaded: ${error instanceof Error ? error.message : 'unknown error'}`}\n </p>\n );\n } else if (loading) {\n body = <p className=\"text-sm text-muted-foreground\">Loading…</p>;\n }\n return (\n <section className=\"space-y-1.5\" data-testid={testId}>\n <h3 className=\"flex items-center gap-2 text-sm font-medium\">\n {icon}\n {title}\n </h3>\n {body}\n </section>\n );\n}\n\nconst money = (amount: number, currency: string) => {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch {\n return `${amount.toFixed(2)} ${currency}`;\n }\n};\n\nexport function ReleaseReadinessCard({\n releaseId,\n projectId,\n releaseStatus,\n plannedDate,\n}: {\n releaseId: string;\n projectId: string | null;\n releaseStatus: string;\n plannedDate: string | null;\n}) {\n const { workspaceId } = usePlanMagnet();\n // Read once: lateness is a property of the page view, not of each render.\n const [now] = useState(() => Date.now());\n const navigate = usePlanMagnetNavigate();\n const byProject = { variables: { workspaceId, projectId }, skip: !workspaceId || !projectId };\n\n const readiness = useQuery<{ planmagnetReleaseReadiness: Readiness }>(READINESS, {\n variables: { releaseId },\n });\n const gates = useQuery<{\n planmagnetRelease: { qualityGates: { id: string; name: string; status: string }[] } | null;\n }>(RELEASE_GATES, { variables: { id: releaseId } });\n const budgets = useQuery<{ planmagnetBudgets: { items: Budget[] } }>(BUDGETS, byProject);\n const risks = useQuery<{ planmagnetRisks: { items: Risk[] } }>(RISKS, byProject);\n const requests = useQuery<{ planmagnetFeatureRequests: { items: Request[] } }>(\n REQUESTS,\n byProject\n );\n\n const shipped = releaseStatus === 'RELEASED';\n const r = readiness.data?.planmagnetReleaseReadiness;\n const openGates = (gates.data?.planmagnetRelease?.qualityGates ?? []).filter(\n (g) => g.status !== 'PASSED' && g.status !== 'WAIVED'\n );\n const budgetRows = budgets.data?.planmagnetBudgets.items ?? [];\n const openRisks = (risks.data?.planmagnetRisks.items ?? [])\n .filter((risk) => !CLOSED_RISK.has(risk.status))\n .sort((a, b) => b.riskScore - a.riskScore);\n const openIds = new Set(r?.openWorkItemIds ?? []);\n const blockedIds = new Set((r?.blockers ?? []).map((b) => b.workItem.id));\n const riding = (requests.data?.planmagnetFeatureRequests.items ?? []).filter(\n (req) => req.workItemId && openIds.has(req.workItemId) && !CLOSED_REQUEST.has(req.status)\n );\n\n const daysLate =\n plannedDate && !shipped ? Math.floor((now - new Date(plannedDate).getTime()) / DAY) : null;\n\n // The headline lists every reason, not just the first one found.\n const reasons: string[] = [];\n if (daysLate !== null && daysLate > 0)\n reasons.push(`${daysLate} day${daysLate === 1 ? '' : 's'} past its planned date`);\n if (r && r.blockers.length > 0)\n reasons.push(`${r.blockers.length} blocked item${r.blockers.length === 1 ? '' : 's'}`);\n if (r && r.openWorkItemIds.length > 0)\n reasons.push(\n `${r.openWorkItemIds.length} unfinished item${r.openWorkItemIds.length === 1 ? '' : 's'}`\n );\n if (openGates.length > 0)\n reasons.push(`${openGates.length} gate${openGates.length === 1 ? '' : 's'} not passed`);\n const overBudget = budgetRows.filter((b) => b.remainingAmount < 0 || b.status === 'EXCEEDED');\n if (overBudget.length > 0) reasons.push('over budget');\n const settled = !readiness.loading && !gates.loading;\n\n return (\n <Card data-testid=\"release-readiness\">\n <CardHeader>\n <CardTitle>What stands between this release and shipping</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {shipped ? (\n <p className=\"flex items-center gap-2 text-sm text-status-success-text\">\n <CheckCircle2 className=\"h-4 w-4\" /> Released.\n </p>\n ) : settled && reasons.length === 0 && r && r.scope !== 'NONE' ? (\n <p\n className=\"flex items-center gap-2 text-sm text-status-success-text\"\n data-testid=\"readiness-headline\"\n >\n <CheckCircle2 className=\"h-4 w-4\" /> Nothing is holding this release.\n </p>\n ) : settled && reasons.length > 0 ? (\n <p\n className=\"flex items-center gap-2 text-sm text-status-warning-text\"\n data-testid=\"readiness-headline\"\n >\n <AlertTriangle className=\"h-4 w-4 shrink-0\" /> Held by: {reasons.join(', ')}.\n </p>\n ) : null}\n\n <Part\n icon={<Link2 className=\"h-4 w-4\" />}\n title=\"Work and blockers\"\n testId=\"readiness-work\"\n loading={readiness.loading && !readiness.data}\n error={readiness.error}\n >\n {r && (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">\n {r.scope === 'NONE'\n ? 'This release has no work items and no project, so there is nothing to track.'\n : r.scope === 'PROJECTS'\n ? `No work items are assigned to this release, so everything open in its project counts: ${r.totalItems} open.`\n : `${r.doneItems} of ${r.totalItems} work items done.`}\n </p>\n {r.blockers.length === 0 && r.scope !== 'NONE' && (\n <p className=\"text-sm text-muted-foreground\">\n No item is waiting on unfinished work.\n </p>\n )}\n <ul className=\"space-y-1.5\">\n {r.blockers.map((b) => (\n <li\n key={b.workItem.id}\n className=\"rounded-md border p-2 text-sm\"\n data-testid=\"readiness-blocker\"\n >\n <button\n type=\"button\"\n className=\"font-medium hover:underline text-left\"\n onClick={() =>\n navigate(\n `/tasks/task-detail-page?taskId=${encodeURIComponent(b.workItem.id)}`\n )\n }\n >\n {b.workItem.key} {b.workItem.title}\n </button>\n <span className=\"text-muted-foreground\"> waits on </span>\n {b.waitingOn.map((w, i) => (\n <span key={w.id}>\n {i > 0 && ', '}\n <button\n type=\"button\"\n className=\"hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(w.id)}`)\n }\n >\n {w.key} {w.title}\n </button>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({w.status.toLowerCase().replace('_', ' ')}\n {projectId && w.projectId && w.projectId !== projectId\n ? ', another project'\n : ''}\n )\n </span>\n </span>\n ))}\n </li>\n ))}\n </ul>\n </div>\n )}\n </Part>\n\n <Part\n icon={<ShieldAlert className=\"h-4 w-4\" />}\n title=\"Approvals\"\n testId=\"readiness-approvals\"\n loading={gates.loading && !gates.data}\n error={gates.error}\n >\n <p className=\"text-sm text-muted-foreground\">\n {openGates.length === 0\n ? 'No quality gate is waiting for a decision.'\n : `Waiting on: ${openGates.map((g) => `${g.name} (${g.status.toLowerCase().replace('_', ' ')})`).join(', ')}.`}\n </p>\n </Part>\n\n {projectId ? (\n <>\n <Part\n icon={<Wallet className=\"h-4 w-4\" />}\n title=\"Budget\"\n testId=\"readiness-budget\"\n loading={budgets.loading && !budgets.data}\n error={budgets.error}\n >\n {budgetRows.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n This release&apos;s project has no budget.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {budgetRows.map((b) => {\n const over = b.remainingAmount < 0;\n return (\n <li key={b.id} className=\"text-sm\" data-testid=\"readiness-budget-row\">\n <span className=\"font-medium\">{b.name}</span>:{' '}\n {money(b.spentAmount, b.currency)} of {money(b.plannedAmount, b.currency)}{' '}\n spent\n {over ? (\n <span className=\"text-status-error-text\">\n {' '}\n — over by {money(-b.remainingAmount, b.currency)}\n </span>\n ) : (\n <span className=\"text-muted-foreground\">\n {' '}\n — {money(b.remainingAmount, b.currency)} left\n </span>\n )}\n {!b.approvedAt && (\n <span className=\"text-status-warning-text\"> · not approved</span>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </Part>\n\n <Part\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n title=\"Open risks\"\n testId=\"readiness-risks\"\n loading={risks.loading && !risks.data}\n error={risks.error}\n >\n {openRisks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No open risks on this release&apos;s project.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {openRisks.slice(0, 5).map((risk) => (\n <li key={risk.id} className=\"text-sm\">\n <span className=\"font-medium\">{risk.title}</span>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({risk.severity.toLowerCase()}, score {risk.riskScore},{' '}\n {risk.status.toLowerCase()})\n </span>\n </li>\n ))}\n {openRisks.length > 5 && (\n <li className=\"text-xs text-muted-foreground\">\n and {openRisks.length - 5} more\n </li>\n )}\n </ul>\n )}\n </Part>\n\n <Part\n icon={<Users className=\"h-4 w-4\" />}\n title=\"Customer requests riding on unfinished work\"\n testId=\"readiness-requests\"\n loading={requests.loading && !requests.data}\n error={requests.error}\n >\n {riding.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No open customer request is tied to unfinished work in this release.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {riding.map((req) => (\n <li key={req.id} className=\"text-sm\" data-testid=\"readiness-request\">\n <span className=\"font-medium\">{req.title}</span>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({req.voteCount} vote{req.voteCount === 1 ? '' : 's'})\n </span>\n {req.workItemId && blockedIds.has(req.workItemId) && (\n <span className=\"text-status-error-text\"> — its work is blocked</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </Part>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n This release has no project, so budget, risks and customer requests cannot be shown.\n </p>\n )}\n\n {plannedDate && daysLate !== null && (\n <p className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Clock className=\"h-3.5 w-3.5\" />\n Planned for {new Date(plannedDate).toLocaleDateString()}\n {daysLate > 0 ? ` (${daysLate} day${daysLate === 1 ? '' : 's'} ago)` : ''}.\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ReleaseReadinessCard.js","names":[],"sources":["../../../src/planmagnet/releases/ReleaseReadinessCard.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n CheckCircle2,\n Link2,\n Wallet,\n Users,\n ShieldAlert,\n Clock,\n} from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { RELEASE_GATES } from './ReleaseGatesCard';\n\n/**\n * Release overview (P1.4): what stands between this release and shipping, in\n * one place: unfinished and dependency-blocked work, gates not yet passed,\n * the budget position of its project, its open risks, and the customer\n * requests that ride on its unfinished work.\n *\n * Each part is read through its OWN gated query. Budgets, risks and feature\n * requests are separate RBAC resources (a product manager can read releases\n * but not budgets), so a part the viewer may not see says so, instead of the\n * release leaking it or the whole card failing.\n */\n\nconst READINESS = gql`\n query ReleaseReadiness($releaseId: ID!) {\n planmagnetReleaseReadiness(releaseId: $releaseId) {\n scope\n totalItems\n doneItems\n openWorkItemIds\n blockers {\n workItem {\n id\n key\n title\n status\n }\n waitingOn {\n id\n key\n title\n status\n projectId\n }\n }\n }\n }\n`;\nconst BUDGETS = gql`\n query ReleaseBudgets($workspaceId: ID!, $projectId: String!) {\n planmagnetBudgets(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 50 }\n ) {\n items {\n id\n name\n status\n currency\n plannedAmount\n spentAmount\n remainingAmount\n approvedAt\n }\n }\n }\n`;\nconst RISKS = gql`\n query ReleaseRisks($workspaceId: ID!, $projectId: String!) {\n planmagnetRisks(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n title\n severity\n status\n riskScore\n }\n }\n }\n`;\nconst REQUESTS = gql`\n query ReleaseCustomerRequests($workspaceId: ID!, $projectId: String!) {\n planmagnetFeatureRequests(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n title\n status\n voteCount\n workItemId\n }\n }\n }\n`;\n\ninterface ItemRef {\n id: string;\n key: string;\n title: string;\n status: string;\n projectId?: string;\n}\ninterface Readiness {\n scope: 'WORK_ITEMS' | 'PROJECTS' | 'NONE';\n totalItems: number;\n doneItems: number;\n openWorkItemIds: string[];\n blockers: { workItem: ItemRef; waitingOn: ItemRef[] }[];\n}\ninterface Budget {\n id: string;\n name: string;\n status: string;\n currency: string;\n plannedAmount: number;\n spentAmount: number;\n remainingAmount: number;\n approvedAt: string | null;\n}\ninterface Risk {\n id: string;\n title: string;\n severity: string;\n status: string;\n riskScore: number;\n}\ninterface Request {\n id: string;\n title: string;\n status: string;\n voteCount: number;\n workItemId: string | null;\n}\n\nconst CLOSED_RISK = new Set(['RESOLVED', 'ACCEPTED', 'CLOSED']);\nconst CLOSED_REQUEST = new Set(['COMPLETED', 'DECLINED', 'DUPLICATE']);\nconst DAY = 24 * 60 * 60 * 1000;\n\n/**\n * Is this the gateway refusing the viewer's role? The gateway answers an RBAC\n * denial with HTTP 403 (400 inside a batch) and `extensions.code: FORBIDDEN`.\n * Depending on the fetch path Apollo surfaces that as GraphQL errors or as a\n * ServerError carrying the body, so both are read.\n */\nfunction denied(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'FORBIDDEN');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 403 || /\"code\"\\s*:\\s*\"FORBIDDEN\"/.test(error.bodyText);\n }\n return false;\n}\n\n/** One part of the overview: its own title, and its own loading/denied/error state. */\nfunction Part({\n icon,\n title,\n testId,\n loading,\n error,\n children,\n}: {\n icon: ReactNode;\n title: string;\n testId: string;\n loading: boolean;\n error: unknown;\n children: ReactNode;\n}) {\n let body = children;\n if (error) {\n body = (\n <p className=\"text-sm text-muted-foreground\">\n {denied(error)\n ? 'Your role cannot see this.'\n : `Could not be loaded: ${error instanceof Error ? error.message : 'unknown error'}`}\n </p>\n );\n } else if (loading) {\n body = <p className=\"text-sm text-muted-foreground\">Loading…</p>;\n }\n return (\n <section className=\"space-y-1.5\" data-testid={testId}>\n <h3 className=\"flex items-center gap-2 text-sm font-medium\">\n {icon}\n {title}\n </h3>\n {body}\n </section>\n );\n}\n\nconst money = (amount: number, currency: string) => {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch {\n return `${amount.toFixed(2)} ${currency}`;\n }\n};\n\nexport function ReleaseReadinessCard({\n releaseId,\n projectId,\n releaseStatus,\n plannedDate,\n}: {\n releaseId: string;\n projectId: string | null;\n releaseStatus: string;\n plannedDate: string | null;\n}) {\n const { workspaceId } = usePlanMagnet();\n // Read once: lateness is a property of the page view, not of each render.\n const [now] = useState(() => Date.now());\n const navigate = usePlanMagnetNavigate();\n const byProject = { variables: { workspaceId, projectId }, skip: !workspaceId || !projectId };\n\n const readiness = useQuery<{ planmagnetReleaseReadiness: Readiness }>(READINESS, {\n variables: { releaseId },\n });\n const gates = useQuery<{\n planmagnetRelease: { qualityGates: { id: string; name: string; status: string }[] } | null;\n }>(RELEASE_GATES, { variables: { id: releaseId } });\n const budgets = useQuery<{ planmagnetBudgets: { items: Budget[] } }>(BUDGETS, byProject);\n const risks = useQuery<{ planmagnetRisks: { items: Risk[] } }>(RISKS, byProject);\n const requests = useQuery<{ planmagnetFeatureRequests: { items: Request[] } }>(\n REQUESTS,\n byProject\n );\n\n const shipped = releaseStatus === 'RELEASED';\n const r = readiness.data?.planmagnetReleaseReadiness;\n const openGates = (gates.data?.planmagnetRelease?.qualityGates ?? []).filter(\n (g) => g.status !== 'PASSED' && g.status !== 'WAIVED'\n );\n const budgetRows = budgets.data?.planmagnetBudgets.items ?? [];\n const openRisks = (risks.data?.planmagnetRisks.items ?? [])\n .filter((risk) => !CLOSED_RISK.has(risk.status))\n .sort((a, b) => b.riskScore - a.riskScore);\n const openIds = new Set(r?.openWorkItemIds ?? []);\n const blockedIds = new Set((r?.blockers ?? []).map((b) => b.workItem.id));\n const riding = (requests.data?.planmagnetFeatureRequests.items ?? []).filter(\n (req) => req.workItemId && openIds.has(req.workItemId) && !CLOSED_REQUEST.has(req.status)\n );\n\n const daysLate =\n plannedDate && !shipped ? Math.floor((now - new Date(plannedDate).getTime()) / DAY) : null;\n\n // The headline lists every reason, not just the first one found.\n const reasons: string[] = [];\n if (daysLate !== null && daysLate > 0)\n reasons.push(`${daysLate} day${daysLate === 1 ? '' : 's'} past its planned date`);\n if (r && r.blockers.length > 0)\n reasons.push(`${r.blockers.length} blocked item${r.blockers.length === 1 ? '' : 's'}`);\n if (r && r.openWorkItemIds.length > 0)\n reasons.push(\n `${r.openWorkItemIds.length} unfinished item${r.openWorkItemIds.length === 1 ? '' : 's'}`\n );\n if (openGates.length > 0)\n reasons.push(`${openGates.length} gate${openGates.length === 1 ? '' : 's'} not passed`);\n const overBudget = budgetRows.filter((b) => b.remainingAmount < 0 || b.status === 'EXCEEDED');\n if (overBudget.length > 0) reasons.push('over budget');\n const settled = !readiness.loading && !gates.loading;\n\n return (\n <Card data-testid=\"release-readiness\">\n <CardHeader>\n <CardTitle>What stands between this release and shipping</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n {shipped ? (\n <p className=\"flex items-center gap-2 text-sm text-status-success-text\">\n <CheckCircle2 className=\"h-4 w-4\" /> Released.\n </p>\n ) : settled && reasons.length === 0 && r && r.scope !== 'NONE' ? (\n <p\n className=\"flex items-center gap-2 text-sm text-status-success-text\"\n data-testid=\"readiness-headline\"\n >\n <CheckCircle2 className=\"h-4 w-4\" /> Nothing is holding this release.\n </p>\n ) : settled && reasons.length > 0 ? (\n <p\n className=\"flex items-center gap-2 text-sm text-status-warning-text\"\n data-testid=\"readiness-headline\"\n >\n <AlertTriangle className=\"h-4 w-4 shrink-0\" /> Held by: {reasons.join(', ')}.\n </p>\n ) : null}\n\n <Part\n icon={<Link2 className=\"h-4 w-4\" />}\n title=\"Work and blockers\"\n testId=\"readiness-work\"\n loading={readiness.loading && !readiness.data}\n error={readiness.error}\n >\n {r && (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground\">\n {r.scope === 'NONE'\n ? 'This release has no work items and no project, so there is nothing to track.'\n : r.scope === 'PROJECTS'\n ? `No work items are assigned to this release, so everything open in its project counts: ${r.totalItems} open.`\n : `${r.doneItems} of ${r.totalItems} work items done.`}\n </p>\n {r.blockers.length === 0 && r.scope !== 'NONE' && (\n <p className=\"text-sm text-muted-foreground\">\n No item is waiting on unfinished work.\n </p>\n )}\n <ul className=\"space-y-1.5\">\n {r.blockers.map((b) => (\n <li\n key={b.workItem.id}\n className=\"rounded-md border p-2 text-sm\"\n data-testid=\"readiness-blocker\"\n >\n <button\n type=\"button\"\n className=\"font-medium hover:underline text-left\"\n onClick={() =>\n navigate(\n `/tasks/task-detail-page?taskId=${encodeURIComponent(b.workItem.id)}`\n )\n }\n >\n {b.workItem.key} {b.workItem.title}\n </button>\n <span className=\"text-muted-foreground\"> waits on </span>\n {b.waitingOn.map((w, i) => (\n <span key={w.id}>\n {i > 0 && ', '}\n <button\n type=\"button\"\n className=\"hover:underline\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(w.id)}`)\n }\n >\n {w.key} {w.title}\n </button>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({w.status.toLowerCase().replace('_', ' ')}\n {projectId && w.projectId && w.projectId !== projectId\n ? ', another project'\n : ''}\n )\n </span>\n </span>\n ))}\n </li>\n ))}\n </ul>\n </div>\n )}\n </Part>\n\n <Part\n icon={<ShieldAlert className=\"h-4 w-4\" />}\n title=\"Approvals\"\n testId=\"readiness-approvals\"\n loading={gates.loading && !gates.data}\n error={gates.error}\n >\n <p className=\"text-sm text-muted-foreground\">\n {openGates.length === 0\n ? 'No quality gate is waiting for a decision.'\n : `Waiting on: ${openGates.map((g) => `${g.name} (${g.status.toLowerCase().replace('_', ' ')})`).join(', ')}.`}\n </p>\n </Part>\n\n {projectId ? (\n <>\n <Part\n icon={<Wallet className=\"h-4 w-4\" />}\n title=\"Budget\"\n testId=\"readiness-budget\"\n loading={budgets.loading && !budgets.data}\n error={budgets.error}\n >\n {budgetRows.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n This release&apos;s project has no budget.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {budgetRows.map((b) => {\n const over = b.remainingAmount < 0;\n return (\n <li key={b.id} className=\"text-sm\" data-testid=\"readiness-budget-row\">\n <span className=\"font-medium\">{b.name}</span>:{' '}\n {money(b.spentAmount, b.currency)} of {money(b.plannedAmount, b.currency)}{' '}\n spent\n {over ? (\n <span className=\"text-status-error-text\">\n {' '}\n — over by {money(-b.remainingAmount, b.currency)}\n </span>\n ) : (\n <span className=\"text-muted-foreground\">\n {' '}\n — {money(b.remainingAmount, b.currency)} left\n </span>\n )}\n {!b.approvedAt && (\n <span className=\"text-status-warning-text\"> · not approved</span>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </Part>\n\n <Part\n icon={<AlertTriangle className=\"h-4 w-4\" />}\n title=\"Open risks\"\n testId=\"readiness-risks\"\n loading={risks.loading && !risks.data}\n error={risks.error}\n >\n {openRisks.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No open risks on this release&apos;s project.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {openRisks.slice(0, 5).map((risk) => (\n <li key={risk.id} className=\"text-sm\">\n <span className=\"font-medium\">{risk.title}</span>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({risk.severity.toLowerCase()}, score {risk.riskScore},{' '}\n {risk.status.toLowerCase()})\n </span>\n </li>\n ))}\n {openRisks.length > 5 && (\n <li className=\"text-xs text-muted-foreground\">\n and {openRisks.length - 5} more\n </li>\n )}\n </ul>\n )}\n </Part>\n\n <Part\n icon={<Users className=\"h-4 w-4\" />}\n title=\"Customer requests riding on unfinished work\"\n testId=\"readiness-requests\"\n loading={requests.loading && !requests.data}\n error={requests.error}\n >\n {riding.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No open customer request is tied to unfinished work in this release.\n </p>\n ) : (\n <ul className=\"space-y-1\">\n {riding.map((req) => (\n <li key={req.id} className=\"text-sm\" data-testid=\"readiness-request\">\n <span className=\"font-medium\">{req.title}</span>{' '}\n <span className=\"text-xs text-muted-foreground\">\n ({req.voteCount} vote{req.voteCount === 1 ? '' : 's'})\n </span>\n {req.workItemId && blockedIds.has(req.workItemId) && (\n <span className=\"text-status-error-text\"> — its work is blocked</span>\n )}\n </li>\n ))}\n </ul>\n )}\n </Part>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n This release has no project, so budget, risks and customer requests cannot be shown.\n </p>\n )}\n\n {plannedDate && daysLate !== null && (\n <p className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Clock className=\"h-3.5 w-3.5\" />\n Planned for {new Date(plannedDate).toLocaleDateString()}\n {daysLate > 0 ? ` (${daysLate} day${daysLate === 1 ? '' : 's'} ago)` : ''}.\n </p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export interface ReleaseWorkItem {
2
+ id: string;
3
+ title: string;
4
+ status: string;
5
+ type: string;
6
+ priority: string;
7
+ }
8
+ export declare function ReleaseWorkItemsCard({ releaseId, projectId, items, }: {
9
+ releaseId: string;
10
+ projectId: string | null;
11
+ items: ReleaseWorkItem[];
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=ReleaseWorkItemsCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReleaseWorkItemsCard.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/releases/ReleaseWorkItemsCard.tsx"],"names":[],"mappings":"AAkDA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,KAAK,GACN,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B,2CAiKA"}
@@ -0,0 +1,189 @@
1
+ import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
+ import { throwOnError as n } from "../utils/mutationResult.js";
3
+ import { useDeferredValue as r, useState as i } from "react";
4
+ import { Plus as a, X as o } from "lucide-react";
5
+ import { gql as s } from "@apollo/client";
6
+ import { useMutation as c, useQuery as l } from "@apollo/client/react";
7
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
8
+ import { Badge as f, Button as p, Card as m, CardContent as h, CardHeader as g, CardTitle as _, Input as v } from "@burdenoff/fe-libs/ui";
9
+ //#region src/planmagnet/releases/ReleaseWorkItemsCard.tsx
10
+ var y = s`
11
+ query ReleaseWorkItemCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {
12
+ planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {
13
+ items {
14
+ id
15
+ key
16
+ title
17
+ status
18
+ }
19
+ }
20
+ }
21
+ `, b = s`
22
+ mutation ReleaseAddWorkItems($releaseId: ID!, $workItemIds: [ID!]!) {
23
+ planmagnetAddWorkItemsToRelease(releaseId: $releaseId, workItemIds: $workItemIds) {
24
+ id
25
+ }
26
+ }
27
+ `, x = s`
28
+ mutation ReleaseRemoveWorkItems($releaseId: ID!, $workItemIds: [ID!]!) {
29
+ planmagnetRemoveWorkItemsFromRelease(releaseId: $releaseId, workItemIds: $workItemIds) {
30
+ id
31
+ }
32
+ }
33
+ `;
34
+ function S({ releaseId: s, projectId: S, items: C }) {
35
+ let { workspaceId: w } = e(), T = t(), [E, D] = i(!1), [O, k] = i(""), [A, j] = i(null), M = r(O.trim()), N = l(y, {
36
+ variables: {
37
+ workspaceId: w,
38
+ filter: {
39
+ projectId: S,
40
+ ...M ? { search: M } : {}
41
+ }
42
+ },
43
+ skip: !E || !w || !S
44
+ }), P = {
45
+ refetchQueries: ["GetRelease", "ReleaseReadiness"],
46
+ awaitRefetchQueries: !0
47
+ }, [F, I] = c(b, P), [L, R] = c(x, P), z = new Set(C.map((e) => e.id)), B = (N.data?.planmagnetWorkItems.items ?? []).filter((e) => !z.has(e.id)), V = async (e, t) => {
48
+ j(null);
49
+ try {
50
+ await n(e());
51
+ } catch (e) {
52
+ j(e instanceof Error ? e.message : t);
53
+ }
54
+ };
55
+ return /* @__PURE__ */ d(m, {
56
+ "data-testid": "release-work-items",
57
+ children: [/* @__PURE__ */ d(g, {
58
+ className: "flex flex-row items-center justify-between space-y-0",
59
+ children: [/* @__PURE__ */ d(_, { children: [
60
+ "Work Items (",
61
+ C.length,
62
+ ")"
63
+ ] }), S && !E && /* @__PURE__ */ d(p, {
64
+ size: "sm",
65
+ variant: "outline",
66
+ onClick: () => D(!0),
67
+ "data-testid": "release-items-add",
68
+ children: [/* @__PURE__ */ u(a, { className: "mr-1 h-4 w-4" }), " Add work items"]
69
+ })]
70
+ }), /* @__PURE__ */ d(h, {
71
+ className: "space-y-2",
72
+ children: [
73
+ A && /* @__PURE__ */ u("p", {
74
+ role: "alert",
75
+ className: "text-sm text-destructive",
76
+ children: A
77
+ }),
78
+ C.length === 0 && /* @__PURE__ */ u("p", {
79
+ className: "text-sm text-muted-foreground",
80
+ children: "No work items are in this release yet, so its overview and release notes cover everything in its project."
81
+ }),
82
+ C.map((e) => /* @__PURE__ */ d("div", {
83
+ className: "flex items-center justify-between gap-2 rounded-md border p-3",
84
+ "data-testid": "release-item",
85
+ children: [/* @__PURE__ */ d("button", {
86
+ type: "button",
87
+ className: "min-w-0 text-left",
88
+ onClick: () => T(`/tasks/task-detail-page?taskId=${encodeURIComponent(e.id)}`),
89
+ children: [/* @__PURE__ */ u("p", {
90
+ className: "truncate text-sm font-medium hover:underline",
91
+ children: e.title
92
+ }), /* @__PURE__ */ d("p", {
93
+ className: "text-xs capitalize text-muted-foreground",
94
+ children: [
95
+ e.type.toLowerCase(),
96
+ " · ",
97
+ e.priority.toLowerCase(),
98
+ " priority"
99
+ ]
100
+ })]
101
+ }), /* @__PURE__ */ d("span", {
102
+ className: "flex shrink-0 items-center gap-1",
103
+ children: [/* @__PURE__ */ u(f, {
104
+ variant: "outline",
105
+ className: "text-xs capitalize",
106
+ children: e.status.toLowerCase().replace("_", " ")
107
+ }), /* @__PURE__ */ u(p, {
108
+ size: "icon",
109
+ variant: "ghost",
110
+ "aria-label": `Remove ${e.title} from this release`,
111
+ disabled: R.loading,
112
+ onClick: () => V(() => L({ variables: {
113
+ releaseId: s,
114
+ workItemIds: [e.id]
115
+ } }), "The work item could not be removed."),
116
+ children: /* @__PURE__ */ u(o, { className: "h-4 w-4" })
117
+ })]
118
+ })]
119
+ }, e.id)),
120
+ E && /* @__PURE__ */ d("div", {
121
+ className: "space-y-2 border-t pt-3",
122
+ children: [
123
+ /* @__PURE__ */ d("div", {
124
+ className: "flex gap-2",
125
+ children: [/* @__PURE__ */ u(v, {
126
+ "aria-label": "Find work items in this release's project",
127
+ placeholder: "Find work items in this project",
128
+ value: O,
129
+ onChange: (e) => k(e.target.value)
130
+ }), /* @__PURE__ */ u(p, {
131
+ size: "sm",
132
+ variant: "ghost",
133
+ onClick: () => {
134
+ D(!1), k("");
135
+ },
136
+ children: "Done"
137
+ })]
138
+ }),
139
+ N.loading && !N.data && /* @__PURE__ */ u("p", {
140
+ className: "text-sm text-muted-foreground",
141
+ children: "Loading…"
142
+ }),
143
+ N.error && /* @__PURE__ */ d("p", {
144
+ role: "alert",
145
+ className: "text-sm text-destructive",
146
+ children: ["Work items could not be loaded: ", N.error.message]
147
+ }),
148
+ N.data && B.length === 0 && /* @__PURE__ */ u("p", {
149
+ className: "text-sm text-muted-foreground",
150
+ children: "No other work items match."
151
+ }),
152
+ /* @__PURE__ */ u("ul", {
153
+ className: "space-y-1",
154
+ children: B.map((e) => /* @__PURE__ */ d("li", {
155
+ className: "flex items-center justify-between gap-2 text-sm",
156
+ children: [/* @__PURE__ */ d("span", {
157
+ className: "min-w-0 truncate",
158
+ children: [
159
+ /* @__PURE__ */ u("span", {
160
+ className: "text-muted-foreground",
161
+ children: e.key
162
+ }),
163
+ " ",
164
+ e.title
165
+ ]
166
+ }), /* @__PURE__ */ u(p, {
167
+ size: "sm",
168
+ variant: "outline",
169
+ className: "h-7 shrink-0 text-xs",
170
+ disabled: I.loading,
171
+ "data-testid": "release-item-candidate-add",
172
+ onClick: () => V(() => F({ variables: {
173
+ releaseId: s,
174
+ workItemIds: [e.id]
175
+ } }), "The work item could not be added."),
176
+ children: "Add"
177
+ })]
178
+ }, e.id))
179
+ })
180
+ ]
181
+ })
182
+ ]
183
+ })]
184
+ });
185
+ }
186
+ //#endregion
187
+ export { S as ReleaseWorkItemsCard };
188
+
189
+ //# sourceMappingURL=ReleaseWorkItemsCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReleaseWorkItemsCard.js","names":[],"sources":["../../../src/planmagnet/releases/ReleaseWorkItemsCard.tsx"],"sourcesContent":["import { useDeferredValue, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { Plus, X } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { throwOnError } from '../utils/mutationResult';\n\n/**\n * What is in this release (BOFF-7931). Until the service could write a\n * release's work items this card never rendered: every release was empty and\n * its scope silently fell back to its whole project. Items can be added from\n * the release's project only; the service refuses anything else and says why.\n */\n\nconst CANDIDATES = gql`\n query ReleaseWorkItemCandidates($workspaceId: ID!, $filter: PlanmagnetWorkItemFilter) {\n planmagnetWorkItems(workspaceId: $workspaceId, filter: $filter, pagination: { first: 20 }) {\n items {\n id\n key\n title\n status\n }\n }\n }\n`;\nconst ADD = gql`\n mutation ReleaseAddWorkItems($releaseId: ID!, $workItemIds: [ID!]!) {\n planmagnetAddWorkItemsToRelease(releaseId: $releaseId, workItemIds: $workItemIds) {\n id\n }\n }\n`;\nconst REMOVE = gql`\n mutation ReleaseRemoveWorkItems($releaseId: ID!, $workItemIds: [ID!]!) {\n planmagnetRemoveWorkItemsFromRelease(releaseId: $releaseId, workItemIds: $workItemIds) {\n id\n }\n }\n`;\n\nexport interface ReleaseWorkItem {\n id: string;\n title: string;\n status: string;\n type: string;\n priority: string;\n}\n\nexport function ReleaseWorkItemsCard({\n releaseId,\n projectId,\n items,\n}: {\n releaseId: string;\n projectId: string | null;\n items: ReleaseWorkItem[];\n}) {\n const { workspaceId } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const [adding, setAdding] = useState(false);\n const [search, setSearch] = useState('');\n const [error, setError] = useState<string | null>(null);\n const term = useDeferredValue(search.trim());\n\n const candidates = useQuery<{\n planmagnetWorkItems: { items: { id: string; key: string; title: string; status: string }[] };\n }>(CANDIDATES, {\n variables: { workspaceId, filter: { projectId, ...(term ? { search: term } : {}) } },\n skip: !adding || !workspaceId || !projectId,\n });\n const refetch = {\n refetchQueries: ['GetRelease', 'ReleaseReadiness'],\n awaitRefetchQueries: true,\n };\n const [add, addState] = useMutation(ADD, refetch);\n const [remove, removeState] = useMutation(REMOVE, refetch);\n\n const inRelease = new Set(items.map((i) => i.id));\n const offered = (candidates.data?.planmagnetWorkItems.items ?? []).filter(\n (c) => !inRelease.has(c.id)\n );\n\n const change = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n } catch (e) {\n setError(e instanceof Error ? e.message : fallback);\n }\n };\n\n return (\n <Card data-testid=\"release-work-items\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Work Items ({items.length})</CardTitle>\n {projectId && !adding && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setAdding(true)}\n data-testid=\"release-items-add\"\n >\n <Plus className=\"mr-1 h-4 w-4\" /> Add work items\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n {items.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n No work items are in this release yet, so its overview and release notes cover\n everything in its project.\n </p>\n )}\n {items.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between gap-2 rounded-md border p-3\"\n data-testid=\"release-item\"\n >\n <button\n type=\"button\"\n className=\"min-w-0 text-left\"\n onClick={() =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(item.id)}`)\n }\n >\n <p className=\"truncate text-sm font-medium hover:underline\">{item.title}</p>\n <p className=\"text-xs capitalize text-muted-foreground\">\n {item.type.toLowerCase()} &middot; {item.priority.toLowerCase()} priority\n </p>\n </button>\n <span className=\"flex shrink-0 items-center gap-1\">\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {item.status.toLowerCase().replace('_', ' ')}\n </Badge>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n aria-label={`Remove ${item.title} from this release`}\n disabled={removeState.loading}\n onClick={() =>\n change(\n () => remove({ variables: { releaseId, workItemIds: [item.id] } }),\n 'The work item could not be removed.'\n )\n }\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </span>\n </div>\n ))}\n {adding && (\n <div className=\"space-y-2 border-t pt-3\">\n <div className=\"flex gap-2\">\n <Input\n aria-label=\"Find work items in this release's project\"\n placeholder=\"Find work items in this project\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setAdding(false);\n setSearch('');\n }}\n >\n Done\n </Button>\n </div>\n {candidates.loading && !candidates.data && (\n <p className=\"text-sm text-muted-foreground\">Loading…</p>\n )}\n {candidates.error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n Work items could not be loaded: {candidates.error.message}\n </p>\n )}\n {candidates.data && offered.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">No other work items match.</p>\n )}\n <ul className=\"space-y-1\">\n {offered.map((c) => (\n <li key={c.id} className=\"flex items-center justify-between gap-2 text-sm\">\n <span className=\"min-w-0 truncate\">\n <span className=\"text-muted-foreground\">{c.key}</span> {c.title}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 shrink-0 text-xs\"\n disabled={addState.loading}\n data-testid=\"release-item-candidate-add\"\n onClick={() =>\n change(\n () => add({ variables: { releaseId, workItemIds: [c.id] } }),\n 'The work item could not be added.'\n )\n }\n >\n Add\n </Button>\n </li>\n ))}\n </ul>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What a roadmap is organised around (BOFF-7934). Separate from how it is
3
+ * displayed: the roadmap page has ONE layout today (items grouped by lane with
4
+ * their dates), so the service accepts only the TIMELINE display and these
5
+ * pages no longer offer Gantt / Swimlane / Custom — a stored label is not a
6
+ * view. Legacy roadmaps were never classified, so a missing focus is shown as
7
+ * "not set", never guessed.
8
+ */
9
+ export type PlanningFocus = 'THEME' | 'FEATURE' | 'TECHNOLOGY' | 'PORTFOLIO';
10
+ export declare const PLANNING_FOCUSES: readonly PlanningFocus[];
11
+ /** The only display the roadmap page has; the create/update inputs require one. */
12
+ export declare const ROADMAP_DISPLAY = "TIMELINE";
13
+ export declare function planningFocusLabel(focus: string | null | undefined): string;
14
+ /** Icon AND label, never colour alone. */
15
+ export declare function PlanningFocusBadge({ focus }: {
16
+ focus: string | null | undefined;
17
+ }): import("react/jsx-runtime").JSX.Element;
18
+ export declare function PlanningFocusSelect({ value, onChange, id, }: {
19
+ value: PlanningFocus | null;
20
+ onChange: (value: PlanningFocus | null) => void;
21
+ id?: string;
22
+ }): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=planningFocus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planningFocus.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/roadmaps/planningFocus.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,CAAC;AAE7E,eAAO,MAAM,gBAAgB,EAAE,SAAS,aAAa,EAKpD,CAAC;AASF,mFAAmF;AACnF,eAAO,MAAM,eAAe,aAAa,CAAC;AAE1C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE3E;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,2CAoBjF;AAED,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,EAAqB,GACtB,EAAE;IACD,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,2CAwBA"}
@@ -0,0 +1,76 @@
1
+ import { Boxes as e, Cpu as t, Layers as n, Lightbulb as r } from "lucide-react";
2
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
3
+ //#region src/planmagnet/roadmaps/planningFocus.tsx
4
+ var o = [
5
+ "THEME",
6
+ "FEATURE",
7
+ "TECHNOLOGY",
8
+ "PORTFOLIO"
9
+ ], s = {
10
+ THEME: {
11
+ label: "Theme",
12
+ icon: r
13
+ },
14
+ FEATURE: {
15
+ label: "Feature",
16
+ icon: n
17
+ },
18
+ TECHNOLOGY: {
19
+ label: "Technology",
20
+ icon: t
21
+ },
22
+ PORTFOLIO: {
23
+ label: "Portfolio",
24
+ icon: e
25
+ }
26
+ }, c = "TIMELINE";
27
+ function l(e) {
28
+ return e && e in s ? s[e].label : "Focus not set";
29
+ }
30
+ function u({ focus: e }) {
31
+ if (!e || !(e in s)) return /* @__PURE__ */ i("span", {
32
+ className: "inline-flex items-center gap-1 text-xs text-muted-foreground",
33
+ "data-testid": "planning-focus",
34
+ children: "Focus not set"
35
+ });
36
+ let { label: t, icon: n } = s[e];
37
+ return /* @__PURE__ */ a("span", {
38
+ className: "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-foreground",
39
+ "data-testid": "planning-focus",
40
+ children: [
41
+ /* @__PURE__ */ i(n, { className: "h-3 w-3" }),
42
+ " ",
43
+ t
44
+ ]
45
+ });
46
+ }
47
+ function d({ value: e, onChange: t, id: n = "planning-focus" }) {
48
+ return /* @__PURE__ */ a("div", { children: [
49
+ /* @__PURE__ */ i("label", {
50
+ htmlFor: n,
51
+ className: "text-sm font-medium",
52
+ children: "Planning focus"
53
+ }),
54
+ /* @__PURE__ */ a("select", {
55
+ id: n,
56
+ className: "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
57
+ value: e ?? "",
58
+ onChange: (e) => t(e.target.value ? e.target.value : null),
59
+ children: [/* @__PURE__ */ i("option", {
60
+ value: "",
61
+ children: "Not set"
62
+ }), o.map((e) => /* @__PURE__ */ i("option", {
63
+ value: e,
64
+ children: s[e].label
65
+ }, e))]
66
+ }),
67
+ /* @__PURE__ */ i("p", {
68
+ className: "mt-1 text-xs text-muted-foreground",
69
+ children: "What the roadmap is organised around. Roadmaps are shown in one layout for now."
70
+ })
71
+ ] });
72
+ }
73
+ //#endregion
74
+ export { o as PLANNING_FOCUSES, u as PlanningFocusBadge, d as PlanningFocusSelect, c as ROADMAP_DISPLAY, l as planningFocusLabel };
75
+
76
+ //# sourceMappingURL=planningFocus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planningFocus.js","names":[],"sources":["../../../src/planmagnet/roadmaps/planningFocus.tsx"],"sourcesContent":["import { Boxes, Cpu, Layers, Lightbulb, type LucideIcon } from 'lucide-react';\n\n/**\n * What a roadmap is organised around (BOFF-7934). Separate from how it is\n * displayed: the roadmap page has ONE layout today (items grouped by lane with\n * their dates), so the service accepts only the TIMELINE display and these\n * pages no longer offer Gantt / Swimlane / Custom — a stored label is not a\n * view. Legacy roadmaps were never classified, so a missing focus is shown as\n * \"not set\", never guessed.\n */\nexport type PlanningFocus = 'THEME' | 'FEATURE' | 'TECHNOLOGY' | 'PORTFOLIO';\n\nexport const PLANNING_FOCUSES: readonly PlanningFocus[] = [\n 'THEME',\n 'FEATURE',\n 'TECHNOLOGY',\n 'PORTFOLIO',\n];\n\nconst FOCUS: Record<PlanningFocus, { label: string; icon: LucideIcon }> = {\n THEME: { label: 'Theme', icon: Lightbulb },\n FEATURE: { label: 'Feature', icon: Layers },\n TECHNOLOGY: { label: 'Technology', icon: Cpu },\n PORTFOLIO: { label: 'Portfolio', icon: Boxes },\n};\n\n/** The only display the roadmap page has; the create/update inputs require one. */\nexport const ROADMAP_DISPLAY = 'TIMELINE';\n\nexport function planningFocusLabel(focus: string | null | undefined): string {\n return focus && focus in FOCUS ? FOCUS[focus as PlanningFocus].label : 'Focus not set';\n}\n\n/** Icon AND label, never colour alone. */\nexport function PlanningFocusBadge({ focus }: { focus: string | null | undefined }) {\n if (!focus || !(focus in FOCUS)) {\n return (\n <span\n className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\"\n data-testid=\"planning-focus\"\n >\n Focus not set\n </span>\n );\n }\n const { label, icon: Icon } = FOCUS[focus as PlanningFocus];\n return (\n <span\n className=\"inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-foreground\"\n data-testid=\"planning-focus\"\n >\n <Icon className=\"h-3 w-3\" /> {label}\n </span>\n );\n}\n\nexport function PlanningFocusSelect({\n value,\n onChange,\n id = 'planning-focus',\n}: {\n value: PlanningFocus | null;\n onChange: (value: PlanningFocus | null) => void;\n id?: string;\n}) {\n return (\n <div>\n <label htmlFor={id} className=\"text-sm font-medium\">\n Planning focus\n </label>\n <select\n id={id}\n className=\"mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm\"\n value={value ?? ''}\n onChange={(e) => onChange(e.target.value ? (e.target.value as PlanningFocus) : null)}\n >\n <option value=\"\">Not set</option>\n {PLANNING_FOCUSES.map((f) => (\n <option key={f} value={f}>\n {FOCUS[f].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n What the roadmap is organised around. Roadmaps are shown in one layout for now.\n </p>\n </div>\n );\n}\n"],"mappings":";;;AAYA,IAAa,IAA6C;CACxD;CACA;CACA;CACA;CACD,EAEK,IAAoE;CACxE,OAAO;EAAE,OAAO;EAAS,MAAM;EAAW;CAC1C,SAAS;EAAE,OAAO;EAAW,MAAM;EAAQ;CAC3C,YAAY;EAAE,OAAO;EAAc,MAAM;EAAK;CAC9C,WAAW;EAAE,OAAO;EAAa,MAAM;EAAO;CAC/C,EAGY,IAAkB;AAE/B,SAAgB,EAAmB,GAA0C;AAC3E,QAAO,KAAS,KAAS,IAAQ,EAAM,GAAwB,QAAQ;;AAIzE,SAAgB,EAAmB,EAAE,YAA+C;AAClF,KAAI,CAAC,KAAS,EAAE,KAAS,GACvB,QACE,kBAAC,QAAD;EACE,WAAU;EACV,eAAY;YACb;EAEM,CAAA;CAGX,IAAM,EAAE,UAAO,MAAM,MAAS,EAAM;AACpC,QACE,kBAAC,QAAD;EACE,WAAU;EACV,eAAY;YAFd;GAIE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;;GAAE;GACzB;;;AAIX,SAAgB,EAAoB,EAClC,UACA,aACA,QAAK,oBAKJ;AACD,QACE,kBAAC,OAAD,EAAA,UAAA;EACE,kBAAC,SAAD;GAAO,SAAS;GAAI,WAAU;aAAsB;GAE5C,CAAA;EACR,kBAAC,UAAD;GACM;GACJ,WAAU;GACV,OAAO,KAAS;GAChB,WAAW,MAAM,EAAS,EAAE,OAAO,QAAS,EAAE,OAAO,QAA0B,KAAK;aAJtF,CAME,kBAAC,UAAD;IAAQ,OAAM;cAAG;IAAgB,CAAA,EAChC,EAAiB,KAAK,MACrB,kBAAC,UAAD;IAAgB,OAAO;cACpB,EAAM,GAAG;IACH,EAFI,EAEJ,CACT,CACK;;EACT,kBAAC,KAAD;GAAG,WAAU;aAAqC;GAE9C,CAAA;EACA,EAAA,CAAA"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A sprint's PLANNED dates next to what actually happened (BOFF-7918).
3
+ *
4
+ * Until 2026-09-26 Start and Complete overwrote the planned start/end with the
5
+ * moment they were clicked, so the plan was lost. The service now keeps the
6
+ * plan, records `startedAt`/`completedAt`, snapshots the plan in force at Start
7
+ * as the baseline (the commitment), and appends every later plan change to a
8
+ * history. Sprints started before the fix have no recorded actual times: they
9
+ * read "not recorded", never a guessed date.
10
+ */
11
+ export interface SprintPlanChange {
12
+ field: string;
13
+ fromValue: string | null;
14
+ toValue: string | null;
15
+ sprintStatus: string;
16
+ changedBy: string;
17
+ changedAt: string;
18
+ }
19
+ export interface SprintDateFields {
20
+ status: string;
21
+ startDate: string | null;
22
+ endDate: string | null;
23
+ startedAt?: string | null;
24
+ completedAt?: string | null;
25
+ baselineStartDate?: string | null;
26
+ baselineEndDate?: string | null;
27
+ planChanges?: SprintPlanChange[];
28
+ }
29
+ /**
30
+ * Whole calendar days between the committed end and the actual completion.
31
+ * Positive = late, negative = early, 0 = on the day.
32
+ */
33
+ export declare function daysAgainstCommitment(completedAt: string, committedEnd: string): number;
34
+ export declare function SprintDates({ sprint }: {
35
+ sprint: SprintDateFields;
36
+ }): import("react/jsx-runtime").JSX.Element;
37
+ //# sourceMappingURL=SprintDates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SprintDates.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/sprints/SprintDates.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAkBD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAMvF;AAqCD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,gBAAgB,CAAA;CAAE,2CA8EnE"}
@@ -0,0 +1,114 @@
1
+ import { PersonName as e } from "../people/People.js";
2
+ import { useState as t } from "react";
3
+ import { AlertTriangle as n, CalendarClock as r, CheckCircle2 as i, History as a } from "lucide-react";
4
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
+ //#region src/planmagnet/sprints/SprintDates.tsx
6
+ var c = (e) => e ? new Date(e).toLocaleDateString(void 0, {
7
+ timeZone: "UTC",
8
+ day: "numeric",
9
+ month: "short",
10
+ year: "numeric"
11
+ }) : "—", l = (e) => new Date(e).toLocaleString(void 0, {
12
+ dateStyle: "medium",
13
+ timeStyle: "short"
14
+ }), u = 1440 * 60 * 1e3;
15
+ function d(e, t) {
16
+ let n = new Date(e), r = Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate()), i = new Date(t), a = Date.UTC(i.getUTCFullYear(), i.getUTCMonth(), i.getUTCDate());
17
+ return Math.round((r - a) / u);
18
+ }
19
+ function f({ completedAt: e, committedEnd: t }) {
20
+ if (!t) return /* @__PURE__ */ o("span", {
21
+ className: "text-xs text-muted-foreground",
22
+ children: "No committed end to compare with."
23
+ });
24
+ let r = d(e, t);
25
+ return r <= 0 ? /* @__PURE__ */ s("span", {
26
+ className: "inline-flex items-center gap-1 text-xs text-status-success-text",
27
+ "data-testid": "sprint-outcome",
28
+ children: [/* @__PURE__ */ o(i, { className: "h-3.5 w-3.5" }), r === 0 ? "Finished on the committed day" : `Finished ${-r} day${r === -1 ? "" : "s"} early`]
29
+ }) : /* @__PURE__ */ s("span", {
30
+ className: "inline-flex items-center gap-1 text-xs text-status-warning-text",
31
+ "data-testid": "sprint-outcome",
32
+ children: [
33
+ /* @__PURE__ */ o(n, { className: "h-3.5 w-3.5" }),
34
+ "Finished ",
35
+ r,
36
+ " day",
37
+ r === 1 ? "" : "s",
38
+ " after the committed end"
39
+ ]
40
+ });
41
+ }
42
+ function p({ sprint: n }) {
43
+ let [i, u] = t(!1), d = n.status !== "PLANNING", p = n.status === "COMPLETED", m = n.planChanges ?? [], h = n.baselineEndDate ?? null, g = m.filter((e) => e.sprintStatus !== "PLANNING").length, _ = d && !n.startedAt && !!n.startDate, v = p && !n.completedAt && !!n.endDate;
44
+ return /* @__PURE__ */ s("div", {
45
+ className: "space-y-1 text-xs text-muted-foreground",
46
+ "data-testid": "sprint-dates",
47
+ children: [
48
+ /* @__PURE__ */ s("p", {
49
+ className: "flex items-center gap-1",
50
+ children: [
51
+ /* @__PURE__ */ o(r, { className: "h-3 w-3" }),
52
+ /* @__PURE__ */ s("span", { children: [
53
+ "Planned ",
54
+ _ ? "start not recorded" : c(n.startDate),
55
+ " –",
56
+ " ",
57
+ v ? "end not recorded" : c(n.endDate)
58
+ ] }),
59
+ h && n.endDate && h !== n.endDate && /* @__PURE__ */ s("span", { children: [
60
+ "(committed end ",
61
+ c(h),
62
+ ")"
63
+ ] })
64
+ ]
65
+ }),
66
+ d && /* @__PURE__ */ s("p", {
67
+ "data-testid": "sprint-actuals",
68
+ children: [
69
+ n.startedAt ? `Started ${l(n.startedAt)}` : _ && n.startDate ? `Started ${l(n.startDate)}` : "Actual start not recorded",
70
+ p && (n.completedAt ? ` · Completed ${l(n.completedAt)}` : v && n.endDate ? ` · Completed ${l(n.endDate)}` : " · Actual completion not recorded"),
71
+ (_ || v) && " (recorded before planned dates were kept)"
72
+ ]
73
+ }),
74
+ p && n.completedAt && /* @__PURE__ */ o(f, {
75
+ completedAt: n.completedAt,
76
+ committedEnd: h
77
+ }),
78
+ m.length > 0 && /* @__PURE__ */ s("div", { children: [/* @__PURE__ */ s("button", {
79
+ type: "button",
80
+ className: "inline-flex items-center gap-1 hover:underline",
81
+ "aria-expanded": i,
82
+ onClick: () => u(!i),
83
+ "data-testid": "sprint-plan-history-toggle",
84
+ children: [
85
+ /* @__PURE__ */ o(a, { className: "h-3 w-3" }),
86
+ "Plan changed ",
87
+ m.length,
88
+ "×",
89
+ g > 0 ? ` (${g} after start)` : ""
90
+ ]
91
+ }), i && /* @__PURE__ */ o("ol", {
92
+ className: "mt-1 space-y-0.5 pl-4",
93
+ "data-testid": "sprint-plan-history",
94
+ children: m.map((t, n) => /* @__PURE__ */ s("li", { children: [
95
+ t.field === "startDate" ? "Start" : "End",
96
+ ": ",
97
+ c(t.fromValue),
98
+ " → ",
99
+ c(t.toValue),
100
+ t.sprintStatus === "PLANNING" ? "" : " (after start)",
101
+ " ·",
102
+ " ",
103
+ /* @__PURE__ */ o(e, { id: t.changedBy }),
104
+ " · ",
105
+ l(t.changedAt)
106
+ ] }, `${t.changedAt}-${t.field}-${n}`))
107
+ })] })
108
+ ]
109
+ });
110
+ }
111
+ //#endregion
112
+ export { p as SprintDates };
113
+
114
+ //# sourceMappingURL=SprintDates.js.map