@burdenoff/microfe-planmagnet 2026.1001.5 → 2026.1001.7
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/PlanMagnetRoutes.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoutes.js +10 -6
- package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
- package/dist/planmagnet/automation/automationRuleTypes.d.ts +31 -19
- package/dist/planmagnet/automation/automationRuleTypes.d.ts.map +1 -1
- package/dist/planmagnet/automation/automationRuleTypes.js +4 -8
- package/dist/planmagnet/automation/automationRuleTypes.js.map +1 -1
- package/dist/planmagnet/controls/CompetitorControls.d.ts +71 -0
- package/dist/planmagnet/controls/CompetitorControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/CompetitorControls.js +346 -12
- package/dist/planmagnet/controls/CompetitorControls.js.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts +23 -1
- package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/FeatureControls.js +431 -229
- package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
- package/dist/planmagnet/controls/FeedbackControls.d.ts +70 -0
- package/dist/planmagnet/controls/FeedbackControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/FeedbackControls.js +153 -0
- package/dist/planmagnet/controls/FeedbackControls.js.map +1 -0
- package/dist/planmagnet/controls/Pickers.d.ts +15 -0
- package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
- package/dist/planmagnet/controls/Pickers.js +61 -1
- package/dist/planmagnet/controls/Pickers.js.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.d.ts +36 -0
- package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/PrioritizationControls.js +118 -1
- package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -1
- package/dist/planmagnet/controls/SyncControls.d.ts.map +1 -1
- package/dist/planmagnet/controls/SyncControls.js +1 -0
- package/dist/planmagnet/controls/SyncControls.js.map +1 -1
- package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts +47 -0
- package/dist/planmagnet/controls/WebhookSubscriptionControls.d.ts.map +1 -0
- package/dist/planmagnet/controls/WebhookSubscriptionControls.js +542 -0
- package/dist/planmagnet/controls/WebhookSubscriptionControls.js.map +1 -0
- package/dist/planmagnet/dashboards/DashboardUI.js +8 -8
- package/dist/planmagnet/dashboards/cards/RiskSeverityCard.js +4 -4
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.d.ts.map +1 -1
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js +67 -59
- package/dist/planmagnet/dashboards/cards/SprintCapacityCard.js.map +1 -1
- package/dist/planmagnet/forecast/DeliveryForecastCard.js +8 -8
- package/dist/planmagnet/forecast/ProjectOutlookCard.js +10 -10
- package/dist/planmagnet/forecast/ProjectRiskSummaryCard.js +9 -9
- package/dist/planmagnet/forecast/ResourceForecastCard.js +6 -6
- package/dist/planmagnet/integrations/syncTypes.d.ts +2 -1
- package/dist/planmagnet/integrations/syncTypes.d.ts.map +1 -1
- package/dist/planmagnet/integrations/syncTypes.js +19 -9
- package/dist/planmagnet/integrations/syncTypes.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +290 -283
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js +126 -63
- package/dist/planmagnet/pages/CompetitorsCompetitorDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js +14 -0
- package/dist/planmagnet/pages/CompetitorsCompetitorsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js +93 -57
- package/dist/planmagnet/pages/CompetitorsCreateCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js +125 -83
- package/dist/planmagnet/pages/CompetitorsEditCompetitorPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +23 -23
- package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js +340 -248
- package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js +172 -167
- package/dist/planmagnet/pages/IntegrationsSyncDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +92 -83
- package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts +2 -0
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.d.ts.map +1 -0
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js +175 -0
- package/dist/planmagnet/pages/IntegrationsWebhooksPagePage.js.map +1 -0
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js +226 -126
- package/dist/planmagnet/pages/PortfoliosPortfolioDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +151 -126
- package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +169 -122
- package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +165 -109
- package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +13 -13
- package/dist/planmagnet/pages/ProjectsAtRiskPagePage.js +9 -9
- package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +8 -8
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +15 -3
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +12 -12
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +23 -23
- package/dist/planmagnet/pages/TasksMyDashboardPagePage.js +18 -18
- package/dist/planmagnet/pages/TasksMyWorkPagePage.js +10 -10
- package/dist/planmagnet/pages/TasksTimeEntriesPagePage.js +5 -5
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +59 -53
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +8 -8
- package/dist/planmagnet/pages/analytics/DeliveryKpisCard.js +6 -6
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts +8 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js +164 -0
- package/dist/planmagnet/pages/analytics/MetricsCard.js.map +1 -0
- package/dist/planmagnet/pages/analytics/QualityMetricsCard.js +6 -6
- package/dist/planmagnet/pages/analytics/SprintDeliveryCards.js +8 -8
- package/dist/planmagnet/pages/analytics/StatusFlowCard.js +7 -7
- package/dist/planmagnet/pages/analytics/ThroughputCard.js +7 -7
- package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +7 -7
- package/dist/planmagnet/people/People.d.ts +14 -0
- package/dist/planmagnet/people/People.d.ts.map +1 -1
- package/dist/planmagnet/people/People.js +36 -22
- package/dist/planmagnet/people/People.js.map +1 -1
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts +2 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.d.ts.map +1 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js +155 -0
- package/dist/planmagnet/team/UnmatchedCrewIdentities.js.map +1 -0
- package/dist/planmagnet/webhooks/webhookTypes.d.ts +54 -0
- package/dist/planmagnet/webhooks/webhookTypes.d.ts.map +1 -0
- package/dist/planmagnet/webhooks/webhookTypes.js +17 -0
- package/dist/planmagnet/webhooks/webhookTypes.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { TrendingUp, Zap, FolderKanban, BarChart3, Target } from 'lucide-react';\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Legend,\n Line,\n LineChart,\n ReferenceArea,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport { countLabel, useProjectCounts, useSprintCounts } from '../hooks/useCounts';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { DeliveryForecastCard } from '../forecast/DeliveryForecastCard';\nimport { BudgetPerformanceCard, CycleTimeCard } from './analytics/DeliveryHealthCards';\nimport { QualityMetricsCard } from './analytics/QualityMetricsCard';\nimport { DeliveryKpisCard } from './analytics/DeliveryKpisCard';\nimport { ThroughputCard } from './analytics/ThroughputCard';\nimport { StatusFlowCard } from './analytics/StatusFlowCard';\nimport { WorkBreakdownCard } from './analytics/WorkBreakdownCard';\nimport {\n ALL_PROJECTS,\n activePreset,\n isValidRange,\n presetRange,\n scopeToProjectId,\n toRangeVariables,\n type DateRange,\n type RangePreset,\n} from './analytics/analyticsRange';\n\n// Server-side totals (QA H2). The work-item list returns at most 100 rows per\n// page, so every figure computed from the loaded page undercounted past 100\n// items — and \"Project Health\" did not filter by project at all.\n// The chosen project's sprints, queried by project (BOFF-7964: the picker was built from the 100 newest sprints\n// of the whole workspace, so an older project's list could be empty).\nconst GET_PROJECT_SPRINTS = gql`\n query AnalyticsProjectSprints($workspaceId: ID!, $projectId: String) {\n planmagnetSprints(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n name\n status\n completedAt\n endDate\n }\n }\n }\n`;\n\n// At risk = missed, or still open and past due — counted on the server over every milestone.\nconst GET_MILESTONE_RISK = gql`\n query AnalyticsMilestoneRisk($workspaceId: ID!, $yesterday: DateTime) {\n missed: planmagnetMilestones(\n workspaceId: $workspaceId\n filter: { status: MISSED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n overdue: planmagnetMilestones(\n workspaceId: $workspaceId\n filter: { open: true, dueTo: $yesterday }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n }\n`;\n\nconst GET_ANALYTICS_COUNTS = gql`\n query GetAnalyticsWorkItemCounts($workspaceId: ID!) {\n planmagnetWorkItemCounts(workspaceId: $workspaceId) {\n total\n points\n blocked\n byStatus {\n status\n count\n points\n }\n byProject {\n projectId\n status\n count\n }\n }\n }\n`;\n\ninterface WorkItemCounts {\n total: number;\n points: number;\n blocked: number;\n byStatus: Array<{ status: string; count: number; points: number }>;\n byProject: Array<{ projectId: string; status: string; count: number }>;\n}\n\n// Only the project list (for the pickers and per-project health). Counts come from the server; the 500 work items,\n// 100 sprints and 100 milestones this used to load only fed page-derived counts (BOFF-7997).\nconst GET_ANALYTICS_DATA = gql`\n query GetAnalyticsData($workspaceId: ID!, $projectPagination: PlanmagnetPaginationInput) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $projectPagination) {\n items {\n id\n name\n key\n status\n startDate\n endDate\n tags\n }\n totalCount\n }\n }\n`;\n\n// BOFF-4447: real server-computed sprint delivery analytics — burndown,\n// velocity, and cumulative flow are all aggregated in wspace-planmagnet-svc\n// (src/modules/analytics) from the Sprint/WorkItem data model plus the\n// WorkItemStatusTransition audit trail, not derived client-side from raw\n// lists like the summary cards above.\nconst GET_SPRINT_BURNDOWN = gql`\n query GetSprintBurndown($workspaceId: ID!, $sprintId: ID!) {\n planmagnetSprintBurndown(workspaceId: $workspaceId, sprintId: $sprintId) {\n sprintId\n startDate\n endDate\n totalPoints\n totalCount\n points {\n date\n remainingPoints\n remainingCount\n completedPoints\n completedCount\n }\n }\n }\n`;\n\nconst GET_VELOCITY = gql`\n query GetVelocity($workspaceId: ID!, $projectId: ID!, $trailingSprintCount: Int) {\n planmagnetVelocity(\n workspaceId: $workspaceId\n projectId: $projectId\n trailingSprintCount: $trailingSprintCount\n ) {\n projectId\n averageVelocity\n forecastNextSprintPoints\n forecastRange {\n p15\n p85\n }\n insufficientHistory\n sprints {\n sprintId\n sprintName\n sequenceNumber\n completedPoints\n completedCount\n plannedPoints\n }\n }\n }\n`;\n\nconst GET_CUMULATIVE_FLOW = gql`\n query GetCumulativeFlow(\n $workspaceId: ID!\n $projectId: ID!\n $startDate: DateTime\n $endDate: DateTime\n ) {\n planmagnetCumulativeFlow(\n workspaceId: $workspaceId\n projectId: $projectId\n startDate: $startDate\n endDate: $endDate\n ) {\n projectId\n startDate\n endDate\n points {\n date\n statuses {\n status\n count\n }\n }\n }\n }\n`;\n\ninterface ProjectNode {\n id: string;\n name: string;\n key: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n tags: string[];\n}\n\ninterface AnalyticsData {\n planmagnetProjects: { items: ProjectNode[]; totalCount: number };\n}\n\ninterface BurndownData {\n planmagnetSprintBurndown: {\n sprintId: string;\n startDate: string | null;\n endDate: string | null;\n totalPoints: number;\n totalCount: number;\n points: {\n date: string;\n remainingPoints: number;\n remainingCount: number;\n completedPoints: number;\n completedCount: number;\n }[];\n } | null;\n}\n\ninterface VelocityData {\n planmagnetVelocity: {\n projectId: string;\n averageVelocity: number;\n forecastNextSprintPoints: number;\n forecastRange: { p15: number; p85: number } | null;\n insufficientHistory: boolean;\n sprints: {\n sprintId: string;\n sprintName: string;\n sequenceNumber: number;\n completedPoints: number;\n completedCount: number;\n plannedPoints: number | null;\n }[];\n };\n}\n\ninterface CumulativeFlowData {\n planmagnetCumulativeFlow: {\n projectId: string;\n startDate: string;\n endDate: string;\n points: {\n date: string;\n statuses: { status: string; count: number }[];\n }[];\n };\n}\n\n// recharts' `stroke`/`fill` props render as literal SVG paint attributes, not\n// Tailwind classNames — they cannot consume the semantic `bg-*`/`text-*`\n// utility tokens the lint rule expects everywhere else on this page. This is\n// the one place on the page raw color values are load-bearing rather than\n// stylistic, matching the same constraint every other MFE chart primitive in\n// the fleet (e.g. microfe-assethandler's `ChartCard`/`DonutChart`) hits.\nconst CFD_STATUS_COLORS: Record<string, string> = {\n BACKLOG: '#9ca3af',\n TODO: '#60a5fa',\n IN_PROGRESS: '#fbbf24',\n IN_REVIEW: '#a78bfa',\n TESTING: '#f472b6',\n DONE: '#4ade80',\n CANCELLED: '#f87171',\n};\n\nfunction formatDay(iso: string): string {\n const d = new Date(iso);\n return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;\n}\n\nexport function AnalyticsAnalyticsPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n const [selectedSprintId, setSelectedSprintId] = useState<string>('');\n\n // ANR-09: date range + project scope live in the URL (?from=&to=&project=) so a view is linkable, and so a\n // Dashboard KPI tile can deep-link straight into a pre-filtered view (see DashboardDashboardPagePage.tsx).\n const [searchParams, setSearchParams] = useSearchParams();\n const urlFrom = searchParams.get('from');\n const urlTo = searchParams.get('to');\n const [range, setRange] = useState<DateRange>(() =>\n urlFrom && urlTo && isValidRange(urlFrom, urlTo)\n ? { from: urlFrom, to: urlTo }\n : presetRange('30')\n );\n // The last range that was actually valid (from <= to), kept separate from `range` itself so an in-progress,\n // still-invalid edit in the inputs below can never leak into the query variables. `range` is always a valid\n // pair on mount (see the initializer above), so seeding this with it is safe.\n const [lastValidRange, setLastValidRange] = useState<DateRange>(range);\n const [scope, setScope] = useState<string>(searchParams.get('project') || ALL_PROJECTS);\n const scopeProjectId = scopeToProjectId(scope);\n const preset = activePreset(range);\n // Derived straight from `range` on every render — never a separately-tracked flag — so it can never go stale\n // relative to what the queries below actually send, however `range` got into an invalid state.\n const rangeIsValid = isValidRange(range.from, range.to);\n const rangeError = rangeIsValid\n ? null\n : t('planmagnet.analytics.range.invalid', '\"From\" must be on or before \"To\".');\n\n function applyRange(next: DateRange, nextScope: string) {\n setRange(next);\n if (isValidRange(next.from, next.to)) setLastValidRange(next);\n const params = new URLSearchParams(searchParams);\n params.set('from', next.from);\n params.set('to', next.to);\n if (nextScope !== ALL_PROJECTS) params.set('project', nextScope);\n else params.delete('project');\n setSearchParams(params, { replace: true });\n }\n\n function onPreset(p: RangePreset) {\n applyRange(presetRange(p), scope);\n }\n\n function onFromChange(value: string) {\n if (value && range.to && isValidRange(value, range.to)) {\n applyRange({ from: value, to: range.to }, scope);\n } else {\n setRange((r) => ({ ...r, from: value }));\n }\n }\n\n function onToChange(value: string) {\n if (value && range.from && isValidRange(range.from, value)) {\n applyRange({ from: range.from, to: value }, scope);\n } else {\n setRange((r) => ({ ...r, to: value }));\n }\n }\n\n function onScopeChange(value: string) {\n setScope(value);\n // The sprint select below is always scoped to the current project — an old selection would otherwise\n // silently carry over into whichever project happens to load first for the new scope.\n setSelectedSprintId('');\n applyRange(range, value);\n }\n\n // ANR-09 fix: while the visible `range` is invalid (rangeError above), every query below keeps using the last\n // known-good range instead of re-firing with an inverted from/to — `applyRange` only ever advances\n // `lastValidRange` on a genuinely valid pair, so this can never itself go out of range.\n const { startDate, endDate } = toRangeVariables(rangeIsValid ? range : lastValidRange);\n\n const { data, loading, error } = useQuery<AnalyticsData>(GET_ANALYTICS_DATA, {\n variables: { workspaceId, projectPagination: { first: 100 } },\n skip: !workspaceId,\n });\n\n const { data: countsData } = useQuery<{ planmagnetWorkItemCounts: WorkItemCounts }>(\n GET_ANALYTICS_COUNTS,\n { variables: { workspaceId }, skip: !workspaceId }\n );\n const counts = countsData?.planmagnetWorkItemCounts;\n const projectCounts = useProjectCounts();\n const sprintCounts = useSprintCounts();\n // Fixed for the life of the page (render must stay pure).\n const [yesterday] = useState(() => new Date(Date.now() - 86_400_000).toISOString().slice(0, 10));\n const { data: riskData } = useQuery<{\n missed: { totalCount: number };\n overdue: { totalCount: number };\n }>(GET_MILESTONE_RISK, { variables: { workspaceId, yesterday }, skip: !workspaceId });\n\n const projects = data?.planmagnetProjects?.items ?? [];\n\n // Default the project/sprint selectors to the first available option once\n // the summary data loads, so the delivery-analytics charts render without\n // requiring a manual pick every time the page opens. Filtering a list of\n // at most a few hundred sprints is cheap enough to redo on every render —\n // memoizing it would only need its own dependency-array workaround since\n // the `?? []` fallback above hands back a fresh array identity per render\n // whenever the query hasn't resolved yet.\n // Burndown/velocity/CFD/cycle-time/throughput all require a concrete project (the server fields take\n // `projectId: ID!`, not optional): \"All projects\" scope still needs SOME project for this section, so it falls\n // back to the first loaded project — the same fallback this page always used before ANR-09 added a scope select.\n const effectiveProjectId = scopeProjectId || projects[0]?.id || '';\n const { data: projectSprintsData } = useQuery<{\n planmagnetSprints: {\n items: {\n id: string;\n name: string;\n status: string;\n completedAt: string | null;\n endDate: string | null;\n }[];\n };\n }>(GET_PROJECT_SPRINTS, {\n variables: { workspaceId, projectId: effectiveProjectId },\n skip: !workspaceId || !effectiveProjectId,\n });\n const sprintsForProject = projectSprintsData?.planmagnetSprints.items ?? [];\n // Default to the running sprint (a PLANNING one has a future start and an empty burndown), else the latest\n // completed one, else whatever there is.\n const lastCompleted = sprintsForProject\n .filter((s) => s.status === 'COMPLETED')\n .sort((a, b) =>\n (b.completedAt ?? b.endDate ?? '').localeCompare(a.completedAt ?? a.endDate ?? '')\n )[0];\n const defaultSprint =\n sprintsForProject.find((s) => s.status === 'ACTIVE' || s.status === 'REVIEW') ??\n lastCompleted ??\n sprintsForProject[0];\n const effectiveSprintId = selectedSprintId || defaultSprint?.id || '';\n const effectiveProjectName = projects.find((p) => p.id === effectiveProjectId)?.name ?? '';\n\n const { data: burndownData, loading: burndownLoading } = useQuery<BurndownData>(\n GET_SPRINT_BURNDOWN,\n {\n variables: { workspaceId, sprintId: effectiveSprintId },\n skip: !workspaceId || !effectiveSprintId,\n }\n );\n\n const { data: velocityData, loading: velocityLoading } = useQuery<VelocityData>(GET_VELOCITY, {\n variables: { workspaceId, projectId: effectiveProjectId, trailingSprintCount: 6 },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n const { data: cfdData, loading: cfdLoading } = useQuery<CumulativeFlowData>(GET_CUMULATIVE_FLOW, {\n // ANR-09: the page's shared date range, not the field's own unbounded default.\n variables: { workspaceId, projectId: effectiveProjectId, startDate, endDate },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\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 }\n\n if (error) {\n return (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.analytics.errorLoading', 'Error loading analytics')}: {error.message}\n </div>\n );\n }\n\n // Every figure comes from the server counts; while they load (or if they fail) the tiles say \"—\" instead of\n // quietly falling back to the loaded page (BOFF-7997).\n const statusCount = (status: string) => counts?.byStatus.find((c) => c.status === status);\n const cancelled = statusCount('CANCELLED')?.count ?? 0;\n const doneTasks = counts ? (statusCount('DONE')?.count ?? 0) : null;\n const totalTasks = counts ? counts.total : null;\n // Blocked = held by a dependency (the release readiness rule), not a status nobody can set (BOFF-7973).\n const blockedTasks = counts ? counts.blocked : null;\n const totalPoints = counts ? counts.points : null;\n const completedPoints = counts ? (statusCount('DONE')?.points ?? 0) : null;\n const projectProgress = (projectId: string) => {\n const rows = counts?.byProject.filter((r) => r.projectId === projectId) ?? [];\n // Cancelled work no longer needs doing (BOFF-7989).\n const live = rows.filter((r) => r.status !== 'CANCELLED').reduce((sum, r) => sum + r.count, 0);\n const done = rows.filter((r) => r.status === 'DONE').reduce((sum, r) => sum + r.count, 0);\n return live > 0 ? Math.round((done / live) * 100) : 0;\n };\n\n const activeSprints = sprintCounts?.running ?? null;\n const atRiskMs = riskData ? riskData.missed.totalCount + riskData.overdue.totalCount : null;\n const activeProjects = projectCounts?.active ?? null;\n const completionRate =\n totalTasks != null && doneTasks != null && totalTasks - cancelled > 0\n ? Math.round((doneTasks / (totalTasks - cancelled)) * 100)\n : null;\n\n const burndown = burndownData?.planmagnetSprintBurndown;\n const burndownChartData =\n burndown?.points.map((p) => ({\n day: formatDay(p.date),\n remaining: p.remainingPoints,\n completed: p.completedPoints,\n })) ?? [];\n\n const velocity = velocityData?.planmagnetVelocity;\n const velocityChartData =\n velocity?.sprints.map((s) => ({\n name: s.sprintName,\n completed: s.completedPoints,\n planned: s.plannedPoints ?? undefined,\n })) ?? [];\n const avgVelocity = velocity ? Math.round(velocity.averageVelocity) : 0;\n const forecastNextSprint = velocity ? Math.round(velocity.forecastNextSprintPoints) : 0;\n const velocityForecastRange = velocity?.forecastRange ?? null;\n const velocityInsufficientHistory = velocity?.insufficientHistory ?? false;\n\n const cfd = cfdData?.planmagnetCumulativeFlow;\n const cfdStatusKeys = Array.from(\n new Set(cfd?.points.flatMap((p) => p.statuses.map((s) => s.status)) ?? [])\n );\n const cfdChartData =\n cfd?.points.map((p) => {\n const row: Record<string, string | number> = { day: formatDay(p.date) };\n for (const s of p.statuses) row[s.status] = s.count;\n return row;\n }) ?? [];\n\n const metrics = [\n {\n label: t('planmagnet.analytics.metrics.totalWorkItems.label', 'Total Work Items'),\n value: countLabel(totalTasks),\n trend: `${countLabel(completionRate)}% ${t('planmagnet.analytics.metrics.totalWorkItems.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.storyPoints.label', 'Story Points'),\n value: countLabel(totalPoints),\n trend: `${countLabel(completedPoints)} ${t('planmagnet.analytics.metrics.storyPoints.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.activeProjects.label', 'Active Projects'),\n value: countLabel(activeProjects),\n trend: `${countLabel(projectCounts?.all)} ${t('planmagnet.analytics.metrics.activeProjects.trend', 'total')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.blockedItems.label', 'Blocked Items'),\n value: countLabel(blockedTasks),\n trend:\n blockedTasks == null\n ? ''\n : blockedTasks > 0\n ? t('planmagnet.analytics.metrics.blockedItems.attention', 'Needs attention')\n : t('planmagnet.analytics.metrics.blockedItems.clear', 'All clear'),\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n {t('planmagnet.analytics.title', 'Analytics Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.subtitle', 'Overview of project health and team performance')}\n </p>\n </div>\n\n {/* ANR-09: date range + project scope. Both live in the URL, so this view is linkable and a Dashboard tile\n can deep-link straight into it pre-filtered. Everything below — CFD, cycle time, throughput, the new\n Quality/Delivery cards, and the budget card — reads this same range and scope. */}\n <div\n className=\"flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between\"\n data-testid=\"analytics-range\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <div\n className=\"flex items-center gap-1 rounded-md border p-0.5\"\n role=\"group\"\n aria-label={t('planmagnet.analytics.range.presetLabel', 'Date range preset')}\n data-testid=\"analytics-range-preset\"\n >\n {(['7', '30', '90'] as const).map((p) => (\n <button\n key={p}\n type=\"button\"\n aria-pressed={preset === p}\n onClick={() => onPreset(p)}\n className={`px-2.5 py-1 text-xs rounded ${\n preset === p\n ? 'bg-primary text-primary-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n >\n {t('planmagnet.analytics.range.last', 'Last {{days}} days', { days: p })}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-1.5\">\n <label htmlFor=\"analytics-range-from\" className=\"sr-only\">\n {t('planmagnet.analytics.range.from', 'From')}\n </label>\n <Input\n id=\"analytics-range-from\"\n type=\"date\"\n className=\"h-8 w-36\"\n value={range.from}\n max={range.to}\n onChange={(e) => onFromChange(e.target.value)}\n />\n <span className=\"text-muted-foreground text-sm\">–</span>\n <label htmlFor=\"analytics-range-to\" className=\"sr-only\">\n {t('planmagnet.analytics.range.to', 'To')}\n </label>\n <Input\n id=\"analytics-range-to\"\n type=\"date\"\n className=\"h-8 w-36\"\n value={range.to}\n min={range.from}\n onChange={(e) => onToChange(e.target.value)}\n />\n </div>\n {rangeError && (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {rangeError}\n </p>\n )}\n </div>\n\n <Select value={scope} onValueChange={onScopeChange}>\n <SelectTrigger className=\"h-8 w-56\" data-testid=\"analytics-project\">\n <SelectValue\n placeholder={t('planmagnet.analytics.range.allProjects', 'All projects')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_PROJECTS}>\n {t('planmagnet.analytics.range.allProjects', 'All projects')}\n </SelectItem>\n {projects.map((p) => (\n <SelectItem key={p.id} value={p.id}>\n {p.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">{m.label}</p>\n <p className=\"text-xl font-bold mt-1\">{m.value}</p>\n <p className=\"text-xs text-green-600 mt-1\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\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 <FolderKanban className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.activeProjects', 'Active Projects')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{countLabel(activeProjects)}</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 {t('planmagnet.analytics.summary.activeSprints', 'Active Sprints')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{countLabel(activeSprints)}</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 <TrendingUp className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.avgVelocity', 'Avg Velocity')}\n </div>\n {/* This one is per project — name it (BOFF-7987: it silently followed the picker below). */}\n <p className=\"text-xs text-muted-foreground truncate\">{effectiveProjectName || '—'}</p>\n <p className=\"text-xl font-bold mt-1\">\n {avgVelocity} {t('planmagnet.analytics.summary.pointsShort', 'pts')}\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 <Target className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.atRisk', 'At Risk')}\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {countLabel(atRiskMs)} {t('planmagnet.analytics.summary.milestones', 'milestones')}\n </p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-start sm:items-center\">\n <div>\n <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.delivery.title', 'Sprint Delivery Analytics')}\n </h2>\n {/* Project comes from the scope select above (ANR-09) — a sprint is always per-project, so burndown\n follows it here rather than repeating its own project picker. */}\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.analytics.delivery.forProject', 'For {{project}}', {\n project:\n effectiveProjectName || t('planmagnet.analytics.delivery.noProject', 'no project'),\n })}\n </p>\n </div>\n <div className=\"flex gap-2 sm:ml-auto\">\n <Select value={effectiveSprintId} onValueChange={setSelectedSprintId}>\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t('planmagnet.analytics.delivery.sprintPlaceholder', 'Select sprint')}\n />\n </SelectTrigger>\n <SelectContent>\n {sprintsForProject.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n {t('planmagnet.analytics.burndown.title', 'Sprint Burndown')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {burndownLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : burndownChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={220}>\n <LineChart data={burndownChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n <Line\n type=\"monotone\"\n dataKey=\"remaining\"\n name={t('planmagnet.analytics.burndown.remaining', 'Remaining points')}\n stroke=\"#f59e0b\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"completed\"\n name={t('planmagnet.analytics.burndown.completed', 'Completed points')}\n stroke=\"#22c55e\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n </LineChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.burndown.empty',\n 'No sprint selected, or the sprint has no work items yet'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <TrendingUp className=\"h-4 w-4\" />\n {t('planmagnet.analytics.velocity.title', 'Sprint Velocity')} (\n {velocityChartData.length} {t('planmagnet.analytics.velocity.sprints', 'Sprints')})\n </CardTitle>\n </CardHeader>\n <CardContent>\n {velocityLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : velocityChartData.length > 0 ? (\n <>\n <ResponsiveContainer width=\"100%\" height={190}>\n <BarChart data={velocityChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"name\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n {/* ANR-02: the empirical p15-p85 band around next sprint's forecast, drawn as a\n full-width shaded region (no x1/x2) since it isn't tied to a historical sprint's\n x position. */}\n {velocityForecastRange && (\n <ReferenceArea\n y1={velocityForecastRange.p15}\n y2={velocityForecastRange.p85}\n fill=\"var(--color-chart-2)\"\n fillOpacity={0.12}\n stroke=\"none\"\n />\n )}\n <Bar\n dataKey=\"planned\"\n name={t('planmagnet.analytics.velocity.planned', 'Planned')}\n fill=\"#cbd5e1\"\n />\n <Bar\n dataKey=\"completed\"\n name={t('planmagnet.analytics.velocity.completed', 'Completed')}\n fill=\"#6366f1\"\n />\n <ReferenceLine\n y={avgVelocity}\n stroke=\"#22c55e\"\n strokeDasharray=\"4 4\"\n label={{\n value: t('planmagnet.analytics.velocity.avgLabel', 'avg'),\n fontSize: 10,\n position: 'right',\n }}\n />\n </BarChart>\n </ResponsiveContainer>\n {/* Accessible text restatement of the shaded band, for screen readers. */}\n <p className=\"text-xs text-muted-foreground text-center mt-2\">\n {velocityChartData.length === 1\n ? t(\n 'planmagnet.analytics.velocity.trailingAverageOne',\n 'Median of the last completed sprint: {{points}} pts per sprint',\n { points: forecastNextSprint }\n )\n : t(\n 'planmagnet.analytics.velocity.trailingAverage',\n 'Median of the last {{count}} completed sprints: {{points}} pts per sprint',\n { points: forecastNextSprint, count: velocityChartData.length }\n )}\n {velocityForecastRange &&\n ` · ${t(\n 'planmagnet.analytics.velocity.range',\n 'likely range (p15–p85): {{low}}–{{high}} pts',\n {\n low: Math.round(velocityForecastRange.p15),\n high: Math.round(velocityForecastRange.p85),\n }\n )}`}\n </p>\n {velocityInsufficientHistory && (\n <p className=\"text-xs text-status-warning-text text-center mt-1\">\n {t(\n 'planmagnet.analytics.velocity.insufficientHistory',\n 'Fewer than 3 sprints with completed points — this forecast is low-confidence.'\n )}\n </p>\n )}\n <p className=\"text-xs text-muted-foreground text-center mt-1\">\n {/* Median + empirical percentile of real past sprints — statistical, never AI.\n Not Monte Carlo (that label is for planmagnetForecastDelivery's multi-sprint\n simulation below): a single next-period estimate reads straight off this\n sample's own percentiles. */}\n {t(\n 'planmagnet.analytics.velocity.methodLabel',\n 'Statistical forecast (not AI-generated).'\n )}\n </p>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.velocity.empty',\n 'No completed sprints yet for this project'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n {/* ANR-02: the Monte Carlo delivery forecast for the same project — the \"only this sprint\" toggle\n scopes it to the sprint selected above instead of the project's full open scope. */}\n <DeliveryForecastCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n sprintId={effectiveSprintId || undefined}\n sprintName={sprintsForProject.find((s) => s.id === effectiveSprintId)?.name}\n />\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n {t('planmagnet.analytics.cfd.title', 'Cumulative Flow Diagram')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.analytics.cfd.subtitle', 'Selected date range, for {{project}}.', {\n project:\n effectiveProjectName || t('planmagnet.analytics.delivery.noProject', 'no project'),\n })}\n </p>\n </CardHeader>\n <CardContent>\n {cfdLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : cfdChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={260}>\n <AreaChart data={cfdChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n {cfdStatusKeys.map((status) => (\n <Area\n key={status}\n type=\"monotone\"\n dataKey={status}\n name={status.toLowerCase().replace('_', ' ')}\n stackId=\"cfd\"\n stroke={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fill={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fillOpacity={0.7}\n />\n ))}\n </AreaChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t('planmagnet.analytics.cfd.empty', 'No work items found for this project')}\n </p>\n )}\n </CardContent>\n </Card>\n\n {/* ANR-09: throughput's own group-by (day/week bucket). */}\n <ThroughputCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n\n {/* ANR-20 / ANR-21: quality and delivery KPIs, scoped to the page's project select (undefined = every\n project) and the shared date range. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <QualityMetricsCard\n workspaceId={workspaceId}\n projectId={scopeProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n <DeliveryKpisCard\n workspaceId={workspaceId}\n projectId={scopeProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n </div>\n\n {/* Cycle time follows the delivery selector above; budgets follow the project scope select. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <CycleTimeCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n <BudgetPerformanceCard workspaceId={workspaceId} projectId={scopeProjectId} />\n </div>\n\n {/* ANR-24: advanced visualizations — status-flow Sankey (one project, the delivery selector above) and the\n work-breakdown Treemap (project scope select; live counts, not date-ranged). */}\n <div>\n <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.advanced.title', 'Advanced visualizations')}\n </h2>\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <StatusFlowCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n <WorkBreakdownCard workspaceId={workspaceId} projectId={scopeProjectId} />\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('planmagnet.analytics.projectHealth.title', 'Project Health')}</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {projects\n .filter((p) => p.status === 'ACTIVE')\n .map((p) => {\n // Per project, from the server. This used every ASSIGNED item in\n // the workspace for every project, so all bars were identical.\n const progress = projectProgress(p.id);\n const hasOverdue = p.endDate && new Date(p.endDate) < new Date() && progress < 100;\n const health = hasOverdue ? 'at_risk' : progress >= 75 ? 'on_track' : 'in_progress';\n return (\n <div key={p.id} className=\"flex items-center gap-4\">\n <span className=\"text-sm font-medium w-40 truncate\">{p.name}</span>\n <div className=\"flex-1\">\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${health === 'at_risk' ? 'bg-orange-400' : health === 'on_track' ? 'bg-green-400' : 'bg-blue-400'}`}\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n <span className=\"text-sm w-12 text-right\">{progress}%</span>\n <Badge variant=\"outline\" className=\"capitalize text-xs w-20 justify-center\">\n {health.replace('_', ' ')}\n </Badge>\n </div>\n );\n })}\n {projects.filter((p) => p.status === 'ACTIVE').length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.analytics.projectHealth.empty', 'No active projects found')}\n </p>\n )}\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { TrendingUp, Zap, FolderKanban, BarChart3, Target } from 'lucide-react';\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Legend,\n Line,\n LineChart,\n ReferenceArea,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport { countLabel, useProjectCounts, useSprintCounts } from '../hooks/useCounts';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { DeliveryForecastCard } from '../forecast/DeliveryForecastCard';\nimport { BudgetPerformanceCard, CycleTimeCard } from './analytics/DeliveryHealthCards';\nimport { QualityMetricsCard } from './analytics/QualityMetricsCard';\nimport { DeliveryKpisCard } from './analytics/DeliveryKpisCard';\nimport { MetricsSection } from './analytics/MetricsCard';\nimport { ThroughputCard } from './analytics/ThroughputCard';\nimport { StatusFlowCard } from './analytics/StatusFlowCard';\nimport { WorkBreakdownCard } from './analytics/WorkBreakdownCard';\nimport {\n ALL_PROJECTS,\n activePreset,\n isValidRange,\n presetRange,\n scopeToProjectId,\n toRangeVariables,\n type DateRange,\n type RangePreset,\n} from './analytics/analyticsRange';\n\n// Server-side totals (QA H2). The work-item list returns at most 100 rows per\n// page, so every figure computed from the loaded page undercounted past 100\n// items — and \"Project Health\" did not filter by project at all.\n// The chosen project's sprints, queried by project (BOFF-7964: the picker was built from the 100 newest sprints\n// of the whole workspace, so an older project's list could be empty).\nconst GET_PROJECT_SPRINTS = gql`\n query AnalyticsProjectSprints($workspaceId: ID!, $projectId: String) {\n planmagnetSprints(\n workspaceId: $workspaceId\n filter: { projectId: $projectId }\n pagination: { first: 100 }\n ) {\n items {\n id\n name\n status\n completedAt\n endDate\n }\n }\n }\n`;\n\n// At risk = missed, or still open and past due — counted on the server over every milestone.\nconst GET_MILESTONE_RISK = gql`\n query AnalyticsMilestoneRisk($workspaceId: ID!, $yesterday: DateTime) {\n missed: planmagnetMilestones(\n workspaceId: $workspaceId\n filter: { status: MISSED }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n overdue: planmagnetMilestones(\n workspaceId: $workspaceId\n filter: { open: true, dueTo: $yesterday }\n pagination: { first: 1 }\n ) {\n totalCount\n }\n }\n`;\n\nconst GET_ANALYTICS_COUNTS = gql`\n query GetAnalyticsWorkItemCounts($workspaceId: ID!) {\n planmagnetWorkItemCounts(workspaceId: $workspaceId) {\n total\n points\n blocked\n byStatus {\n status\n count\n points\n }\n byProject {\n projectId\n status\n count\n }\n }\n }\n`;\n\ninterface WorkItemCounts {\n total: number;\n points: number;\n blocked: number;\n byStatus: Array<{ status: string; count: number; points: number }>;\n byProject: Array<{ projectId: string; status: string; count: number }>;\n}\n\n// Only the project list (for the pickers and per-project health). Counts come from the server; the 500 work items,\n// 100 sprints and 100 milestones this used to load only fed page-derived counts (BOFF-7997).\nconst GET_ANALYTICS_DATA = gql`\n query GetAnalyticsData($workspaceId: ID!, $projectPagination: PlanmagnetPaginationInput) {\n planmagnetProjects(workspaceId: $workspaceId, pagination: $projectPagination) {\n items {\n id\n name\n key\n status\n startDate\n endDate\n tags\n }\n totalCount\n }\n }\n`;\n\n// BOFF-4447: real server-computed sprint delivery analytics — burndown,\n// velocity, and cumulative flow are all aggregated in wspace-planmagnet-svc\n// (src/modules/analytics) from the Sprint/WorkItem data model plus the\n// WorkItemStatusTransition audit trail, not derived client-side from raw\n// lists like the summary cards above.\nconst GET_SPRINT_BURNDOWN = gql`\n query GetSprintBurndown($workspaceId: ID!, $sprintId: ID!) {\n planmagnetSprintBurndown(workspaceId: $workspaceId, sprintId: $sprintId) {\n sprintId\n startDate\n endDate\n totalPoints\n totalCount\n points {\n date\n remainingPoints\n remainingCount\n completedPoints\n completedCount\n }\n }\n }\n`;\n\nconst GET_VELOCITY = gql`\n query GetVelocity($workspaceId: ID!, $projectId: ID!, $trailingSprintCount: Int) {\n planmagnetVelocity(\n workspaceId: $workspaceId\n projectId: $projectId\n trailingSprintCount: $trailingSprintCount\n ) {\n projectId\n averageVelocity\n forecastNextSprintPoints\n forecastRange {\n p15\n p85\n }\n insufficientHistory\n sprints {\n sprintId\n sprintName\n sequenceNumber\n completedPoints\n completedCount\n plannedPoints\n }\n }\n }\n`;\n\nconst GET_CUMULATIVE_FLOW = gql`\n query GetCumulativeFlow(\n $workspaceId: ID!\n $projectId: ID!\n $startDate: DateTime\n $endDate: DateTime\n ) {\n planmagnetCumulativeFlow(\n workspaceId: $workspaceId\n projectId: $projectId\n startDate: $startDate\n endDate: $endDate\n ) {\n projectId\n startDate\n endDate\n points {\n date\n statuses {\n status\n count\n }\n }\n }\n }\n`;\n\ninterface ProjectNode {\n id: string;\n name: string;\n key: string;\n status: string;\n startDate: string | null;\n endDate: string | null;\n tags: string[];\n}\n\ninterface AnalyticsData {\n planmagnetProjects: { items: ProjectNode[]; totalCount: number };\n}\n\ninterface BurndownData {\n planmagnetSprintBurndown: {\n sprintId: string;\n startDate: string | null;\n endDate: string | null;\n totalPoints: number;\n totalCount: number;\n points: {\n date: string;\n remainingPoints: number;\n remainingCount: number;\n completedPoints: number;\n completedCount: number;\n }[];\n } | null;\n}\n\ninterface VelocityData {\n planmagnetVelocity: {\n projectId: string;\n averageVelocity: number;\n forecastNextSprintPoints: number;\n forecastRange: { p15: number; p85: number } | null;\n insufficientHistory: boolean;\n sprints: {\n sprintId: string;\n sprintName: string;\n sequenceNumber: number;\n completedPoints: number;\n completedCount: number;\n plannedPoints: number | null;\n }[];\n };\n}\n\ninterface CumulativeFlowData {\n planmagnetCumulativeFlow: {\n projectId: string;\n startDate: string;\n endDate: string;\n points: {\n date: string;\n statuses: { status: string; count: number }[];\n }[];\n };\n}\n\n// recharts' `stroke`/`fill` props render as literal SVG paint attributes, not\n// Tailwind classNames — they cannot consume the semantic `bg-*`/`text-*`\n// utility tokens the lint rule expects everywhere else on this page. This is\n// the one place on the page raw color values are load-bearing rather than\n// stylistic, matching the same constraint every other MFE chart primitive in\n// the fleet (e.g. microfe-assethandler's `ChartCard`/`DonutChart`) hits.\nconst CFD_STATUS_COLORS: Record<string, string> = {\n BACKLOG: '#9ca3af',\n TODO: '#60a5fa',\n IN_PROGRESS: '#fbbf24',\n IN_REVIEW: '#a78bfa',\n TESTING: '#f472b6',\n DONE: '#4ade80',\n CANCELLED: '#f87171',\n};\n\nfunction formatDay(iso: string): string {\n const d = new Date(iso);\n return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;\n}\n\nexport function AnalyticsAnalyticsPagePage() {\n const { workspaceId } = usePlanMagnet();\n const { t } = useI18n();\n const [selectedSprintId, setSelectedSprintId] = useState<string>('');\n\n // ANR-09: date range + project scope live in the URL (?from=&to=&project=) so a view is linkable, and so a\n // Dashboard KPI tile can deep-link straight into a pre-filtered view (see DashboardDashboardPagePage.tsx).\n const [searchParams, setSearchParams] = useSearchParams();\n const urlFrom = searchParams.get('from');\n const urlTo = searchParams.get('to');\n const [range, setRange] = useState<DateRange>(() =>\n urlFrom && urlTo && isValidRange(urlFrom, urlTo)\n ? { from: urlFrom, to: urlTo }\n : presetRange('30')\n );\n // The last range that was actually valid (from <= to), kept separate from `range` itself so an in-progress,\n // still-invalid edit in the inputs below can never leak into the query variables. `range` is always a valid\n // pair on mount (see the initializer above), so seeding this with it is safe.\n const [lastValidRange, setLastValidRange] = useState<DateRange>(range);\n const [scope, setScope] = useState<string>(searchParams.get('project') || ALL_PROJECTS);\n const scopeProjectId = scopeToProjectId(scope);\n const preset = activePreset(range);\n // Derived straight from `range` on every render — never a separately-tracked flag — so it can never go stale\n // relative to what the queries below actually send, however `range` got into an invalid state.\n const rangeIsValid = isValidRange(range.from, range.to);\n const rangeError = rangeIsValid\n ? null\n : t('planmagnet.analytics.range.invalid', '\"From\" must be on or before \"To\".');\n\n function applyRange(next: DateRange, nextScope: string) {\n setRange(next);\n if (isValidRange(next.from, next.to)) setLastValidRange(next);\n const params = new URLSearchParams(searchParams);\n params.set('from', next.from);\n params.set('to', next.to);\n if (nextScope !== ALL_PROJECTS) params.set('project', nextScope);\n else params.delete('project');\n setSearchParams(params, { replace: true });\n }\n\n function onPreset(p: RangePreset) {\n applyRange(presetRange(p), scope);\n }\n\n function onFromChange(value: string) {\n if (value && range.to && isValidRange(value, range.to)) {\n applyRange({ from: value, to: range.to }, scope);\n } else {\n setRange((r) => ({ ...r, from: value }));\n }\n }\n\n function onToChange(value: string) {\n if (value && range.from && isValidRange(range.from, value)) {\n applyRange({ from: range.from, to: value }, scope);\n } else {\n setRange((r) => ({ ...r, to: value }));\n }\n }\n\n function onScopeChange(value: string) {\n setScope(value);\n // The sprint select below is always scoped to the current project — an old selection would otherwise\n // silently carry over into whichever project happens to load first for the new scope.\n setSelectedSprintId('');\n applyRange(range, value);\n }\n\n // ANR-09 fix: while the visible `range` is invalid (rangeError above), every query below keeps using the last\n // known-good range instead of re-firing with an inverted from/to — `applyRange` only ever advances\n // `lastValidRange` on a genuinely valid pair, so this can never itself go out of range.\n const { startDate, endDate } = toRangeVariables(rangeIsValid ? range : lastValidRange);\n\n const { data, loading, error } = useQuery<AnalyticsData>(GET_ANALYTICS_DATA, {\n variables: { workspaceId, projectPagination: { first: 100 } },\n skip: !workspaceId,\n });\n\n const { data: countsData } = useQuery<{ planmagnetWorkItemCounts: WorkItemCounts }>(\n GET_ANALYTICS_COUNTS,\n { variables: { workspaceId }, skip: !workspaceId }\n );\n const counts = countsData?.planmagnetWorkItemCounts;\n const projectCounts = useProjectCounts();\n const sprintCounts = useSprintCounts();\n // Fixed for the life of the page (render must stay pure).\n const [yesterday] = useState(() => new Date(Date.now() - 86_400_000).toISOString().slice(0, 10));\n const { data: riskData } = useQuery<{\n missed: { totalCount: number };\n overdue: { totalCount: number };\n }>(GET_MILESTONE_RISK, { variables: { workspaceId, yesterday }, skip: !workspaceId });\n\n const projects = data?.planmagnetProjects?.items ?? [];\n\n // Default the project/sprint selectors to the first available option once\n // the summary data loads, so the delivery-analytics charts render without\n // requiring a manual pick every time the page opens. Filtering a list of\n // at most a few hundred sprints is cheap enough to redo on every render —\n // memoizing it would only need its own dependency-array workaround since\n // the `?? []` fallback above hands back a fresh array identity per render\n // whenever the query hasn't resolved yet.\n // Burndown/velocity/CFD/cycle-time/throughput all require a concrete project (the server fields take\n // `projectId: ID!`, not optional): \"All projects\" scope still needs SOME project for this section, so it falls\n // back to the first loaded project — the same fallback this page always used before ANR-09 added a scope select.\n const effectiveProjectId = scopeProjectId || projects[0]?.id || '';\n const { data: projectSprintsData } = useQuery<{\n planmagnetSprints: {\n items: {\n id: string;\n name: string;\n status: string;\n completedAt: string | null;\n endDate: string | null;\n }[];\n };\n }>(GET_PROJECT_SPRINTS, {\n variables: { workspaceId, projectId: effectiveProjectId },\n skip: !workspaceId || !effectiveProjectId,\n });\n const sprintsForProject = projectSprintsData?.planmagnetSprints.items ?? [];\n // Default to the running sprint (a PLANNING one has a future start and an empty burndown), else the latest\n // completed one, else whatever there is.\n const lastCompleted = sprintsForProject\n .filter((s) => s.status === 'COMPLETED')\n .sort((a, b) =>\n (b.completedAt ?? b.endDate ?? '').localeCompare(a.completedAt ?? a.endDate ?? '')\n )[0];\n const defaultSprint =\n sprintsForProject.find((s) => s.status === 'ACTIVE' || s.status === 'REVIEW') ??\n lastCompleted ??\n sprintsForProject[0];\n const effectiveSprintId = selectedSprintId || defaultSprint?.id || '';\n const effectiveProjectName = projects.find((p) => p.id === effectiveProjectId)?.name ?? '';\n\n const { data: burndownData, loading: burndownLoading } = useQuery<BurndownData>(\n GET_SPRINT_BURNDOWN,\n {\n variables: { workspaceId, sprintId: effectiveSprintId },\n skip: !workspaceId || !effectiveSprintId,\n }\n );\n\n const { data: velocityData, loading: velocityLoading } = useQuery<VelocityData>(GET_VELOCITY, {\n variables: { workspaceId, projectId: effectiveProjectId, trailingSprintCount: 6 },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n const { data: cfdData, loading: cfdLoading } = useQuery<CumulativeFlowData>(GET_CUMULATIVE_FLOW, {\n // ANR-09: the page's shared date range, not the field's own unbounded default.\n variables: { workspaceId, projectId: effectiveProjectId, startDate, endDate },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\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 }\n\n if (error) {\n return (\n <div className=\"p-6 text-red-500\">\n {t('planmagnet.analytics.errorLoading', 'Error loading analytics')}: {error.message}\n </div>\n );\n }\n\n // Every figure comes from the server counts; while they load (or if they fail) the tiles say \"—\" instead of\n // quietly falling back to the loaded page (BOFF-7997).\n const statusCount = (status: string) => counts?.byStatus.find((c) => c.status === status);\n const cancelled = statusCount('CANCELLED')?.count ?? 0;\n const doneTasks = counts ? (statusCount('DONE')?.count ?? 0) : null;\n const totalTasks = counts ? counts.total : null;\n // Blocked = held by a dependency (the release readiness rule), not a status nobody can set (BOFF-7973).\n const blockedTasks = counts ? counts.blocked : null;\n const totalPoints = counts ? counts.points : null;\n const completedPoints = counts ? (statusCount('DONE')?.points ?? 0) : null;\n const projectProgress = (projectId: string) => {\n const rows = counts?.byProject.filter((r) => r.projectId === projectId) ?? [];\n // Cancelled work no longer needs doing (BOFF-7989).\n const live = rows.filter((r) => r.status !== 'CANCELLED').reduce((sum, r) => sum + r.count, 0);\n const done = rows.filter((r) => r.status === 'DONE').reduce((sum, r) => sum + r.count, 0);\n return live > 0 ? Math.round((done / live) * 100) : 0;\n };\n\n const activeSprints = sprintCounts?.running ?? null;\n const atRiskMs = riskData ? riskData.missed.totalCount + riskData.overdue.totalCount : null;\n const activeProjects = projectCounts?.active ?? null;\n const completionRate =\n totalTasks != null && doneTasks != null && totalTasks - cancelled > 0\n ? Math.round((doneTasks / (totalTasks - cancelled)) * 100)\n : null;\n\n const burndown = burndownData?.planmagnetSprintBurndown;\n const burndownChartData =\n burndown?.points.map((p) => ({\n day: formatDay(p.date),\n remaining: p.remainingPoints,\n completed: p.completedPoints,\n })) ?? [];\n\n const velocity = velocityData?.planmagnetVelocity;\n const velocityChartData =\n velocity?.sprints.map((s) => ({\n name: s.sprintName,\n completed: s.completedPoints,\n planned: s.plannedPoints ?? undefined,\n })) ?? [];\n const avgVelocity = velocity ? Math.round(velocity.averageVelocity) : 0;\n const forecastNextSprint = velocity ? Math.round(velocity.forecastNextSprintPoints) : 0;\n const velocityForecastRange = velocity?.forecastRange ?? null;\n const velocityInsufficientHistory = velocity?.insufficientHistory ?? false;\n\n const cfd = cfdData?.planmagnetCumulativeFlow;\n const cfdStatusKeys = Array.from(\n new Set(cfd?.points.flatMap((p) => p.statuses.map((s) => s.status)) ?? [])\n );\n const cfdChartData =\n cfd?.points.map((p) => {\n const row: Record<string, string | number> = { day: formatDay(p.date) };\n for (const s of p.statuses) row[s.status] = s.count;\n return row;\n }) ?? [];\n\n const metrics = [\n {\n label: t('planmagnet.analytics.metrics.totalWorkItems.label', 'Total Work Items'),\n value: countLabel(totalTasks),\n trend: `${countLabel(completionRate)}% ${t('planmagnet.analytics.metrics.totalWorkItems.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.storyPoints.label', 'Story Points'),\n value: countLabel(totalPoints),\n trend: `${countLabel(completedPoints)} ${t('planmagnet.analytics.metrics.storyPoints.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.activeProjects.label', 'Active Projects'),\n value: countLabel(activeProjects),\n trend: `${countLabel(projectCounts?.all)} ${t('planmagnet.analytics.metrics.activeProjects.trend', 'total')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.blockedItems.label', 'Blocked Items'),\n value: countLabel(blockedTasks),\n trend:\n blockedTasks == null\n ? ''\n : blockedTasks > 0\n ? t('planmagnet.analytics.metrics.blockedItems.attention', 'Needs attention')\n : t('planmagnet.analytics.metrics.blockedItems.clear', 'All clear'),\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n {t('planmagnet.analytics.title', 'Analytics Dashboard')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('planmagnet.analytics.subtitle', 'Overview of project health and team performance')}\n </p>\n </div>\n\n {/* ANR-09: date range + project scope. Both live in the URL, so this view is linkable and a Dashboard tile\n can deep-link straight into it pre-filtered. Everything below — CFD, cycle time, throughput, the new\n Quality/Delivery cards, and the budget card — reads this same range and scope. */}\n <div\n className=\"flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between\"\n data-testid=\"analytics-range\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <div\n className=\"flex items-center gap-1 rounded-md border p-0.5\"\n role=\"group\"\n aria-label={t('planmagnet.analytics.range.presetLabel', 'Date range preset')}\n data-testid=\"analytics-range-preset\"\n >\n {(['7', '30', '90'] as const).map((p) => (\n <button\n key={p}\n type=\"button\"\n aria-pressed={preset === p}\n onClick={() => onPreset(p)}\n className={`px-2.5 py-1 text-xs rounded ${\n preset === p\n ? 'bg-primary text-primary-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n >\n {t('planmagnet.analytics.range.last', 'Last {{days}} days', { days: p })}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-1.5\">\n <label htmlFor=\"analytics-range-from\" className=\"sr-only\">\n {t('planmagnet.analytics.range.from', 'From')}\n </label>\n <Input\n id=\"analytics-range-from\"\n type=\"date\"\n className=\"h-8 w-36\"\n value={range.from}\n max={range.to}\n onChange={(e) => onFromChange(e.target.value)}\n />\n <span className=\"text-muted-foreground text-sm\">–</span>\n <label htmlFor=\"analytics-range-to\" className=\"sr-only\">\n {t('planmagnet.analytics.range.to', 'To')}\n </label>\n <Input\n id=\"analytics-range-to\"\n type=\"date\"\n className=\"h-8 w-36\"\n value={range.to}\n min={range.from}\n onChange={(e) => onToChange(e.target.value)}\n />\n </div>\n {rangeError && (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {rangeError}\n </p>\n )}\n </div>\n\n <Select value={scope} onValueChange={onScopeChange}>\n <SelectTrigger className=\"h-8 w-56\" data-testid=\"analytics-project\">\n <SelectValue\n placeholder={t('planmagnet.analytics.range.allProjects', 'All projects')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_PROJECTS}>\n {t('planmagnet.analytics.range.allProjects', 'All projects')}\n </SelectItem>\n {projects.map((p) => (\n <SelectItem key={p.id} value={p.id}>\n {p.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4\">\n {metrics.map((m) => (\n <Card key={m.label}>\n <CardContent className=\"pt-4\">\n <p className=\"text-xs text-muted-foreground\">{m.label}</p>\n <p className=\"text-xl font-bold mt-1\">{m.value}</p>\n <p className=\"text-xs text-green-600 mt-1\">{m.trend}</p>\n </CardContent>\n </Card>\n ))}\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 <FolderKanban className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.activeProjects', 'Active Projects')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{countLabel(activeProjects)}</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 {t('planmagnet.analytics.summary.activeSprints', 'Active Sprints')}\n </div>\n <p className=\"text-xl font-bold mt-1\">{countLabel(activeSprints)}</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 <TrendingUp className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.avgVelocity', 'Avg Velocity')}\n </div>\n {/* This one is per project — name it (BOFF-7987: it silently followed the picker below). */}\n <p className=\"text-xs text-muted-foreground truncate\">{effectiveProjectName || '—'}</p>\n <p className=\"text-xl font-bold mt-1\">\n {avgVelocity} {t('planmagnet.analytics.summary.pointsShort', 'pts')}\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 <Target className=\"h-4 w-4\" />\n {t('planmagnet.analytics.summary.atRisk', 'At Risk')}\n </div>\n <p className=\"text-xl font-bold mt-1\">\n {countLabel(atRiskMs)} {t('planmagnet.analytics.summary.milestones', 'milestones')}\n </p>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-start sm:items-center\">\n <div>\n <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.delivery.title', 'Sprint Delivery Analytics')}\n </h2>\n {/* Project comes from the scope select above (ANR-09) — a sprint is always per-project, so burndown\n follows it here rather than repeating its own project picker. */}\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.analytics.delivery.forProject', 'For {{project}}', {\n project:\n effectiveProjectName || t('planmagnet.analytics.delivery.noProject', 'no project'),\n })}\n </p>\n </div>\n <div className=\"flex gap-2 sm:ml-auto\">\n <Select value={effectiveSprintId} onValueChange={setSelectedSprintId}>\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t('planmagnet.analytics.delivery.sprintPlaceholder', 'Select sprint')}\n />\n </SelectTrigger>\n <SelectContent>\n {sprintsForProject.map((s) => (\n <SelectItem key={s.id} value={s.id}>\n {s.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n {t('planmagnet.analytics.burndown.title', 'Sprint Burndown')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {burndownLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : burndownChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={220}>\n <LineChart data={burndownChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n <Line\n type=\"monotone\"\n dataKey=\"remaining\"\n name={t('planmagnet.analytics.burndown.remaining', 'Remaining points')}\n stroke=\"#f59e0b\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"completed\"\n name={t('planmagnet.analytics.burndown.completed', 'Completed points')}\n stroke=\"#22c55e\"\n strokeWidth={2}\n dot={{ r: 2 }}\n />\n </LineChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.burndown.empty',\n 'No sprint selected, or the sprint has no work items yet'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <TrendingUp className=\"h-4 w-4\" />\n {t('planmagnet.analytics.velocity.title', 'Sprint Velocity')} (\n {velocityChartData.length} {t('planmagnet.analytics.velocity.sprints', 'Sprints')})\n </CardTitle>\n </CardHeader>\n <CardContent>\n {velocityLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : velocityChartData.length > 0 ? (\n <>\n <ResponsiveContainer width=\"100%\" height={190}>\n <BarChart data={velocityChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"name\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n {/* ANR-02: the empirical p15-p85 band around next sprint's forecast, drawn as a\n full-width shaded region (no x1/x2) since it isn't tied to a historical sprint's\n x position. */}\n {velocityForecastRange && (\n <ReferenceArea\n y1={velocityForecastRange.p15}\n y2={velocityForecastRange.p85}\n fill=\"var(--color-chart-2)\"\n fillOpacity={0.12}\n stroke=\"none\"\n />\n )}\n <Bar\n dataKey=\"planned\"\n name={t('planmagnet.analytics.velocity.planned', 'Planned')}\n fill=\"#cbd5e1\"\n />\n <Bar\n dataKey=\"completed\"\n name={t('planmagnet.analytics.velocity.completed', 'Completed')}\n fill=\"#6366f1\"\n />\n <ReferenceLine\n y={avgVelocity}\n stroke=\"#22c55e\"\n strokeDasharray=\"4 4\"\n label={{\n value: t('planmagnet.analytics.velocity.avgLabel', 'avg'),\n fontSize: 10,\n position: 'right',\n }}\n />\n </BarChart>\n </ResponsiveContainer>\n {/* Accessible text restatement of the shaded band, for screen readers. */}\n <p className=\"text-xs text-muted-foreground text-center mt-2\">\n {velocityChartData.length === 1\n ? t(\n 'planmagnet.analytics.velocity.trailingAverageOne',\n 'Median of the last completed sprint: {{points}} pts per sprint',\n { points: forecastNextSprint }\n )\n : t(\n 'planmagnet.analytics.velocity.trailingAverage',\n 'Median of the last {{count}} completed sprints: {{points}} pts per sprint',\n { points: forecastNextSprint, count: velocityChartData.length }\n )}\n {velocityForecastRange &&\n ` · ${t(\n 'planmagnet.analytics.velocity.range',\n 'likely range (p15–p85): {{low}}–{{high}} pts',\n {\n low: Math.round(velocityForecastRange.p15),\n high: Math.round(velocityForecastRange.p85),\n }\n )}`}\n </p>\n {velocityInsufficientHistory && (\n <p className=\"text-xs text-status-warning-text text-center mt-1\">\n {t(\n 'planmagnet.analytics.velocity.insufficientHistory',\n 'Fewer than 3 sprints with completed points — this forecast is low-confidence.'\n )}\n </p>\n )}\n <p className=\"text-xs text-muted-foreground text-center mt-1\">\n {/* Median + empirical percentile of real past sprints — statistical, never AI.\n Not Monte Carlo (that label is for planmagnetForecastDelivery's multi-sprint\n simulation below): a single next-period estimate reads straight off this\n sample's own percentiles. */}\n {t(\n 'planmagnet.analytics.velocity.methodLabel',\n 'Statistical forecast (not AI-generated).'\n )}\n </p>\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.velocity.empty',\n 'No completed sprints yet for this project'\n )}\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n {/* ANR-02: the Monte Carlo delivery forecast for the same project — the \"only this sprint\" toggle\n scopes it to the sprint selected above instead of the project's full open scope. */}\n <DeliveryForecastCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n sprintId={effectiveSprintId || undefined}\n sprintName={sprintsForProject.find((s) => s.id === effectiveSprintId)?.name}\n />\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <BarChart3 className=\"h-4 w-4\" />\n {t('planmagnet.analytics.cfd.title', 'Cumulative Flow Diagram')}\n </CardTitle>\n <p className=\"text-xs text-muted-foreground\">\n {t('planmagnet.analytics.cfd.subtitle', 'Selected date range, for {{project}}.', {\n project:\n effectiveProjectName || t('planmagnet.analytics.delivery.noProject', 'no project'),\n })}\n </p>\n </CardHeader>\n <CardContent>\n {cfdLoading ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : cfdChartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={260}>\n <AreaChart data={cfdChartData}>\n <CartesianGrid strokeDasharray=\"3 3\" opacity={0.2} />\n <XAxis dataKey=\"day\" tick={{ fontSize: 11 }} />\n <YAxis tick={{ fontSize: 11 }} allowDecimals={false} />\n <Tooltip />\n <Legend wrapperStyle={{ fontSize: 12 }} />\n {cfdStatusKeys.map((status) => (\n <Area\n key={status}\n type=\"monotone\"\n dataKey={status}\n name={status.toLowerCase().replace('_', ' ')}\n stackId=\"cfd\"\n stroke={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fill={CFD_STATUS_COLORS[status] ?? '#a3a3a3'}\n fillOpacity={0.7}\n />\n ))}\n </AreaChart>\n </ResponsiveContainer>\n ) : (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t('planmagnet.analytics.cfd.empty', 'No work items found for this project')}\n </p>\n )}\n </CardContent>\n </Card>\n\n {/* ANR-09: throughput's own group-by (day/week bucket). */}\n <ThroughputCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n\n {/* ANR-20 / ANR-21: quality and delivery KPIs, scoped to the page's project select (undefined = every\n project) and the shared date range. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <QualityMetricsCard\n workspaceId={workspaceId}\n projectId={scopeProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n <DeliveryKpisCard\n workspaceId={workspaceId}\n projectId={scopeProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n </div>\n\n {/* ANR-18 / ANR-19: the generic Metric/Analytics framework, composed from the same queries above —\n scoped by the project select (undefined = workspace-only half) and the shared date range. */}\n <MetricsSection\n workspaceId={workspaceId}\n projectId={scopeProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n\n {/* Cycle time follows the delivery selector above; budgets follow the project scope select. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <CycleTimeCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n <BudgetPerformanceCard workspaceId={workspaceId} projectId={scopeProjectId} />\n </div>\n\n {/* ANR-24: advanced visualizations — status-flow Sankey (one project, the delivery selector above) and the\n work-breakdown Treemap (project scope select; live counts, not date-ranged). */}\n <div>\n <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.advanced.title', 'Advanced visualizations')}\n </h2>\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <StatusFlowCard\n workspaceId={workspaceId}\n projectId={effectiveProjectId}\n startDate={startDate}\n endDate={endDate}\n />\n <WorkBreakdownCard workspaceId={workspaceId} projectId={scopeProjectId} />\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('planmagnet.analytics.projectHealth.title', 'Project Health')}</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {projects\n .filter((p) => p.status === 'ACTIVE')\n .map((p) => {\n // Per project, from the server. This used every ASSIGNED item in\n // the workspace for every project, so all bars were identical.\n const progress = projectProgress(p.id);\n const hasOverdue = p.endDate && new Date(p.endDate) < new Date() && progress < 100;\n const health = hasOverdue ? 'at_risk' : progress >= 75 ? 'on_track' : 'in_progress';\n return (\n <div key={p.id} className=\"flex items-center gap-4\">\n <span className=\"text-sm font-medium w-40 truncate\">{p.name}</span>\n <div className=\"flex-1\">\n <div className=\"w-full h-2 rounded-full bg-muted overflow-hidden\">\n <div\n className={`h-full rounded-full ${health === 'at_risk' ? 'bg-orange-400' : health === 'on_track' ? 'bg-green-400' : 'bg-blue-400'}`}\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n <span className=\"text-sm w-12 text-right\">{progress}%</span>\n <Badge variant=\"outline\" className=\"capitalize text-xs w-20 justify-center\">\n {health.replace('_', ' ')}\n </Badge>\n </div>\n );\n })}\n {projects.filter((p) => p.status === 'ACTIVE').length === 0 && (\n <p className=\"text-sm text-muted-foreground text-center py-4\">\n {t('planmagnet.analytics.projectHealth.empty', 'No active projects found')}\n </p>\n )}\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompetitorsCompetitorDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/CompetitorsCompetitorDetailPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CompetitorsCompetitorDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/CompetitorsCompetitorDetailPagePage.tsx"],"names":[],"mappings":"AA4EA,wBAAgB,mCAAmC,4CAwLlD"}
|
|
@@ -17,6 +17,10 @@ var h = o`
|
|
|
17
17
|
website
|
|
18
18
|
description
|
|
19
19
|
tags
|
|
20
|
+
strengths
|
|
21
|
+
weaknesses
|
|
22
|
+
pricing
|
|
23
|
+
features
|
|
20
24
|
createdBy
|
|
21
25
|
createdAt
|
|
22
26
|
updatedAt
|
|
@@ -28,21 +32,41 @@ var h = o`
|
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
`;
|
|
31
|
-
function g() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
function g({ json: e }) {
|
|
36
|
+
if (!e || typeof e != "object" || Array.isArray(e)) return null;
|
|
37
|
+
let t = Object.entries(e);
|
|
38
|
+
return t.length === 0 ? null : /* @__PURE__ */ c("dl", {
|
|
39
|
+
className: "space-y-2 text-sm",
|
|
40
|
+
children: t.map(([e, t]) => {
|
|
41
|
+
let n = typeof t == "object" && !!t;
|
|
42
|
+
return /* @__PURE__ */ l("div", {
|
|
43
|
+
className: n ? "space-y-1" : "flex justify-between gap-3",
|
|
44
|
+
children: [/* @__PURE__ */ c("dt", {
|
|
45
|
+
className: "text-muted-foreground",
|
|
46
|
+
children: e
|
|
47
|
+
}), /* @__PURE__ */ c("dd", {
|
|
48
|
+
className: n ? "whitespace-pre-wrap break-words rounded-md bg-muted p-2 font-mono text-xs" : "font-medium text-right",
|
|
49
|
+
children: n ? JSON.stringify(t, null, 2) : typeof t == "boolean" ? t ? "Yes" : "No" : String(t)
|
|
50
|
+
})]
|
|
51
|
+
}, e);
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function _() {
|
|
56
|
+
let o = e(), _ = a().id ?? "", { data: v, loading: y, error: b } = s(h, {
|
|
57
|
+
variables: { id: _ },
|
|
58
|
+
skip: !_
|
|
35
59
|
});
|
|
36
|
-
if (
|
|
60
|
+
if (y && !v) return /* @__PURE__ */ c("div", {
|
|
37
61
|
className: "flex items-center justify-center p-12",
|
|
38
62
|
children: /* @__PURE__ */ c(r, { className: "h-8 w-8 animate-spin text-muted-foreground" })
|
|
39
63
|
});
|
|
40
|
-
if (
|
|
64
|
+
if (b) return /* @__PURE__ */ l("div", {
|
|
41
65
|
className: "p-6 text-destructive",
|
|
42
|
-
children: ["Failed to load competitor: ",
|
|
66
|
+
children: ["Failed to load competitor: ", b.message]
|
|
43
67
|
});
|
|
44
|
-
let
|
|
45
|
-
return
|
|
68
|
+
let x = v?.planmagnetCompetitor;
|
|
69
|
+
return x ? /* @__PURE__ */ l("div", {
|
|
46
70
|
className: "space-y-6 p-6",
|
|
47
71
|
children: [/* @__PURE__ */ l("div", {
|
|
48
72
|
className: "flex items-center justify-between",
|
|
@@ -50,70 +74,109 @@ function g() {
|
|
|
50
74
|
className: "flex items-center gap-2",
|
|
51
75
|
children: [/* @__PURE__ */ c(i, { className: "h-5 w-5 text-primary" }), /* @__PURE__ */ c("h1", {
|
|
52
76
|
className: "text-2xl font-semibold",
|
|
53
|
-
children:
|
|
77
|
+
children: x.name
|
|
54
78
|
})]
|
|
55
|
-
}),
|
|
79
|
+
}), x.website && /* @__PURE__ */ c("p", {
|
|
56
80
|
className: "text-sm text-primary mt-1",
|
|
57
|
-
children:
|
|
81
|
+
children: x.website
|
|
58
82
|
})] }), /* @__PURE__ */ c(n, {
|
|
59
|
-
competitorId:
|
|
60
|
-
name:
|
|
83
|
+
competitorId: x.id,
|
|
84
|
+
name: x.name
|
|
61
85
|
})]
|
|
62
86
|
}), /* @__PURE__ */ l("div", {
|
|
63
87
|
className: "grid grid-cols-1 lg:grid-cols-3 gap-6",
|
|
64
88
|
children: [/* @__PURE__ */ l("div", {
|
|
65
89
|
className: "lg:col-span-2 space-y-6",
|
|
66
|
-
children: [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
children:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
children: [
|
|
91
|
+
/* @__PURE__ */ l(d, { children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ c(m, { children: "Notes" }) }), /* @__PURE__ */ c(f, { children: /* @__PURE__ */ c("p", {
|
|
92
|
+
className: "text-sm",
|
|
93
|
+
children: x.description ?? "No notes yet."
|
|
94
|
+
}) })] }),
|
|
95
|
+
(x.strengths.length > 0 || x.weaknesses.length > 0) && /* @__PURE__ */ l(d, {
|
|
96
|
+
"data-testid": "competitor-strengths-weaknesses-card",
|
|
97
|
+
children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ c(m, { children: "Strengths & Weaknesses" }) }), /* @__PURE__ */ l(f, {
|
|
98
|
+
className: "grid grid-cols-1 sm:grid-cols-2 gap-4",
|
|
99
|
+
children: [/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
|
|
100
|
+
className: "text-xs font-medium text-muted-foreground mb-1",
|
|
101
|
+
children: "Strengths"
|
|
102
|
+
}), x.strengths.length === 0 ? /* @__PURE__ */ c("p", {
|
|
103
|
+
className: "text-sm text-muted-foreground",
|
|
104
|
+
children: "None noted."
|
|
105
|
+
}) : /* @__PURE__ */ c("ul", {
|
|
106
|
+
className: "space-y-1 text-sm list-disc pl-4",
|
|
107
|
+
children: x.strengths.map((e, t) => /* @__PURE__ */ c("li", { children: e }, t))
|
|
108
|
+
})] }), /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("p", {
|
|
109
|
+
className: "text-xs font-medium text-muted-foreground mb-1",
|
|
110
|
+
children: "Weaknesses"
|
|
111
|
+
}), x.weaknesses.length === 0 ? /* @__PURE__ */ c("p", {
|
|
112
|
+
className: "text-sm text-muted-foreground",
|
|
113
|
+
children: "None noted."
|
|
114
|
+
}) : /* @__PURE__ */ c("ul", {
|
|
115
|
+
className: "space-y-1 text-sm list-disc pl-4",
|
|
116
|
+
children: x.weaknesses.map((e, t) => /* @__PURE__ */ c("li", { children: e }, t))
|
|
117
|
+
})] })]
|
|
89
118
|
})]
|
|
90
|
-
},
|
|
91
|
-
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ l(d, { children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ l(m, { children: [
|
|
121
|
+
"Linked Feature Requests (",
|
|
122
|
+
x.featureRequests.length,
|
|
123
|
+
")"
|
|
124
|
+
] }) }), /* @__PURE__ */ l(f, {
|
|
125
|
+
className: "space-y-2",
|
|
126
|
+
children: [x.featureRequests.length === 0 && /* @__PURE__ */ c("p", {
|
|
127
|
+
className: "text-sm text-muted-foreground",
|
|
128
|
+
children: "Not linked to any feature request yet. Add it from a feature request’s detail page."
|
|
129
|
+
}), x.featureRequests.map((e) => /* @__PURE__ */ l("button", {
|
|
130
|
+
type: "button",
|
|
131
|
+
className: "flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted",
|
|
132
|
+
onClick: () => o(t("/products/feature-detail-page", e.id)),
|
|
133
|
+
children: [/* @__PURE__ */ c("span", {
|
|
134
|
+
className: "text-sm font-medium",
|
|
135
|
+
children: e.title
|
|
136
|
+
}), /* @__PURE__ */ c(u, {
|
|
137
|
+
variant: "outline",
|
|
138
|
+
className: "text-xs capitalize",
|
|
139
|
+
children: e.status.toLowerCase().replace("_", " ")
|
|
140
|
+
})]
|
|
141
|
+
}, e.id))]
|
|
142
|
+
})] })
|
|
143
|
+
]
|
|
92
144
|
}), /* @__PURE__ */ l("div", {
|
|
93
145
|
className: "space-y-6",
|
|
94
|
-
children: [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
146
|
+
children: [
|
|
147
|
+
/* @__PURE__ */ l(d, { children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ c(m, { children: "Details" }) }), /* @__PURE__ */ l(f, {
|
|
148
|
+
className: "space-y-2 text-sm",
|
|
149
|
+
children: [/* @__PURE__ */ l("div", {
|
|
150
|
+
className: "flex justify-between",
|
|
151
|
+
children: [/* @__PURE__ */ c("span", {
|
|
152
|
+
className: "text-muted-foreground",
|
|
153
|
+
children: "Created"
|
|
154
|
+
}), /* @__PURE__ */ c("span", { children: new Date(x.createdAt).toLocaleDateString() })]
|
|
155
|
+
}), /* @__PURE__ */ l("div", {
|
|
156
|
+
className: "flex justify-between",
|
|
157
|
+
children: [/* @__PURE__ */ c("span", {
|
|
158
|
+
className: "text-muted-foreground",
|
|
159
|
+
children: "Updated"
|
|
160
|
+
}), /* @__PURE__ */ c("span", { children: new Date(x.updatedAt).toLocaleDateString() })]
|
|
161
|
+
})]
|
|
162
|
+
})] }),
|
|
163
|
+
x.pricing != null && /* @__PURE__ */ l(d, {
|
|
164
|
+
"data-testid": "competitor-pricing-card",
|
|
165
|
+
children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ c(m, { children: "Pricing" }) }), /* @__PURE__ */ c(f, { children: /* @__PURE__ */ c(g, { json: x.pricing }) })]
|
|
166
|
+
}),
|
|
167
|
+
x.features != null && /* @__PURE__ */ l(d, {
|
|
168
|
+
"data-testid": "competitor-features-card",
|
|
169
|
+
children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ c(m, { children: "Feature Comparison" }) }), /* @__PURE__ */ c(f, { children: /* @__PURE__ */ c(g, { json: x.features }) })]
|
|
170
|
+
}),
|
|
171
|
+
x.tags.length > 0 && /* @__PURE__ */ l(d, { children: [/* @__PURE__ */ c(p, { children: /* @__PURE__ */ c(m, { children: "Tags" }) }), /* @__PURE__ */ c(f, {
|
|
172
|
+
className: "flex flex-wrap gap-1",
|
|
173
|
+
children: x.tags.map((e) => /* @__PURE__ */ c(u, {
|
|
174
|
+
variant: "outline",
|
|
175
|
+
className: "text-xs",
|
|
176
|
+
children: e
|
|
177
|
+
}, e))
|
|
178
|
+
})] })
|
|
179
|
+
]
|
|
117
180
|
})]
|
|
118
181
|
})]
|
|
119
182
|
}) : /* @__PURE__ */ c("div", {
|
|
@@ -122,6 +185,6 @@ function g() {
|
|
|
122
185
|
});
|
|
123
186
|
}
|
|
124
187
|
//#endregion
|
|
125
|
-
export {
|
|
188
|
+
export { _ as CompetitorsCompetitorDetailPagePage };
|
|
126
189
|
|
|
127
190
|
//# sourceMappingURL=CompetitorsCompetitorDetailPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompetitorsCompetitorDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/CompetitorsCompetitorDetailPagePage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { Swords, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { CompetitorHeaderActions } from '../controls/CompetitorControls';\n\nconst GET_COMPETITOR = gql`\n query GetCompetitor($id: ID!) {\n planmagnetCompetitor(id: $id) {\n id\n workspaceId\n name\n website\n description\n tags\n createdBy\n createdAt\n updatedAt\n featureRequests {\n id\n title\n status\n }\n }\n }\n`;\n\ninterface FeatureRequestNode {\n id: string;\n title: string;\n status: string;\n}\n\nexport function CompetitorsCompetitorDetailPagePage() {\n const navigate = usePlanMagnetNavigate();\n const params = useParams<{ id: string }>();\n const competitorId = params.id ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetCompetitor: {\n id: string;\n name: string;\n website: string | null;\n description: string | null;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n featureRequests: FeatureRequestNode[];\n } | null;\n }>(GET_COMPETITOR, { variables: { id: competitorId }, skip: !competitorId });\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it and\n // loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load competitor: {error.message}</div>;\n }\n\n const competitor = data?.planmagnetCompetitor;\n if (!competitor) return <div className=\"p-6 text-muted-foreground\">Competitor not found.</div>;\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 <Swords className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{competitor.name}</h1>\n </div>\n {competitor.website && <p className=\"text-sm text-primary mt-1\">{competitor.website}</p>}\n </div>\n <CompetitorHeaderActions competitorId={competitor.id} name={competitor.name} />\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>Notes</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{competitor.description ?? 'No notes yet.'}</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Linked Feature Requests ({competitor.featureRequests.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {competitor.featureRequests.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n Not linked to any feature request yet. Add it from a feature request’s\n detail page.\n </p>\n )}\n {competitor.featureRequests.map((fr) => (\n <button\n key={fr.id}\n type=\"button\"\n className=\"flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted\"\n onClick={() => navigate(detailPath('/products/feature-detail-page', fr.id))}\n >\n <span className=\"text-sm font-medium\">{fr.title}</span>\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {fr.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </button>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\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\">Created</span>\n <span>{new Date(competitor.createdAt).toLocaleDateString()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{new Date(competitor.updatedAt).toLocaleDateString()}</span>\n </div>\n </CardContent>\n </Card>\n\n {competitor.tags.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Tags</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-wrap gap-1\">\n {competitor.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </CardContent>\n </Card>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CompetitorsCompetitorDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/CompetitorsCompetitorDetailPagePage.tsx"],"sourcesContent":["import { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle, Badge } from '@burdenoff/fe-libs/ui';\nimport { Swords, Loader2 } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { detailPath } from '../utils/rowNavigation';\nimport { CompetitorHeaderActions } from '../controls/CompetitorControls';\n\nconst GET_COMPETITOR = gql`\n query GetCompetitor($id: ID!) {\n planmagnetCompetitor(id: $id) {\n id\n workspaceId\n name\n website\n description\n tags\n strengths\n weaknesses\n pricing\n features\n createdBy\n createdAt\n updatedAt\n featureRequests {\n id\n title\n status\n }\n }\n }\n`;\n\n/** `pricing`/`features` are free-form JSON (PRM-31) — rendered as the same flat key: value rows the edit form\n * writes, falling back to formatted JSON text (never `String()`, which would print `[object Object]`) for a\n * nested object/array shape the editor itself cannot produce. */\nfunction JsonFieldList({ json }: { json: unknown }) {\n if (!json || typeof json !== 'object' || Array.isArray(json)) return null;\n const entries = Object.entries(json as Record<string, unknown>);\n if (entries.length === 0) return null;\n return (\n <dl className=\"space-y-2 text-sm\">\n {entries.map(([key, value]) => {\n const nested = value !== null && typeof value === 'object';\n return (\n <div key={key} className={nested ? 'space-y-1' : 'flex justify-between gap-3'}>\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd\n className={\n nested\n ? 'whitespace-pre-wrap break-words rounded-md bg-muted p-2 font-mono text-xs'\n : 'font-medium text-right'\n }\n >\n {nested\n ? JSON.stringify(value, null, 2)\n : typeof value === 'boolean'\n ? value\n ? 'Yes'\n : 'No'\n : String(value)}\n </dd>\n </div>\n );\n })}\n </dl>\n );\n}\n\ninterface FeatureRequestNode {\n id: string;\n title: string;\n status: string;\n}\n\nexport function CompetitorsCompetitorDetailPagePage() {\n const navigate = usePlanMagnetNavigate();\n const params = useParams<{ id: string }>();\n const competitorId = params.id ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetCompetitor: {\n id: string;\n name: string;\n website: string | null;\n description: string | null;\n tags: string[];\n strengths: string[];\n weaknesses: string[];\n pricing: unknown;\n features: unknown;\n createdAt: string;\n updatedAt: string;\n featureRequests: FeatureRequestNode[];\n } | null;\n }>(GET_COMPETITOR, { variables: { id: competitorId }, skip: !competitorId });\n\n // Only before the first load: a refetch must not blank the page (it unmounts every component on it and\n // loses their state).\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-destructive\">Failed to load competitor: {error.message}</div>;\n }\n\n const competitor = data?.planmagnetCompetitor;\n if (!competitor) return <div className=\"p-6 text-muted-foreground\">Competitor not found.</div>;\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 <Swords className=\"h-5 w-5 text-primary\" />\n <h1 className=\"text-2xl font-semibold\">{competitor.name}</h1>\n </div>\n {competitor.website && <p className=\"text-sm text-primary mt-1\">{competitor.website}</p>}\n </div>\n <CompetitorHeaderActions competitorId={competitor.id} name={competitor.name} />\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>Notes</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{competitor.description ?? 'No notes yet.'}</p>\n </CardContent>\n </Card>\n\n {/* PRM-31 */}\n {(competitor.strengths.length > 0 || competitor.weaknesses.length > 0) && (\n <Card data-testid=\"competitor-strengths-weaknesses-card\">\n <CardHeader>\n <CardTitle>Strengths & Weaknesses</CardTitle>\n </CardHeader>\n <CardContent className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <p className=\"text-xs font-medium text-muted-foreground mb-1\">Strengths</p>\n {competitor.strengths.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">None noted.</p>\n ) : (\n <ul className=\"space-y-1 text-sm list-disc pl-4\">\n {competitor.strengths.map((s, i) => (\n <li key={i}>{s}</li>\n ))}\n </ul>\n )}\n </div>\n <div>\n <p className=\"text-xs font-medium text-muted-foreground mb-1\">Weaknesses</p>\n {competitor.weaknesses.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">None noted.</p>\n ) : (\n <ul className=\"space-y-1 text-sm list-disc pl-4\">\n {competitor.weaknesses.map((w, i) => (\n <li key={i}>{w}</li>\n ))}\n </ul>\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Linked Feature Requests ({competitor.featureRequests.length})</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-2\">\n {competitor.featureRequests.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n Not linked to any feature request yet. Add it from a feature request’s\n detail page.\n </p>\n )}\n {competitor.featureRequests.map((fr) => (\n <button\n key={fr.id}\n type=\"button\"\n className=\"flex w-full items-center justify-between rounded-md border p-3 text-left hover:bg-muted\"\n onClick={() => navigate(detailPath('/products/feature-detail-page', fr.id))}\n >\n <span className=\"text-sm font-medium\">{fr.title}</span>\n <Badge variant=\"outline\" className=\"text-xs capitalize\">\n {fr.status.toLowerCase().replace('_', ' ')}\n </Badge>\n </button>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"space-y-6\">\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\">Created</span>\n <span>{new Date(competitor.createdAt).toLocaleDateString()}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{new Date(competitor.updatedAt).toLocaleDateString()}</span>\n </div>\n </CardContent>\n </Card>\n\n {/* PRM-31 */}\n {competitor.pricing != null && (\n <Card data-testid=\"competitor-pricing-card\">\n <CardHeader>\n <CardTitle>Pricing</CardTitle>\n </CardHeader>\n <CardContent>\n <JsonFieldList json={competitor.pricing} />\n </CardContent>\n </Card>\n )}\n\n {competitor.features != null && (\n <Card data-testid=\"competitor-features-card\">\n <CardHeader>\n <CardTitle>Feature Comparison</CardTitle>\n </CardHeader>\n <CardContent>\n <JsonFieldList json={competitor.features} />\n </CardContent>\n </Card>\n )}\n\n {competitor.tags.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Tags</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-wrap gap-1\">\n {competitor.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </CardContent>\n </Card>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompetitorsCompetitorsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/CompetitorsCompetitorsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CompetitorsCompetitorsPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/CompetitorsCompetitorsPagePage.tsx"],"names":[],"mappings":"AAoDA,wBAAgB,8BAA8B,4CAgH7C"}
|