@burdenoff/microfe-planmagnet 2026.1001.2 → 2026.1001.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +160 -153
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +121 -109
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/controls/MilestoneControls.d.ts +2 -0
- package/dist/planmagnet/controls/MilestoneControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/MilestoneControls.js +89 -72
- package/dist/planmagnet/controls/MilestoneControls.js.map +1 -1
- package/dist/planmagnet/meetings/MeetingActionItemsPanel.d.ts +17 -0
- package/dist/planmagnet/meetings/MeetingActionItemsPanel.d.ts.map +1 -0
- package/dist/planmagnet/meetings/MeetingActionItemsPanel.js +218 -0
- package/dist/planmagnet/meetings/MeetingActionItemsPanel.js.map +1 -0
- package/dist/planmagnet/meetings/MeetingsCard.d.ts +7 -0
- package/dist/planmagnet/meetings/MeetingsCard.d.ts.map +1 -0
- package/dist/planmagnet/meetings/MeetingsCard.js +108 -0
- package/dist/planmagnet/meetings/MeetingsCard.js.map +1 -0
- package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.d.ts +9 -0
- package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.d.ts.map +1 -0
- package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.js +232 -0
- package/dist/planmagnet/meetings/ScheduleCeremoniesDialog.js.map +1 -0
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts +19 -0
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.d.ts.map +1 -0
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js +226 -0
- package/dist/planmagnet/meetings/ScheduleMeetingDialog.js.map +1 -0
- package/dist/planmagnet/meetings/UnlinkMeetingDialog.d.ts +14 -0
- package/dist/planmagnet/meetings/UnlinkMeetingDialog.d.ts.map +1 -0
- package/dist/planmagnet/meetings/UnlinkMeetingDialog.js +81 -0
- package/dist/planmagnet/meetings/UnlinkMeetingDialog.js.map +1 -0
- package/dist/planmagnet/meetings/meetingActions.d.ts +57 -0
- package/dist/planmagnet/meetings/meetingActions.d.ts.map +1 -0
- package/dist/planmagnet/meetings/meetingActions.js +51 -0
- package/dist/planmagnet/meetings/meetingActions.js.map +1 -0
- package/dist/planmagnet/meetings/meetingGraphql.d.ts +29 -0
- package/dist/planmagnet/meetings/meetingGraphql.d.ts.map +1 -0
- package/dist/planmagnet/meetings/meetingGraphql.js +166 -0
- package/dist/planmagnet/meetings/meetingGraphql.js.map +1 -0
- package/dist/planmagnet/meetings/meetingModel.d.ts +74 -0
- package/dist/planmagnet/meetings/meetingModel.d.ts.map +1 -0
- package/dist/planmagnet/meetings/meetingModel.js +88 -0
- package/dist/planmagnet/meetings/meetingModel.js.map +1 -0
- package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.js +197 -0
- package/dist/planmagnet/pages/MeetingsMeetingDetailPagePage.js.map +1 -0
- package/dist/planmagnet/pages/MeetingsMeetingsPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/MeetingsMeetingsPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/MeetingsMeetingsPagePage.js +173 -0
- package/dist/planmagnet/pages/MeetingsMeetingsPagePage.js.map +1 -0
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js +73 -52
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +205 -190
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +192 -187
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/reports/ShareDialog.js +65 -65
- package/dist/planmagnet/reports/ShareDialog.js.map +1 -1
- 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'} · 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'} · 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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"}
|
|
@@ -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
|
|
11
|
-
import { copyTextToClipboard as
|
|
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
|
|
14
|
-
function
|
|
15
|
-
return new Date(Date.now() + e *
|
|
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
|
|
17
|
+
function A(e) {
|
|
18
18
|
return new Date(e).toLocaleString();
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
let [
|
|
22
|
-
variables: { reportId:
|
|
23
|
-
skip: !
|
|
24
|
-
}), [
|
|
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:
|
|
27
|
-
}] }), [
|
|
26
|
+
variables: { reportId: E }
|
|
27
|
+
}] }), [Y] = f(s, { refetchQueries: [{
|
|
28
28
|
query: o,
|
|
29
|
-
variables: { reportId:
|
|
30
|
-
}] }),
|
|
31
|
-
if (
|
|
32
|
-
|
|
29
|
+
variables: { reportId: E }
|
|
30
|
+
}] }), X = async () => {
|
|
31
|
+
if (j) {
|
|
32
|
+
V(null), U(null);
|
|
33
33
|
try {
|
|
34
|
-
let e = (await t(
|
|
35
|
-
runId:
|
|
34
|
+
let e = (await t(q({ variables: {
|
|
35
|
+
runId: j,
|
|
36
36
|
input: {
|
|
37
|
-
expiresAt: `${
|
|
38
|
-
password:
|
|
39
|
-
watermark:
|
|
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 && (
|
|
42
|
+
e && (U(e), G(!1), L(""), z(""));
|
|
43
43
|
} catch (t) {
|
|
44
|
-
|
|
44
|
+
V(e(t, "The share link could not be created."));
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
},
|
|
48
|
-
|
|
47
|
+
}, Z = async (n) => {
|
|
48
|
+
V(null);
|
|
49
49
|
try {
|
|
50
|
-
await t(
|
|
50
|
+
await t(Y({ variables: { id: n } })), H?.id === n && U(null);
|
|
51
51
|
} catch (t) {
|
|
52
|
-
|
|
52
|
+
V(e(t, "The share could not be revoked."));
|
|
53
53
|
}
|
|
54
|
-
},
|
|
54
|
+
}, ne = async (e) => {
|
|
55
55
|
try {
|
|
56
|
-
await
|
|
56
|
+
await T(i(e)), G(!0);
|
|
57
57
|
} catch {
|
|
58
|
-
|
|
58
|
+
V("Could not copy — copy the link manually.");
|
|
59
59
|
}
|
|
60
|
-
},
|
|
60
|
+
}, Q = K.data?.planmagnetReportShares ?? [], $ = () => {
|
|
61
|
+
N(!1), U(null), V(null);
|
|
62
|
+
};
|
|
61
63
|
return /* @__PURE__ */ h(b, {
|
|
62
|
-
open:
|
|
63
|
-
onOpenChange: (e) =>
|
|
64
|
-
|
|
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(
|
|
78
|
-
!
|
|
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
|
-
|
|
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:
|
|
93
|
-
min:
|
|
94
|
-
max:
|
|
95
|
-
onChange: (e) =>
|
|
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:
|
|
105
|
-
onChange: (e) =>
|
|
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:
|
|
115
|
-
onChange: (e) =>
|
|
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:
|
|
121
|
-
disabled:
|
|
120
|
+
onClick: X,
|
|
121
|
+
disabled: J.loading,
|
|
122
122
|
"data-testid": "report-share-create",
|
|
123
|
-
children:
|
|
123
|
+
children: J.loading ? "Creating…" : "Create share link"
|
|
124
124
|
})
|
|
125
125
|
]
|
|
126
126
|
}),
|
|
127
|
-
|
|
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(
|
|
143
|
+
value: i(H.token),
|
|
144
144
|
className: "text-xs"
|
|
145
145
|
}), /* @__PURE__ */ h(_, {
|
|
146
146
|
size: "sm",
|
|
147
147
|
variant: "outline",
|
|
148
|
-
onClick: () =>
|
|
149
|
-
children: [m(
|
|
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
|
-
|
|
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
|
-
}),
|
|
161
|
+
}), K.loading && !K.data ? /* @__PURE__ */ m("p", {
|
|
162
162
|
className: "text-sm text-muted-foreground",
|
|
163
163
|
children: "Loading…"
|
|
164
|
-
}) :
|
|
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:
|
|
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
|
-
|
|
190
|
+
A(e.createdAt),
|
|
191
191
|
" · Expires",
|
|
192
192
|
" ",
|
|
193
|
-
|
|
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 ${
|
|
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
|
|
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:
|
|
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 {
|
|
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":";;;;;;;;;;;;AA4BA,IAAM,IAAS,OACT,IAAsB,GACtB,IAAkB;AAExB,SAAS,EAAe,GAA6B;AACnD,QAAO,IAAI,KAAK,KAAK,KAAK,GAAG,IAAc,EAAO,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG;;AAG/E,SAAS,EAAW,GAAqB;AACvC,QAAO,IAAI,KAAK,EAAI,CAAC,gBAAgB;;AAGvC,SAAgB,EAAY,EAAE,aAAU,YAAqD;CAC3F,IAAM,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAW,KAAgB,QAAe,EAAe,EAAoB,CAAC,EAC/E,CAAC,GAAU,KAAe,EAAS,GAAG,EACtC,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAa,KAAkB,EAA6B,KAAK,EAClE,CAAC,GAAQ,KAAa,EAAS,GAAM,EAErC,IAAS,EAAoD,GAAoB;EACrF,WAAW,EAAE,aAAU;EACvB,MAAM,CAAC;EACR,CAAC,EACI,CAAC,GAAa,KAAe,EAAY,GAAqB,EAClE,gBAAgB,CAAC;EAAE,OAAO;EAAoB,WAAW,EAAE,aAAU;EAAE,CAAC,EACzE,CAAC,EACI,CAAC,KAAe,EAAY,GAAqB,EACrD,gBAAgB,CAAC;EAAE,OAAO;EAAoB,WAAW,EAAE,aAAU;EAAE,CAAC,EACzE,CAAC,EAEI,IAAe,YAAY;AAC1B,SAEL;GADA,EAAS,KAAK,EACd,EAAe,KAAK;AACpB,OAAI;IAaF,IAAM,KAZS,MAAM,EACnB,EAAY,EACV,WAAW;KACT;KACA,OAAO;MACL,WAAW,GAAG,EAAU;MACxB,UAAU,EAAS,MAAM,IAAI,KAAA;MAC7B,WAAW,EAAU,MAAM,IAAI,KAAA;MAChC;KACF,EACF,CAAC,CACH,EACqB,MAClB;AACJ,IAAI,MACF,EAAe,EAAM,EACrB,EAAU,GAAM,EAChB,EAAY,GAAG,EACf,EAAa,GAAG;YAEX,GAAG;AACV,MAAS,EAAqB,GAAG,uCAAuC,CAAC;;;IAIvE,IAAe,OAAO,MAAe;AACzC,IAAS,KAAK;AACd,MAAI;AAEF,GADA,MAAM,EAAa,EAAY,EAAE,WAAW,EAAE,OAAI,EAAE,CAAC,CAAC,EAClD,GAAa,OAAO,KAAI,EAAe,KAAK;WACzC,GAAG;AACV,KAAS,EAAqB,GAAG,kCAAkC,CAAC;;IAIlE,KAAW,OAAO,MAAkB;AACxC,MAAI;AAEF,GADA,MAAM,EAAoB,EAAiB,EAAM,CAAC,EAClD,EAAU,GAAK;UACT;AACN,KAAS,2CAA2C;;IAIlD,IAAO,EAAO,MAAM,0BAA0B,EAAE;AAEtD,QACE,kBAAC,GAAD;EACQ;EACN,eAAe,MAAS;AAEtB,GADA,EAAQ,EAAK,EACR,MACH,EAAe,KAAK,EACpB,EAAS,KAAK;;YANpB,CAUE,kBAAC,GAAD;GAAyB,SAAA;aACvB,kBAAC,GAAD;IAAQ,MAAK;IAAK,SAAQ;IAAU,eAAY;cAAhD,CACE,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,QAE5B;;GACe,CAAA,EAC1B,kBAAC,GAAD;GAAyB,WAAU;aAAnC;IACE,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,qBAAyC,CAAA,EAChE,kBAAC,GAAD,EAAA,UAA6B,8FAGC,CAAA,CACP,EAAA,CAAA;IAExB,CAAC,KACA,kBAAC,KAAD;KAAG,WAAU;eAAgC;KAEzC,CAAA;IAGL,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAO,SAAQ;iBAAe;OAAe,CAAA,EAC7C,kBAAC,GAAD;OACE,IAAG;OACH,MAAK;OACL,WAAU;OACV,OAAO;OACP,KAAK,EAAe,EAAE;OACtB,KAAK,EAAe,EAAgB;OACpC,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;OAC7C,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAO,SAAQ;iBAAiB;OAA2B,CAAA,EAC3D,kBAAC,GAAD;OACE,IAAG;OACH,MAAK;OACL,WAAU;OACV,OAAO;OACP,WAAW,MAAM,EAAY,EAAE,OAAO,MAAM;OAC5C,aAAY;OACZ,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAO,SAAQ;iBAAkB;OAAiC,CAAA,EAClE,kBAAC,GAAD;OACE,IAAG;OACH,WAAU;OACV,OAAO;OACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;OAC7C,aAAa,6CAA4B,IAAI,MAAM,EAAC,oBAAoB;OACxE,CAAA,CACE,EAAA,CAAA;MACN,kBAAC,GAAD;OACE,MAAK;OACL,SAAS;OACT,UAAU,EAAY;OACtB,eAAY;iBAEX,EAAY,UAAU,cAAc;OAC9B,CAAA;MACL;;IAGP,GAAa,SACZ,kBAAC,OAAD;KACE,WAAU;KACV,eAAY;eAFd;MAIE,kBAAC,KAAD;OAAG,WAAU;iBAAsB;OAA+B,CAAA;MAClE,kBAAC,KAAD;OAAG,WAAU;iBAAgC;OAGzC,CAAA;MACJ,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,UAAA;QAAS,OAAO,EAAiB,EAAY,MAAM;QAAE,WAAU;QAAY,CAAA,EAClF,kBAAC,GAAD;QACE,MAAK;QACL,SAAQ;QACR,eAAe,GAAS,EAAY,MAAgB;kBAHtD,CAKY,EAAT,IAAU,IAAgC,GAAjC,EAAO,WAAU,WAAY,CAA+B,EACrE,IAAS,WAAW,OACd;UACL;;MACF;;IAGP,KAAS,kBAAC,GAAD,EAAW,SAAS,GAAS,CAAA;IAEvC,kBAAC,OAAD;KAAK,WAAU;KAAY,eAAY;eAAvC,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAwB;MAAoB,CAAA,EACzD,EAAO,WAAW,CAAC,EAAO,OACzB,kBAAC,KAAD;MAAG,WAAU;gBAAgC;MAAY,CAAA,GACvD,EAAK,WAAW,IAClB,kBAAC,KAAD;MAAG,WAAU;gBAAgC;MAAkB,CAAA,GAE/D,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAK,KAAK,MAAU;OACnB,IAAM,IAAU,CAAC,CAAC,EAAM,WAClB,IACJ,CAAC,KAAW,IAAI,KAAK,EAAM,UAAU,CAAC,SAAS,oBAAG,IAAI,MAAM,EAAC,SAAS;AACxE,cACE,kBAAC,MAAD;QAEE,WAAU;kBAFZ,CAIE,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,GAAD;YAAO,SAAS,IAAU,cAAc,IAAU,YAAY;sBAC3D,IAAU,YAAY,IAAU,YAAY;YACvC,CAAA,EACP,EAAM,eAAe,kBAAC,GAAD;YAAO,SAAQ;sBAAU;YAAgB,CAAA,CAC3D;;UACN,kBAAC,KAAD;WAAG,WAAU;qBAAb;YAAqC;YAC1B,EAAW,EAAM,UAAU;YAAC;YAAW;YAC/C,EAAW,EAAM,UAAU;YAC1B;;UACJ,kBAAC,KAAD;WAAG,WAAU;qBAAb;YAAqC;YACzB,EAAM;YAAY;YAAM,EAAM,gBAAgB,IAAI,KAAK;YAChE,EAAM,iBAAiB,WAAW,EAAW,EAAM,eAAe,KAAK;YACtE;;UACA;YACL,CAAC,KACA,kBAAC,GAAD;SACE,OAAM;SACN,MAAM;SACN,OAAM;SACN,aAAY;SACZ,cAAa;SACb,aAAA;SACA,SAAQ;SACR,UAAU;SACV,iBAAiB,KAAK,EAAa,EAAM,GAAG;SAC5C,CAAA,CAED;UAhCE,EAAM,GAgCR;QAEP;MACC,CAAA,CAEH;;IAEN,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,eAAe,EAAQ,GAAM;eAAE;KAEhD,CAAA,EACc,CAAA;IACD;KACT"}
|
|
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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"}
|