@burdenoff/microfe-planmagnet 2026.1001.1 → 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 +163 -150
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +124 -108
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/comments/CommentThread.js +36 -36
- package/dist/planmagnet/comments/RoadmapComments.js +21 -21
- package/dist/planmagnet/controls/AutomationRuleControls.js +24 -24
- package/dist/planmagnet/controls/FeatureControls.js +60 -60
- package/dist/planmagnet/controls/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/controls/SyncControls.js +59 -59
- package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +15 -15
- package/dist/planmagnet/docs/DocSuggestionsPanel.js +12 -12
- package/dist/planmagnet/docs/DocVersionCompare.js +3 -3
- package/dist/planmagnet/forecast/DeliveryForecastCard.d.ts +12 -0
- package/dist/planmagnet/forecast/DeliveryForecastCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/DeliveryForecastCard.js +208 -0
- package/dist/planmagnet/forecast/DeliveryForecastCard.js.map +1 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.d.ts +5 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.js +190 -0
- package/dist/planmagnet/forecast/ProjectOutlookCard.js.map +1 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.d.ts +5 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +99 -0
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js.map +1 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.d.ts +8 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.d.ts.map +1 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.js +185 -0
- package/dist/planmagnet/forecast/ResourceForecastCard.js.map +1 -0
- package/dist/planmagnet/forecast/riskFlags.d.ts +35 -0
- package/dist/planmagnet/forecast/riskFlags.d.ts.map +1 -0
- package/dist/planmagnet/forecast/riskFlags.js +37 -0
- package/dist/planmagnet/forecast/riskFlags.js.map +1 -0
- package/dist/planmagnet/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/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +216 -182
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/AutomationAutomationRuleDetailPagePage.js +11 -11
- package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +4 -4
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +163 -163
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js +165 -165
- package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js.map +1 -1
- package/dist/planmagnet/pages/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/ProjectsAtRiskPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +234 -0
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js.map +1 -0
- package/dist/planmagnet/pages/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 +211 -182
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportPagePage.js +22 -22
- package/dist/planmagnet/pages/ReportsReportPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +21 -21
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js +88 -83
- package/dist/planmagnet/pages/TeamTeamDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.d.ts.map +1 -1
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +139 -112
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js.map +1 -1
- package/dist/planmagnet/pages/analytics/StatusFlowCard.js +21 -21
- package/dist/planmagnet/product/ProductControls.js +91 -91
- package/dist/planmagnet/reports/SchedulesPanel.js +33 -33
- package/dist/planmagnet/reports/ShareDialog.js +76 -76
- package/dist/planmagnet/reports/ShareDialog.js.map +1 -1
- package/dist/planmagnet/reports/sharedReportRoute.d.ts +3 -2
- package/dist/planmagnet/reports/sharedReportRoute.d.ts.map +1 -1
- package/dist/planmagnet/reports/sharedReportRoute.js.map +1 -1
- package/dist/planmagnet/roadmaps/RoadmapControls.js +60 -60
- package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +30 -30
- package/dist/planmagnet/roadmaps/RoadmapReviewPanel.js +21 -21
- package/dist/planmagnet/roadmaps/RoadmapVersionsPanel.js +22 -22
- package/package.json +1 -1
|
@@ -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 { 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 <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 <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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"}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { PersonName as n } from "../people/People.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { mutationErrorMessage as r } from "../utils/graphqlErrors.js";
|
|
4
|
+
import { LEAVES_PAGE_CONTEXT as i, throwOnError as a } from "../utils/mutationResult.js";
|
|
5
|
+
import { ConfirmButton as o, ErrorLine as s } from "../controls/ConfirmButton.js";
|
|
6
6
|
import { detailPath as c } from "../utils/rowNavigation.js";
|
|
7
7
|
import "../reports/reportModel.js";
|
|
8
8
|
import { ReportWidgetRenderer as l } from "../reports/ReportWidgetRenderer.js";
|
|
9
9
|
import { PrintReportView as u } from "../reports/PrintReportView.js";
|
|
10
|
-
import { DELETE_REPORT as
|
|
10
|
+
import { DELETE_REPORT as d, DUPLICATE_REPORT as f, GET_REPORT as p, GET_REPORT_RUN as ee, LIST_REPORT_RUNS as te, RUN_REPORT as ne } from "../reports/reportGraphql.js";
|
|
11
11
|
import { ExportMenu as re } from "../reports/ExportMenu.js";
|
|
12
12
|
import { ReportRunHistory as ie } from "../reports/ReportRunHistory.js";
|
|
13
13
|
import { SchedulesPanel as m } from "../reports/SchedulesPanel.js";
|
|
14
14
|
import { ShareDialog as h } from "../reports/ShareDialog.js";
|
|
15
15
|
import { useEffect as g, useState as _ } from "react";
|
|
16
|
-
import { Pencil as
|
|
16
|
+
import { Pencil as v, Play as ae, Trash2 as oe } from "lucide-react";
|
|
17
17
|
import { useParams as y } from "react-router";
|
|
18
18
|
import { useMutation as b, useQuery as x } from "@apollo/client/react";
|
|
19
19
|
import { jsx as S, jsxs as C } from "react/jsx-runtime";
|
|
20
20
|
import { Badge as w, Button as T, Card as E, CardContent as D } from "@burdenoff/fe-libs/ui";
|
|
21
21
|
//#region src/planmagnet/pages/ReportsReportPagePage.tsx
|
|
22
22
|
function O() {
|
|
23
|
-
let { id: O } = y(), { currentUser: k } = e(), A = t(), { data: j, loading: M, error: N } = x(
|
|
23
|
+
let { id: O } = y(), { currentUser: k } = e(), A = t(), { data: j, loading: M, error: N } = x(p, {
|
|
24
24
|
variables: { id: O ?? "" },
|
|
25
25
|
skip: !O
|
|
26
|
-
}), P = j?.planmagnetReport, [F, I] = b(
|
|
26
|
+
}), P = j?.planmagnetReport, [F, I] = b(ne), [L, R] = b(d), [z, B] = b(f), [V, H] = _(null), [U, W] = _(null), [G, K] = _(null), [q, J] = _(null), [Y, X] = _(!1), se = x(ee, {
|
|
27
27
|
variables: { id: U ?? "" },
|
|
28
28
|
skip: !U
|
|
29
29
|
}), Z = U ? se.data?.planmagnetReportRun ?? null : V;
|
|
@@ -40,10 +40,10 @@ function O() {
|
|
|
40
40
|
if (O) {
|
|
41
41
|
K(null);
|
|
42
42
|
try {
|
|
43
|
-
let e = (await
|
|
43
|
+
let e = (await a(F({
|
|
44
44
|
variables: { id: O },
|
|
45
45
|
refetchQueries: [{
|
|
46
|
-
query:
|
|
46
|
+
query: te,
|
|
47
47
|
variables: {
|
|
48
48
|
reportId: O,
|
|
49
49
|
pagination: { first: 25 }
|
|
@@ -52,32 +52,32 @@ function O() {
|
|
|
52
52
|
}))).data?.planmagnetRunReport;
|
|
53
53
|
e && (H(e), W(null));
|
|
54
54
|
} catch (e) {
|
|
55
|
-
K(
|
|
55
|
+
K(r(e, "This report could not be run."));
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}, le = async () => {
|
|
59
59
|
if (O) {
|
|
60
60
|
J(null);
|
|
61
61
|
try {
|
|
62
|
-
let e = (await
|
|
62
|
+
let e = (await a(z({
|
|
63
63
|
variables: { id: O },
|
|
64
|
-
context:
|
|
64
|
+
context: i
|
|
65
65
|
}))).data?.planmagnetDuplicateReport;
|
|
66
66
|
e && A(c("/reports/edit-report-page", e.id));
|
|
67
67
|
} catch (e) {
|
|
68
|
-
J(
|
|
68
|
+
J(r(e, "This report could not be duplicated."));
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}, ue = async () => {
|
|
72
72
|
if (O) {
|
|
73
73
|
J(null);
|
|
74
74
|
try {
|
|
75
|
-
await
|
|
75
|
+
await a(L({
|
|
76
76
|
variables: { id: O },
|
|
77
|
-
context:
|
|
77
|
+
context: i
|
|
78
78
|
})), A("/reports/reports-page");
|
|
79
79
|
} catch (e) {
|
|
80
|
-
J(
|
|
80
|
+
J(r(e, "This report could not be deleted."));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
};
|
|
@@ -142,7 +142,7 @@ function O() {
|
|
|
142
142
|
onClick: () => void ce(),
|
|
143
143
|
disabled: I.loading,
|
|
144
144
|
"data-testid": "report-run",
|
|
145
|
-
children: [/* @__PURE__ */ S(
|
|
145
|
+
children: [/* @__PURE__ */ S(ae, { className: "mr-1 h-4 w-4" }), I.loading ? "Running…" : "Run now"]
|
|
146
146
|
}),
|
|
147
147
|
/* @__PURE__ */ S(re, {
|
|
148
148
|
reportName: P.name,
|
|
@@ -158,7 +158,7 @@ function O() {
|
|
|
158
158
|
size: "sm",
|
|
159
159
|
variant: "outline",
|
|
160
160
|
onClick: () => A(c("/reports/edit-report-page", P.id)),
|
|
161
|
-
children: [/* @__PURE__ */ S(
|
|
161
|
+
children: [/* @__PURE__ */ S(v, { className: "mr-1 h-4 w-4" }), "Edit"]
|
|
162
162
|
}),
|
|
163
163
|
/* @__PURE__ */ S(T, {
|
|
164
164
|
size: "sm",
|
|
@@ -167,9 +167,9 @@ function O() {
|
|
|
167
167
|
disabled: B.loading,
|
|
168
168
|
children: "Duplicate"
|
|
169
169
|
}),
|
|
170
|
-
Q && /* @__PURE__ */ S(
|
|
170
|
+
Q && /* @__PURE__ */ S(o, {
|
|
171
171
|
label: "Delete",
|
|
172
|
-
icon: /* @__PURE__ */ S(
|
|
172
|
+
icon: /* @__PURE__ */ S(oe, { className: "mr-1 h-4 w-4" }),
|
|
173
173
|
title: "Delete this report?",
|
|
174
174
|
description: "Its runs, shares and schedules are deleted too. This cannot be undone.",
|
|
175
175
|
confirmLabel: "Delete",
|
|
@@ -181,8 +181,8 @@ function O() {
|
|
|
181
181
|
]
|
|
182
182
|
})]
|
|
183
183
|
}),
|
|
184
|
-
G && /* @__PURE__ */ S(
|
|
185
|
-
q && /* @__PURE__ */ S(
|
|
184
|
+
G && /* @__PURE__ */ S(s, { message: G }),
|
|
185
|
+
q && /* @__PURE__ */ S(s, { message: q }),
|
|
186
186
|
/* @__PURE__ */ C("div", {
|
|
187
187
|
className: "grid grid-cols-1 gap-6 lg:grid-cols-[1fr_280px]",
|
|
188
188
|
children: [/* @__PURE__ */ C("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReportsReportPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ReportsReportPagePage.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Badge, Button, Card, CardContent } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Play, Trash2 } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { LEAVES_PAGE_CONTEXT, throwOnError } from '../utils/mutationResult';\nimport { ExportMenu } from '../reports/ExportMenu';\nimport { PrintReportView } from '../reports/PrintReportView';\nimport { ReportRunHistory } from '../reports/ReportRunHistory';\nimport { ReportWidgetRenderer } from '../reports/ReportWidgetRenderer';\nimport { SchedulesPanel } from '../reports/SchedulesPanel';\nimport { ShareDialog } from '../reports/ShareDialog';\nimport {\n DELETE_REPORT,\n DUPLICATE_REPORT,\n GET_REPORT,\n GET_REPORT_RUN,\n LIST_REPORT_RUNS,\n RUN_REPORT,\n} from '../reports/reportGraphql';\nimport { GRID_COLUMNS, type ReportRecord, type ReportRun } from '../reports/reportModel';\n\n/**\n * View + run a report (ANR-13/15/16). \"Run now\" persists a new `ReportRun` snapshot (not a live view — see\n * `PlanmagnetReportRun`'s own schema doc): re-running the same report later never changes what an earlier run\n * showed. Older runs are opened from `ReportRunHistory` via `planmagnetReportRun`.\n */\nexport function ReportsReportPagePage() {\n const { id } = useParams<{ id: string }>();\n const { currentUser } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n\n const { data, loading, error } = useQuery<{ planmagnetReport: ReportRecord | null }>(GET_REPORT, {\n variables: { id: id ?? '' },\n skip: !id,\n });\n const report = data?.planmagnetReport;\n\n const [runReport, runState] = useMutation(RUN_REPORT);\n const [deleteReport, deleteState] = useMutation(DELETE_REPORT);\n const [duplicateReport, duplicateState] = useMutation(DUPLICATE_REPORT);\n\n const [currentRun, setCurrentRun] = useState<ReportRun | null>(null);\n const [selectedRunId, setSelectedRunId] = useState<string | null>(null);\n const [runError, setRunError] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n const [printMode, setPrintMode] = useState(false);\n\n const olderRun = useQuery<{ planmagnetReportRun: ReportRun | null }>(GET_REPORT_RUN, {\n variables: { id: selectedRunId ?? '' },\n skip: !selectedRunId,\n });\n const displayedRun: ReportRun | null = selectedRunId\n ? (olderRun.data?.planmagnetReportRun ?? null)\n : currentRun;\n\n useEffect(() => {\n if (!printMode) return;\n const stop = () => setPrintMode(false);\n window.addEventListener('afterprint', stop);\n const timer = window.setTimeout(() => window.print(), 50);\n return () => {\n window.removeEventListener('afterprint', stop);\n window.clearTimeout(timer);\n };\n }, [printMode]);\n\n const handleRun = async () => {\n if (!id) return;\n setRunError(null);\n try {\n const result = await throwOnError(\n runReport({\n variables: { id },\n refetchQueries: [\n { query: LIST_REPORT_RUNS, variables: { reportId: id, pagination: { first: 25 } } },\n ],\n })\n );\n const run = (result.data as { planmagnetRunReport?: ReportRun } | undefined)\n ?.planmagnetRunReport;\n if (run) {\n setCurrentRun(run);\n setSelectedRunId(null);\n }\n } catch (e) {\n setRunError(mutationErrorMessage(e, 'This report could not be run.'));\n }\n };\n\n const handleDuplicate = async () => {\n if (!id) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n duplicateReport({ variables: { id }, context: LEAVES_PAGE_CONTEXT })\n );\n const copy = (result.data as { planmagnetDuplicateReport?: { id: string } } | undefined)\n ?.planmagnetDuplicateReport;\n if (copy) navigate(detailPath('/reports/edit-report-page', copy.id));\n } catch (e) {\n setActionError(mutationErrorMessage(e, 'This report could not be duplicated.'));\n }\n };\n\n const handleDelete = async () => {\n if (!id) return;\n setActionError(null);\n try {\n await throwOnError(deleteReport({ variables: { id }, context: LEAVES_PAGE_CONTEXT }));\n navigate('/reports/reports-page');\n } catch (e) {\n setActionError(mutationErrorMessage(e, 'This report could not be deleted.'));\n }\n };\n\n // Only before the first load: a refetch must not blank the page.\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent\" />\n </div>\n );\n }\n if (error) {\n return <div className=\"p-6 text-destructive\">Report could not be loaded: {error.message}</div>;\n }\n if (!report) {\n return (\n <div className=\"space-y-3 p-6\">\n <p className=\"text-sm text-muted-foreground\">\n This report does not exist, or you do not have access to it.\n </p>\n <Button size=\"sm\" variant=\"outline\" onClick={() => navigate('/reports/reports-page')}>\n Back to reports\n </Button>\n </div>\n );\n }\n\n const isOwner = !!currentUser?.id && currentUser.id === report.ownerId;\n const resultsByWidget = new Map((displayedRun?.widgets ?? []).map((w) => [w.widgetId, w]));\n\n if (printMode) {\n return (\n <PrintReportView\n reportName={report.name}\n generatedAt={displayedRun?.generatedAt ?? new Date().toISOString()}\n widgets={report.definition.widgets}\n results={resultsByWidget}\n onClose={() => setPrintMode(false)}\n onPrintNow={() => window.print()}\n />\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"report-view\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h1 className=\"text-2xl font-semibold\">{report.name}</h1>\n <Badge variant={report.visibility === 'WORKSPACE' ? 'secondary' : 'outline'}>\n {report.visibility === 'WORKSPACE' ? 'Workspace' : 'Private'}\n </Badge>\n </div>\n {report.description && (\n <p className=\"mt-1 text-sm text-muted-foreground\">{report.description}</p>\n )}\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Owned by <PersonName id={report.ownerId} />\n </p>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n size=\"sm\"\n onClick={() => void handleRun()}\n disabled={runState.loading}\n data-testid=\"report-run\"\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {runState.loading ? 'Running…' : 'Run now'}\n </Button>\n <ExportMenu\n reportName={report.name}\n widgets={report.definition.widgets}\n run={displayedRun}\n onPrint={() => setPrintMode(true)}\n />\n {isOwner && <ShareDialog reportId={report.id} runId={displayedRun?.id ?? null} />}\n {isOwner && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/reports/edit-report-page', report.id))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n )}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void handleDuplicate()}\n disabled={duplicateState.loading}\n >\n Duplicate\n </Button>\n {isOwner && (\n <ConfirmButton\n label=\"Delete\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title=\"Delete this report?\"\n description=\"Its runs, shares and schedules are deleted too. This cannot be undone.\"\n confirmLabel=\"Delete\"\n destructive\n variant=\"outline\"\n disabled={deleteState.loading}\n onConfirm={() => void handleDelete()}\n />\n )}\n </div>\n </div>\n\n {runError && <ErrorLine message={runError} />}\n {actionError && <ErrorLine message={actionError} />}\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-[1fr_280px]\">\n <div className=\"space-y-4\">\n {!displayedRun ? (\n <Card>\n <CardContent className=\"py-12 text-center text-sm text-muted-foreground\">\n Not run yet. Choose “Run now” to generate the first snapshot.\n </CardContent>\n </Card>\n ) : (\n <div className=\"flex flex-wrap\">\n {report.definition.widgets.map((widget) => (\n <div\n key={widget.id}\n style={{ width: `${(widget.layout.w / GRID_COLUMNS) * 100}%` }}\n className=\"p-1.5\"\n >\n <div\n className=\"h-full rounded-md border border-border bg-background p-3\"\n data-testid=\"report-view-widget\"\n >\n {widget.type !== 'TEXT' && (\n <h3 className=\"mb-2 text-sm font-semibold\">{widget.title}</h3>\n )}\n <ReportWidgetRenderer\n widget={widget}\n runResult={resultsByWidget.get(widget.id)}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {isOwner && (\n <Card>\n <CardContent className=\"pt-6\">\n <SchedulesPanel reportId={report.id} />\n </CardContent>\n </Card>\n )}\n </div>\n\n <div>\n <ReportRunHistory\n reportId={report.id}\n selectedRunId={selectedRunId ?? currentRun?.id ?? null}\n onSelect={setSelectedRunId}\n />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReportsReportPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/ReportsReportPagePage.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Badge, Button, Card, CardContent } from '@burdenoff/fe-libs/ui';\nimport { Pencil, Play, Trash2 } from 'lucide-react';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { PersonName } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { mutationErrorMessage } from '../utils/graphqlErrors';\nimport { LEAVES_PAGE_CONTEXT, throwOnError } from '../utils/mutationResult';\nimport { ExportMenu } from '../reports/ExportMenu';\nimport { PrintReportView } from '../reports/PrintReportView';\nimport { ReportRunHistory } from '../reports/ReportRunHistory';\nimport { ReportWidgetRenderer } from '../reports/ReportWidgetRenderer';\nimport { SchedulesPanel } from '../reports/SchedulesPanel';\nimport { ShareDialog } from '../reports/ShareDialog';\nimport {\n DELETE_REPORT,\n DUPLICATE_REPORT,\n GET_REPORT,\n GET_REPORT_RUN,\n LIST_REPORT_RUNS,\n RUN_REPORT,\n} from '../reports/reportGraphql';\nimport { GRID_COLUMNS, type ReportRecord, type ReportRun } from '../reports/reportModel';\n\n/**\n * View + run a report (ANR-13/15/16). \"Run now\" persists a new `ReportRun` snapshot (not a live view — see\n * `PlanmagnetReportRun`'s own schema doc): re-running the same report later never changes what an earlier run\n * showed. Older runs are opened from `ReportRunHistory` via `planmagnetReportRun`.\n */\nexport function ReportsReportPagePage() {\n const { id } = useParams<{ id: string }>();\n const { currentUser } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n\n const { data, loading, error } = useQuery<{ planmagnetReport: ReportRecord | null }>(GET_REPORT, {\n variables: { id: id ?? '' },\n skip: !id,\n });\n const report = data?.planmagnetReport;\n\n const [runReport, runState] = useMutation(RUN_REPORT);\n const [deleteReport, deleteState] = useMutation(DELETE_REPORT);\n const [duplicateReport, duplicateState] = useMutation(DUPLICATE_REPORT);\n\n const [currentRun, setCurrentRun] = useState<ReportRun | null>(null);\n const [selectedRunId, setSelectedRunId] = useState<string | null>(null);\n const [runError, setRunError] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n const [printMode, setPrintMode] = useState(false);\n\n const olderRun = useQuery<{ planmagnetReportRun: ReportRun | null }>(GET_REPORT_RUN, {\n variables: { id: selectedRunId ?? '' },\n skip: !selectedRunId,\n });\n const displayedRun: ReportRun | null = selectedRunId\n ? (olderRun.data?.planmagnetReportRun ?? null)\n : currentRun;\n\n useEffect(() => {\n if (!printMode) return;\n const stop = () => setPrintMode(false);\n window.addEventListener('afterprint', stop);\n const timer = window.setTimeout(() => window.print(), 50);\n return () => {\n window.removeEventListener('afterprint', stop);\n window.clearTimeout(timer);\n };\n }, [printMode]);\n\n const handleRun = async () => {\n if (!id) return;\n setRunError(null);\n try {\n const result = await throwOnError(\n runReport({\n variables: { id },\n refetchQueries: [\n { query: LIST_REPORT_RUNS, variables: { reportId: id, pagination: { first: 25 } } },\n ],\n })\n );\n const run = (result.data as { planmagnetRunReport?: ReportRun } | undefined)\n ?.planmagnetRunReport;\n if (run) {\n setCurrentRun(run);\n setSelectedRunId(null);\n }\n } catch (e) {\n setRunError(mutationErrorMessage(e, 'This report could not be run.'));\n }\n };\n\n const handleDuplicate = async () => {\n if (!id) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n duplicateReport({ variables: { id }, context: LEAVES_PAGE_CONTEXT })\n );\n const copy = (result.data as { planmagnetDuplicateReport?: { id: string } } | undefined)\n ?.planmagnetDuplicateReport;\n if (copy) navigate(detailPath('/reports/edit-report-page', copy.id));\n } catch (e) {\n setActionError(mutationErrorMessage(e, 'This report could not be duplicated.'));\n }\n };\n\n const handleDelete = async () => {\n if (!id) return;\n setActionError(null);\n try {\n await throwOnError(deleteReport({ variables: { id }, context: LEAVES_PAGE_CONTEXT }));\n navigate('/reports/reports-page');\n } catch (e) {\n setActionError(mutationErrorMessage(e, 'This report could not be deleted.'));\n }\n };\n\n // Only before the first load: a refetch must not blank the page.\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent\" />\n </div>\n );\n }\n if (error) {\n return <div className=\"p-6 text-destructive\">Report could not be loaded: {error.message}</div>;\n }\n if (!report) {\n return (\n <div className=\"space-y-3 p-6\">\n <p className=\"text-sm text-muted-foreground\">\n This report does not exist, or you do not have access to it.\n </p>\n <Button size=\"sm\" variant=\"outline\" onClick={() => navigate('/reports/reports-page')}>\n Back to reports\n </Button>\n </div>\n );\n }\n\n const isOwner = !!currentUser?.id && currentUser.id === report.ownerId;\n const resultsByWidget = new Map((displayedRun?.widgets ?? []).map((w) => [w.widgetId, w]));\n\n if (printMode) {\n return (\n <PrintReportView\n reportName={report.name}\n generatedAt={displayedRun?.generatedAt ?? new Date().toISOString()}\n widgets={report.definition.widgets}\n results={resultsByWidget}\n onClose={() => setPrintMode(false)}\n onPrintNow={() => window.print()}\n />\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\" data-testid=\"report-view\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h1 className=\"text-2xl font-semibold\">{report.name}</h1>\n <Badge variant={report.visibility === 'WORKSPACE' ? 'secondary' : 'outline'}>\n {report.visibility === 'WORKSPACE' ? 'Workspace' : 'Private'}\n </Badge>\n </div>\n {report.description && (\n <p className=\"mt-1 text-sm text-muted-foreground\">{report.description}</p>\n )}\n <p className=\"mt-1 text-xs text-muted-foreground\">\n Owned by <PersonName id={report.ownerId} />\n </p>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n size=\"sm\"\n onClick={() => void handleRun()}\n disabled={runState.loading}\n data-testid=\"report-run\"\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {runState.loading ? 'Running…' : 'Run now'}\n </Button>\n <ExportMenu\n reportName={report.name}\n widgets={report.definition.widgets}\n run={displayedRun}\n onPrint={() => setPrintMode(true)}\n />\n {isOwner && <ShareDialog reportId={report.id} runId={displayedRun?.id ?? null} />}\n {isOwner && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => navigate(detailPath('/reports/edit-report-page', report.id))}\n >\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n )}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void handleDuplicate()}\n disabled={duplicateState.loading}\n >\n Duplicate\n </Button>\n {isOwner && (\n <ConfirmButton\n label=\"Delete\"\n icon={<Trash2 className=\"mr-1 h-4 w-4\" />}\n title=\"Delete this report?\"\n description=\"Its runs, shares and schedules are deleted too. This cannot be undone.\"\n confirmLabel=\"Delete\"\n destructive\n variant=\"outline\"\n disabled={deleteState.loading}\n onConfirm={() => void handleDelete()}\n />\n )}\n </div>\n </div>\n\n {runError && <ErrorLine message={runError} />}\n {actionError && <ErrorLine message={actionError} />}\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-[1fr_280px]\">\n <div className=\"space-y-4\">\n {!displayedRun ? (\n <Card>\n <CardContent className=\"py-12 text-center text-sm text-muted-foreground\">\n Not run yet. Choose “Run now” to generate the first snapshot.\n </CardContent>\n </Card>\n ) : (\n <div className=\"flex flex-wrap\">\n {report.definition.widgets.map((widget) => (\n <div\n key={widget.id}\n style={{ width: `${(widget.layout.w / GRID_COLUMNS) * 100}%` }}\n className=\"p-1.5\"\n >\n <div\n className=\"h-full rounded-md border border-border bg-background p-3\"\n data-testid=\"report-view-widget\"\n >\n {widget.type !== 'TEXT' && (\n <h3 className=\"mb-2 text-sm font-semibold\">{widget.title}</h3>\n )}\n <ReportWidgetRenderer\n widget={widget}\n runResult={resultsByWidget.get(widget.id)}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {isOwner && (\n <Card>\n <CardContent className=\"pt-6\">\n <SchedulesPanel reportId={report.id} />\n </CardContent>\n </Card>\n )}\n </div>\n\n <div>\n <ReportRunHistory\n reportId={report.id}\n selectedRunId={selectedRunId ?? currentRun?.id ?? null}\n onSelect={setSelectedRunId}\n />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
|
|
2
2
|
import { PersonName as n } from "../people/People.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { mutationErrorMessage as r } from "../utils/graphqlErrors.js";
|
|
4
|
+
import { LEAVES_PAGE_CONTEXT as i, throwOnError as a } from "../utils/mutationResult.js";
|
|
5
|
+
import { ConfirmButton as o, ErrorLine as s } from "../controls/ConfirmButton.js";
|
|
6
6
|
import { detailPath as c, rowLinkProps as l, stopRowActivation as u } from "../utils/rowNavigation.js";
|
|
7
7
|
import { CREATE_REPORT as d, DELETE_REPORT as f, DUPLICATE_REPORT as p, LIST_REPORTS as m, LIST_REPORT_TEMPLATES as h } from "../reports/reportGraphql.js";
|
|
8
8
|
import { useDeferredValue as g, useState as _ } from "react";
|
|
@@ -12,7 +12,7 @@ import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
|
12
12
|
import { useI18n as T } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
13
|
import { Badge as E, Button as D, Card as O, CardContent as k, CardDescription as A, CardHeader as j, CardTitle as M, Input as N, Tabs as P, TabsContent as F, TabsList as I, TabsTrigger as L } from "@burdenoff/fe-libs/ui";
|
|
14
14
|
//#region src/planmagnet/pages/ReportsReportsPagePage.tsx
|
|
15
|
-
function R({ report: e, isOwner: r, onDuplicate: i, onDelete:
|
|
15
|
+
function R({ report: e, isOwner: r, onDuplicate: i, onDelete: a }) {
|
|
16
16
|
let s = t();
|
|
17
17
|
return /* @__PURE__ */ w("div", {
|
|
18
18
|
...l(() => s(c("/reports/report-page", e.id)), e.name),
|
|
@@ -53,7 +53,7 @@ function R({ report: e, isOwner: r, onDuplicate: i, onDelete: o }) {
|
|
|
53
53
|
variant: "outline",
|
|
54
54
|
onClick: () => i(e.id),
|
|
55
55
|
children: "Duplicate"
|
|
56
|
-
}), r && /* @__PURE__ */ C(
|
|
56
|
+
}), r && /* @__PURE__ */ C(o, {
|
|
57
57
|
label: "Delete",
|
|
58
58
|
icon: null,
|
|
59
59
|
title: `Delete "${e.name}"?`,
|
|
@@ -62,14 +62,14 @@ function R({ report: e, isOwner: r, onDuplicate: i, onDelete: o }) {
|
|
|
62
62
|
destructive: !0,
|
|
63
63
|
variant: "outline",
|
|
64
64
|
disabled: !1,
|
|
65
|
-
onConfirm: () =>
|
|
65
|
+
onConfirm: () => a(e.id)
|
|
66
66
|
})]
|
|
67
67
|
})]
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
function z({ mine: n }) {
|
|
71
|
-
let { workspaceId:
|
|
72
|
-
workspaceId:
|
|
71
|
+
let { workspaceId: i, currentUser: o } = e(), l = t(), [u, d] = _(""), h = g(u.trim()), [v, y] = _(null), b = {
|
|
72
|
+
workspaceId: i ?? "",
|
|
73
73
|
filter: {
|
|
74
74
|
mine: n || void 0,
|
|
75
75
|
search: h || void 0
|
|
@@ -77,7 +77,7 @@ function z({ mine: n }) {
|
|
|
77
77
|
pagination: { first: 50 }
|
|
78
78
|
}, { data: T, loading: E, error: D } = S(m, {
|
|
79
79
|
variables: b,
|
|
80
|
-
skip: !
|
|
80
|
+
skip: !i
|
|
81
81
|
}), [O] = x(p, { refetchQueries: [{
|
|
82
82
|
query: m,
|
|
83
83
|
variables: b
|
|
@@ -87,17 +87,17 @@ function z({ mine: n }) {
|
|
|
87
87
|
}] }), A = n ? T?.planmagnetReports.items ?? [] : (T?.planmagnetReports.items ?? []).filter((e) => e.visibility === "WORKSPACE"), j = async (e) => {
|
|
88
88
|
y(null);
|
|
89
89
|
try {
|
|
90
|
-
let t = (await
|
|
90
|
+
let t = (await a(O({ variables: { id: e } }))).data?.planmagnetDuplicateReport;
|
|
91
91
|
t && l(c("/reports/edit-report-page", t.id));
|
|
92
92
|
} catch (e) {
|
|
93
|
-
y(
|
|
93
|
+
y(r(e, "This report could not be duplicated."));
|
|
94
94
|
}
|
|
95
95
|
}, M = async (e) => {
|
|
96
96
|
y(null);
|
|
97
97
|
try {
|
|
98
|
-
await
|
|
98
|
+
await a(k({ variables: { id: e } }));
|
|
99
99
|
} catch (e) {
|
|
100
|
-
y(
|
|
100
|
+
y(r(e, "This report could not be deleted."));
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
103
|
return /* @__PURE__ */ w("div", {
|
|
@@ -110,7 +110,7 @@ function z({ mine: n }) {
|
|
|
110
110
|
"aria-label": "Search reports",
|
|
111
111
|
className: "max-w-sm"
|
|
112
112
|
}),
|
|
113
|
-
v && /* @__PURE__ */ C(
|
|
113
|
+
v && /* @__PURE__ */ C(s, { message: v }),
|
|
114
114
|
D ? /* @__PURE__ */ w("p", {
|
|
115
115
|
className: "text-sm text-destructive",
|
|
116
116
|
children: ["Reports could not be loaded: ", D.message]
|
|
@@ -124,7 +124,7 @@ function z({ mine: n }) {
|
|
|
124
124
|
className: "space-y-2",
|
|
125
125
|
children: A.map((e) => /* @__PURE__ */ C(R, {
|
|
126
126
|
report: e,
|
|
127
|
-
isOwner: !!
|
|
127
|
+
isOwner: !!o?.id && o.id === e.ownerId,
|
|
128
128
|
onDuplicate: (e) => void j(e),
|
|
129
129
|
onDelete: (e) => void M(e)
|
|
130
130
|
}, e.id))
|
|
@@ -133,11 +133,11 @@ function z({ mine: n }) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
function B() {
|
|
136
|
-
let { workspaceId: n } = e(),
|
|
136
|
+
let { workspaceId: n } = e(), o = t(), { data: l, loading: u, error: f } = S(h), [p, { loading: m }] = x(d), [g, v] = _(null), [b, T] = _(null), E = async (e) => {
|
|
137
137
|
if (n) {
|
|
138
138
|
T(null), v(e.key);
|
|
139
139
|
try {
|
|
140
|
-
let t = (await
|
|
140
|
+
let t = (await a(p({
|
|
141
141
|
variables: {
|
|
142
142
|
workspaceId: n,
|
|
143
143
|
input: {
|
|
@@ -148,11 +148,11 @@ function B() {
|
|
|
148
148
|
definition: e.definition
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
|
-
context:
|
|
151
|
+
context: i
|
|
152
152
|
}))).data?.planmagnetCreateReport?.id;
|
|
153
|
-
t &&
|
|
153
|
+
t && o(c("/reports/edit-report-page", t));
|
|
154
154
|
} catch (e) {
|
|
155
|
-
T(
|
|
155
|
+
T(r(e, "This report could not be created from the template."));
|
|
156
156
|
} finally {
|
|
157
157
|
v(null);
|
|
158
158
|
}
|
|
@@ -169,7 +169,7 @@ function B() {
|
|
|
169
169
|
let N = l?.planmagnetReportTemplates ?? [];
|
|
170
170
|
return /* @__PURE__ */ w("div", {
|
|
171
171
|
className: "space-y-3",
|
|
172
|
-
children: [b && /* @__PURE__ */ C(
|
|
172
|
+
children: [b && /* @__PURE__ */ C(s, { message: b }), /* @__PURE__ */ C("div", {
|
|
173
173
|
className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3",
|
|
174
174
|
children: N.map((e) => /* @__PURE__ */ w(O, {
|
|
175
175
|
"data-testid": "report-template",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamTeamDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamTeamDetailPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TeamTeamDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamTeamDetailPagePage.tsx"],"names":[],"mappings":"AA4FA,wBAAgB,sBAAsB,4CAwLrC"}
|