@burdenoff/microfe-planmagnet 2026.1001.2 → 2026.1001.4

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 (62) hide show
  1. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  2. package/dist/planmagnet/PlanMagnetRoot.js +160 -153
  3. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  4. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.js +121 -109
  6. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  7. package/dist/planmagnet/controls/MilestoneControls.d.ts +2 -0
  8. package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -1
  9. package/dist/planmagnet/controls/MilestoneControls.js +89 -72
  10. package/dist/planmagnet/controls/MilestoneControls.js.map +1 -1
  11. package/dist/planmagnet/meetings/MeetingActionItemsPanel.d.ts +17 -0
  12. package/dist/planmagnet/meetings/MeetingActionItemsPanel.d.ts.map +1 -0
  13. package/dist/planmagnet/meetings/MeetingActionItemsPanel.js +218 -0
  14. package/dist/planmagnet/meetings/MeetingActionItemsPanel.js.map +1 -0
  15. package/dist/planmagnet/meetings/MeetingsCard.d.ts +7 -0
  16. package/dist/planmagnet/meetings/MeetingsCard.d.ts.map +1 -0
  17. package/dist/planmagnet/meetings/MeetingsCard.js +108 -0
  18. package/dist/planmagnet/meetings/MeetingsCard.js.map +1 -0
  19. package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.d.ts +9 -0
  20. package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.d.ts.map +1 -0
  21. package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.js +232 -0
  22. package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.js.map +1 -0
  23. package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts +19 -0
  24. package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts.map +1 -0
  25. package/dist/planmagnet/meetings/ScheduleMeetingDialog.js +226 -0
  26. package/dist/planmagnet/meetings/ScheduleMeetingDialog.js.map +1 -0
  27. package/dist/planmagnet/meetings/UnlinkMeetingDialog.d.ts +14 -0
  28. package/dist/planmagnet/meetings/UnlinkMeetingDialog.d.ts.map +1 -0
  29. package/dist/planmagnet/meetings/UnlinkMeetingDialog.js +78 -0
  30. package/dist/planmagnet/meetings/UnlinkMeetingDialog.js.map +1 -0
  31. package/dist/planmagnet/meetings/meetingActions.d.ts +58 -0
  32. package/dist/planmagnet/meetings/meetingActions.d.ts.map +1 -0
  33. package/dist/planmagnet/meetings/meetingActions.js +48 -0
  34. package/dist/planmagnet/meetings/meetingActions.js.map +1 -0
  35. package/dist/planmagnet/meetings/meetingGraphql.d.ts +29 -0
  36. package/dist/planmagnet/meetings/meetingGraphql.d.ts.map +1 -0
  37. package/dist/planmagnet/meetings/meetingGraphql.js +166 -0
  38. package/dist/planmagnet/meetings/meetingGraphql.js.map +1 -0
  39. package/dist/planmagnet/meetings/meetingModel.d.ts +74 -0
  40. package/dist/planmagnet/meetings/meetingModel.d.ts.map +1 -0
  41. package/dist/planmagnet/meetings/meetingModel.js +88 -0
  42. package/dist/planmagnet/meetings/meetingModel.js.map +1 -0
  43. package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.d.ts +2 -0
  44. package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.d.ts.map +1 -0
  45. package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.js +197 -0
  46. package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.js.map +1 -0
  47. package/dist/planmagnet/pages/MeetingsMeetingsPagePage.d.ts +2 -0
  48. package/dist/planmagnet/pages/MeetingsMeetingsPagePage.d.ts.map +1 -0
  49. package/dist/planmagnet/pages/MeetingsMeetingsPagePage.js +173 -0
  50. package/dist/planmagnet/pages/MeetingsMeetingsPagePage.js.map +1 -0
  51. package/dist/planmagnet/pages/ProjectsMilestonesPagePage.d.ts.map +1 -1
  52. package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js +73 -52
  53. package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js.map +1 -1
  54. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
  55. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +205 -190
  56. package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
  57. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  58. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +192 -187
  59. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  60. package/dist/planmagnet/reports/ShareDialog.js +65 -65
  61. package/dist/planmagnet/reports/ShareDialog.js.map +1 -1
  62. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectsProjectDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsProjectDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport {\n Bot,\n FolderKanban,\n CheckSquare,\n Users,\n Zap,\n Calendar,\n LayoutTemplate,\n ShieldAlert,\n ShieldCheck,\n Route,\n} from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { PersonName } from '../people/People';\nimport { ProjectHeaderActions } from '../controls/ProjectControls';\nimport { NewTemplateFromProjectDialog, NewRiskDialog } from '../create/CreateDialogs';\nimport { workProgress } from '../hooks/useCounts';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectBudgetsSummary } from '../budgets/ProjectBudgetsSummary';\nimport { ProjectComments } from '../comments/TargetComments';\nimport { DeliveryForecastCard } from '../forecast/DeliveryForecastCard';\nimport { ProjectOutlookCard } from '../forecast/ProjectOutlookCard';\nimport { ResourceForecastCard } from '../forecast/ResourceForecastCard';\nimport { ProjectRiskSummaryCard } from '../forecast/ProjectRiskSummaryCard';\nimport { ROW_LINK_CLASS, rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { useLiveRefresh, combineLiveRefreshModes } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\nimport { usePresence } from '../hooks/usePresence';\nimport { PresenceAvatars } from '../presence/PresenceAvatars';\n\n/** This page's one query — GET_PROJECT_DETAIL nests the project's own fields plus its sprints and work items, so\n * a change on either (PDM-05 project events, PDM-21 sprint events, PDM-11 work item events, all scoped to this\n * project) means the same re-read. Milestones and risks are also nested here but have no event stream of their\n * own yet — out of scope (PDM-05/11/21 cover project/sprint/work item only). */\nconst PROJECT_DETAIL_QUERIES = ['GetProjectDetail'];\n\nconst GET_PROJECT_DETAIL = gql`\n query GetProjectDetail($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n key\n description\n status\n methodology\n ownerId\n startDate\n endDate\n tags\n createdAt\n programId\n program {\n id\n name\n }\n sprints {\n id\n name\n status\n goal\n capacity\n velocity\n startDate\n endDate\n workItems {\n id\n status\n storyPoints\n }\n }\n milestones {\n id\n name\n description\n status\n dueDate\n ownerId\n }\n risks {\n id\n title\n severity\n status\n riskScore\n dueDate\n }\n workItems {\n id\n key\n title\n description\n status\n priority\n storyPoints\n assigneeId\n labels\n createdAt\n sprint {\n id\n name\n status\n team {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst statusColor: Record<string, string> = {\n ACTIVE: 'bg-green-100 text-green-800',\n ON_HOLD: 'bg-orange-100 text-orange-800',\n CANCELLED: 'bg-red-100 text-red-800',\n COMPLETED: 'bg-blue-100 text-blue-800',\n PLANNING: 'bg-gray-100 text-gray-800',\n ARCHIVED: 'bg-gray-100 text-gray-800',\n};\n\nconst riskSeverityColor: Record<string, string> = {\n CRITICAL: 'bg-red-100 text-red-800',\n HIGH: 'bg-orange-100 text-orange-800',\n MEDIUM: 'bg-yellow-100 text-yellow-800',\n LOW: 'bg-blue-100 text-blue-800',\n NEGLIGIBLE: 'bg-gray-100 text-gray-800',\n};\nconst OPEN_RISK_STATUSES = new Set(['IDENTIFIED', 'ANALYZING', 'MITIGATING', 'ACCEPTED']);\n\nexport function ProjectsProjectDetailPagePage() {\n const projectId = useEntityId('projectId');\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_PROJECT_DETAIL, {\n variables: { id: projectId },\n skip: !projectId,\n });\n\n // Live updates (PDM-05/11/21): the project's own fields, its sprints and its work items — see\n // PROJECT_DETAIL_QUERIES above for why one re-read covers all three streams.\n const projectLive = useLiveRefresh({\n kind: 'project',\n projectId,\n queries: PROJECT_DETAIL_QUERIES,\n });\n const sprintLive = useLiveRefresh({\n kind: 'sprint',\n projectId,\n enabled: Boolean(projectId),\n queries: PROJECT_DETAIL_QUERIES,\n });\n const workItemLive = useLiveRefresh({\n kind: 'workItem',\n projectId,\n queries: PROJECT_DETAIL_QUERIES,\n });\n const liveMode = combineLiveRefreshModes(projectLive.mode, sprintLive.mode, workItemLive.mode);\n // CWF-24: who else is looking at this project right now, and who's mid-comment.\n const presence = usePresence('PROJECT', projectId);\n\n if (!projectId)\n return (\n <div className=\"p-6 text-muted-foreground\">No project selected. Open one from the list.</div>\n );\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\n if (loading && !data)\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n if (error) return <div className=\"p-6 text-red-500\">Error loading project: {error.message}</div>;\n\n const project = data?.planmagnetProject;\n if (!project) return <div className=\"p-6 text-muted-foreground\">Project not found.</div>;\n\n const projectWorkItems = project.workItems ?? [];\n const projectSprints = project.sprints ?? [];\n const projectMilestones = project.milestones ?? [];\n const projectRisks = project.risks ?? [];\n const openRisks = projectRisks.filter((r: { status: string }) =>\n OPEN_RISK_STATUSES.has(r.status)\n );\n // Highest score first, so the register's top exposure is what a project owner sees without scrolling.\n const topRisks = [...openRisks]\n .sort((a: { riskScore: number }, b: { riskScore: number }) => b.riskScore - a.riskScore)\n .slice(0, 3);\n const activeSprint = projectSprints.find((s: { status: string }) => s.status === 'ACTIVE');\n // Velocity exists only for a COMPLETED sprint (BOFF-7944: this tile read the running sprint's velocity, which is\n // never set, so it always showed 0). Show the latest completed sprint's instead.\n const lastCompleted = projectSprints\n .filter(\n (s: { status: string; velocity: number | null }) =>\n s.status === 'COMPLETED' && s.velocity != null\n )\n .sort((a: { endDate: string | null }, b: { endDate: string | null }) =>\n (b.endDate ?? '').localeCompare(a.endDate ?? '')\n )[0];\n // Done over everything not cancelled (BOFF-7989), labelled as tasks — it sat under \"Duration\" as if it were time.\n // Weighted by storyPoints when the project has any (BOFF-7989 follow-up): mapping to { status } only dropped\n // storyPoints here, so this always fell back to a count-based percentage even though GET_PROJECT_DETAIL selects\n // storyPoints and workProgress already weights by it.\n const tasksDonePct = workProgress(\n projectWorkItems.map((wi: { status: string; storyPoints: number | null }) => ({\n status: wi.status,\n storyPoints: wi.storyPoints,\n }))\n );\n // The Work Items tile counts the same way as tasksDonePct: done over everything not cancelled (BOFF-7989).\n // project.workItems is every non-deleted item (no page cap), so these are exact.\n const liveWorkItems = projectWorkItems.filter(\n (wi: { status: string }) => wi.status !== 'CANCELLED'\n );\n const doneWorkItems = liveWorkItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n // Newest first: the list is ordered oldest-first, so slice(0, 5) showed the five OLDEST items (BOFF-7993).\n const latestWorkItems = [...projectWorkItems]\n .sort((a: { createdAt: string }, b: { createdAt: string }) =>\n b.createdAt.localeCompare(a.createdAt)\n )\n .slice(0, 5);\n\n // BOFF-8044: this project's own sprints (project.sprints) structurally exclude team sprints — a\n // team sprint has no projectId. A work item from this project can still sit inside ANOTHER team's\n // sprint, invisibly, since nothing else on this page surfaces that. Grouped client-side over data\n // the Sprint.team field resolver already serves — no new backend query.\n const teamSprintGroups = Object.values(\n projectWorkItems.reduce(\n (\n acc: Record<\n string,\n {\n sprint: {\n id: string;\n name: string;\n status: string;\n team: { id: string; name: string };\n };\n items: { id: string; key: string; title: string }[];\n }\n >,\n wi: {\n id: string;\n key: string;\n title: string;\n sprint: {\n id: string;\n name: string;\n status: string;\n team: { id: string; name: string } | null;\n } | null;\n }\n ) => {\n if (!wi.sprint?.team) return acc;\n const entry = acc[wi.sprint.id] ?? {\n sprint: { ...wi.sprint, team: wi.sprint.team },\n items: [],\n };\n entry.items.push({ id: wi.id, key: wi.key, title: wi.title });\n acc[wi.sprint.id] = entry;\n return acc;\n },\n {}\n )\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <FolderKanban className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{project.name}</h1>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${statusColor[project.status] ?? 'bg-gray-100'}`}\n >\n {project.status.toLowerCase().replace('_', ' ')}\n </span>\n <LiveStatus mode={liveMode} />\n </div>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {project.description ?? 'No description'}\n </p>\n <PresenceAvatars others={presence.others} />\n </div>\n <div className=\"flex flex-col items-end gap-2\">\n {/* Status, Edit and Delete (BOFF-7940): a project could never leave PLANNING from the UI. */}\n <ProjectHeaderActions\n projectId={project.id}\n name={project.name}\n status={project.status}\n />\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate('/automation/automation-rules-page')}\n data-testid=\"project-automation-rules\"\n >\n <Bot className=\"mr-1 h-4 w-4\" />\n Automation rules\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n navigate(`/projects/quality-gates-page?projectId=${encodeURIComponent(project.id)}`)\n }\n data-testid=\"project-quality-gates\"\n >\n <ShieldCheck className=\"mr-1 h-4 w-4\" />\n Quality gates\n </Button>\n <NewTemplateFromProjectDialog\n projectId={project.id}\n projectName={project.name}\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n trigger={\n <Button size=\"sm\" variant=\"outline\" data-testid=\"project-save-as-template\">\n <LayoutTemplate className=\"mr-1 h-4 w-4\" />\n Save as template\n </Button>\n }\n />\n </div>\n <Badge variant=\"outline\" className=\"capitalize\">\n {project.methodology.toLowerCase()}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/projects/critical-path-page', project.id))}\n >\n <Route className=\"mr-1 h-4 w-4\" />\n Critical path\n </Button>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <CheckSquare className=\"h-4 w-4\" />\n Work Items\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {doneWorkItems}/{liveWorkItems.length}\n </p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Zap className=\"h-4 w-4\" />\n Sprints\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectSprints.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Calendar className=\"h-4 w-4\" />\n Milestones\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectMilestones.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Users className=\"h-4 w-4\" />\n Owner\n </div>\n <p className=\"text-sm font-bold mt-1 truncate\">\n <PersonName id={project.ownerId} />\n </p>\n </CardContent>\n </Card>\n </div>\n\n {/* ANR-02 / ANR-10: statistical delivery forecast (Monte Carlo) and the rule-based/heuristic\n outlook built on top of it — both project-scoped, both clearly labelled as not-AI. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <DeliveryForecastCard\n workspaceId={workspaceId}\n projectId={project.id}\n sprintId={activeSprint?.id}\n sprintName={activeSprint?.name}\n />\n <ProjectOutlookCard workspaceId={workspaceId} projectId={project.id} />\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Timeline</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex justify-between text-sm mb-2\">\n <span className=\"text-muted-foreground\">Tasks done</span>\n <span className=\"font-medium\">\n {tasksDonePct === null ? 'No open tasks' : `${tasksDonePct}%`}\n </span>\n </div>\n <div className=\"w-full h-4 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: `${tasksDonePct ?? 0}%` }}\n />\n </div>\n <div className=\"flex justify-between text-xs text-muted-foreground mt-2\">\n <span>\n {project.startDate ? new Date(project.startDate).toLocaleDateString() : 'Not set'}\n </span>\n <span>\n {project.endDate ? new Date(project.endDate).toLocaleDateString() : 'Not set'}\n </span>\n </div>\n </CardContent>\n </Card>\n\n {activeSprint && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Zap className=\"h-4 w-4 text-primary\" />\n Active Sprint\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex justify-between\">\n <span className=\"font-medium\">{activeSprint.name}</span>\n <Badge>Active</Badge>\n </div>\n {activeSprint.goal && (\n <p className=\"text-sm text-muted-foreground\">{activeSprint.goal}</p>\n )}\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems\n ?.filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n )}\n /\n {activeSprint.workItems?.reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n ) ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Points</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems?.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length ?? 0}\n /{activeSprint.workItems?.length ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Tasks</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {lastCompleted ? `${lastCompleted.velocity} pts` : '—'}\n </p>\n <p className=\"text-xs text-muted-foreground\">Velocity of the last sprint</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>\n Work items ({projectWorkItems.length})\n {projectWorkItems.length > 5 ? ' · latest 5' : ''}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {latestWorkItems.map(\n (task: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n labels: string[];\n }) => (\n <div\n key={task.id}\n {...rowLinkProps(\n () =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(task.id)}`),\n task.key\n )}\n className={`flex items-center justify-between border rounded-md p-2.5 ${ROW_LINK_CLASS}`}\n >\n <div>\n <p className=\"text-sm font-medium\">\n {task.key} <span className=\"font-normal\">{task.title}</span>\n </p>\n <div className=\"flex gap-1 mt-1\">\n {task.labels.map((l: string) => (\n <span key={l} className=\"text-[10px] px-1.5 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {task.storyPoints ?? 0} pts\n </span>\n <Badge variant=\"secondary\" className=\"capitalize text-xs\">\n {task.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {projectWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">No work items yet.</p>\n )}\n </CardContent>\n </Card>\n\n <ProjectComments projectId={project.id} presence={presence} />\n </div>\n\n <div className=\"space-y-6\">\n <ProjectBudgetsSummary workspaceId={workspaceId} projectId={project.id} />\n {/* ANR-11: capacity vs. this project's own open backlog. */}\n <ResourceForecastCard workspaceId={workspaceId} projectId={project.id} />\n {/* AI-10: rule-based at-risk flags, with a link through to the full list. */}\n <ProjectRiskSummaryCard workspaceId={workspaceId} projectId={project.id} />\n <Card>\n <CardHeader>\n <CardTitle>Milestones ({projectMilestones.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {projectMilestones.map(\n (ms: { id: string; name: string; status: string; dueDate: string | null }) => (\n <div key={ms.id} className=\"border-b pb-2 last:border-0\">\n <div className=\"flex justify-between\">\n <span className=\"text-sm font-medium\">{ms.name}</span>\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground mt-1\">\n <Calendar className=\"h-3 w-3\" />\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : 'No due date'}\n </div>\n </div>\n )\n )}\n {projectMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">No milestones.</p>\n )}\n </CardContent>\n </Card>\n\n {teamSprintGroups.length > 0 && (\n <Card data-testid=\"team-sprint-work\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Users className=\"h-4 w-4\" />\n Team sprint work\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {/* BOFF-8044: an item from this project sitting in ANOTHER team's sprint — this\n project's own sprints (above) structurally exclude team sprints. */}\n {teamSprintGroups.map((group) => (\n <div key={group.sprint.id} className=\"border-b pb-2 last:border-0\">\n <button\n type=\"button\"\n className=\"text-sm font-medium hover:underline text-left\"\n onClick={() =>\n navigate(detailPath('/projects/planning-detail-page', group.sprint.id))\n }\n >\n {group.sprint.name}\n </button>\n <button\n type=\"button\"\n className=\"block text-xs text-muted-foreground hover:underline\"\n onClick={() =>\n navigate(detailPath('/team/team-detail-page', group.sprint.team.id))\n }\n >\n {group.items.length} item{group.items.length === 1 ? '' : 's'} &middot; team{' '}\n {group.sprint.team.name}\n </button>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldAlert className=\"h-4 w-4 text-primary\" />\n Risks ({openRisks.length} open)\n </CardTitle>\n <NewRiskDialog\n projectId={project.id}\n onCreated={(id) => navigate(detailPath('/risks/risk-detail-page', id))}\n />\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {topRisks.map(\n (risk: {\n id: string;\n title: string;\n severity: string;\n status: string;\n riskScore: number;\n }) => (\n <div\n key={risk.id}\n {...rowLinkProps(\n () => navigate(detailPath('/risks/risk-detail-page', risk.id)),\n risk.title\n )}\n className={`border-b pb-2 last:border-0 ${ROW_LINK_CLASS}`}\n >\n <div className=\"flex justify-between gap-2\">\n <span className=\"text-sm font-medium truncate\">{risk.title}</span>\n <span\n className={`text-xs px-2 py-0.5 rounded-full shrink-0 ${riskSeverityColor[risk.severity] ?? 'bg-gray-100'}`}\n >\n {enumLabel(risk.severity)}\n </span>\n </div>\n <div className=\"text-xs text-muted-foreground mt-1\">\n Score {risk.riskScore} · {enumLabel(risk.status)}\n </div>\n </div>\n )\n )}\n {openRisks.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">No open risks.</p>\n )}\n <button\n type=\"button\"\n className=\"text-xs text-primary hover:underline\"\n onClick={() =>\n navigate(`/risks/risk-register-page?projectId=${encodeURIComponent(project.id)}`)\n }\n >\n View risk register ({projectRisks.length}) →\n </button>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <span>\n <PersonName id={project.ownerId} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span className=\"font-mono\">{project.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Methodology</span>\n <span className=\"capitalize\">{project.methodology.toLowerCase()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Program</span>\n {project.program ? (\n <button\n type=\"button\"\n className=\"text-primary hover:underline underline-offset-2\"\n onClick={() =>\n navigate(detailPath('/products/program-detail-page', project.program.id))\n }\n >\n {project.program.name}\n </button>\n ) : (\n <span className=\"text-muted-foreground\">No program</span>\n )}\n </div>\n {project.tags.length > 0 && (\n <div className=\"flex justify-between items-start\">\n <span className=\"text-muted-foreground\">Tags</span>\n <div className=\"flex gap-1 flex-wrap justify-end\">\n {project.tags.map((tag: string) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProjectsProjectDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ProjectsProjectDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Button, Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport {\n Bot,\n FolderKanban,\n CheckSquare,\n Users,\n Zap,\n Calendar,\n LayoutTemplate,\n ShieldAlert,\n ShieldCheck,\n Route,\n} from 'lucide-react';\nimport { useEntityId } from '../hooks/useEntityId';\nimport { PersonName } from '../people/People';\nimport { ProjectHeaderActions } from '../controls/ProjectControls';\nimport { NewTemplateFromProjectDialog, NewRiskDialog } from '../create/CreateDialogs';\nimport { workProgress } from '../hooks/useCounts';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { ProjectBudgetsSummary } from '../budgets/ProjectBudgetsSummary';\nimport { ProjectComments } from '../comments/TargetComments';\nimport { DeliveryForecastCard } from '../forecast/DeliveryForecastCard';\nimport { ProjectOutlookCard } from '../forecast/ProjectOutlookCard';\nimport { ResourceForecastCard } from '../forecast/ResourceForecastCard';\nimport { ProjectRiskSummaryCard } from '../forecast/ProjectRiskSummaryCard';\nimport { MeetingsCard } from '../meetings/MeetingsCard';\nimport { ROW_LINK_CLASS, rowLinkProps, detailPath } from '../utils/rowNavigation';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { useLiveRefresh, combineLiveRefreshModes } from '../hooks/useLiveRefresh';\nimport { LiveStatus } from '../controls/LiveStatus';\nimport { usePresence } from '../hooks/usePresence';\nimport { PresenceAvatars } from '../presence/PresenceAvatars';\n\n/** This page's one query — GET_PROJECT_DETAIL nests the project's own fields plus its sprints and work items, so\n * a change on either (PDM-05 project events, PDM-21 sprint events, PDM-11 work item events, all scoped to this\n * project) means the same re-read. Milestones and risks are also nested here but have no event stream of their\n * own yet — out of scope (PDM-05/11/21 cover project/sprint/work item only). */\nconst PROJECT_DETAIL_QUERIES = ['GetProjectDetail'];\n\nconst GET_PROJECT_DETAIL = gql`\n query GetProjectDetail($id: ID!) {\n planmagnetProject(id: $id) {\n id\n name\n key\n description\n status\n methodology\n ownerId\n startDate\n endDate\n tags\n createdAt\n programId\n program {\n id\n name\n }\n sprints {\n id\n name\n status\n goal\n capacity\n velocity\n startDate\n endDate\n workItems {\n id\n status\n storyPoints\n }\n }\n milestones {\n id\n name\n description\n status\n dueDate\n ownerId\n }\n risks {\n id\n title\n severity\n status\n riskScore\n dueDate\n }\n workItems {\n id\n key\n title\n description\n status\n priority\n storyPoints\n assigneeId\n labels\n createdAt\n sprint {\n id\n name\n status\n team {\n id\n name\n }\n }\n }\n }\n }\n`;\n\nconst statusColor: Record<string, string> = {\n ACTIVE: 'bg-green-100 text-green-800',\n ON_HOLD: 'bg-orange-100 text-orange-800',\n CANCELLED: 'bg-red-100 text-red-800',\n COMPLETED: 'bg-blue-100 text-blue-800',\n PLANNING: 'bg-gray-100 text-gray-800',\n ARCHIVED: 'bg-gray-100 text-gray-800',\n};\n\nconst riskSeverityColor: Record<string, string> = {\n CRITICAL: 'bg-red-100 text-red-800',\n HIGH: 'bg-orange-100 text-orange-800',\n MEDIUM: 'bg-yellow-100 text-yellow-800',\n LOW: 'bg-blue-100 text-blue-800',\n NEGLIGIBLE: 'bg-gray-100 text-gray-800',\n};\nconst OPEN_RISK_STATUSES = new Set(['IDENTIFIED', 'ANALYZING', 'MITIGATING', 'ACCEPTED']);\n\nexport function ProjectsProjectDetailPagePage() {\n const projectId = useEntityId('projectId');\n const navigate = usePlanMagnetNavigate();\n const { workspaceId } = usePlanMagnet();\n\n const {\n data,\n loading,\n error,\n } = // eslint-disable-next-line @typescript-eslint/no-explicit-any\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n useQuery<Record<string, any>>(GET_PROJECT_DETAIL, {\n variables: { id: projectId },\n skip: !projectId,\n });\n\n // Live updates (PDM-05/11/21): the project's own fields, its sprints and its work items — see\n // PROJECT_DETAIL_QUERIES above for why one re-read covers all three streams.\n const projectLive = useLiveRefresh({\n kind: 'project',\n projectId,\n queries: PROJECT_DETAIL_QUERIES,\n });\n const sprintLive = useLiveRefresh({\n kind: 'sprint',\n projectId,\n enabled: Boolean(projectId),\n queries: PROJECT_DETAIL_QUERIES,\n });\n const workItemLive = useLiveRefresh({\n kind: 'workItem',\n projectId,\n queries: PROJECT_DETAIL_QUERIES,\n });\n const liveMode = combineLiveRefreshModes(projectLive.mode, sprintLive.mode, workItemLive.mode);\n // CWF-24: who else is looking at this project right now, and who's mid-comment.\n const presence = usePresence('PROJECT', projectId);\n\n if (!projectId)\n return (\n <div className=\"p-6 text-muted-foreground\">No project selected. Open one from the list.</div>\n );\n // Only before the first load: a refetch must not blank the page (it\n // unmounts every component on it and loses their state).\n if (loading && !data)\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n if (error) return <div className=\"p-6 text-red-500\">Error loading project: {error.message}</div>;\n\n const project = data?.planmagnetProject;\n if (!project) return <div className=\"p-6 text-muted-foreground\">Project not found.</div>;\n\n const projectWorkItems = project.workItems ?? [];\n const projectSprints = project.sprints ?? [];\n const projectMilestones = project.milestones ?? [];\n const projectRisks = project.risks ?? [];\n const openRisks = projectRisks.filter((r: { status: string }) =>\n OPEN_RISK_STATUSES.has(r.status)\n );\n // Highest score first, so the register's top exposure is what a project owner sees without scrolling.\n const topRisks = [...openRisks]\n .sort((a: { riskScore: number }, b: { riskScore: number }) => b.riskScore - a.riskScore)\n .slice(0, 3);\n const activeSprint = projectSprints.find((s: { status: string }) => s.status === 'ACTIVE');\n // Velocity exists only for a COMPLETED sprint (BOFF-7944: this tile read the running sprint's velocity, which is\n // never set, so it always showed 0). Show the latest completed sprint's instead.\n const lastCompleted = projectSprints\n .filter(\n (s: { status: string; velocity: number | null }) =>\n s.status === 'COMPLETED' && s.velocity != null\n )\n .sort((a: { endDate: string | null }, b: { endDate: string | null }) =>\n (b.endDate ?? '').localeCompare(a.endDate ?? '')\n )[0];\n // Done over everything not cancelled (BOFF-7989), labelled as tasks — it sat under \"Duration\" as if it were time.\n // Weighted by storyPoints when the project has any (BOFF-7989 follow-up): mapping to { status } only dropped\n // storyPoints here, so this always fell back to a count-based percentage even though GET_PROJECT_DETAIL selects\n // storyPoints and workProgress already weights by it.\n const tasksDonePct = workProgress(\n projectWorkItems.map((wi: { status: string; storyPoints: number | null }) => ({\n status: wi.status,\n storyPoints: wi.storyPoints,\n }))\n );\n // The Work Items tile counts the same way as tasksDonePct: done over everything not cancelled (BOFF-7989).\n // project.workItems is every non-deleted item (no page cap), so these are exact.\n const liveWorkItems = projectWorkItems.filter(\n (wi: { status: string }) => wi.status !== 'CANCELLED'\n );\n const doneWorkItems = liveWorkItems.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length;\n // Newest first: the list is ordered oldest-first, so slice(0, 5) showed the five OLDEST items (BOFF-7993).\n const latestWorkItems = [...projectWorkItems]\n .sort((a: { createdAt: string }, b: { createdAt: string }) =>\n b.createdAt.localeCompare(a.createdAt)\n )\n .slice(0, 5);\n\n // BOFF-8044: this project's own sprints (project.sprints) structurally exclude team sprints — a\n // team sprint has no projectId. A work item from this project can still sit inside ANOTHER team's\n // sprint, invisibly, since nothing else on this page surfaces that. Grouped client-side over data\n // the Sprint.team field resolver already serves — no new backend query.\n const teamSprintGroups = Object.values(\n projectWorkItems.reduce(\n (\n acc: Record<\n string,\n {\n sprint: {\n id: string;\n name: string;\n status: string;\n team: { id: string; name: string };\n };\n items: { id: string; key: string; title: string }[];\n }\n >,\n wi: {\n id: string;\n key: string;\n title: string;\n sprint: {\n id: string;\n name: string;\n status: string;\n team: { id: string; name: string } | null;\n } | null;\n }\n ) => {\n if (!wi.sprint?.team) return acc;\n const entry = acc[wi.sprint.id] ?? {\n sprint: { ...wi.sprint, team: wi.sprint.team },\n items: [],\n };\n entry.items.push({ id: wi.id, key: wi.key, title: wi.title });\n acc[wi.sprint.id] = entry;\n return acc;\n },\n {}\n )\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <FolderKanban className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{project.name}</h1>\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${statusColor[project.status] ?? 'bg-gray-100'}`}\n >\n {project.status.toLowerCase().replace('_', ' ')}\n </span>\n <LiveStatus mode={liveMode} />\n </div>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {project.description ?? 'No description'}\n </p>\n <PresenceAvatars others={presence.others} />\n </div>\n <div className=\"flex flex-col items-end gap-2\">\n {/* Status, Edit and Delete (BOFF-7940): a project could never leave PLANNING from the UI. */}\n <ProjectHeaderActions\n projectId={project.id}\n name={project.name}\n status={project.status}\n />\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate('/automation/automation-rules-page')}\n data-testid=\"project-automation-rules\"\n >\n <Bot className=\"mr-1 h-4 w-4\" />\n Automation rules\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n navigate(`/projects/quality-gates-page?projectId=${encodeURIComponent(project.id)}`)\n }\n data-testid=\"project-quality-gates\"\n >\n <ShieldCheck className=\"mr-1 h-4 w-4\" />\n Quality gates\n </Button>\n <NewTemplateFromProjectDialog\n projectId={project.id}\n projectName={project.name}\n onCreated={(id) => navigate(detailPath('/templates/edit-template-page', id))}\n trigger={\n <Button size=\"sm\" variant=\"outline\" data-testid=\"project-save-as-template\">\n <LayoutTemplate className=\"mr-1 h-4 w-4\" />\n Save as template\n </Button>\n }\n />\n </div>\n <Badge variant=\"outline\" className=\"capitalize\">\n {project.methodology.toLowerCase()}\n </Badge>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/projects/critical-path-page', project.id))}\n >\n <Route className=\"mr-1 h-4 w-4\" />\n Critical path\n </Button>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <CheckSquare className=\"h-4 w-4\" />\n Work Items\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {doneWorkItems}/{liveWorkItems.length}\n </p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Zap className=\"h-4 w-4\" />\n Sprints\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectSprints.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Calendar className=\"h-4 w-4\" />\n Milestones\n </div>\n <p className=\"text-xl font-bold mt-1\">{projectMilestones.length}</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"pt-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground text-xs\">\n <Users className=\"h-4 w-4\" />\n Owner\n </div>\n <p className=\"text-sm font-bold mt-1 truncate\">\n <PersonName id={project.ownerId} />\n </p>\n </CardContent>\n </Card>\n </div>\n\n {/* ANR-02 / ANR-10: statistical delivery forecast (Monte Carlo) and the rule-based/heuristic\n outlook built on top of it — both project-scoped, both clearly labelled as not-AI. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <DeliveryForecastCard\n workspaceId={workspaceId}\n projectId={project.id}\n sprintId={activeSprint?.id}\n sprintName={activeSprint?.name}\n />\n <ProjectOutlookCard workspaceId={workspaceId} projectId={project.id} />\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Timeline</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"flex justify-between text-sm mb-2\">\n <span className=\"text-muted-foreground\">Tasks done</span>\n <span className=\"font-medium\">\n {tasksDonePct === null ? 'No open tasks' : `${tasksDonePct}%`}\n </span>\n </div>\n <div className=\"w-full h-4 rounded-full bg-muted overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: `${tasksDonePct ?? 0}%` }}\n />\n </div>\n <div className=\"flex justify-between text-xs text-muted-foreground mt-2\">\n <span>\n {project.startDate ? new Date(project.startDate).toLocaleDateString() : 'Not set'}\n </span>\n <span>\n {project.endDate ? new Date(project.endDate).toLocaleDateString() : 'Not set'}\n </span>\n </div>\n </CardContent>\n </Card>\n\n {activeSprint && (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Zap className=\"h-4 w-4 text-primary\" />\n Active Sprint\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex justify-between\">\n <span className=\"font-medium\">{activeSprint.name}</span>\n <Badge>Active</Badge>\n </div>\n {activeSprint.goal && (\n <p className=\"text-sm text-muted-foreground\">{activeSprint.goal}</p>\n )}\n <div className=\"grid grid-cols-3 gap-4 text-center\">\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems\n ?.filter((wi: { status: string }) => wi.status === 'DONE')\n .reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n )}\n /\n {activeSprint.workItems?.reduce(\n (s: number, wi: { storyPoints: number | null }) =>\n s + (wi.storyPoints ?? 0),\n 0\n ) ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Points</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {activeSprint.workItems?.filter(\n (wi: { status: string }) => wi.status === 'DONE'\n ).length ?? 0}\n /{activeSprint.workItems?.length ?? 0}\n </p>\n <p className=\"text-xs text-muted-foreground\">Tasks</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold\">\n {lastCompleted ? `${lastCompleted.velocity} pts` : '—'}\n </p>\n <p className=\"text-xs text-muted-foreground\">Velocity of the last sprint</p>\n </div>\n </div>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>\n Work items ({projectWorkItems.length})\n {projectWorkItems.length > 5 ? ' · latest 5' : ''}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {latestWorkItems.map(\n (task: {\n id: string;\n key: string;\n description: string | null;\n status: string;\n storyPoints: number | null;\n labels: string[];\n }) => (\n <div\n key={task.id}\n {...rowLinkProps(\n () =>\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(task.id)}`),\n task.key\n )}\n className={`flex items-center justify-between border rounded-md p-2.5 ${ROW_LINK_CLASS}`}\n >\n <div>\n <p className=\"text-sm font-medium\">\n {task.key} <span className=\"font-normal\">{task.title}</span>\n </p>\n <div className=\"flex gap-1 mt-1\">\n {task.labels.map((l: string) => (\n <span key={l} className=\"text-[10px] px-1.5 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">\n {task.storyPoints ?? 0} pts\n </span>\n <Badge variant=\"secondary\" className=\"capitalize text-xs\">\n {task.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n </div>\n )\n )}\n {projectWorkItems.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">No work items yet.</p>\n )}\n </CardContent>\n </Card>\n\n <ProjectComments projectId={project.id} presence={presence} />\n </div>\n\n <div className=\"space-y-6\">\n <ProjectBudgetsSummary workspaceId={workspaceId} projectId={project.id} />\n {/* ANR-11: capacity vs. this project's own open backlog. */}\n <ResourceForecastCard workspaceId={workspaceId} projectId={project.id} />\n {/* AI-10: rule-based at-risk flags, with a link through to the full list. */}\n <ProjectRiskSummaryCard workspaceId={workspaceId} projectId={project.id} />\n <Card>\n <CardHeader>\n <CardTitle>Milestones ({projectMilestones.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {projectMilestones.map(\n (ms: { id: string; name: string; status: string; dueDate: string | null }) => (\n <div key={ms.id} className=\"border-b pb-2 last:border-0\">\n <div className=\"flex justify-between\">\n <span className=\"text-sm font-medium\">{ms.name}</span>\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {ms.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </div>\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground mt-1\">\n <Calendar className=\"h-3 w-3\" />\n {ms.dueDate ? new Date(ms.dueDate).toLocaleDateString() : 'No due date'}\n </div>\n </div>\n )\n )}\n {projectMilestones.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">No milestones.</p>\n )}\n </CardContent>\n </Card>\n\n <MeetingsCard workspaceId={workspaceId} projectId={project.id} />\n\n {teamSprintGroups.length > 0 && (\n <Card data-testid=\"team-sprint-work\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Users className=\"h-4 w-4\" />\n Team sprint work\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {/* BOFF-8044: an item from this project sitting in ANOTHER team's sprint — this\n project's own sprints (above) structurally exclude team sprints. */}\n {teamSprintGroups.map((group) => (\n <div key={group.sprint.id} className=\"border-b pb-2 last:border-0\">\n <button\n type=\"button\"\n className=\"text-sm font-medium hover:underline text-left\"\n onClick={() =>\n navigate(detailPath('/projects/planning-detail-page', group.sprint.id))\n }\n >\n {group.sprint.name}\n </button>\n <button\n type=\"button\"\n className=\"block text-xs text-muted-foreground hover:underline\"\n onClick={() =>\n navigate(detailPath('/team/team-detail-page', group.sprint.team.id))\n }\n >\n {group.items.length} item{group.items.length === 1 ? '' : 's'} &middot; team{' '}\n {group.sprint.team.name}\n </button>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldAlert className=\"h-4 w-4 text-primary\" />\n Risks ({openRisks.length} open)\n </CardTitle>\n <NewRiskDialog\n projectId={project.id}\n onCreated={(id) => navigate(detailPath('/risks/risk-detail-page', id))}\n />\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {topRisks.map(\n (risk: {\n id: string;\n title: string;\n severity: string;\n status: string;\n riskScore: number;\n }) => (\n <div\n key={risk.id}\n {...rowLinkProps(\n () => navigate(detailPath('/risks/risk-detail-page', risk.id)),\n risk.title\n )}\n className={`border-b pb-2 last:border-0 ${ROW_LINK_CLASS}`}\n >\n <div className=\"flex justify-between gap-2\">\n <span className=\"text-sm font-medium truncate\">{risk.title}</span>\n <span\n className={`text-xs px-2 py-0.5 rounded-full shrink-0 ${riskSeverityColor[risk.severity] ?? 'bg-gray-100'}`}\n >\n {enumLabel(risk.severity)}\n </span>\n </div>\n <div className=\"text-xs text-muted-foreground mt-1\">\n Score {risk.riskScore} · {enumLabel(risk.status)}\n </div>\n </div>\n )\n )}\n {openRisks.length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-2\">No open risks.</p>\n )}\n <button\n type=\"button\"\n className=\"text-xs text-primary hover:underline\"\n onClick={() =>\n navigate(`/risks/risk-register-page?projectId=${encodeURIComponent(project.id)}`)\n }\n >\n View risk register ({projectRisks.length}) →\n </button>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Owner</span>\n <span>\n <PersonName id={project.ownerId} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span className=\"font-mono\">{project.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Methodology</span>\n <span className=\"capitalize\">{project.methodology.toLowerCase()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Program</span>\n {project.program ? (\n <button\n type=\"button\"\n className=\"text-primary hover:underline underline-offset-2\"\n onClick={() =>\n navigate(detailPath('/products/program-detail-page', project.program.id))\n }\n >\n {project.program.name}\n </button>\n ) : (\n <span className=\"text-muted-foreground\">No program</span>\n )}\n </div>\n {project.tags.length > 0 && (\n <div className=\"flex justify-between items-start\">\n <span className=\"text-muted-foreground\">Tags</span>\n <div className=\"flex gap-1 flex-wrap justify-end\">\n {project.tags.map((tag: string) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,IAAyB,CAAC,mBAAmB,EAE7C,IAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2ExB,IAAsC;CAC1C,QAAQ;CACR,SAAS;CACT,WAAW;CACX,WAAW;CACX,UAAU;CACV,UAAU;CACX,EAEK,IAA4C;CAChD,UAAU;CACV,MAAM;CACN,QAAQ;CACR,KAAK;CACL,YAAY;CACb,EACK,IAAqB,IAAI,IAAI;CAAC;CAAc;CAAa;CAAc;CAAW,CAAC;AAEzF,SAAgB,IAAgC;CAC9C,IAAM,IAAY,GAAY,YAAY,EACpC,IAAW,GAAuB,EAClC,EAAE,mBAAgB,GAAe,EAEjC,EACJ,SACA,YACA,aAGA,GAA8B,GAAoB;EAChD,WAAW,EAAE,IAAI,GAAW;EAC5B,MAAM,CAAC;EACR,CAAC,EAIE,KAAc,EAAe;EACjC,MAAM;EACN;EACA,SAAS;EACV,CAAC,EACI,KAAa,EAAe;EAChC,MAAM;EACN;EACA,SAAS,EAAQ;EACjB,SAAS;EACV,CAAC,EACI,KAAe,EAAe;EAClC,MAAM;EACN;EACA,SAAS;EACV,CAAC,EACI,KAAW,GAAwB,GAAY,MAAM,GAAW,MAAM,GAAa,KAAK,EAExF,IAAW,EAAY,WAAW,EAAU;AAElD,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YAA4B;EAAkD,CAAA;AAIjG,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD,EAAK,WAAU,kFAAmF,CAAA;EAC9F,CAAA;AAEV,KAAI,EAAO,QAAO,kBAAC,OAAD;EAAK,WAAU;YAAf,CAAkC,2BAAwB,EAAM,QAAc;;CAEhG,IAAM,IAAU,GAAM;AACtB,KAAI,CAAC,EAAS,QAAO,kBAAC,OAAD;EAAK,WAAU;YAA4B;EAAwB,CAAA;CAExF,IAAM,IAAmB,EAAQ,aAAa,EAAE,EAC1C,IAAiB,EAAQ,WAAW,EAAE,EACtC,IAAoB,EAAQ,cAAc,EAAE,EAC5C,IAAe,EAAQ,SAAS,EAAE,EAClC,IAAY,EAAa,QAAQ,MACrC,EAAmB,IAAI,EAAE,OAAO,CACjC,EAEK,KAAW,CAAC,GAAG,EAAU,CAC5B,MAAM,GAA0B,MAA6B,EAAE,YAAY,EAAE,UAAU,CACvF,MAAM,GAAG,EAAE,EACR,IAAe,EAAe,MAAM,MAA0B,EAAE,WAAW,SAAS,EAGpF,IAAgB,EACnB,QACE,MACC,EAAE,WAAW,eAAe,EAAE,YAAY,KAC7C,CACA,MAAM,GAA+B,OACnC,EAAE,WAAW,IAAI,cAAc,EAAE,WAAW,GAAG,CACjD,CAAC,IAKE,IAAe,GACnB,EAAiB,KAAK,OAAwD;EAC5E,QAAQ,EAAG;EACX,aAAa,EAAG;EACjB,EAAE,CACJ,EAGK,IAAgB,EAAiB,QACpC,MAA2B,EAAG,WAAW,YAC3C,EACK,KAAgB,EAAc,QACjC,MAA2B,EAAG,WAAW,OAC3C,CAAC,QAEI,KAAkB,CAAC,GAAG,EAAiB,CAC1C,MAAM,GAA0B,MAC/B,EAAE,UAAU,cAAc,EAAE,UAAU,CACvC,CACA,MAAM,GAAG,EAAE,EAMR,IAAmB,OAAO,OAC9B,EAAiB,QAEb,GAYA,MAWG;AACH,MAAI,CAAC,EAAG,QAAQ,KAAM,QAAO;EAC7B,IAAM,IAAQ,EAAI,EAAG,OAAO,OAAO;GACjC,QAAQ;IAAE,GAAG,EAAG;IAAQ,MAAM,EAAG,OAAO;IAAM;GAC9C,OAAO,EAAE;GACV;AAGD,SAFA,EAAM,MAAM,KAAK;GAAE,IAAI,EAAG;GAAI,KAAK,EAAG;GAAK,OAAO,EAAG;GAAO,CAAC,EAC7D,EAAI,EAAG,OAAO,MAAM,GACb;IAET,EAAE,CACH,CACF;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD,EAAc,WAAU,wBAAyB,CAAA;OACjD,kBAAC,MAAD;QAAI,WAAU;kBAA0B,EAAQ;QAAU,CAAA;OAC1D,kBAAC,QAAD;QACE,WAAW,+CAA+C,EAAY,EAAQ,WAAW;kBAExF,EAAQ,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;QAC1C,CAAA;OACP,kBAAC,GAAD,EAAY,MAAM,IAAY,CAAA;OAC1B;;KACN,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAQ,eAAe;MACtB,CAAA;KACJ,kBAAC,IAAD,EAAiB,QAAQ,EAAS,QAAU,CAAA;KACxC,EAAA,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEE,kBAAC,GAAD;OACE,WAAW,EAAQ;OACnB,MAAM,EAAQ;OACd,QAAQ,EAAQ;OAChB,CAAA;MACF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SACE,MAAK;SACL,SAAQ;SACR,eAAe,EAAS,oCAAoC;SAC5D,eAAY;mBAJd,CAME,kBAAC,GAAD,EAAK,WAAU,gBAAiB,CAAA,EAAA,mBAEzB;;QACT,kBAAC,GAAD;SACE,MAAK;SACL,SAAQ;SACR,eACE,EAAS,0CAA0C,mBAAmB,EAAQ,GAAG,GAAG;SAEtF,eAAY;mBANd,CAQE,kBAAC,IAAD,EAAa,WAAU,gBAAiB,CAAA,EAAA,gBAEjC;;QACT,kBAAC,IAAD;SACE,WAAW,EAAQ;SACnB,aAAa,EAAQ;SACrB,YAAY,MAAO,EAAS,EAAW,iCAAiC,EAAG,CAAC;SAC5E,SACE,kBAAC,GAAD;UAAQ,MAAK;UAAK,SAAQ;UAAU,eAAY;oBAAhD,CACE,kBAAC,GAAD,EAAgB,WAAU,gBAAiB,CAAA,EAAA,mBAEpC;;SAEX,CAAA;QACE;;MACN,kBAAC,GAAD;OAAO,SAAQ;OAAU,WAAU;iBAChC,EAAQ,YAAY,aAAa;OAC5B,CAAA;MACR,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,eAAe,EAAS,EAAW,gCAAgC,EAAQ,GAAG,CAAC;iBAHjF,CAKE,kBAAC,GAAD,EAAO,WAAU,gBAAiB,CAAA,EAAA,gBAE3B;;MACL;OACF;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAAA,aAE/B;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG;QAAc;QAAE,EAAc;QAC7B;SACQ;SACT,CAAA;KACP,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA,EAAA,UAEvB;UACN,kBAAC,KAAD;OAAG,WAAU;iBAA0B,EAAe;OAAW,CAAA,CACrD;SACT,CAAA;KACP,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAAA,aAE5B;UACN,kBAAC,KAAD;OAAG,WAAU;iBAA0B,EAAkB;OAAW,CAAA,CACxD;SACT,CAAA;KACP,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAAA,QAEzB;UACN,kBAAC,KAAD;OAAG,WAAU;iBACX,kBAAC,GAAD,EAAY,IAAI,EAAQ,SAAW,CAAA;OACjC,CAAA,CACQ;SACT,CAAA;KACH;;GAIN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,IAAD;KACe;KACb,WAAW,EAAQ;KACnB,UAAU,GAAc;KACxB,YAAY,GAAc;KAC1B,CAAA,EACF,kBAAC,IAAD;KAAiC;KAAa,WAAW,EAAQ;KAAM,CAAA,CACnE;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,YAAoB,CAAA,EACpB,CAAA,EACb,kBAAC,GAAD,EAAA,UAAA;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAwB;SAAiB,CAAA,EACzD,kBAAC,QAAD;SAAM,WAAU;mBACb,MAAiB,OAAO,kBAAkB,GAAG,EAAa;SACtD,CAAA,CACH;;OACN,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,KAAgB,EAAE,IAAI;SACzC,CAAA;QACE,CAAA;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD,EAAA,UACG,EAAQ,YAAY,IAAI,KAAK,EAAQ,UAAU,CAAC,oBAAoB,GAAG,WACnE,CAAA,EACP,kBAAC,QAAD,EAAA,UACG,EAAQ,UAAU,IAAI,KAAK,EAAQ,QAAQ,CAAC,oBAAoB,GAAG,WAC/D,CAAA,CACH;;OACM,EAAA,CAAA,CACT,EAAA,CAAA;MAEN,KACC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;OAAW,WAAU;iBAArB,CACE,kBAAC,GAAD,EAAK,WAAU,wBAAyB,CAAA,EAAA,gBAE9B;UACD,CAAA,EACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAe,EAAa;UAAY,CAAA,EACxD,kBAAC,GAAD,EAAA,UAAO,UAAc,CAAA,CACjB;;QACL,EAAa,QACZ,kBAAC,KAAD;SAAG,WAAU;mBAAiC,EAAa;SAAS,CAAA;QAEtE,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAa,WACV,QAAQ,MAA2B,EAAG,WAAW,OAAO,CACzD,QACE,GAAW,MACV,KAAK,EAAG,eAAe,IACzB,EACD;YAAC;YAEH,EAAa,WAAW,QACtB,GAAW,MACV,KAAK,EAAG,eAAe,IACzB,EACD,IAAI;YACH;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAU,CAAA,CACnD,EAAA,CAAA;UACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb;YACG,EAAa,WAAW,QACtB,MAA2B,EAAG,WAAW,OAC3C,CAAC,UAAU;YAAE;YACZ,EAAa,WAAW,UAAU;YAClC;cACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAAS,CAAA,CAClD,EAAA,CAAA;UACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,IAAgB,GAAG,EAAc,SAAS,QAAQ;WACjD,CAAA,EACJ,kBAAC,KAAD;WAAG,WAAU;qBAAgC;WAA+B,CAAA,CACxE,EAAA,CAAA;UACF;;QACM;SACT,EAAA,CAAA;MAGT,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA;OAAW;OACI,EAAiB;OAAO;OACpC,EAAiB,SAAS,IAAI,gBAAgB;OACrC,EAAA,CAAA,EACD,CAAA,EACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB,CACG,GAAgB,KACd,MAQC,kBAAC,OAAD;QAEE,GAAI,QAEA,EAAS,kCAAkC,mBAAmB,EAAK,GAAG,GAAG,EAC3E,EAAK,IACN;QACD,WAAW,6DAA6D;kBAP1E,CASE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAb;UACG,EAAK;UAAI;UAAC,kBAAC,QAAD;WAAM,WAAU;qBAAe,EAAK;WAAa,CAAA;UAC1D;YACJ,kBAAC,OAAD;SAAK,WAAU;mBACZ,EAAK,OAAO,KAAK,MAChB,kBAAC,QAAD;UAAc,WAAU;oBACrB;UACI,EAFI,EAEJ,CACP;SACE,CAAA,CACF,EAAA,CAAA,EACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACG,EAAK,eAAe,GAAE,OAClB;aACP,kBAAC,GAAD;UAAO,SAAQ;UAAY,WAAU;oBAClC,EAAK,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;UACtC,CAAA,CACJ;WACF;UA5BC,EAAK,GA4BN,CAET,EACA,EAAiB,WAAW,KAC3B,kBAAC,KAAD;QAAG,WAAU;kBAAiD;QAAsB,CAAA,CAE1E;SACT,EAAA,CAAA;MAEP,kBAAC,GAAD;OAAiB,WAAW,EAAQ;OAAc;OAAY,CAAA;MAC1D;QAEN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,IAAD;OAAoC;OAAa,WAAW,EAAQ;OAAM,CAAA;MAE1E,kBAAC,IAAD;OAAmC;OAAa,WAAW,EAAQ;OAAM,CAAA;MAEzE,kBAAC,GAAD;OAAqC;OAAa,WAAW,EAAQ;OAAM,CAAA;MAC3E,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA;OAAW;OAAa,EAAkB;OAAO;OAAa,EAAA,CAAA,EACnD,CAAA,EACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB,CACG,EAAkB,KAChB,MACC,kBAAC,OAAD;QAAiB,WAAU;kBAA3B,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAuB,EAAG;UAAY,CAAA,EACtD,kBAAC,GAAD;UAAO,SAAQ;UAAU,WAAU;oBAChC,EAAG,OAAO,aAAa,CAAC,QAAQ,KAAK,IAAI;UACpC,CAAA,CACJ;YACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAC/B,EAAG,UAAU,IAAI,KAAK,EAAG,QAAQ,CAAC,oBAAoB,GAAG,cACtD;WACF;UAXI,EAAG,GAWP,CAET,EACA,EAAkB,WAAW,KAC5B,kBAAC,KAAD;QAAG,WAAU;kBAAiD;QAAkB,CAAA,CAEtE;SACT,EAAA,CAAA;MAEP,kBAAC,GAAD;OAA2B;OAAa,WAAW,EAAQ;OAAM,CAAA;MAEhE,EAAiB,SAAS,KACzB,kBAAC,GAAD;OAAM,eAAY;iBAAlB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;QAAW,WAAU;kBAArB,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAAA,mBAEnB;WACD,CAAA,EACb,kBAAC,GAAD;QAAa,WAAU;kBAGpB,EAAiB,KAAK,MACrB,kBAAC,OAAD;SAA2B,WAAU;mBAArC,CACE,kBAAC,UAAD;UACE,MAAK;UACL,WAAU;UACV,eACE,EAAS,EAAW,kCAAkC,EAAM,OAAO,GAAG,CAAC;oBAGxE,EAAM,OAAO;UACP,CAAA,EACT,kBAAC,UAAD;UACE,MAAK;UACL,WAAU;UACV,eACE,EAAS,EAAW,0BAA0B,EAAM,OAAO,KAAK,GAAG,CAAC;oBAJxE;WAOG,EAAM,MAAM;WAAO;WAAM,EAAM,MAAM,WAAW,IAAI,KAAK;WAAI;WAAe;WAC5E,EAAM,OAAO,KAAK;WACZ;YACL;WApBI,EAAM,OAAO,GAoBjB,CACN;QACU,CAAA,CACT;;MAGT,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAArB;SACE,kBAAC,IAAD,EAAa,WAAU,wBAAyB,CAAA;;SACxC,EAAU;SAAO;SACf;WACZ,kBAAC,GAAD;QACE,WAAW,EAAQ;QACnB,YAAY,MAAO,EAAS,EAAW,2BAA2B,EAAG,CAAC;QACtE,CAAA,CACS;UACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB;QACG,GAAS,KACP,MAOC,kBAAC,OAAD;SAEE,GAAI,QACI,EAAS,EAAW,2BAA2B,EAAK,GAAG,CAAC,EAC9D,EAAK,MACN;SACD,WAAW,+BAA+B;mBAN5C,CAQE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAgC,EAAK;WAAa,CAAA,EAClE,kBAAC,QAAD;WACE,WAAW,6CAA6C,EAAkB,EAAK,aAAa;qBAE3F,EAAU,EAAK,SAAS;WACpB,CAAA,CACH;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf;WAAoD;WAC3C,EAAK;WAAU;WAAI,EAAU,EAAK,OAAO;WAC5C;YACF;WAlBC,EAAK,GAkBN,CAET;QACA,EAAU,WAAW,KACpB,kBAAC,KAAD;SAAG,WAAU;mBAAiD;SAAkB,CAAA;QAElF,kBAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,eACE,EAAS,uCAAuC,mBAAmB,EAAQ,GAAG,GAAG;mBAJrF;UAMC;UACsB,EAAa;UAAO;UAClC;;QACG;SACT,EAAA,CAAA;MAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,WAAmB,CAAA,EACnB,CAAA,EACb,kBAAC,GAAD;OAAa,WAAU;iBAAvB;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAwB;UAAY,CAAA,EACpD,kBAAC,QAAD,EAAA,UACE,kBAAC,GAAD,EAAY,IAAI,EAAQ,SAAW,CAAA,EAC9B,CAAA,CACH;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAwB;UAAU,CAAA,EAClD,kBAAC,QAAD;UAAM,WAAU;oBAAa,EAAQ;UAAW,CAAA,CAC5C;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAwB;UAAkB,CAAA,EAC1D,kBAAC,QAAD;UAAM,WAAU;oBAAc,EAAQ,YAAY,aAAa;UAAQ,CAAA,CACnE;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAwB;UAAc,CAAA,EACrD,EAAQ,UACP,kBAAC,UAAD;UACE,MAAK;UACL,WAAU;UACV,eACE,EAAS,EAAW,iCAAiC,EAAQ,QAAQ,GAAG,CAAC;oBAG1E,EAAQ,QAAQ;UACV,CAAA,GAET,kBAAC,QAAD;UAAM,WAAU;oBAAwB;UAAiB,CAAA,CAEvD;;QACL,EAAQ,KAAK,SAAS,KACrB,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAwB;UAAW,CAAA,EACnD,kBAAC,OAAD;UAAK,WAAU;oBACZ,EAAQ,KAAK,KAAK,MACjB,kBAAC,GAAD;WAAiB,SAAQ;WAAU,WAAU;qBAC1C;WACK,EAFI,EAEJ,CACR;UACE,CAAA,CACF;;QAEI;SACT,EAAA,CAAA;MACH;OACF;;GACF"}
@@ -7,63 +7,63 @@ import { useState as c } from "react";
7
7
  import { Check as l, Copy as u, Share2 as d } from "lucide-react";
8
8
  import { useMutation as f, useQuery as p } from "@apollo/client/react";
9
9
  import { jsx as m, jsxs as h } from "react/jsx-runtime";
10
- import { Badge as g, Button as _, Input as v, Label as y, ResponsiveDialog as b, ResponsiveDialogContent as x, ResponsiveDialogDescription as S, ResponsiveDialogFooter as C, ResponsiveDialogHeader as ee, ResponsiveDialogTitle as w, ResponsiveDialogTrigger as T } from "@burdenoff/fe-libs/ui";
11
- import { copyTextToClipboard as E } from "@burdenoff/fe-libs/sharing";
10
+ import { Badge as g, Button as _, Input as v, Label as y, ResponsiveDialog as b, ResponsiveDialogContent as x, ResponsiveDialogDescription as S, ResponsiveDialogFooter as C, ResponsiveDialogHeader as ee, ResponsiveDialogTitle as te, ResponsiveDialogTrigger as w } from "@burdenoff/fe-libs/ui";
11
+ import { copyTextToClipboard as T } from "@burdenoff/fe-libs/sharing";
12
12
  //#region src/planmagnet/reports/ShareDialog.tsx
13
- var D = 864e5, O = 7, k = 90;
14
- function A(e) {
15
- return new Date(Date.now() + e * D).toISOString().slice(0, 10);
13
+ var E = 864e5, D = 7, O = 90;
14
+ function k(e) {
15
+ return new Date(Date.now() + e * E).toISOString().slice(0, 10);
16
16
  }
17
- function j(e) {
17
+ function A(e) {
18
18
  return new Date(e).toLocaleString();
19
19
  }
20
- function M({ reportId: D, runId: M }) {
21
- let [N, P] = c(!1), [F, I] = c(() => A(O)), [L, R] = c(""), [z, B] = c(""), [V, H] = c(null), [U, W] = c(null), [G, K] = c(!1), q = p(o, {
22
- variables: { reportId: D },
23
- skip: !N
24
- }), [J, Y] = f(a, { refetchQueries: [{
20
+ function j({ reportId: E, runId: j }) {
21
+ let [M, N] = c(!1), [P, F] = c(() => k(D)), [I, L] = c(""), [R, z] = c(""), [B, V] = c(null), [H, U] = c(null), [W, G] = c(!1), K = p(o, {
22
+ variables: { reportId: E },
23
+ skip: !M
24
+ }), [q, J] = f(a, { refetchQueries: [{
25
25
  query: o,
26
- variables: { reportId: D }
27
- }] }), [X] = f(s, { refetchQueries: [{
26
+ variables: { reportId: E }
27
+ }] }), [Y] = f(s, { refetchQueries: [{
28
28
  query: o,
29
- variables: { reportId: D }
30
- }] }), Z = async () => {
31
- if (M) {
32
- H(null), W(null);
29
+ variables: { reportId: E }
30
+ }] }), X = async () => {
31
+ if (j) {
32
+ V(null), U(null);
33
33
  try {
34
- let e = (await t(J({ variables: {
35
- runId: M,
34
+ let e = (await t(q({ variables: {
35
+ runId: j,
36
36
  input: {
37
- expiresAt: `${F}T23:59:59.999Z`,
38
- password: L.trim() || void 0,
39
- watermark: z.trim() || void 0
37
+ expiresAt: `${P}T23:59:59.999Z`,
38
+ password: I.trim() || void 0,
39
+ watermark: R.trim() || void 0
40
40
  }
41
41
  } }))).data?.planmagnetCreateReportShare;
42
- e && (W(e), K(!1), R(""), B(""));
42
+ e && (U(e), G(!1), L(""), z(""));
43
43
  } catch (t) {
44
- H(e(t, "The share link could not be created."));
44
+ V(e(t, "The share link could not be created."));
45
45
  }
46
46
  }
47
- }, Q = async (n) => {
48
- H(null);
47
+ }, Z = async (n) => {
48
+ V(null);
49
49
  try {
50
- await t(X({ variables: { id: n } })), U?.id === n && W(null);
50
+ await t(Y({ variables: { id: n } })), H?.id === n && U(null);
51
51
  } catch (t) {
52
- H(e(t, "The share could not be revoked."));
52
+ V(e(t, "The share could not be revoked."));
53
53
  }
54
- }, te = async (e) => {
54
+ }, ne = async (e) => {
55
55
  try {
56
- await E(i(e)), K(!0);
56
+ await T(i(e)), G(!0);
57
57
  } catch {
58
- H("Could not copy — copy the link manually.");
58
+ V("Could not copy — copy the link manually.");
59
59
  }
60
- }, $ = q.data?.planmagnetReportShares ?? [];
60
+ }, Q = K.data?.planmagnetReportShares ?? [], $ = () => {
61
+ N(!1), U(null), V(null);
62
+ };
61
63
  return /* @__PURE__ */ h(b, {
62
- open: N,
63
- onOpenChange: (e) => {
64
- P(e), e || (W(null), H(null));
65
- },
66
- children: [/* @__PURE__ */ m(T, {
64
+ open: M,
65
+ onOpenChange: (e) => e ? N(!0) : $(),
66
+ children: [/* @__PURE__ */ m(w, {
67
67
  asChild: !0,
68
68
  children: /* @__PURE__ */ h(_, {
69
69
  size: "sm",
@@ -74,12 +74,12 @@ function M({ reportId: D, runId: M }) {
74
74
  }), /* @__PURE__ */ h(x, {
75
75
  className: "max-h-[85vh] overflow-y-auto sm:max-w-lg",
76
76
  children: [
77
- /* @__PURE__ */ h(ee, { children: [/* @__PURE__ */ m(w, { children: "Share this report" }), /* @__PURE__ */ m(S, { children: "Anyone with the link can view the most recently shared run, read-only, without signing in." })] }),
78
- !M && /* @__PURE__ */ m("p", {
77
+ /* @__PURE__ */ h(ee, { children: [/* @__PURE__ */ m(te, { children: "Share this report" }), /* @__PURE__ */ m(S, { children: "Anyone with the link can view the most recently shared run, read-only, without signing in." })] }),
78
+ !j && /* @__PURE__ */ m("p", {
79
79
  className: "text-sm text-muted-foreground",
80
80
  children: "Run this report at least once before sharing it — a share links to one specific run."
81
81
  }),
82
- M && /* @__PURE__ */ h("div", {
82
+ j && /* @__PURE__ */ h("div", {
83
83
  className: "space-y-3 rounded-md border border-border p-3",
84
84
  children: [
85
85
  /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m(y, {
@@ -89,10 +89,10 @@ function M({ reportId: D, runId: M }) {
89
89
  id: "share-expiry",
90
90
  type: "date",
91
91
  className: "mt-1",
92
- value: F,
93
- min: A(1),
94
- max: A(k),
95
- onChange: (e) => I(e.target.value)
92
+ value: P,
93
+ min: k(1),
94
+ max: k(O),
95
+ onChange: (e) => F(e.target.value)
96
96
  })] }),
97
97
  /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m(y, {
98
98
  htmlFor: "share-password",
@@ -101,8 +101,8 @@ function M({ reportId: D, runId: M }) {
101
101
  id: "share-password",
102
102
  type: "password",
103
103
  className: "mt-1",
104
- value: L,
105
- onChange: (e) => R(e.target.value),
104
+ value: I,
105
+ onChange: (e) => L(e.target.value),
106
106
  placeholder: "Leave blank for no password"
107
107
  })] }),
108
108
  /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m(y, {
@@ -111,20 +111,20 @@ function M({ reportId: D, runId: M }) {
111
111
  }), /* @__PURE__ */ m(v, {
112
112
  id: "share-watermark",
113
113
  className: "mt-1",
114
- value: z,
115
- onChange: (e) => B(e.target.value),
114
+ value: R,
115
+ onChange: (e) => z(e.target.value),
116
116
  placeholder: `Shared via PlanMagnet on ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`
117
117
  })] }),
118
118
  /* @__PURE__ */ m(_, {
119
119
  size: "sm",
120
- onClick: Z,
121
- disabled: Y.loading,
120
+ onClick: X,
121
+ disabled: J.loading,
122
122
  "data-testid": "report-share-create",
123
- children: Y.loading ? "Creating…" : "Create share link"
123
+ children: J.loading ? "Creating…" : "Create share link"
124
124
  })
125
125
  ]
126
126
  }),
127
- U?.token && /* @__PURE__ */ h("div", {
127
+ H?.token && /* @__PURE__ */ h("div", {
128
128
  className: "space-y-2 rounded-md border border-primary bg-primary/5 p-3",
129
129
  "data-testid": "report-share-link",
130
130
  children: [
@@ -140,33 +140,33 @@ function M({ reportId: D, runId: M }) {
140
140
  className: "flex items-center gap-2",
141
141
  children: [/* @__PURE__ */ m(v, {
142
142
  readOnly: !0,
143
- value: i(U.token),
143
+ value: i(H.token),
144
144
  className: "text-xs"
145
145
  }), /* @__PURE__ */ h(_, {
146
146
  size: "sm",
147
147
  variant: "outline",
148
- onClick: () => te(U.token),
149
- children: [m(G ? l : u, { className: "h-4 w-4" }), G ? "Copied" : "Copy"]
148
+ onClick: () => ne(H.token),
149
+ children: [m(W ? l : u, { className: "h-4 w-4" }), W ? "Copied" : "Copy"]
150
150
  })]
151
151
  })
152
152
  ]
153
153
  }),
154
- V && /* @__PURE__ */ m(r, { message: V }),
154
+ B && /* @__PURE__ */ m(r, { message: B }),
155
155
  /* @__PURE__ */ h("div", {
156
156
  className: "space-y-2",
157
157
  "data-testid": "report-shares",
158
158
  children: [/* @__PURE__ */ m("h3", {
159
159
  className: "text-sm font-semibold",
160
160
  children: "Existing shares"
161
- }), q.loading && !q.data ? /* @__PURE__ */ m("p", {
161
+ }), K.loading && !K.data ? /* @__PURE__ */ m("p", {
162
162
  className: "text-sm text-muted-foreground",
163
163
  children: "Loading…"
164
- }) : $.length === 0 ? /* @__PURE__ */ m("p", {
164
+ }) : Q.length === 0 ? /* @__PURE__ */ m("p", {
165
165
  className: "text-sm text-muted-foreground",
166
166
  children: "No shares yet."
167
167
  }) : /* @__PURE__ */ m("ul", {
168
168
  className: "space-y-2",
169
- children: $.map((e) => {
169
+ children: Q.map((e) => {
170
170
  let t = !!e.revokedAt, r = !t && new Date(e.expiresAt).getTime() < (/* @__PURE__ */ new Date()).getTime();
171
171
  return /* @__PURE__ */ h("li", {
172
172
  className: "flex flex-wrap items-center justify-between gap-2 rounded-md border border-border p-2 text-xs",
@@ -187,10 +187,10 @@ function M({ reportId: D, runId: M }) {
187
187
  className: "text-muted-foreground",
188
188
  children: [
189
189
  "Created ",
190
- j(e.createdAt),
190
+ A(e.createdAt),
191
191
  " · Expires",
192
192
  " ",
193
- j(e.expiresAt)
193
+ A(e.expiresAt)
194
194
  ]
195
195
  }),
196
196
  /* @__PURE__ */ h("p", {
@@ -200,7 +200,7 @@ function M({ reportId: D, runId: M }) {
200
200
  e.accessCount,
201
201
  " time",
202
202
  e.accessCount === 1 ? "" : "s",
203
- e.lastAccessedAt ? ` · last ${j(e.lastAccessedAt)}` : ""
203
+ e.lastAccessedAt ? ` · last ${A(e.lastAccessedAt)}` : ""
204
204
  ]
205
205
  })
206
206
  ]
@@ -213,7 +213,7 @@ function M({ reportId: D, runId: M }) {
213
213
  destructive: !0,
214
214
  variant: "outline",
215
215
  disabled: !1,
216
- onConfirm: () => void Q(e.id)
216
+ onConfirm: () => void Z(e.id)
217
217
  })]
218
218
  }, e.id);
219
219
  })
@@ -221,7 +221,7 @@ function M({ reportId: D, runId: M }) {
221
221
  }),
222
222
  /* @__PURE__ */ m(C, { children: /* @__PURE__ */ m(_, {
223
223
  variant: "outline",
224
- onClick: () => P(!1),
224
+ onClick: $,
225
225
  children: "Close"
226
226
  }) })
227
227
  ]
@@ -229,6 +229,6 @@ function M({ reportId: D, runId: M }) {
229
229
  });
230
230
  }
231
231
  //#endregion
232
- export { M as ShareDialog };
232
+ export { j as ShareDialog };
233
233
 
234
234
  //# sourceMappingURL=ShareDialog.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ShareDialog.js","names":[],"sources":["../../../src/planmagnet/reports/ShareDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Input,\n Label,\n} from '@burdenoff/fe-libs/ui';\nimport { Check, Copy, Share2 } from 'lucide-react';\nimport { copyTextToClipboard } from '@burdenoff/fe-libs/sharing';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { throwOnError } from '../utils/mutationResult';\nimport { CREATE_REPORT_SHARE, LIST_REPORT_SHARES, REVOKE_REPORT_SHARE } from './reportGraphql';\nimport { sharedReportLink } from './sharedReportRoute';\nimport type { ReportShare } from './reportModel';\n\n/** Share links (ANR-16), owner-only — a report's owner creates a link to one persisted run; anyone with the\n * link (and the password, if set) can view that run's snapshot read-only at `/public/report/<token>`, no\n * workspace login required. The raw token is shown exactly once, right after creation. */\n\nconst DAY_MS = 86_400_000;\nconst DEFAULT_EXPIRY_DAYS = 7;\nconst MAX_EXPIRY_DAYS = 90;\n\nfunction dateInputValue(daysFromNow: number): string {\n return new Date(Date.now() + daysFromNow * DAY_MS).toISOString().slice(0, 10);\n}\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleString();\n}\n\nexport function ShareDialog({ reportId, runId }: { reportId: string; runId: string | null }) {\n const [open, setOpen] = useState(false);\n const [expiresAt, setExpiresAt] = useState(() => dateInputValue(DEFAULT_EXPIRY_DAYS));\n const [password, setPassword] = useState('');\n const [watermark, setWatermark] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [justCreated, setJustCreated] = useState<ReportShare | null>(null);\n const [copied, setCopied] = useState(false);\n\n const shares = useQuery<{ planmagnetReportShares: ReportShare[] }>(LIST_REPORT_SHARES, {\n variables: { reportId },\n skip: !open,\n });\n const [createShare, createState] = useMutation(CREATE_REPORT_SHARE, {\n refetchQueries: [{ query: LIST_REPORT_SHARES, variables: { reportId } }],\n });\n const [revokeShare] = useMutation(REVOKE_REPORT_SHARE, {\n refetchQueries: [{ query: LIST_REPORT_SHARES, variables: { reportId } }],\n });\n\n const handleCreate = async () => {\n if (!runId) return;\n setError(null);\n setJustCreated(null);\n try {\n const result = await throwOnError(\n createShare({\n variables: {\n runId,\n input: {\n expiresAt: `${expiresAt}T23:59:59.999Z`,\n password: password.trim() || undefined,\n watermark: watermark.trim() || undefined,\n },\n },\n })\n );\n const share = (result.data as { planmagnetCreateReportShare?: ReportShare } | undefined)\n ?.planmagnetCreateReportShare;\n if (share) {\n setJustCreated(share);\n setCopied(false);\n setPassword('');\n setWatermark('');\n }\n } catch (e) {\n setError(mutationErrorMessage(e, 'The share link could not be created.'));\n }\n };\n\n const handleRevoke = async (id: string) => {\n setError(null);\n try {\n await throwOnError(revokeShare({ variables: { id } }));\n if (justCreated?.id === id) setJustCreated(null);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The share could not be revoked.'));\n }\n };\n\n const copyLink = async (token: string) => {\n try {\n await copyTextToClipboard(sharedReportLink(token));\n setCopied(true);\n } catch {\n setError('Could not copy — copy the link manually.');\n }\n };\n\n const list = shares.data?.planmagnetReportShares ?? [];\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) {\n setJustCreated(null);\n setError(null);\n }\n }}\n >\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"report-share\">\n <Share2 className=\"mr-1 h-4 w-4\" />\n Share\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Share this report</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Anyone with the link can view the most recently shared run, read-only, without signing\n in.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n {!runId && (\n <p className=\"text-sm text-muted-foreground\">\n Run this report at least once before sharing it — a share links to one specific run.\n </p>\n )}\n\n {runId && (\n <div className=\"space-y-3 rounded-md border border-border p-3\">\n <div>\n <Label htmlFor=\"share-expiry\">Expires</Label>\n <Input\n id=\"share-expiry\"\n type=\"date\"\n className=\"mt-1\"\n value={expiresAt}\n min={dateInputValue(1)}\n max={dateInputValue(MAX_EXPIRY_DAYS)}\n onChange={(e) => setExpiresAt(e.target.value)}\n />\n </div>\n <div>\n <Label htmlFor=\"share-password\">Password (optional)</Label>\n <Input\n id=\"share-password\"\n type=\"password\"\n className=\"mt-1\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n placeholder=\"Leave blank for no password\"\n />\n </div>\n <div>\n <Label htmlFor=\"share-watermark\">Watermark text (optional)</Label>\n <Input\n id=\"share-watermark\"\n className=\"mt-1\"\n value={watermark}\n onChange={(e) => setWatermark(e.target.value)}\n placeholder={`Shared via PlanMagnet on ${new Date().toLocaleDateString()}`}\n />\n </div>\n <Button\n size=\"sm\"\n onClick={handleCreate}\n disabled={createState.loading}\n data-testid=\"report-share-create\"\n >\n {createState.loading ? 'Creating…' : 'Create share link'}\n </Button>\n </div>\n )}\n\n {justCreated?.token && (\n <div\n className=\"space-y-2 rounded-md border border-primary bg-primary/5 p-3\"\n data-testid=\"report-share-link\"\n >\n <p className=\"text-sm font-medium\">Link created — copy it now.</p>\n <p className=\"text-xs text-muted-foreground\">\n You will not see this link again. If you lose it, revoke this share and create a new\n one.\n </p>\n <div className=\"flex items-center gap-2\">\n <Input readOnly value={sharedReportLink(justCreated.token)} className=\"text-xs\" />\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => copyLink(justCreated.token as string)}\n >\n {copied ? <Check className=\"h-4 w-4\" /> : <Copy className=\"h-4 w-4\" />}\n {copied ? 'Copied' : 'Copy'}\n </Button>\n </div>\n </div>\n )}\n\n {error && <ErrorLine message={error} />}\n\n <div className=\"space-y-2\" data-testid=\"report-shares\">\n <h3 className=\"text-sm font-semibold\">Existing shares</h3>\n {shares.loading && !shares.data ? (\n <p className=\"text-sm text-muted-foreground\">Loading…</p>\n ) : list.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">No shares yet.</p>\n ) : (\n <ul className=\"space-y-2\">\n {list.map((share) => {\n const revoked = !!share.revokedAt;\n const expired =\n !revoked && new Date(share.expiresAt).getTime() < new Date().getTime();\n return (\n <li\n key={share.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-md border border-border p-2 text-xs\"\n >\n <div className=\"space-y-0.5\">\n <div className=\"flex items-center gap-1.5\">\n <Badge variant={revoked ? 'secondary' : expired ? 'warning' : 'success'}>\n {revoked ? 'Revoked' : expired ? 'Expired' : 'Active'}\n </Badge>\n {share.hasPassword && <Badge variant=\"outline\">Password</Badge>}\n </div>\n <p className=\"text-muted-foreground\">\n Created {formatDate(share.createdAt)} · Expires{' '}\n {formatDate(share.expiresAt)}\n </p>\n <p className=\"text-muted-foreground\">\n Accessed {share.accessCount} time{share.accessCount === 1 ? '' : 's'}\n {share.lastAccessedAt ? ` · last ${formatDate(share.lastAccessedAt)}` : ''}\n </p>\n </div>\n {!revoked && (\n <ConfirmButton\n label=\"Revoke\"\n icon={null}\n title=\"Revoke this share?\"\n description=\"Anyone with this link loses access immediately. This cannot be undone.\"\n confirmLabel=\"Revoke\"\n destructive\n variant=\"outline\"\n disabled={false}\n onConfirm={() => void handleRevoke(share.id)}\n />\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => setOpen(false)}>\n Close\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ShareDialog.js","names":[],"sources":["../../../src/planmagnet/reports/ShareDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogTrigger,\n Input,\n Label,\n} from '@burdenoff/fe-libs/ui';\nimport { Check, Copy, Share2 } from 'lucide-react';\nimport { copyTextToClipboard } from '@burdenoff/fe-libs/sharing';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { throwOnError } from '../utils/mutationResult';\nimport { CREATE_REPORT_SHARE, LIST_REPORT_SHARES, REVOKE_REPORT_SHARE } from './reportGraphql';\nimport { sharedReportLink } from './sharedReportRoute';\nimport type { ReportShare } from './reportModel';\n\n/** Share links (ANR-16), owner-only — a report's owner creates a link to one persisted run; anyone with the\n * link (and the password, if set) can view that run's snapshot read-only at `/public/report/<token>`, no\n * workspace login required. The raw token is shown exactly once, right after creation. */\n\nconst DAY_MS = 86_400_000;\nconst DEFAULT_EXPIRY_DAYS = 7;\nconst MAX_EXPIRY_DAYS = 90;\n\nfunction dateInputValue(daysFromNow: number): string {\n return new Date(Date.now() + daysFromNow * DAY_MS).toISOString().slice(0, 10);\n}\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleString();\n}\n\nexport function ShareDialog({ reportId, runId }: { reportId: string; runId: string | null }) {\n const [open, setOpen] = useState(false);\n const [expiresAt, setExpiresAt] = useState(() => dateInputValue(DEFAULT_EXPIRY_DAYS));\n const [password, setPassword] = useState('');\n const [watermark, setWatermark] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [justCreated, setJustCreated] = useState<ReportShare | null>(null);\n const [copied, setCopied] = useState(false);\n\n const shares = useQuery<{ planmagnetReportShares: ReportShare[] }>(LIST_REPORT_SHARES, {\n variables: { reportId },\n skip: !open,\n });\n const [createShare, createState] = useMutation(CREATE_REPORT_SHARE, {\n refetchQueries: [{ query: LIST_REPORT_SHARES, variables: { reportId } }],\n });\n const [revokeShare] = useMutation(REVOKE_REPORT_SHARE, {\n refetchQueries: [{ query: LIST_REPORT_SHARES, variables: { reportId } }],\n });\n\n const handleCreate = async () => {\n if (!runId) return;\n setError(null);\n setJustCreated(null);\n try {\n const result = await throwOnError(\n createShare({\n variables: {\n runId,\n input: {\n expiresAt: `${expiresAt}T23:59:59.999Z`,\n password: password.trim() || undefined,\n watermark: watermark.trim() || undefined,\n },\n },\n })\n );\n const share = (result.data as { planmagnetCreateReportShare?: ReportShare } | undefined)\n ?.planmagnetCreateReportShare;\n if (share) {\n setJustCreated(share);\n setCopied(false);\n setPassword('');\n setWatermark('');\n }\n } catch (e) {\n setError(mutationErrorMessage(e, 'The share link could not be created.'));\n }\n };\n\n const handleRevoke = async (id: string) => {\n setError(null);\n try {\n await throwOnError(revokeShare({ variables: { id } }));\n if (justCreated?.id === id) setJustCreated(null);\n } catch (e) {\n setError(mutationErrorMessage(e, 'The share could not be revoked.'));\n }\n };\n\n const copyLink = async (token: string) => {\n try {\n await copyTextToClipboard(sharedReportLink(token));\n setCopied(true);\n } catch {\n setError('Could not copy — copy the link manually.');\n }\n };\n\n const list = shares.data?.planmagnetReportShares ?? [];\n\n /** Every way of closing (the footer button, the X, Escape, an outside click) goes through here, so the share\n * token shown once after creation can never reappear when the dialog is reopened. Setting `open` directly on\n * the controlled dialog does not call `onOpenChange`, which is how the footer button used to skip the reset. */\n const close = () => {\n setOpen(false);\n setJustCreated(null);\n setError(null);\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={(next) => (next ? setOpen(true) : close())}>\n <ResponsiveDialogTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" data-testid=\"report-share\">\n <Share2 className=\"mr-1 h-4 w-4\" />\n Share\n </Button>\n </ResponsiveDialogTrigger>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Share this report</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Anyone with the link can view the most recently shared run, read-only, without signing\n in.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n {!runId && (\n <p className=\"text-sm text-muted-foreground\">\n Run this report at least once before sharing it — a share links to one specific run.\n </p>\n )}\n\n {runId && (\n <div className=\"space-y-3 rounded-md border border-border p-3\">\n <div>\n <Label htmlFor=\"share-expiry\">Expires</Label>\n <Input\n id=\"share-expiry\"\n type=\"date\"\n className=\"mt-1\"\n value={expiresAt}\n min={dateInputValue(1)}\n max={dateInputValue(MAX_EXPIRY_DAYS)}\n onChange={(e) => setExpiresAt(e.target.value)}\n />\n </div>\n <div>\n <Label htmlFor=\"share-password\">Password (optional)</Label>\n <Input\n id=\"share-password\"\n type=\"password\"\n className=\"mt-1\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n placeholder=\"Leave blank for no password\"\n />\n </div>\n <div>\n <Label htmlFor=\"share-watermark\">Watermark text (optional)</Label>\n <Input\n id=\"share-watermark\"\n className=\"mt-1\"\n value={watermark}\n onChange={(e) => setWatermark(e.target.value)}\n placeholder={`Shared via PlanMagnet on ${new Date().toLocaleDateString()}`}\n />\n </div>\n <Button\n size=\"sm\"\n onClick={handleCreate}\n disabled={createState.loading}\n data-testid=\"report-share-create\"\n >\n {createState.loading ? 'Creating…' : 'Create share link'}\n </Button>\n </div>\n )}\n\n {justCreated?.token && (\n <div\n className=\"space-y-2 rounded-md border border-primary bg-primary/5 p-3\"\n data-testid=\"report-share-link\"\n >\n <p className=\"text-sm font-medium\">Link created — copy it now.</p>\n <p className=\"text-xs text-muted-foreground\">\n You will not see this link again. If you lose it, revoke this share and create a new\n one.\n </p>\n <div className=\"flex items-center gap-2\">\n <Input readOnly value={sharedReportLink(justCreated.token)} className=\"text-xs\" />\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => copyLink(justCreated.token as string)}\n >\n {copied ? <Check className=\"h-4 w-4\" /> : <Copy className=\"h-4 w-4\" />}\n {copied ? 'Copied' : 'Copy'}\n </Button>\n </div>\n </div>\n )}\n\n {error && <ErrorLine message={error} />}\n\n <div className=\"space-y-2\" data-testid=\"report-shares\">\n <h3 className=\"text-sm font-semibold\">Existing shares</h3>\n {shares.loading && !shares.data ? (\n <p className=\"text-sm text-muted-foreground\">Loading…</p>\n ) : list.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">No shares yet.</p>\n ) : (\n <ul className=\"space-y-2\">\n {list.map((share) => {\n const revoked = !!share.revokedAt;\n const expired =\n !revoked && new Date(share.expiresAt).getTime() < new Date().getTime();\n return (\n <li\n key={share.id}\n className=\"flex flex-wrap items-center justify-between gap-2 rounded-md border border-border p-2 text-xs\"\n >\n <div className=\"space-y-0.5\">\n <div className=\"flex items-center gap-1.5\">\n <Badge variant={revoked ? 'secondary' : expired ? 'warning' : 'success'}>\n {revoked ? 'Revoked' : expired ? 'Expired' : 'Active'}\n </Badge>\n {share.hasPassword && <Badge variant=\"outline\">Password</Badge>}\n </div>\n <p className=\"text-muted-foreground\">\n Created {formatDate(share.createdAt)} · Expires{' '}\n {formatDate(share.expiresAt)}\n </p>\n <p className=\"text-muted-foreground\">\n Accessed {share.accessCount} time{share.accessCount === 1 ? '' : 's'}\n {share.lastAccessedAt ? ` · last ${formatDate(share.lastAccessedAt)}` : ''}\n </p>\n </div>\n {!revoked && (\n <ConfirmButton\n label=\"Revoke\"\n icon={null}\n title=\"Revoke this share?\"\n description=\"Anyone with this link loses access immediately. This cannot be undone.\"\n confirmLabel=\"Revoke\"\n destructive\n variant=\"outline\"\n disabled={false}\n onConfirm={() => void handleRevoke(share.id)}\n />\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={close}>\n Close\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.1001.2",
3
+ "version": "2026.1001.4",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [