@burdenoff/microfe-planmagnet 2026.924.2 → 2026.924.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
- package/dist/planmagnet/PlanMagnetRoot.js +93 -142
- package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.d.ts.map +1 -1
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js +150 -149
- package/dist/planmagnet/admin/PlanMagnetAdminRoot.js.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +180 -172
- package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js +113 -116
- package/dist/planmagnet/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsGoalsPagePage.js +59 -58
- package/dist/planmagnet/pages/GoalsGoalsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js +150 -149
- package/dist/planmagnet/pages/GoalsObjectiveDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js +108 -107
- package/dist/planmagnet/pages/ProductsEditProductPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +93 -92
- package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsProductsPagePage.js +89 -87
- package/dist/planmagnet/pages/ProductsProductsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js +94 -93
- package/dist/planmagnet/pages/ProductsReleaseDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js +57 -56
- package/dist/planmagnet/pages/ProductsReleasesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +82 -81
- package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js +72 -71
- package/dist/planmagnet/pages/ProductsRoadmapsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js +38 -37
- package/dist/planmagnet/pages/ProjectsActiveProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js +72 -71
- package/dist/planmagnet/pages/ProjectsMilestoneDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js +47 -46
- package/dist/planmagnet/pages/ProjectsMilestonesPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js +120 -119
- package/dist/planmagnet/pages/ProjectsPlanningDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +131 -130
- package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js +77 -75
- package/dist/planmagnet/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/ReportsReportsPagePage.js +71 -70
- package/dist/planmagnet/pages/ReportsReportsPagePage.js.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/TeamTeamPagePage.js +58 -57
- package/dist/planmagnet/pages/TeamTeamPagePage.js.map +1 -1
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.d.ts.map +1 -1
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.js +81 -72
- package/dist/planmagnet/pages/WorkspaceCreateWorkspacePagePage.js.map +1 -1
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts +15 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.d.ts.map +1 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js +302 -0
- package/dist/planmagnet/pages/analytics/DeliveryHealthCards.js.map +1 -0
- package/dist/planmagnet/people/People.d.ts +20 -0
- package/dist/planmagnet/people/People.d.ts.map +1 -0
- package/dist/planmagnet/people/People.js +42 -0
- package/dist/planmagnet/people/People.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 { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\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 ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\n\nconst GET_ANALYTICS_DATA = gql`\n query GetAnalyticsData(\n $workspaceId: ID!\n $projectPagination: PlanmagnetPaginationInput\n $workItemPagination: PlanmagnetPaginationInput\n $sprintPagination: PlanmagnetPaginationInput\n $milestonePagination: PlanmagnetPaginationInput\n ) {\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 planmagnetWorkItems(workspaceId: $workspaceId, pagination: $workItemPagination) {\n items {\n id\n status\n priority\n storyPoints\n assigneeId\n }\n totalCount\n }\n planmagnetSprints(workspaceId: $workspaceId, pagination: $sprintPagination) {\n items {\n id\n name\n status\n velocity\n capacity\n projectId\n startDate\n endDate\n }\n totalCount\n }\n planmagnetMilestones(workspaceId: $workspaceId, pagination: $milestonePagination) {\n items {\n id\n name\n status\n dueDate\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 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($workspaceId: ID!, $projectId: ID!) {\n planmagnetCumulativeFlow(workspaceId: $workspaceId, projectId: $projectId) {\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 WorkItemNode {\n id: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n}\n\ninterface SprintNode {\n id: string;\n name: string;\n status: string;\n velocity: number | null;\n capacity: number | null;\n projectId: string;\n startDate: string | null;\n endDate: string | null;\n}\n\ninterface MilestoneNode {\n id: string;\n name: string;\n status: string;\n dueDate: string | null;\n}\n\ninterface AnalyticsData {\n planmagnetProjects: { items: ProjectNode[]; totalCount: number };\n planmagnetWorkItems: { items: WorkItemNode[]; totalCount: number };\n planmagnetSprints: { items: SprintNode[]; totalCount: number };\n planmagnetMilestones: { items: MilestoneNode[]; 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 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 [selectedProjectId, setSelectedProjectId] = useState<string>('');\n const [selectedSprintId, setSelectedSprintId] = useState<string>('');\n\n const { data, loading, error } = useQuery<AnalyticsData>(GET_ANALYTICS_DATA, {\n variables: {\n workspaceId,\n projectPagination: { first: 100 },\n workItemPagination: { first: 500 },\n sprintPagination: { first: 100 },\n milestonePagination: { first: 100 },\n },\n skip: !workspaceId,\n });\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const tasks = data?.planmagnetWorkItems?.items ?? [];\n const sprints = data?.planmagnetSprints?.items ?? [];\n const milestones = data?.planmagnetMilestones?.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 const effectiveProjectId = selectedProjectId || projects[0]?.id || '';\n const sprintsForProject = sprints.filter((s) => s.projectId === effectiveProjectId);\n const effectiveSprintId = selectedSprintId || sprintsForProject[0]?.id || '';\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 variables: { workspaceId, projectId: effectiveProjectId },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n if (loading) {\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 const totalTasks = tasks.length;\n const doneTasks = tasks.filter((t) => t.status === 'DONE').length;\n const blockedTasks = tasks.filter((t) => t.status === 'BLOCKED').length;\n const totalPoints = tasks.reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n const completedPoints = tasks\n .filter((t) => t.status === 'DONE')\n .reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n\n const activeSprints = sprints.filter((s) => s.status === 'ACTIVE').length;\n const atRiskMs = milestones.filter(\n (m) => m.status === 'AT_RISK' || m.status === 'OVERDUE'\n ).length;\n const activeProjects = projects.filter((p) => p.status === 'ACTIVE').length;\n const completionRate = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;\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\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: String(totalTasks),\n trend: `${completionRate}% ${t('planmagnet.analytics.metrics.totalWorkItems.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.storyPoints.label', 'Story Points'),\n value: String(totalPoints),\n trend: `${completedPoints} ${t('planmagnet.analytics.metrics.storyPoints.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.activeProjects.label', 'Active Projects'),\n value: String(activeProjects),\n trend: `${projects.length} ${t('planmagnet.analytics.metrics.activeProjects.trend', 'total')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.blockedItems.label', 'Blocked Items'),\n value: String(blockedTasks),\n trend:\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 <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\">{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\">{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 <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 {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 <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.delivery.title', 'Sprint Delivery Analytics')}\n </h2>\n <div className=\"flex gap-2 sm:ml-auto\">\n <Select\n value={effectiveProjectId}\n onValueChange={(v) => {\n setSelectedProjectId(v);\n setSelectedSprintId('');\n }}\n >\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t(\n 'planmagnet.analytics.delivery.projectPlaceholder',\n 'Select project'\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {projects.map((p) => (\n <SelectItem key={p.id} value={p.id}>\n {p.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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 <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 <p className=\"text-xs text-muted-foreground text-center mt-2\">\n {t(\n 'planmagnet.analytics.velocity.forecast',\n 'Forecast for next sprint: {{points}} pts (trailing average)',\n { points: forecastNextSprint }\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 <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 </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 <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 const projectTasks = tasks.filter((t) => t.assigneeId != null);\n const projectDone = projectTasks.filter((t) => t.status === 'DONE').length;\n const progress =\n projectTasks.length > 0\n ? Math.round((projectDone / projectTasks.length) * 100)\n : 0;\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 { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\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 ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { BudgetPerformanceCard, CycleTimeCard } from './analytics/DeliveryHealthCards';\n\nconst GET_ANALYTICS_DATA = gql`\n query GetAnalyticsData(\n $workspaceId: ID!\n $projectPagination: PlanmagnetPaginationInput\n $workItemPagination: PlanmagnetPaginationInput\n $sprintPagination: PlanmagnetPaginationInput\n $milestonePagination: PlanmagnetPaginationInput\n ) {\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 planmagnetWorkItems(workspaceId: $workspaceId, pagination: $workItemPagination) {\n items {\n id\n status\n priority\n storyPoints\n assigneeId\n }\n totalCount\n }\n planmagnetSprints(workspaceId: $workspaceId, pagination: $sprintPagination) {\n items {\n id\n name\n status\n velocity\n capacity\n projectId\n startDate\n endDate\n }\n totalCount\n }\n planmagnetMilestones(workspaceId: $workspaceId, pagination: $milestonePagination) {\n items {\n id\n name\n status\n dueDate\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 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($workspaceId: ID!, $projectId: ID!) {\n planmagnetCumulativeFlow(workspaceId: $workspaceId, projectId: $projectId) {\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 WorkItemNode {\n id: string;\n status: string;\n priority: string;\n storyPoints: number | null;\n assigneeId: string | null;\n}\n\ninterface SprintNode {\n id: string;\n name: string;\n status: string;\n velocity: number | null;\n capacity: number | null;\n projectId: string;\n startDate: string | null;\n endDate: string | null;\n}\n\ninterface MilestoneNode {\n id: string;\n name: string;\n status: string;\n dueDate: string | null;\n}\n\ninterface AnalyticsData {\n planmagnetProjects: { items: ProjectNode[]; totalCount: number };\n planmagnetWorkItems: { items: WorkItemNode[]; totalCount: number };\n planmagnetSprints: { items: SprintNode[]; totalCount: number };\n planmagnetMilestones: { items: MilestoneNode[]; 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 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 [selectedProjectId, setSelectedProjectId] = useState<string>('');\n const [selectedSprintId, setSelectedSprintId] = useState<string>('');\n\n const { data, loading, error } = useQuery<AnalyticsData>(GET_ANALYTICS_DATA, {\n variables: {\n workspaceId,\n projectPagination: { first: 100 },\n workItemPagination: { first: 500 },\n sprintPagination: { first: 100 },\n milestonePagination: { first: 100 },\n },\n skip: !workspaceId,\n });\n\n const projects = data?.planmagnetProjects?.items ?? [];\n const tasks = data?.planmagnetWorkItems?.items ?? [];\n const sprints = data?.planmagnetSprints?.items ?? [];\n const milestones = data?.planmagnetMilestones?.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 const effectiveProjectId = selectedProjectId || projects[0]?.id || '';\n const sprintsForProject = sprints.filter((s) => s.projectId === effectiveProjectId);\n const effectiveSprintId = selectedSprintId || sprintsForProject[0]?.id || '';\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 variables: { workspaceId, projectId: effectiveProjectId },\n skip: !workspaceId || !effectiveProjectId,\n });\n\n if (loading) {\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 const totalTasks = tasks.length;\n const doneTasks = tasks.filter((t) => t.status === 'DONE').length;\n const blockedTasks = tasks.filter((t) => t.status === 'BLOCKED').length;\n const totalPoints = tasks.reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n const completedPoints = tasks\n .filter((t) => t.status === 'DONE')\n .reduce((s, t) => s + (t.storyPoints ?? 0), 0);\n\n const activeSprints = sprints.filter((s) => s.status === 'ACTIVE').length;\n const atRiskMs = milestones.filter(\n (m) => m.status === 'AT_RISK' || m.status === 'OVERDUE'\n ).length;\n const activeProjects = projects.filter((p) => p.status === 'ACTIVE').length;\n const completionRate = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;\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\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: String(totalTasks),\n trend: `${completionRate}% ${t('planmagnet.analytics.metrics.totalWorkItems.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.storyPoints.label', 'Story Points'),\n value: String(totalPoints),\n trend: `${completedPoints} ${t('planmagnet.analytics.metrics.storyPoints.trend', 'completed')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.activeProjects.label', 'Active Projects'),\n value: String(activeProjects),\n trend: `${projects.length} ${t('planmagnet.analytics.metrics.activeProjects.trend', 'total')}`,\n },\n {\n label: t('planmagnet.analytics.metrics.blockedItems.label', 'Blocked Items'),\n value: String(blockedTasks),\n trend:\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 <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\">{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\">{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 <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 {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 <h2 className=\"text-lg font-semibold\">\n {t('planmagnet.analytics.delivery.title', 'Sprint Delivery Analytics')}\n </h2>\n <div className=\"flex gap-2 sm:ml-auto\">\n <Select\n value={effectiveProjectId}\n onValueChange={(v) => {\n setSelectedProjectId(v);\n setSelectedSprintId('');\n }}\n >\n <SelectTrigger className=\"h-8 w-48\">\n <SelectValue\n placeholder={t(\n 'planmagnet.analytics.delivery.projectPlaceholder',\n 'Select project'\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {projects.map((p) => (\n <SelectItem key={p.id} value={p.id}>\n {p.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\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 <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 <p className=\"text-xs text-muted-foreground text-center mt-2\">\n {t(\n 'planmagnet.analytics.velocity.forecast',\n 'Forecast for next sprint: {{points}} pts (trailing average)',\n { points: forecastNextSprint }\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 <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 </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 {/* Cycle time follows the delivery selector above; budgets are workspace-wide. */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n <CycleTimeCard workspaceId={workspaceId} projectId={effectiveProjectId} />\n <BudgetPerformanceCard workspaceId={workspaceId} />\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 const projectTasks = tasks.filter((t) => t.assigneeId != null);\n const projectDone = projectTasks.filter((t) => t.status === 'DONE').length;\n const progress =\n projectTasks.length > 0\n ? Math.round((projectDone / projectTasks.length) * 100)\n : 0;\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":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"AAwFA,wBAAgB,0BAA0B,4CAyVzC"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { usePlanMagnet as e } from "../providers/PlanMagnetProvider.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { PersonName as t } from "../people/People.js";
|
|
3
|
+
import { AlertTriangle as n, ArrowUpRight as r, Calendar as i, CheckSquare as a, FolderKanban as o, LayoutDashboard as s, TrendingUp as c, Users as l, Zap as u } from "lucide-react";
|
|
4
|
+
import { gql as d } from "@apollo/client";
|
|
5
|
+
import { useQuery as f } from "@apollo/client/react";
|
|
6
|
+
import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
7
|
+
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
8
|
+
import { Badge as _, Card as v, CardContent as y, CardHeader as b, CardTitle as x } from "@burdenoff/fe-libs/ui";
|
|
8
9
|
//#region src/planmagnet/pages/DashboardDashboardPagePage.tsx
|
|
9
|
-
var
|
|
10
|
+
var S = d`
|
|
10
11
|
query GetDashboardData($workspaceId: ID!) {
|
|
11
12
|
planmagnetProjects(workspaceId: $workspaceId, pagination: { first: 50 }) {
|
|
12
13
|
items {
|
|
@@ -73,126 +74,126 @@ var b = u`
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
`;
|
|
76
|
-
function
|
|
77
|
-
let { workspaceId:
|
|
78
|
-
if (
|
|
77
|
+
function C() {
|
|
78
|
+
let { workspaceId: d } = e(), { t: C } = g(), { data: w, loading: T, error: E } = f(S, { variables: { workspaceId: d ?? "default" } });
|
|
79
|
+
if (T) return /* @__PURE__ */ m("div", {
|
|
79
80
|
className: "flex items-center justify-center p-12",
|
|
80
|
-
children: /* @__PURE__ */
|
|
81
|
+
children: /* @__PURE__ */ m("div", { className: "animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" })
|
|
81
82
|
});
|
|
82
|
-
if (
|
|
83
|
+
if (E) return /* @__PURE__ */ h("div", {
|
|
83
84
|
className: "p-6 text-red-500",
|
|
84
85
|
children: [
|
|
85
|
-
|
|
86
|
+
C("planmagnet.dashboard.errorLoading", "Error loading dashboard"),
|
|
86
87
|
": ",
|
|
87
|
-
|
|
88
|
+
E.message
|
|
88
89
|
]
|
|
89
90
|
});
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
let
|
|
91
|
+
let D = w?.planmagnetProjects?.items ?? [], O = w?.planmagnetSprints?.items ?? [], k = w?.planmagnetMilestones?.items ?? [], A = w?.planmagnetWorkItems?.items ?? [], j = D.filter((e) => e.status === "ACTIVE");
|
|
92
|
+
D.filter((e) => e.status === "COMPLETED");
|
|
93
|
+
let M = O.filter((e) => e.status === "ACTIVE"), N = k.filter((e) => e.status === "PENDING" || e.status === "IN_PROGRESS").slice(0, 4), P = A.slice(0, 5), F = new Map(D.map((e) => [e.id, e.name])), I = [
|
|
93
94
|
{
|
|
94
|
-
label:
|
|
95
|
-
value: String(
|
|
96
|
-
trend: `${
|
|
95
|
+
label: C("planmagnet.dashboard.metrics.totalProjects.label", "Total Projects"),
|
|
96
|
+
value: String(D.length),
|
|
97
|
+
trend: `${j.length} ${C("planmagnet.dashboard.metrics.totalProjects.trend", "active")}`
|
|
97
98
|
},
|
|
98
99
|
{
|
|
99
|
-
label:
|
|
100
|
-
value: String(
|
|
101
|
-
trend: `${
|
|
100
|
+
label: C("planmagnet.dashboard.metrics.activeSprints.label", "Active Sprints"),
|
|
101
|
+
value: String(M.length),
|
|
102
|
+
trend: `${O.length} ${C("planmagnet.dashboard.metrics.activeSprints.trend", "total")}`
|
|
102
103
|
},
|
|
103
104
|
{
|
|
104
|
-
label:
|
|
105
|
-
value: String(
|
|
106
|
-
trend:
|
|
105
|
+
label: C("planmagnet.dashboard.metrics.workItems.label", "Work Items"),
|
|
106
|
+
value: String(w?.planmagnetWorkItems?.totalCount ?? 0),
|
|
107
|
+
trend: C("planmagnet.dashboard.metrics.workItems.trend", "across all projects")
|
|
107
108
|
},
|
|
108
109
|
{
|
|
109
|
-
label:
|
|
110
|
-
value: String(
|
|
111
|
-
trend: `${
|
|
110
|
+
label: C("planmagnet.dashboard.metrics.milestones.label", "Milestones"),
|
|
111
|
+
value: String(k.length),
|
|
112
|
+
trend: `${N.length} ${C("planmagnet.dashboard.metrics.milestones.trend", "upcoming")}`
|
|
112
113
|
}
|
|
113
114
|
];
|
|
114
|
-
return /* @__PURE__ */
|
|
115
|
+
return /* @__PURE__ */ h("div", {
|
|
115
116
|
className: "space-y-6 p-6",
|
|
116
117
|
children: [
|
|
117
|
-
/* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ m("div", {
|
|
118
119
|
className: "flex items-center justify-between",
|
|
119
|
-
children: /* @__PURE__ */
|
|
120
|
+
children: /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ h("h1", {
|
|
120
121
|
className: "text-2xl font-semibold flex items-center gap-2",
|
|
121
|
-
children: [/* @__PURE__ */
|
|
122
|
-
}), /* @__PURE__ */
|
|
122
|
+
children: [/* @__PURE__ */ m(s, { className: "h-6 w-6" }), C("planmagnet.dashboard.title", "Dashboard")]
|
|
123
|
+
}), /* @__PURE__ */ m("p", {
|
|
123
124
|
className: "text-sm text-muted-foreground mt-1",
|
|
124
|
-
children:
|
|
125
|
+
children: C("planmagnet.dashboard.subtitle", "Welcome back! Here is your project overview.")
|
|
125
126
|
})] })
|
|
126
127
|
}),
|
|
127
|
-
/* @__PURE__ */
|
|
128
|
+
/* @__PURE__ */ m("div", {
|
|
128
129
|
className: "grid gap-4 md:grid-cols-2 lg:grid-cols-4",
|
|
129
|
-
children:
|
|
130
|
+
children: I.map((e) => /* @__PURE__ */ h(v, { children: [/* @__PURE__ */ h(b, {
|
|
130
131
|
className: "flex flex-row items-center justify-between space-y-0 pb-2",
|
|
131
|
-
children: [/* @__PURE__ */
|
|
132
|
+
children: [/* @__PURE__ */ m(x, {
|
|
132
133
|
className: "text-sm font-medium",
|
|
133
134
|
children: e.label
|
|
134
|
-
}), /* @__PURE__ */
|
|
135
|
-
}), /* @__PURE__ */
|
|
135
|
+
}), /* @__PURE__ */ m(c, { className: "h-4 w-4 text-muted-foreground" })]
|
|
136
|
+
}), /* @__PURE__ */ h(y, { children: [/* @__PURE__ */ m("div", {
|
|
136
137
|
className: "text-2xl font-bold",
|
|
137
138
|
children: e.value
|
|
138
|
-
}), /* @__PURE__ */
|
|
139
|
+
}), /* @__PURE__ */ m("p", {
|
|
139
140
|
className: "text-xs text-muted-foreground",
|
|
140
141
|
children: e.trend
|
|
141
142
|
})] })] }, e.label))
|
|
142
143
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ h("div", {
|
|
144
145
|
className: "grid gap-6 lg:grid-cols-12",
|
|
145
|
-
children: [/* @__PURE__ */
|
|
146
|
+
children: [/* @__PURE__ */ h(v, {
|
|
146
147
|
className: "lg:col-span-8",
|
|
147
|
-
children: [/* @__PURE__ */
|
|
148
|
+
children: [/* @__PURE__ */ m(b, { children: /* @__PURE__ */ h(x, {
|
|
148
149
|
className: "flex items-center gap-2 text-base",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
150
|
-
}) }), /* @__PURE__ */
|
|
150
|
+
children: [/* @__PURE__ */ m(u, { className: "h-5 w-5 text-blue-500" }), C("planmagnet.dashboard.activeSprints.title", "Active Sprints")]
|
|
151
|
+
}) }), /* @__PURE__ */ h(y, {
|
|
151
152
|
className: "space-y-4",
|
|
152
|
-
children: [
|
|
153
|
+
children: [M.map((e) => {
|
|
153
154
|
let t = e.workItems.reduce((e, t) => e + (t.storyPoints ?? 0), 0), n = e.workItems.filter((e) => e.status === "DONE").reduce((e, t) => e + (t.storyPoints ?? 0), 0), r = e.workItems.length, i = e.workItems.filter((e) => e.status === "DONE").length, a = t > 0 ? Math.round(n / t * 100) : 0;
|
|
154
|
-
return /* @__PURE__ */
|
|
155
|
+
return /* @__PURE__ */ h("div", {
|
|
155
156
|
className: "rounded-lg border p-4",
|
|
156
157
|
children: [
|
|
157
|
-
/* @__PURE__ */
|
|
158
|
+
/* @__PURE__ */ h("div", {
|
|
158
159
|
className: "flex items-center justify-between mb-2",
|
|
159
|
-
children: [/* @__PURE__ */
|
|
160
|
+
children: [/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("p", {
|
|
160
161
|
className: "font-medium",
|
|
161
162
|
children: e.name
|
|
162
|
-
}), /* @__PURE__ */
|
|
163
|
+
}), /* @__PURE__ */ h("p", {
|
|
163
164
|
className: "text-xs text-muted-foreground",
|
|
164
165
|
children: [
|
|
165
|
-
|
|
166
|
+
C("planmagnet.dashboard.shared.project", "Project"),
|
|
166
167
|
": ",
|
|
167
168
|
e.projectId
|
|
168
169
|
]
|
|
169
|
-
})] }), /* @__PURE__ */
|
|
170
|
+
})] }), /* @__PURE__ */ h(_, {
|
|
170
171
|
variant: "secondary",
|
|
171
172
|
children: [
|
|
172
173
|
n,
|
|
173
174
|
"/",
|
|
174
175
|
t,
|
|
175
176
|
" ",
|
|
176
|
-
|
|
177
|
+
C("planmagnet.dashboard.shared.pointsShort", "pts")
|
|
177
178
|
]
|
|
178
179
|
})]
|
|
179
180
|
}),
|
|
180
|
-
/* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ m("div", {
|
|
181
182
|
className: "w-full h-2 bg-muted rounded-full overflow-hidden",
|
|
182
|
-
children: /* @__PURE__ */
|
|
183
|
+
children: /* @__PURE__ */ m("div", {
|
|
183
184
|
className: "h-full bg-primary rounded-full transition-all",
|
|
184
185
|
style: { width: `${a}%` }
|
|
185
186
|
})
|
|
186
187
|
}),
|
|
187
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ h("div", {
|
|
188
189
|
className: "flex items-center justify-between mt-2 text-xs text-muted-foreground",
|
|
189
|
-
children: [/* @__PURE__ */
|
|
190
|
+
children: [/* @__PURE__ */ h("span", { children: [
|
|
190
191
|
i,
|
|
191
192
|
"/",
|
|
192
193
|
r,
|
|
193
194
|
" ",
|
|
194
|
-
|
|
195
|
-
] }), /* @__PURE__ */
|
|
195
|
+
C("planmagnet.dashboard.shared.tasks", "tasks")
|
|
196
|
+
] }), /* @__PURE__ */ h("span", { children: [
|
|
196
197
|
e.startDate ? new Date(e.startDate).toLocaleDateString() : "—",
|
|
197
198
|
" -",
|
|
198
199
|
" ",
|
|
@@ -201,143 +202,139 @@ function x() {
|
|
|
201
202
|
})
|
|
202
203
|
]
|
|
203
204
|
}, e.id);
|
|
204
|
-
}),
|
|
205
|
+
}), M.length === 0 && /* @__PURE__ */ m("p", {
|
|
205
206
|
className: "text-sm text-muted-foreground text-center py-4",
|
|
206
|
-
children:
|
|
207
|
+
children: C("planmagnet.dashboard.activeSprints.empty", "No active sprints.")
|
|
207
208
|
})]
|
|
208
209
|
})]
|
|
209
|
-
}), /* @__PURE__ */
|
|
210
|
+
}), /* @__PURE__ */ h("div", {
|
|
210
211
|
className: "lg:col-span-4 space-y-6",
|
|
211
|
-
children: [
|
|
212
|
+
children: [D.filter((e) => e.status === "ON_HOLD").length > 0 && /* @__PURE__ */ h(v, { children: [/* @__PURE__ */ m(b, { children: /* @__PURE__ */ h(x, {
|
|
212
213
|
className: "flex items-center gap-2 text-base",
|
|
213
|
-
children: [/* @__PURE__ */
|
|
214
|
-
}) }), /* @__PURE__ */
|
|
214
|
+
children: [/* @__PURE__ */ m(n, { className: "h-5 w-5 text-orange-500" }), C("planmagnet.dashboard.onHoldProjects.title", "On-Hold Projects")]
|
|
215
|
+
}) }), /* @__PURE__ */ m(y, {
|
|
215
216
|
className: "space-y-3",
|
|
216
|
-
children:
|
|
217
|
+
children: D.filter((e) => e.status === "ON_HOLD").map((e) => /* @__PURE__ */ h("div", {
|
|
217
218
|
className: "flex items-center justify-between rounded border p-2",
|
|
218
|
-
children: [/* @__PURE__ */
|
|
219
|
+
children: [/* @__PURE__ */ m("div", { children: /* @__PURE__ */ m("p", {
|
|
219
220
|
className: "text-sm font-medium",
|
|
220
221
|
children: e.name
|
|
221
|
-
}) }), /* @__PURE__ */
|
|
222
|
+
}) }), /* @__PURE__ */ m(_, {
|
|
222
223
|
variant: "destructive",
|
|
223
224
|
children: e.status.toLowerCase().replace("_", " ")
|
|
224
225
|
})]
|
|
225
226
|
}, e.id))
|
|
226
|
-
})] }), /* @__PURE__ */
|
|
227
|
+
})] }), /* @__PURE__ */ h(v, { children: [/* @__PURE__ */ m(b, { children: /* @__PURE__ */ h(x, {
|
|
227
228
|
className: "flex items-center gap-2 text-base",
|
|
228
|
-
children: [/* @__PURE__ */
|
|
229
|
-
}) }), /* @__PURE__ */
|
|
229
|
+
children: [/* @__PURE__ */ m(i, { className: "h-5 w-5 text-green-500" }), C("planmagnet.dashboard.upcomingMilestones.title", "Upcoming Milestones")]
|
|
230
|
+
}) }), /* @__PURE__ */ h(y, {
|
|
230
231
|
className: "space-y-3",
|
|
231
|
-
children: [
|
|
232
|
+
children: [N.map((e) => /* @__PURE__ */ h("div", {
|
|
232
233
|
className: "flex items-start gap-3 rounded border p-2",
|
|
233
|
-
children: [/* @__PURE__ */
|
|
234
|
+
children: [/* @__PURE__ */ h("div", {
|
|
234
235
|
className: "flex-1 min-w-0",
|
|
235
236
|
children: [
|
|
236
|
-
/* @__PURE__ */
|
|
237
|
+
/* @__PURE__ */ m("p", {
|
|
237
238
|
className: "text-sm font-medium truncate",
|
|
238
239
|
children: e.name
|
|
239
240
|
}),
|
|
240
|
-
/* @__PURE__ */
|
|
241
|
+
/* @__PURE__ */ h("p", {
|
|
241
242
|
className: "text-xs text-muted-foreground",
|
|
242
243
|
children: [
|
|
243
|
-
|
|
244
|
+
C("planmagnet.dashboard.shared.project", "Project"),
|
|
244
245
|
": ",
|
|
245
246
|
e.projectId
|
|
246
247
|
]
|
|
247
248
|
}),
|
|
248
|
-
/* @__PURE__ */
|
|
249
|
+
/* @__PURE__ */ h("p", {
|
|
249
250
|
className: "text-xs text-muted-foreground",
|
|
250
251
|
children: [
|
|
251
|
-
|
|
252
|
+
C("planmagnet.dashboard.shared.due", "Due"),
|
|
252
253
|
" ",
|
|
253
254
|
e.dueDate ? new Date(e.dueDate).toLocaleDateString() : "—"
|
|
254
255
|
]
|
|
255
256
|
})
|
|
256
257
|
]
|
|
257
|
-
}), /* @__PURE__ */
|
|
258
|
+
}), /* @__PURE__ */ m(_, {
|
|
258
259
|
variant: "secondary",
|
|
259
260
|
className: "text-xs flex-shrink-0 capitalize",
|
|
260
261
|
children: e.status.toLowerCase().replace("_", " ")
|
|
261
262
|
})]
|
|
262
|
-
}, e.id)),
|
|
263
|
+
}, e.id)), N.length === 0 && /* @__PURE__ */ m("p", {
|
|
263
264
|
className: "text-sm text-muted-foreground text-center py-2",
|
|
264
|
-
children:
|
|
265
|
+
children: C("planmagnet.dashboard.upcomingMilestones.empty", "No upcoming milestones.")
|
|
265
266
|
})]
|
|
266
267
|
})] })]
|
|
267
268
|
})]
|
|
268
269
|
}),
|
|
269
|
-
/* @__PURE__ */
|
|
270
|
+
/* @__PURE__ */ h("div", {
|
|
270
271
|
className: "grid gap-6 lg:grid-cols-2",
|
|
271
|
-
children: [/* @__PURE__ */
|
|
272
|
+
children: [/* @__PURE__ */ h(v, { children: [/* @__PURE__ */ m(b, { children: /* @__PURE__ */ h(x, {
|
|
272
273
|
className: "flex items-center gap-2 text-base",
|
|
273
|
-
children: [/* @__PURE__ */
|
|
274
|
-
}) }), /* @__PURE__ */
|
|
274
|
+
children: [/* @__PURE__ */ m(a, { className: "h-5 w-5 text-purple-500" }), C("planmagnet.dashboard.recentWorkItems.title", "Recent Work Items")]
|
|
275
|
+
}) }), /* @__PURE__ */ h(y, {
|
|
275
276
|
className: "space-y-3",
|
|
276
|
-
children: [
|
|
277
|
+
children: [P.map((e) => /* @__PURE__ */ h("div", {
|
|
277
278
|
className: "flex items-center justify-between rounded border p-2",
|
|
278
|
-
children: [/* @__PURE__ */
|
|
279
|
+
children: [/* @__PURE__ */ h("div", {
|
|
279
280
|
className: "min-w-0 flex-1",
|
|
280
|
-
children: [/* @__PURE__ */
|
|
281
|
+
children: [/* @__PURE__ */ m("p", {
|
|
281
282
|
className: "text-sm font-medium truncate",
|
|
282
283
|
children: e.key
|
|
283
|
-
}), /* @__PURE__ */
|
|
284
|
+
}), /* @__PURE__ */ h("p", {
|
|
284
285
|
className: "text-xs text-muted-foreground",
|
|
285
|
-
children: [
|
|
286
|
-
e.projectId,
|
|
287
|
-
" ",
|
|
288
|
-
e.assigneeId ? `\u00b7 ${e.assigneeId}` : ""
|
|
289
|
-
]
|
|
286
|
+
children: [F.get(e.projectId) ?? `Project ${e.projectId.slice(0, 6)}`, e.assigneeId ? /* @__PURE__ */ h(p, { children: [" · ", /* @__PURE__ */ m(t, { id: e.assigneeId })] }) : null]
|
|
290
287
|
})]
|
|
291
|
-
}), /* @__PURE__ */
|
|
288
|
+
}), /* @__PURE__ */ h("div", {
|
|
292
289
|
className: "flex items-center gap-2 flex-shrink-0",
|
|
293
|
-
children: [/* @__PURE__ */
|
|
290
|
+
children: [/* @__PURE__ */ h(_, {
|
|
294
291
|
variant: "outline",
|
|
295
292
|
className: "text-xs",
|
|
296
293
|
children: [
|
|
297
294
|
e.storyPoints ?? 0,
|
|
298
295
|
" ",
|
|
299
|
-
|
|
296
|
+
C("planmagnet.dashboard.shared.pointsShort", "pts")
|
|
300
297
|
]
|
|
301
|
-
}), /* @__PURE__ */
|
|
298
|
+
}), /* @__PURE__ */ m(_, {
|
|
302
299
|
variant: "secondary",
|
|
303
300
|
className: "text-xs capitalize",
|
|
304
301
|
children: e.status.toLowerCase().replace("_", " ")
|
|
305
302
|
})]
|
|
306
303
|
})]
|
|
307
|
-
}, e.id)),
|
|
304
|
+
}, e.id)), P.length === 0 && /* @__PURE__ */ m("p", {
|
|
308
305
|
className: "text-sm text-muted-foreground text-center py-4",
|
|
309
|
-
children:
|
|
306
|
+
children: C("planmagnet.dashboard.recentWorkItems.empty", "No work items yet.")
|
|
310
307
|
})]
|
|
311
|
-
})] }), /* @__PURE__ */
|
|
308
|
+
})] }), /* @__PURE__ */ h(v, { children: [/* @__PURE__ */ m(b, { children: /* @__PURE__ */ h(x, {
|
|
312
309
|
className: "flex items-center gap-2 text-base",
|
|
313
|
-
children: [/* @__PURE__ */
|
|
314
|
-
}) }), /* @__PURE__ */
|
|
310
|
+
children: [/* @__PURE__ */ m(o, { className: "h-5 w-5 text-cyan-500" }), C("planmagnet.dashboard.projectStatus.title", "Project Status")]
|
|
311
|
+
}) }), /* @__PURE__ */ h(y, {
|
|
315
312
|
className: "space-y-3",
|
|
316
|
-
children: [
|
|
313
|
+
children: [j.map((e) => /* @__PURE__ */ h("div", {
|
|
317
314
|
className: "flex items-center justify-between rounded border p-2",
|
|
318
|
-
children: [/* @__PURE__ */
|
|
315
|
+
children: [/* @__PURE__ */ h("div", {
|
|
319
316
|
className: "min-w-0 flex-1",
|
|
320
|
-
children: [/* @__PURE__ */
|
|
317
|
+
children: [/* @__PURE__ */ m("p", {
|
|
321
318
|
className: "text-sm font-medium truncate",
|
|
322
319
|
children: e.name
|
|
323
|
-
}), /* @__PURE__ */
|
|
320
|
+
}), /* @__PURE__ */ m("p", {
|
|
324
321
|
className: "text-xs text-muted-foreground capitalize",
|
|
325
322
|
children: e.status.toLowerCase().replace("_", " ")
|
|
326
323
|
})]
|
|
327
|
-
}), /* @__PURE__ */
|
|
324
|
+
}), /* @__PURE__ */ h("div", {
|
|
328
325
|
className: "flex items-center gap-2 ml-3 flex-shrink-0",
|
|
329
326
|
children: [
|
|
330
|
-
/* @__PURE__ */
|
|
331
|
-
/* @__PURE__ */
|
|
327
|
+
/* @__PURE__ */ m(l, { className: "h-3 w-3 text-muted-foreground" }),
|
|
328
|
+
/* @__PURE__ */ m("span", {
|
|
332
329
|
className: "text-xs text-muted-foreground",
|
|
333
|
-
children: e.ownerId
|
|
330
|
+
children: /* @__PURE__ */ m(t, { id: e.ownerId })
|
|
334
331
|
}),
|
|
335
|
-
/* @__PURE__ */
|
|
332
|
+
/* @__PURE__ */ m(r, { className: "h-3 w-3 text-muted-foreground" })
|
|
336
333
|
]
|
|
337
334
|
})]
|
|
338
|
-
}, e.id)),
|
|
335
|
+
}, e.id)), j.length === 0 && /* @__PURE__ */ m("p", {
|
|
339
336
|
className: "text-sm text-muted-foreground text-center py-4",
|
|
340
|
-
children:
|
|
337
|
+
children: C("planmagnet.dashboard.projectStatus.empty", "No active projects.")
|
|
341
338
|
})]
|
|
342
339
|
})] })]
|
|
343
340
|
})
|
|
@@ -345,6 +342,6 @@ function x() {
|
|
|
345
342
|
});
|
|
346
343
|
}
|
|
347
344
|
//#endregion
|
|
348
|
-
export {
|
|
345
|
+
export { C as DashboardDashboardPagePage };
|
|
349
346
|
|
|
350
347
|
//# sourceMappingURL=DashboardDashboardPagePage.js.map
|