@burdenoff/microfe-atheletebridge 2026.720.1 → 2026.731.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
  2. package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
  3. package/dist/atheletebridge/admin/AdminDashboardPage.js.map +1 -1
  4. package/dist/atheletebridge/admin/AdminRoot.js.map +1 -1
  5. package/dist/atheletebridge/components/AthleteSelect.js.map +1 -1
  6. package/dist/atheletebridge/components/Crest.js.map +1 -1
  7. package/dist/atheletebridge/components/CursorPager.js.map +1 -1
  8. package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -1
  9. package/dist/atheletebridge/components/EmptyState.js.map +1 -1
  10. package/dist/atheletebridge/components/FilterSelect.js.map +1 -1
  11. package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
  12. package/dist/atheletebridge/components/LoadingState.js.map +1 -1
  13. package/dist/atheletebridge/components/MatchupRow.js.map +1 -1
  14. package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
  15. package/dist/atheletebridge/components/PageLayout.js.map +1 -1
  16. package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
  17. package/dist/atheletebridge/components/RouteAnalytics.js.map +1 -1
  18. package/dist/atheletebridge/components/Scoreboard.js.map +1 -1
  19. package/dist/atheletebridge/components/SessionCard.js.map +1 -1
  20. package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -1
  21. package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
  22. package/dist/atheletebridge/components/TeamCard.js.map +1 -1
  23. package/dist/atheletebridge/components/UserAvatar.js.map +1 -1
  24. package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -1
  25. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts +9 -0
  26. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts.map +1 -0
  27. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js +118 -0
  28. package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js.map +1 -0
  29. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts +9 -0
  30. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts.map +1 -0
  31. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js +88 -0
  32. package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js.map +1 -0
  33. package/dist/atheletebridge/components/domain.js.map +1 -1
  34. package/dist/atheletebridge/components/form.js.map +1 -1
  35. package/dist/atheletebridge/components/viz/BarChart.js.map +1 -1
  36. package/dist/atheletebridge/components/viz/DonutChart.js +1 -1
  37. package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -1
  38. package/dist/atheletebridge/components/viz/LineChart.js.map +1 -1
  39. package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -1
  40. package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -1
  41. package/dist/atheletebridge/components/viz/StatCard.js.map +1 -1
  42. package/dist/atheletebridge/components/viz/Timeline.js.map +1 -1
  43. package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -1
  44. package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -1
  45. package/dist/atheletebridge/components/viz/lib/format.js.map +1 -1
  46. package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -1
  47. package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -1
  48. package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -1
  49. package/dist/atheletebridge/context/AthleteBridgeContext.js.map +1 -1
  50. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +18 -1
  51. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
  52. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +71 -33
  53. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
  54. package/dist/atheletebridge/i18n.js.map +1 -1
  55. package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
  56. package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
  57. package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
  58. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -1
  59. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
  60. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +87 -75
  61. package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
  62. package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
  63. package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
  64. package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
  65. package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
  66. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
  67. package/dist/atheletebridge/pages/programs/ProgramFormPage.js.map +1 -1
  68. package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
  69. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
  70. package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
  71. package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
  72. package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
  73. package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
  74. package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
  75. package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -1
  76. package/dist/atheletebridge/store/atheletebridgeStore.js.map +1 -1
  77. package/dist/atheletebridge/types/index.d.ts +27 -0
  78. package/dist/atheletebridge/types/index.d.ts.map +1 -1
  79. package/dist/atheletebridge/utils/cn.js.map +1 -1
  80. package/dist/atheletebridge/utils/datetime.js.map +1 -1
  81. package/dist/atheletebridge/utils/formatters.js.map +1 -1
  82. package/dist/atheletebridge/utils/identity.js.map +1 -1
  83. package/dist/atheletebridge/utils/navigation.js.map +1 -1
  84. package/dist/atheletebridge/utils/pageContext.js.map +1 -1
  85. package/dist/atheletebridge/utils/timeAgo.js.map +1 -1
  86. package/dist/atheletebridge/utils/url.js.map +1 -1
  87. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  88. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  89. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  90. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  91. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"CoachAnalyticsPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"sourcesContent":["/**\n * Coach Analytics\n * @module atheletebridge/pages/analytics\n *\n * An honest coaching-analytics dashboard built ENTIRELY from real aggregates via\n * `useCoachAnalytics()` — total sessions, average session rating, completion\n * rate, session-status mix, average rating by coach, weekly training-load /\n * session trends, and a league-standings snapshot.\n *\n * Deliberately OMITS the mock's fabricated composite indices (Form Index,\n * Fitness Score, Fatigue Index, Injury Status, Availability %) — there is no\n * wellness / injury / availability data in the schema, so those are not faked.\n * The one derived metric shown (ACWR training load) uses standard sports-science\n * maths over real `workout.totalLoad` and carries an explanatory tooltip.\n * Semantic tokens only; responsive.\n */\n\nimport { useMemo } from 'react';\nimport { Activity, BarChart3, CheckCircle2, Info, ListChecks, Star } from 'lucide-react';\nimport {\n Badge,\n GlassCard,\n GradientText,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { Crest } from '../../components/Crest';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { LineChart, BarChart, DonutChart, RatingStars, STATUS } from '../../components/viz';\nimport { humanizeEnum } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useCoachAnalytics, useUserDirectory } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionStatus, AthStanding } from '../../types';\n\n/** Tokenized color per session status for the status donut. */\nconst STATUS_COLOR: Record<AthSessionStatus, string> = {\n COMPLETED: STATUS.success,\n CONFIRMED: STATUS.info,\n REQUESTED: STATUS.warning,\n CANCELLED: STATUS.danger,\n NO_SHOW: 'var(--color-text-muted)',\n};\n\n/** ACWR banding (acute:chronic workload ratio) — standard sweet-spot 0.8–1.3. */\nfunction acwrBand(ratio: number | null): {\n label: string;\n variant: 'default' | 'secondary' | 'destructive';\n} {\n if (ratio == null) {\n return { label: 'No data', variant: 'secondary' };\n }\n if (ratio > 1.5) {\n return { label: 'At risk', variant: 'destructive' };\n }\n if (ratio < 0.8 || ratio > 1.3) {\n return { label: 'Watch', variant: 'secondary' };\n }\n return { label: 'Optimal', variant: 'default' };\n}\n\n/** Short coach display: resolved name, else a monogram-friendly id fragment. */\nfunction coachShortName(name: string | undefined, id: string): string {\n if (name && name.trim()) {\n return name;\n }\n return `Coach ${id.slice(0, 4)}`;\n}\n\nexport function CoachAnalyticsPage() {\n const { navigateTo } = useAthleteBridge();\n const analytics = useCoachAnalytics();\n const directory = useUserDirectory(analytics.coachIds);\n\n const statusSegments = useMemo(\n () =>\n analytics.sessionsByStatus.map((entry) => ({\n label: humanizeEnum(entry.status),\n value: entry.count,\n color: STATUS_COLOR[entry.status],\n })),\n [analytics.sessionsByStatus]\n );\n\n const coachBars = useMemo(\n () =>\n analytics.ratingByCoach.map((entry) => ({\n label: coachShortName(directory.nameOf(entry.coachId), entry.coachId),\n value: Number(entry.avgRating.toFixed(2)),\n })),\n [analytics.ratingByCoach, directory]\n );\n\n const standingsColumns: ResponsiveTableColumn<AthStanding>[] = [\n {\n key: 'rank',\n header: '#',\n priority: 'secondary',\n cell: (row, i) => (\n <span className=\"tabular-nums text-text-secondary\">{row.rank ?? i + 1}</span>\n ),\n },\n {\n key: 'team',\n header: 'Team',\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <Crest\n name={row.team?.name ?? 'Team'}\n sport={row.team?.sport}\n size={20}\n showSportGlyph={false}\n />\n <span className=\"truncate font-medium text-text-primary\">{row.team?.name ?? 'Team'}</span>\n </span>\n ),\n },\n {\n key: 'played',\n header: 'P',\n priority: 'hidden-on-mobile',\n cell: (row) => <span className=\"tabular-nums text-text-secondary\">{row.played}</span>,\n },\n {\n key: 'gd',\n header: 'GD',\n priority: 'hidden-on-mobile',\n cell: (row) => {\n const gd = row.goalsFor - row.goalsAgainst;\n return (\n <span className=\"tabular-nums text-text-secondary\">\n {gd > 0 ? '+' : ''}\n {gd}\n </span>\n );\n },\n },\n {\n key: 'points',\n header: 'Pts',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"font-semibold tabular-nums text-text-primary\">{row.points}</span>\n ),\n },\n ];\n\n if (analytics.isLoading) {\n return (\n <PageLayout title=\"Coach analytics\">\n <LoadingState label=\"Crunching coaching data…\" />\n </PageLayout>\n );\n }\n\n if (analytics.isError) {\n return (\n <PageLayout title=\"Coach analytics\">\n <ErrorState\n title=\"Analytics unavailable\"\n message=\"We could not load coaching analytics. Please try again.\"\n />\n </PageLayout>\n );\n }\n\n const hasData = analytics.totalSessions > 0 || analytics.standings.length > 0;\n const band = acwrBand(analytics.trainingLoad.ratio);\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-2\">\n <GradientText>Coach analytics</GradientText>\n <Badge variant=\"outline\">Beta</Badge>\n </span>\n }\n description=\"Honest aggregates over your real sessions, workouts and results.\"\n >\n {!hasData ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 className=\"h-10 w-10\" />}\n title=\"No coaching data yet\"\n description=\"Book and complete sessions, log workouts and record fixtures to unlock analytics.\"\n />\n ) : (\n <div className=\"space-y-8\">\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={analytics.totalSessions}\n label=\"Total sessions\"\n tone=\"accent\"\n />\n <GlassCard treatment=\"metric\" className=\"flex flex-col justify-between gap-3 p-5\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">Avg session rating</span>\n <Star className=\"h-5 w-5 text-text-muted\" />\n </div>\n {analytics.avgRating != null ? (\n <div className=\"flex items-end gap-3\">\n <span className=\"text-3xl font-semibold leading-none tabular-nums text-text-primary\">\n {analytics.avgRating.toFixed(1)}\n <span className=\"ml-1 text-base text-text-secondary\">/5</span>\n </span>\n <div className=\"pb-1\">\n <RatingStars value={analytics.avgRating} max={5} size={16} />\n </div>\n </div>\n ) : (\n <span className=\"text-sm text-text-muted\">No rated sessions yet</span>\n )}\n <span className=\"text-xs text-text-muted tabular-nums\">\n {analytics.ratedCount} rated of {analytics.totalSessions}\n </span>\n </GlassCard>\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={`${Math.round(analytics.completedPct)}%`}\n label=\"Sessions completed\"\n tone=\"success\"\n />\n </div>\n\n {/* Charts row: status donut + rating-by-coach bars */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n Session status overview\n </h2>\n {statusSegments.length > 0 ? (\n <DonutChart\n segments={statusSegments}\n centerLabel={analytics.totalSessions}\n centerCaption=\"sessions\"\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">No sessions to break down yet.</p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n Average rating by coach\n </h2>\n {coachBars.length > 0 ? (\n <>\n <BarChart\n series={coachBars}\n height={220}\n colorful\n valueFormatter={(v) => v.toFixed(1)}\n />\n <div className=\"mt-3 flex flex-wrap gap-x-4 gap-y-2\">\n {analytics.ratingByCoach.map((entry) => (\n <span\n key={entry.coachId}\n className=\"flex items-center gap-2 text-xs text-text-secondary\"\n >\n <UserAvatar\n name={directory.nameOf(entry.coachId)}\n id={entry.coachId}\n size=\"xs\"\n />\n <span className=\"truncate\">\n {coachShortName(directory.nameOf(entry.coachId), entry.coachId)}\n <span className=\"ml-1 text-text-muted tabular-nums\">({entry.count})</span>\n </span>\n </span>\n ))}\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n Rate completed sessions to compare coaches.\n </p>\n )}\n </GlassCard>\n </div>\n\n {/* Trends row: training load + sessions per week */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Weekly training load</h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"About ACWR training load\"\n className=\"rounded-full p-1 text-text-muted transition-colors hover:text-text-primary\"\n >\n <Info className=\"h-4 w-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">\n Total workout load per week (sum of real <code>totalLoad</code>). The badge is\n the acute:chronic workload ratio (last 7 days vs 28-day average) — a standard\n sports-science load indicator; 0.8–1.3 is the typical sweet spot.\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n {analytics.loadTrend.some((p) => p.value > 0) ? (\n <>\n <LineChart\n series={analytics.loadTrend}\n seriesLabel=\"Training load\"\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n <div className=\"mt-4 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm\">\n <span className=\"text-text-secondary\">\n Acute (7d){' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.acute).toLocaleString()}\n </span>\n </span>\n <span className=\"text-text-secondary\">\n Chronic (28d avg){' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.chronic).toLocaleString()}\n </span>\n </span>\n <span className=\"flex items-center gap-2 text-text-secondary\">\n ACWR\n <Badge variant={band.variant} className=\"tabular-nums\">\n {analytics.trainingLoad.ratio != null\n ? `${analytics.trainingLoad.ratio.toFixed(2)} · ${band.label}`\n : band.label}\n </Badge>\n </span>\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n Log workouts with a load to see training trend.\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Sessions per week</h2>\n {analytics.sessionTrend.some((p) => p.value > 0) ? (\n <BarChart\n series={analytics.sessionTrend}\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">Schedule sessions to see weekly volume.</p>\n )}\n </GlassCard>\n </div>\n\n {/* League standings snapshot */}\n {analytics.standings.length > 0 ? (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Activity className=\"h-5 w-5 text-text-muted\" />\n League standings\n {analytics.competition ? (\n <Badge variant=\"secondary\">{analytics.competition}</Badge>\n ) : null}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/standings')}\n className=\"text-sm font-medium text-[var(--color-accent-teal)] transition-colors hover:underline\"\n >\n View full standings\n </button>\n </div>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={standingsColumns}\n data={analytics.standings}\n rowKey={(row) => row.id}\n />\n </GlassCard>\n </section>\n ) : null}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CoachAnalyticsPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/analytics/CoachAnalyticsPage.tsx"],"sourcesContent":["/**\n * Coach Analytics\n * @module atheletebridge/pages/analytics\n *\n * An honest coaching-analytics dashboard built ENTIRELY from real aggregates via\n * `useCoachAnalytics()` — total sessions, average session rating, completion\n * rate, session-status mix, average rating by coach, weekly training-load /\n * session trends, and a league-standings snapshot.\n *\n * Deliberately OMITS the mock's fabricated composite indices (Form Index,\n * Fitness Score, Fatigue Index, Injury Status, Availability %) — there is no\n * wellness / injury / availability data in the schema, so those are not faked.\n * The one derived metric shown (ACWR training load) uses standard sports-science\n * maths over real `workout.totalLoad` and carries an explanatory tooltip.\n * Semantic tokens only; responsive.\n */\n\nimport { useMemo } from 'react';\nimport { Activity, BarChart3, CheckCircle2, Info, ListChecks, Star } from 'lucide-react';\nimport {\n Badge,\n GlassCard,\n GradientText,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { Crest } from '../../components/Crest';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { LineChart, BarChart, DonutChart, RatingStars, STATUS } from '../../components/viz';\nimport { humanizeEnum } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useCoachAnalytics, useUserDirectory } from '../../hooks/useAthleteBridgeApi';\nimport type { AthSessionStatus, AthStanding } from '../../types';\n\n/** Tokenized color per session status for the status donut. */\nconst STATUS_COLOR: Record<AthSessionStatus, string> = {\n COMPLETED: STATUS.success,\n CONFIRMED: STATUS.info,\n REQUESTED: STATUS.warning,\n CANCELLED: STATUS.danger,\n NO_SHOW: 'var(--color-text-muted)',\n};\n\n/** ACWR banding (acute:chronic workload ratio) — standard sweet-spot 0.8–1.3. */\nfunction acwrBand(ratio: number | null): {\n label: string;\n variant: 'default' | 'secondary' | 'destructive';\n} {\n if (ratio == null) {\n return { label: 'No data', variant: 'secondary' };\n }\n if (ratio > 1.5) {\n return { label: 'At risk', variant: 'destructive' };\n }\n if (ratio < 0.8 || ratio > 1.3) {\n return { label: 'Watch', variant: 'secondary' };\n }\n return { label: 'Optimal', variant: 'default' };\n}\n\n/** Short coach display: resolved name, else a monogram-friendly id fragment. */\nfunction coachShortName(name: string | undefined, id: string): string {\n if (name && name.trim()) {\n return name;\n }\n return `Coach ${id.slice(0, 4)}`;\n}\n\nexport function CoachAnalyticsPage() {\n const { navigateTo } = useAthleteBridge();\n const analytics = useCoachAnalytics();\n const directory = useUserDirectory(analytics.coachIds);\n\n const statusSegments = useMemo(\n () =>\n analytics.sessionsByStatus.map((entry) => ({\n label: humanizeEnum(entry.status),\n value: entry.count,\n color: STATUS_COLOR[entry.status],\n })),\n [analytics.sessionsByStatus]\n );\n\n const coachBars = useMemo(\n () =>\n analytics.ratingByCoach.map((entry) => ({\n label: coachShortName(directory.nameOf(entry.coachId), entry.coachId),\n value: Number(entry.avgRating.toFixed(2)),\n })),\n [analytics.ratingByCoach, directory]\n );\n\n const standingsColumns: ResponsiveTableColumn<AthStanding>[] = [\n {\n key: 'rank',\n header: '#',\n priority: 'secondary',\n cell: (row, i) => (\n <span className=\"tabular-nums text-text-secondary\">{row.rank ?? i + 1}</span>\n ),\n },\n {\n key: 'team',\n header: 'Team',\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <Crest\n name={row.team?.name ?? 'Team'}\n sport={row.team?.sport}\n size={20}\n showSportGlyph={false}\n />\n <span className=\"truncate font-medium text-text-primary\">{row.team?.name ?? 'Team'}</span>\n </span>\n ),\n },\n {\n key: 'played',\n header: 'P',\n priority: 'hidden-on-mobile',\n cell: (row) => <span className=\"tabular-nums text-text-secondary\">{row.played}</span>,\n },\n {\n key: 'gd',\n header: 'GD',\n priority: 'hidden-on-mobile',\n cell: (row) => {\n const gd = row.goalsFor - row.goalsAgainst;\n return (\n <span className=\"tabular-nums text-text-secondary\">\n {gd > 0 ? '+' : ''}\n {gd}\n </span>\n );\n },\n },\n {\n key: 'points',\n header: 'Pts',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"font-semibold tabular-nums text-text-primary\">{row.points}</span>\n ),\n },\n ];\n\n if (analytics.isLoading) {\n return (\n <PageLayout title=\"Coach analytics\">\n <LoadingState label=\"Crunching coaching data…\" />\n </PageLayout>\n );\n }\n\n if (analytics.isError) {\n return (\n <PageLayout title=\"Coach analytics\">\n <ErrorState\n title=\"Analytics unavailable\"\n message=\"We could not load coaching analytics. Please try again.\"\n />\n </PageLayout>\n );\n }\n\n const hasData = analytics.totalSessions > 0 || analytics.standings.length > 0;\n const band = acwrBand(analytics.trainingLoad.ratio);\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-2\">\n <GradientText>Coach analytics</GradientText>\n <Badge variant=\"outline\">Beta</Badge>\n </span>\n }\n description=\"Honest aggregates over your real sessions, workouts and results.\"\n >\n {!hasData ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BarChart3 className=\"h-10 w-10\" />}\n title=\"No coaching data yet\"\n description=\"Book and complete sessions, log workouts and record fixtures to unlock analytics.\"\n />\n ) : (\n <div className=\"space-y-8\">\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={analytics.totalSessions}\n label=\"Total sessions\"\n tone=\"accent\"\n />\n <GlassCard treatment=\"metric\" className=\"flex flex-col justify-between gap-3 p-5\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">Avg session rating</span>\n <Star className=\"h-5 w-5 text-text-muted\" />\n </div>\n {analytics.avgRating != null ? (\n <div className=\"flex items-end gap-3\">\n <span className=\"text-3xl font-semibold leading-none tabular-nums text-text-primary\">\n {analytics.avgRating.toFixed(1)}\n <span className=\"ml-1 text-base text-text-secondary\">/5</span>\n </span>\n <div className=\"pb-1\">\n <RatingStars value={analytics.avgRating} max={5} size={16} />\n </div>\n </div>\n ) : (\n <span className=\"text-sm text-text-muted\">No rated sessions yet</span>\n )}\n <span className=\"text-xs text-text-muted tabular-nums\">\n {analytics.ratedCount} rated of {analytics.totalSessions}\n </span>\n </GlassCard>\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={`${Math.round(analytics.completedPct)}%`}\n label=\"Sessions completed\"\n tone=\"success\"\n />\n </div>\n\n {/* Charts row: status donut + rating-by-coach bars */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n Session status overview\n </h2>\n {statusSegments.length > 0 ? (\n <DonutChart\n segments={statusSegments}\n centerLabel={analytics.totalSessions}\n centerCaption=\"sessions\"\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">No sessions to break down yet.</p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n Average rating by coach\n </h2>\n {coachBars.length > 0 ? (\n <>\n <BarChart\n series={coachBars}\n height={220}\n colorful\n valueFormatter={(v) => v.toFixed(1)}\n />\n <div className=\"mt-3 flex flex-wrap gap-x-4 gap-y-2\">\n {analytics.ratingByCoach.map((entry) => (\n <span\n key={entry.coachId}\n className=\"flex items-center gap-2 text-xs text-text-secondary\"\n >\n <UserAvatar\n name={directory.nameOf(entry.coachId)}\n id={entry.coachId}\n size=\"xs\"\n />\n <span className=\"truncate\">\n {coachShortName(directory.nameOf(entry.coachId), entry.coachId)}\n <span className=\"ml-1 text-text-muted tabular-nums\">({entry.count})</span>\n </span>\n </span>\n ))}\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n Rate completed sessions to compare coaches.\n </p>\n )}\n </GlassCard>\n </div>\n\n {/* Trends row: training load + sessions per week */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Weekly training load</h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"About ACWR training load\"\n className=\"rounded-full p-1 text-text-muted transition-colors hover:text-text-primary\"\n >\n <Info className=\"h-4 w-4\" />\n </button>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs\">\n Total workout load per week (sum of real <code>totalLoad</code>). The badge is\n the acute:chronic workload ratio (last 7 days vs 28-day average) — a standard\n sports-science load indicator; 0.8–1.3 is the typical sweet spot.\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n {analytics.loadTrend.some((p) => p.value > 0) ? (\n <>\n <LineChart\n series={analytics.loadTrend}\n seriesLabel=\"Training load\"\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n <div className=\"mt-4 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm\">\n <span className=\"text-text-secondary\">\n Acute (7d){' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.acute).toLocaleString()}\n </span>\n </span>\n <span className=\"text-text-secondary\">\n Chronic (28d avg){' '}\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(analytics.trainingLoad.chronic).toLocaleString()}\n </span>\n </span>\n <span className=\"flex items-center gap-2 text-text-secondary\">\n ACWR\n <Badge variant={band.variant} className=\"tabular-nums\">\n {analytics.trainingLoad.ratio != null\n ? `${analytics.trainingLoad.ratio.toFixed(2)} · ${band.label}`\n : band.label}\n </Badge>\n </span>\n </div>\n </>\n ) : (\n <p className=\"text-sm text-text-muted\">\n Log workouts with a load to see training trend.\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Sessions per week</h2>\n {analytics.sessionTrend.some((p) => p.value > 0) ? (\n <BarChart\n series={analytics.sessionTrend}\n height={200}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">Schedule sessions to see weekly volume.</p>\n )}\n </GlassCard>\n </div>\n\n {/* League standings snapshot */}\n {analytics.standings.length > 0 ? (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Activity className=\"h-5 w-5 text-text-muted\" />\n League standings\n {analytics.competition ? (\n <Badge variant=\"secondary\">{analytics.competition}</Badge>\n ) : null}\n </h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/standings')}\n className=\"text-sm font-medium text-[var(--color-accent-teal)] transition-colors hover:underline\"\n >\n View full standings\n </button>\n </div>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={standingsColumns}\n data={analytics.standings}\n rowKey={(row) => row.id}\n />\n </GlassCard>\n </section>\n ) : null}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AthleteProfilePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/athletes/AthleteProfilePage.tsx"],"sourcesContent":["/**\n * Athlete Profile\n * @module atheletebridge/pages/athletes\n *\n * A consolidated single-athlete view composed PURELY from real data via\n * `useAthleteProfile(teamId, memberId)` — the resolved team member (identity +\n * physical stats), their performance metrics (trend charts + improvement\n * tiles), workout history and coaching sessions (with read-only ratings).\n *\n * Honesty rules: there is no photo, age/DOB, or \"fitness score\" field on the\n * schema, so this screen renders an initials/jersey monogram (never a\n * fabricated headshot) and OMITS any composite wellness score. Every number\n * traces to a real row. Semantic tokens only; fully responsive.\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Dumbbell,\n Gauge,\n LineChart as LineChartIcon,\n Ruler,\n Star,\n Users,\n Weight,\n} from 'lucide-react';\nimport {\n Badge,\n GlassCard,\n GradientText,\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { SessionCard } from '../../components/SessionCard';\nimport { Crest } from '../../components/Crest';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { LineChart, StatCard, RatingStars } from '../../components/viz';\nimport { MEMBER_ROLE_LABELS, METRIC_TYPE_LABELS, SPORT_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useAthleteProfile, useTeam } from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport type { AthMetricType, AthPerformanceMetric, AthWorkoutLog } from '../../types';\n\n// ── Trend range chips ────────────────────────────────────────────────────────\n\ntype RangeKey = '1M' | '3M' | '6M' | 'YTD' | '1Y' | 'ALL';\n\nconst RANGE_ORDER: RangeKey[] = ['1M', '3M', '6M', 'YTD', '1Y', 'ALL'];\nconst RANGE_LABELS: Record<RangeKey, string> = {\n '1M': '1M',\n '3M': '3M',\n '6M': '6M',\n YTD: 'YTD',\n '1Y': '1Y',\n ALL: 'All',\n};\n\n/** Lower bound (ms epoch) for a given range, or 0 for \"all time\". */\nfunction rangeStart(range: RangeKey): number {\n const now = Date.now();\n const DAY = 86_400_000;\n switch (range) {\n case '1M':\n return now - 30 * DAY;\n case '3M':\n return now - 90 * DAY;\n case '6M':\n return now - 180 * DAY;\n case '1Y':\n return now - 365 * DAY;\n case 'YTD':\n return new Date(new Date().getFullYear(), 0, 1).getTime();\n case 'ALL':\n return 0;\n }\n}\n\nconst shortDate = (iso: string): string =>\n new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n\ninterface MetricGroup {\n type: AthMetricType;\n /** Ascending by recordedAt. */\n points: AthPerformanceMetric[];\n unit: string | null;\n first: number;\n last: number;\n peak: number;\n deltaPct: number | null;\n}\n\n/** Group metrics by type (ascending in time) + derive first/last/peak/delta. */\nfunction buildMetricGroups(metrics: AthPerformanceMetric[], start: number): MetricGroup[] {\n const groups = new Map<AthMetricType, AthPerformanceMetric[]>();\n for (const m of metrics) {\n const at = new Date(m.recordedAt).getTime();\n if (Number.isNaN(at) || at < start) {\n continue;\n }\n const list = groups.get(m.type) ?? [];\n list.push(m);\n groups.set(m.type, list);\n }\n return Array.from(groups.entries())\n .map(([type, list]) => {\n const points = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const first = points[0]?.value ?? 0;\n const last = points[points.length - 1]?.value ?? 0;\n const peak = points.reduce((acc, p) => Math.max(acc, p.value), Number.NEGATIVE_INFINITY);\n const deltaPct =\n points.length >= 2 && first !== 0 ? ((last - first) / Math.abs(first)) * 100 : null;\n return { type, points, unit: points[0]?.unit ?? null, first, last, peak, deltaPct };\n })\n .sort((a, b) => b.points.length - a.points.length);\n}\n\nexport function AthleteProfilePage() {\n const { teamId, memberId } = useParams<{ teamId: string; memberId: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const profile = useAthleteProfile(teamId, memberId);\n const team = useTeam(teamId);\n const [range, setRange] = useState<RangeKey>('6M');\n\n const start = useMemo(() => rangeStart(range), [range]);\n const metricGroups = useMemo(\n () => buildMetricGroups(profile.metrics, start),\n [profile.metrics, start]\n );\n const allTimeGroups = useMemo(() => buildMetricGroups(profile.metrics, 0), [profile.metrics]);\n\n const ratedSessions = useMemo(\n () => profile.sessions.filter((s) => s.rating !== null),\n [profile.sessions]\n );\n const avgRating = useMemo(\n () =>\n ratedSessions.length > 0\n ? ratedSessions.reduce((acc, s) => acc + (s.rating ?? 0), 0) / ratedSessions.length\n : null,\n [ratedSessions]\n );\n\n const upcomingSessions = useMemo(\n () =>\n profile.sessions\n .filter((s) => s.status === 'REQUESTED' || s.status === 'CONFIRMED')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime()),\n [profile.sessions]\n );\n\n const recentWorkouts = useMemo(\n () =>\n [...profile.workouts].sort(\n (a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime()\n ),\n [profile.workouts]\n );\n\n const completedSessions = useMemo(\n () => profile.sessions.filter((s) => s.status === 'COMPLETED').length,\n [profile.sessions]\n );\n\n if (profile.isLoading) {\n return (\n <PageLayout title=\"Athlete\">\n <LoadingState label=\"Loading athlete profile…\" />\n </PageLayout>\n );\n }\n\n if (profile.isError || !profile.member) {\n return (\n <PageLayout title=\"Athlete\">\n <ErrorState\n title=\"Athlete not found\"\n message=\"This athlete is not on the roster, or you do not have access.\"\n onRetry={() => (teamId ? navigateTo(`/teams/${teamId}`) : navigateTo('/teams'))}\n />\n </PageLayout>\n );\n }\n\n const member = profile.member;\n const teamName = team.data?.name ?? null;\n const teamSport = team.data?.sport;\n\n const physicalStats: Array<{ icon: React.ReactNode; label: string; value: string }> = [];\n if (member.height != null) {\n physicalStats.push({\n icon: <Ruler className=\"h-4 w-4\" />,\n label: 'Height',\n value: String(member.height),\n });\n }\n if (member.weight != null) {\n physicalStats.push({\n icon: <Weight className=\"h-4 w-4\" />,\n label: 'Weight',\n value: String(member.weight),\n });\n }\n if (member.dominantSide) {\n physicalStats.push({\n icon: <Gauge className=\"h-4 w-4\" />,\n label: 'Dominant side',\n value: member.dominantSide,\n });\n }\n if (member.position) {\n physicalStats.push({\n icon: <Users className=\"h-4 w-4\" />,\n label: 'Position',\n value: member.position,\n });\n }\n if (member.joinedAt) {\n physicalStats.push({\n icon: <CalendarClock className=\"h-4 w-4\" />,\n label: 'Joined',\n value: formatDate(member.joinedAt),\n });\n }\n\n const primaryGroup = metricGroups[0] ?? null;\n\n const workoutColumns: ResponsiveTableColumn<AthWorkoutLog>[] = [\n {\n key: 'title',\n header: 'Workout',\n priority: 'primary',\n cell: (row) => <span className=\"font-medium text-text-primary\">{row.title}</span>,\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"tabular-nums text-text-secondary\">\n {row.durationMin != null ? formatDuration(row.durationMin) : '—'}\n </span>\n ),\n },\n {\n key: 'load',\n header: 'Load',\n priority: 'hidden-on-mobile',\n cell: (row) => (\n <span className=\"tabular-nums text-text-secondary\">\n {row.totalLoad != null ? row.totalLoad.toLocaleString() : '—'}\n </span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'hidden-on-mobile',\n cell: (row) => (\n <span className=\"tabular-nums text-text-secondary\">{row.rpe != null ? row.rpe : '—'}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'secondary',\n cell: (row) => <StatusBadge status={row.status} />,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"whitespace-nowrap text-text-secondary\">{formatDate(row.performedAt)}</span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={<GradientText>{member.displayName}</GradientText>}\n description={\n teamName ? `${teamName}${teamSport ? ` · ${SPORT_LABELS[teamSport]}` : ''}` : undefined\n }\n actions={\n <button\n type=\"button\"\n onClick={() => (teamId ? navigateTo(`/teams/${teamId}`) : navigateTo('/teams'))}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to team\n </button>\n }\n >\n {/* Identity header */}\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-col gap-6 md:flex-row md:items-start\">\n <UserAvatar\n name={member.displayName}\n id={member.userId}\n jersey={member.jerseyNumber}\n size=\"xl\"\n ring\n className=\"shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge variant=\"default\">{MEMBER_ROLE_LABELS[member.role]}</Badge>\n {member.position ? <Badge variant=\"secondary\">{member.position}</Badge> : null}\n {member.jerseyNumber != null ? (\n <Badge variant=\"outline\" className=\"tabular-nums\">\n #{member.jerseyNumber}\n </Badge>\n ) : null}\n {teamName ? (\n <Badge variant=\"secondary\" className=\"gap-1.5\">\n <Crest name={teamName} sport={teamSport} size={16} showSportGlyph={false} />\n {teamName}\n </Badge>\n ) : null}\n <StatusBadge status={member.status} />\n </div>\n\n {physicalStats.length > 0 ? (\n <dl className=\"mt-5 grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3 lg:grid-cols-5\">\n {physicalStats.map((stat) => (\n <div key={stat.label} className=\"min-w-0\">\n <dt className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <span className=\"text-text-muted\">{stat.icon}</span>\n {stat.label}\n </dt>\n <dd className=\"mt-1 truncate text-lg font-semibold text-text-primary\">\n {stat.value}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n </div>\n </GlassCard>\n\n <Tabs defaultValue=\"overview\">\n <TabsList className=\"mb-6 flex-wrap\">\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"metrics\">Metrics</TabsTrigger>\n <TabsTrigger value=\"workouts\">Workouts</TabsTrigger>\n <TabsTrigger value=\"sessions\">Sessions</TabsTrigger>\n </TabsList>\n\n {/* ── Overview ─────────────────────────────────────────────────────── */}\n <TabsContent value=\"overview\" className=\"space-y-8\">\n {/* KPI / improvement tiles */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-2 p-5\">\n <span className=\"text-sm font-medium text-text-secondary\">Avg session rating</span>\n {avgRating != null ? (\n <>\n <RatingStars\n value={avgRating}\n max={5}\n size={20}\n showValue\n count={ratedSessions.length}\n />\n </>\n ) : (\n <span className=\"text-sm text-text-muted\">No rated sessions yet</span>\n )}\n </GlassCard>\n <StatCard\n label=\"Sessions\"\n value={profile.sessions.length}\n icon={<CalendarClock className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Completed\"\n value={completedSessions}\n icon={<Star className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Workouts logged\"\n value={profile.workouts.length}\n icon={<Dumbbell className=\"h-4 w-4\" />}\n />\n </div>\n\n {/* Improvement tiles from real metric series */}\n {allTimeGroups.some((g) => g.deltaPct != null) ? (\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Improvement</h2>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {allTimeGroups\n .filter((g) => g.deltaPct != null)\n .slice(0, 4)\n .map((g) => (\n <StatCard\n key={g.type}\n label={`${METRIC_TYPE_LABELS[g.type]}${g.unit ? ` (${g.unit})` : ''}`}\n value={g.last.toLocaleString()}\n delta={g.deltaPct ?? undefined}\n deltaLabel=\"first → latest\"\n sparkline={g.points.map((p) => p.value)}\n />\n ))}\n </div>\n </section>\n ) : null}\n\n {/* Performance trend */}\n <TrendCard\n title=\"Performance trend\"\n groups={metricGroups}\n primary={primaryGroup}\n range={range}\n onRangeChange={setRange}\n />\n\n {/* Upcoming sessions */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n Upcoming sessions ({upcomingSessions.length})\n </h2>\n {upcomingSessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarClock className=\"h-10 w-10\" />}\n title=\"No upcoming sessions\"\n description=\"Confirmed and requested coaching sessions for this athlete appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {upcomingSessions.slice(0, 6).map((s) => (\n <SessionCard\n key={s.id}\n session={s}\n athleteName={member.displayName}\n onClick={() => navigateTo(`/sessions/${s.id}`)}\n />\n ))}\n </div>\n )}\n </section>\n </TabsContent>\n\n {/* ── Metrics ──────────────────────────────────────────────────────── */}\n <TabsContent value=\"metrics\" className=\"space-y-8\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Performance metrics</h2>\n <RangeChips value={range} onChange={setRange} />\n </div>\n {metricGroups.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<LineChartIcon className=\"h-10 w-10\" />}\n title=\"No metrics in this range\"\n description=\"Record metrics for this athlete — speed, strength, endurance — to chart progress over time.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n {metricGroups.map((g) => (\n <GlassCard key={g.type} treatment=\"metric\" className=\"p-5\">\n <div className=\"mb-3 flex items-start justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold uppercase tracking-wide text-text-secondary\">\n {METRIC_TYPE_LABELS[g.type]}\n </h3>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {g.last.toLocaleString()}\n {g.unit ? (\n <span className=\"ml-1 text-sm text-text-secondary\">{g.unit}</span>\n ) : null}\n </p>\n </div>\n {g.deltaPct != null ? (\n <Badge\n variant={g.deltaPct >= 0 ? 'default' : 'destructive'}\n className=\"tabular-nums\"\n >\n {g.deltaPct >= 0 ? '+' : ''}\n {g.deltaPct.toFixed(1)}%\n </Badge>\n ) : null}\n </div>\n <LineChart\n series={g.points.map((p) => ({\n label: shortDate(p.recordedAt),\n value: p.value,\n }))}\n seriesLabel={METRIC_TYPE_LABELS[g.type]}\n height={160}\n valueFormatter={(v) => `${v.toLocaleString()}${g.unit ? ` ${g.unit}` : ''}`}\n />\n <p className=\"mt-2 text-xs text-text-muted tabular-nums\">\n Peak {g.peak.toLocaleString()}\n {g.unit ? ` ${g.unit}` : ''} · {g.points.length} readings\n </p>\n </GlassCard>\n ))}\n </div>\n )}\n </TabsContent>\n\n {/* ── Workouts ─────────────────────────────────────────────────────── */}\n <TabsContent value=\"workouts\">\n {recentWorkouts.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Dumbbell className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"This athlete's logged training sessions will appear here.\"\n />\n ) : (\n <ResponsiveTable\n columns={workoutColumns}\n data={recentWorkouts}\n rowKey={(row) => row.id}\n />\n )}\n </TabsContent>\n\n {/* ── Sessions ─────────────────────────────────────────────────────── */}\n <TabsContent value=\"sessions\">\n {profile.sessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarClock className=\"h-10 w-10\" />}\n title=\"No coaching sessions\"\n description=\"Booked, completed and cancelled sessions for this athlete will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {[...profile.sessions]\n .sort(\n (a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime()\n )\n .map((s) => (\n <SessionCard\n key={s.id}\n session={s}\n athleteName={member.displayName}\n onClick={() => navigateTo(`/sessions/${s.id}`)}\n />\n ))}\n </div>\n )}\n </TabsContent>\n </Tabs>\n </PageLayout>\n );\n}\n\n// ── Range chips ──────────────────────────────────────────────────────────────\n\ninterface RangeChipsProps {\n value: RangeKey;\n onChange: (value: RangeKey) => void;\n}\n\nfunction RangeChips({ value, onChange }: RangeChipsProps) {\n return (\n <div\n role=\"group\"\n aria-label=\"Trend range\"\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam bg-bg-surface p-1\"\n >\n {RANGE_ORDER.map((key) => {\n const active = key === value;\n return (\n <button\n key={key}\n type=\"button\"\n aria-pressed={active}\n onClick={() => onChange(key)}\n className={\n active\n ? 'rounded-md bg-action-primary-bg px-2.5 py-1 text-xs font-medium text-action-primary-text'\n : 'rounded-md px-2.5 py-1 text-xs font-medium text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary'\n }\n >\n {RANGE_LABELS[key]}\n </button>\n );\n })}\n </div>\n );\n}\n\n// ── Trend card ───────────────────────────────────────────────────────────────\n\ninterface TrendCardProps {\n title: string;\n groups: MetricGroup[];\n primary: MetricGroup | null;\n range: RangeKey;\n onRangeChange: (value: RangeKey) => void;\n}\n\nfunction TrendCard({ title, groups, primary, range, onRangeChange }: TrendCardProps) {\n // Overlay a secondary metric series (aligned by index) when available, so the\n // trend reads like the mock's dual-line without fabricating a shared scale.\n const secondary = groups.find((g) => g.type !== primary?.type) ?? null;\n\n return (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {primary ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n className=\"h-2 w-2 rounded-full bg-[var(--color-accent-primary)]\"\n aria-hidden=\"true\"\n />\n {METRIC_TYPE_LABELS[primary.type]}\n {primary.unit ? ` (${primary.unit})` : ''}\n </span>\n {secondary ? (\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n className=\"h-2 w-2 rounded-full bg-[var(--color-accent-teal)]\"\n aria-hidden=\"true\"\n />\n {METRIC_TYPE_LABELS[secondary.type]}\n {secondary.unit ? ` (${secondary.unit})` : ''}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n <RangeChips value={range} onChange={onRangeChange} />\n </div>\n {primary ? (\n <LineChart\n series={primary.points.map((p) => ({ label: shortDate(p.recordedAt), value: p.value }))}\n compare={secondary ? secondary.points.map((p) => p.value) : undefined}\n seriesLabel={METRIC_TYPE_LABELS[primary.type]}\n compareLabel={secondary ? METRIC_TYPE_LABELS[secondary.type] : undefined}\n height={240}\n />\n ) : (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-muted\">\n No metric readings in this range.\n </div>\n )}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AthleteProfilePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/athletes/AthleteProfilePage.tsx"],"sourcesContent":["/**\n * Athlete Profile\n * @module atheletebridge/pages/athletes\n *\n * A consolidated single-athlete view composed PURELY from real data via\n * `useAthleteProfile(teamId, memberId)` — the resolved team member (identity +\n * physical stats), their performance metrics (trend charts + improvement\n * tiles), workout history and coaching sessions (with read-only ratings).\n *\n * Honesty rules: there is no photo, age/DOB, or \"fitness score\" field on the\n * schema, so this screen renders an initials/jersey monogram (never a\n * fabricated headshot) and OMITS any composite wellness score. Every number\n * traces to a real row. Semantic tokens only; fully responsive.\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Dumbbell,\n Gauge,\n LineChart as LineChartIcon,\n Ruler,\n Star,\n Users,\n Weight,\n} from 'lucide-react';\nimport {\n Badge,\n GlassCard,\n GradientText,\n Tabs,\n TabsList,\n TabsTrigger,\n TabsContent,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { SessionCard } from '../../components/SessionCard';\nimport { Crest } from '../../components/Crest';\nimport { UserAvatar } from '../../components/UserAvatar';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { LineChart, StatCard, RatingStars } from '../../components/viz';\nimport { MEMBER_ROLE_LABELS, METRIC_TYPE_LABELS, SPORT_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useAthleteProfile, useTeam } from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport type { AthMetricType, AthPerformanceMetric, AthWorkoutLog } from '../../types';\n\n// ── Trend range chips ────────────────────────────────────────────────────────\n\ntype RangeKey = '1M' | '3M' | '6M' | 'YTD' | '1Y' | 'ALL';\n\nconst RANGE_ORDER: RangeKey[] = ['1M', '3M', '6M', 'YTD', '1Y', 'ALL'];\nconst RANGE_LABELS: Record<RangeKey, string> = {\n '1M': '1M',\n '3M': '3M',\n '6M': '6M',\n YTD: 'YTD',\n '1Y': '1Y',\n ALL: 'All',\n};\n\n/** Lower bound (ms epoch) for a given range, or 0 for \"all time\". */\nfunction rangeStart(range: RangeKey): number {\n const now = Date.now();\n const DAY = 86_400_000;\n switch (range) {\n case '1M':\n return now - 30 * DAY;\n case '3M':\n return now - 90 * DAY;\n case '6M':\n return now - 180 * DAY;\n case '1Y':\n return now - 365 * DAY;\n case 'YTD':\n return new Date(new Date().getFullYear(), 0, 1).getTime();\n case 'ALL':\n return 0;\n }\n}\n\nconst shortDate = (iso: string): string =>\n new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n\ninterface MetricGroup {\n type: AthMetricType;\n /** Ascending by recordedAt. */\n points: AthPerformanceMetric[];\n unit: string | null;\n first: number;\n last: number;\n peak: number;\n deltaPct: number | null;\n}\n\n/** Group metrics by type (ascending in time) + derive first/last/peak/delta. */\nfunction buildMetricGroups(metrics: AthPerformanceMetric[], start: number): MetricGroup[] {\n const groups = new Map<AthMetricType, AthPerformanceMetric[]>();\n for (const m of metrics) {\n const at = new Date(m.recordedAt).getTime();\n if (Number.isNaN(at) || at < start) {\n continue;\n }\n const list = groups.get(m.type) ?? [];\n list.push(m);\n groups.set(m.type, list);\n }\n return Array.from(groups.entries())\n .map(([type, list]) => {\n const points = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const first = points[0]?.value ?? 0;\n const last = points[points.length - 1]?.value ?? 0;\n const peak = points.reduce((acc, p) => Math.max(acc, p.value), Number.NEGATIVE_INFINITY);\n const deltaPct =\n points.length >= 2 && first !== 0 ? ((last - first) / Math.abs(first)) * 100 : null;\n return { type, points, unit: points[0]?.unit ?? null, first, last, peak, deltaPct };\n })\n .sort((a, b) => b.points.length - a.points.length);\n}\n\nexport function AthleteProfilePage() {\n const { teamId, memberId } = useParams<{ teamId: string; memberId: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const profile = useAthleteProfile(teamId, memberId);\n const team = useTeam(teamId);\n const [range, setRange] = useState<RangeKey>('6M');\n\n const start = useMemo(() => rangeStart(range), [range]);\n const metricGroups = useMemo(\n () => buildMetricGroups(profile.metrics, start),\n [profile.metrics, start]\n );\n const allTimeGroups = useMemo(() => buildMetricGroups(profile.metrics, 0), [profile.metrics]);\n\n const ratedSessions = useMemo(\n () => profile.sessions.filter((s) => s.rating !== null),\n [profile.sessions]\n );\n const avgRating = useMemo(\n () =>\n ratedSessions.length > 0\n ? ratedSessions.reduce((acc, s) => acc + (s.rating ?? 0), 0) / ratedSessions.length\n : null,\n [ratedSessions]\n );\n\n const upcomingSessions = useMemo(\n () =>\n profile.sessions\n .filter((s) => s.status === 'REQUESTED' || s.status === 'CONFIRMED')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime()),\n [profile.sessions]\n );\n\n const recentWorkouts = useMemo(\n () =>\n [...profile.workouts].sort(\n (a, b) => new Date(b.performedAt).getTime() - new Date(a.performedAt).getTime()\n ),\n [profile.workouts]\n );\n\n const completedSessions = useMemo(\n () => profile.sessions.filter((s) => s.status === 'COMPLETED').length,\n [profile.sessions]\n );\n\n if (profile.isLoading) {\n return (\n <PageLayout title=\"Athlete\">\n <LoadingState label=\"Loading athlete profile…\" />\n </PageLayout>\n );\n }\n\n if (profile.isError || !profile.member) {\n return (\n <PageLayout title=\"Athlete\">\n <ErrorState\n title=\"Athlete not found\"\n message=\"This athlete is not on the roster, or you do not have access.\"\n onRetry={() => (teamId ? navigateTo(`/teams/${teamId}`) : navigateTo('/teams'))}\n />\n </PageLayout>\n );\n }\n\n const member = profile.member;\n const teamName = team.data?.name ?? null;\n const teamSport = team.data?.sport;\n\n const physicalStats: Array<{ icon: React.ReactNode; label: string; value: string }> = [];\n if (member.height != null) {\n physicalStats.push({\n icon: <Ruler className=\"h-4 w-4\" />,\n label: 'Height',\n value: String(member.height),\n });\n }\n if (member.weight != null) {\n physicalStats.push({\n icon: <Weight className=\"h-4 w-4\" />,\n label: 'Weight',\n value: String(member.weight),\n });\n }\n if (member.dominantSide) {\n physicalStats.push({\n icon: <Gauge className=\"h-4 w-4\" />,\n label: 'Dominant side',\n value: member.dominantSide,\n });\n }\n if (member.position) {\n physicalStats.push({\n icon: <Users className=\"h-4 w-4\" />,\n label: 'Position',\n value: member.position,\n });\n }\n if (member.joinedAt) {\n physicalStats.push({\n icon: <CalendarClock className=\"h-4 w-4\" />,\n label: 'Joined',\n value: formatDate(member.joinedAt),\n });\n }\n\n const primaryGroup = metricGroups[0] ?? null;\n\n const workoutColumns: ResponsiveTableColumn<AthWorkoutLog>[] = [\n {\n key: 'title',\n header: 'Workout',\n priority: 'primary',\n cell: (row) => <span className=\"font-medium text-text-primary\">{row.title}</span>,\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"tabular-nums text-text-secondary\">\n {row.durationMin != null ? formatDuration(row.durationMin) : '—'}\n </span>\n ),\n },\n {\n key: 'load',\n header: 'Load',\n priority: 'hidden-on-mobile',\n cell: (row) => (\n <span className=\"tabular-nums text-text-secondary\">\n {row.totalLoad != null ? row.totalLoad.toLocaleString() : '—'}\n </span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'hidden-on-mobile',\n cell: (row) => (\n <span className=\"tabular-nums text-text-secondary\">{row.rpe != null ? row.rpe : '—'}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'secondary',\n cell: (row) => <StatusBadge status={row.status} />,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n cell: (row) => (\n <span className=\"whitespace-nowrap text-text-secondary\">{formatDate(row.performedAt)}</span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title={<GradientText>{member.displayName}</GradientText>}\n description={\n teamName ? `${teamName}${teamSport ? ` · ${SPORT_LABELS[teamSport]}` : ''}` : undefined\n }\n actions={\n <button\n type=\"button\"\n onClick={() => (teamId ? navigateTo(`/teams/${teamId}`) : navigateTo('/teams'))}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to team\n </button>\n }\n >\n {/* Identity header */}\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-col gap-6 md:flex-row md:items-start\">\n <UserAvatar\n name={member.displayName}\n id={member.userId}\n jersey={member.jerseyNumber}\n size=\"xl\"\n ring\n className=\"shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge variant=\"default\">{MEMBER_ROLE_LABELS[member.role]}</Badge>\n {member.position ? <Badge variant=\"secondary\">{member.position}</Badge> : null}\n {member.jerseyNumber != null ? (\n <Badge variant=\"outline\" className=\"tabular-nums\">\n #{member.jerseyNumber}\n </Badge>\n ) : null}\n {teamName ? (\n <Badge variant=\"secondary\" className=\"gap-1.5\">\n <Crest name={teamName} sport={teamSport} size={16} showSportGlyph={false} />\n {teamName}\n </Badge>\n ) : null}\n <StatusBadge status={member.status} />\n </div>\n\n {physicalStats.length > 0 ? (\n <dl className=\"mt-5 grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3 lg:grid-cols-5\">\n {physicalStats.map((stat) => (\n <div key={stat.label} className=\"min-w-0\">\n <dt className=\"flex items-center gap-1.5 text-xs font-medium text-text-secondary\">\n <span className=\"text-text-muted\">{stat.icon}</span>\n {stat.label}\n </dt>\n <dd className=\"mt-1 truncate text-lg font-semibold text-text-primary\">\n {stat.value}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n </div>\n </GlassCard>\n\n <Tabs defaultValue=\"overview\">\n <TabsList className=\"mb-6 flex-wrap\">\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"metrics\">Metrics</TabsTrigger>\n <TabsTrigger value=\"workouts\">Workouts</TabsTrigger>\n <TabsTrigger value=\"sessions\">Sessions</TabsTrigger>\n </TabsList>\n\n {/* ── Overview ─────────────────────────────────────────────────────── */}\n <TabsContent value=\"overview\" className=\"space-y-8\">\n {/* KPI / improvement tiles */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-2 p-5\">\n <span className=\"text-sm font-medium text-text-secondary\">Avg session rating</span>\n {avgRating != null ? (\n <>\n <RatingStars\n value={avgRating}\n max={5}\n size={20}\n showValue\n count={ratedSessions.length}\n />\n </>\n ) : (\n <span className=\"text-sm text-text-muted\">No rated sessions yet</span>\n )}\n </GlassCard>\n <StatCard\n label=\"Sessions\"\n value={profile.sessions.length}\n icon={<CalendarClock className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Completed\"\n value={completedSessions}\n icon={<Star className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Workouts logged\"\n value={profile.workouts.length}\n icon={<Dumbbell className=\"h-4 w-4\" />}\n />\n </div>\n\n {/* Improvement tiles from real metric series */}\n {allTimeGroups.some((g) => g.deltaPct != null) ? (\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Improvement</h2>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {allTimeGroups\n .filter((g) => g.deltaPct != null)\n .slice(0, 4)\n .map((g) => (\n <StatCard\n key={g.type}\n label={`${METRIC_TYPE_LABELS[g.type]}${g.unit ? ` (${g.unit})` : ''}`}\n value={g.last.toLocaleString()}\n delta={g.deltaPct ?? undefined}\n deltaLabel=\"first → latest\"\n sparkline={g.points.map((p) => p.value)}\n />\n ))}\n </div>\n </section>\n ) : null}\n\n {/* Performance trend */}\n <TrendCard\n title=\"Performance trend\"\n groups={metricGroups}\n primary={primaryGroup}\n range={range}\n onRangeChange={setRange}\n />\n\n {/* Upcoming sessions */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n Upcoming sessions ({upcomingSessions.length})\n </h2>\n {upcomingSessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarClock className=\"h-10 w-10\" />}\n title=\"No upcoming sessions\"\n description=\"Confirmed and requested coaching sessions for this athlete appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {upcomingSessions.slice(0, 6).map((s) => (\n <SessionCard\n key={s.id}\n session={s}\n athleteName={member.displayName}\n onClick={() => navigateTo(`/sessions/${s.id}`)}\n />\n ))}\n </div>\n )}\n </section>\n </TabsContent>\n\n {/* ── Metrics ──────────────────────────────────────────────────────── */}\n <TabsContent value=\"metrics\" className=\"space-y-8\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Performance metrics</h2>\n <RangeChips value={range} onChange={setRange} />\n </div>\n {metricGroups.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<LineChartIcon className=\"h-10 w-10\" />}\n title=\"No metrics in this range\"\n description=\"Record metrics for this athlete — speed, strength, endurance — to chart progress over time.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n {metricGroups.map((g) => (\n <GlassCard key={g.type} treatment=\"metric\" className=\"p-5\">\n <div className=\"mb-3 flex items-start justify-between gap-3\">\n <div>\n <h3 className=\"text-sm font-semibold uppercase tracking-wide text-text-secondary\">\n {METRIC_TYPE_LABELS[g.type]}\n </h3>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {g.last.toLocaleString()}\n {g.unit ? (\n <span className=\"ml-1 text-sm text-text-secondary\">{g.unit}</span>\n ) : null}\n </p>\n </div>\n {g.deltaPct != null ? (\n <Badge\n variant={g.deltaPct >= 0 ? 'default' : 'destructive'}\n className=\"tabular-nums\"\n >\n {g.deltaPct >= 0 ? '+' : ''}\n {g.deltaPct.toFixed(1)}%\n </Badge>\n ) : null}\n </div>\n <LineChart\n series={g.points.map((p) => ({\n label: shortDate(p.recordedAt),\n value: p.value,\n }))}\n seriesLabel={METRIC_TYPE_LABELS[g.type]}\n height={160}\n valueFormatter={(v) => `${v.toLocaleString()}${g.unit ? ` ${g.unit}` : ''}`}\n />\n <p className=\"mt-2 text-xs text-text-muted tabular-nums\">\n Peak {g.peak.toLocaleString()}\n {g.unit ? ` ${g.unit}` : ''} · {g.points.length} readings\n </p>\n </GlassCard>\n ))}\n </div>\n )}\n </TabsContent>\n\n {/* ── Workouts ─────────────────────────────────────────────────────── */}\n <TabsContent value=\"workouts\">\n {recentWorkouts.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Dumbbell className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"This athlete's logged training sessions will appear here.\"\n />\n ) : (\n <ResponsiveTable\n columns={workoutColumns}\n data={recentWorkouts}\n rowKey={(row) => row.id}\n />\n )}\n </TabsContent>\n\n {/* ── Sessions ─────────────────────────────────────────────────────── */}\n <TabsContent value=\"sessions\">\n {profile.sessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarClock className=\"h-10 w-10\" />}\n title=\"No coaching sessions\"\n description=\"Booked, completed and cancelled sessions for this athlete will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {[...profile.sessions]\n .sort(\n (a, b) => new Date(b.scheduledAt).getTime() - new Date(a.scheduledAt).getTime()\n )\n .map((s) => (\n <SessionCard\n key={s.id}\n session={s}\n athleteName={member.displayName}\n onClick={() => navigateTo(`/sessions/${s.id}`)}\n />\n ))}\n </div>\n )}\n </TabsContent>\n </Tabs>\n </PageLayout>\n );\n}\n\n// ── Range chips ──────────────────────────────────────────────────────────────\n\ninterface RangeChipsProps {\n value: RangeKey;\n onChange: (value: RangeKey) => void;\n}\n\nfunction RangeChips({ value, onChange }: RangeChipsProps) {\n return (\n <div\n role=\"group\"\n aria-label=\"Trend range\"\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam bg-bg-surface p-1\"\n >\n {RANGE_ORDER.map((key) => {\n const active = key === value;\n return (\n <button\n key={key}\n type=\"button\"\n aria-pressed={active}\n onClick={() => onChange(key)}\n className={\n active\n ? 'rounded-md bg-action-primary-bg px-2.5 py-1 text-xs font-medium text-action-primary-text'\n : 'rounded-md px-2.5 py-1 text-xs font-medium text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary'\n }\n >\n {RANGE_LABELS[key]}\n </button>\n );\n })}\n </div>\n );\n}\n\n// ── Trend card ───────────────────────────────────────────────────────────────\n\ninterface TrendCardProps {\n title: string;\n groups: MetricGroup[];\n primary: MetricGroup | null;\n range: RangeKey;\n onRangeChange: (value: RangeKey) => void;\n}\n\nfunction TrendCard({ title, groups, primary, range, onRangeChange }: TrendCardProps) {\n // Overlay a secondary metric series (aligned by index) when available, so the\n // trend reads like the mock's dual-line without fabricating a shared scale.\n const secondary = groups.find((g) => g.type !== primary?.type) ?? null;\n\n return (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {primary ? (\n <div className=\"mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n className=\"h-2 w-2 rounded-full bg-[var(--color-accent-primary)]\"\n aria-hidden=\"true\"\n />\n {METRIC_TYPE_LABELS[primary.type]}\n {primary.unit ? ` (${primary.unit})` : ''}\n </span>\n {secondary ? (\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n className=\"h-2 w-2 rounded-full bg-[var(--color-accent-teal)]\"\n aria-hidden=\"true\"\n />\n {METRIC_TYPE_LABELS[secondary.type]}\n {secondary.unit ? ` (${secondary.unit})` : ''}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n <RangeChips value={range} onChange={onRangeChange} />\n </div>\n {primary ? (\n <LineChart\n series={primary.points.map((p) => ({ label: shortDate(p.recordedAt), value: p.value }))}\n compare={secondary ? secondary.points.map((p) => p.value) : undefined}\n seriesLabel={METRIC_TYPE_LABELS[primary.type]}\n compareLabel={secondary ? METRIC_TYPE_LABELS[secondary.type] : undefined}\n height={240}\n />\n ) : (\n <div className=\"flex h-40 items-center justify-center text-sm text-text-muted\">\n No metric readings in this range.\n </div>\n )}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FixtureDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAeH,wBAAgB,iBAAiB,gCA2KhC"}
1
+ {"version":3,"file":"FixtureDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAiBH,wBAAgB,iBAAiB,gCAiLhC"}
@@ -6,178 +6,190 @@ import { Scoreboard as i } from "../../components/Scoreboard.js";
6
6
  import { ErrorState as a, LoadingState as o } from "../../components/LoadingState.js";
7
7
  import { useFixture as s, useRecordFixtureResult as c } from "../../hooks/useAthleteBridgeApi.js";
8
8
  import { FormField as l, fieldInputClass as u } from "../../components/form.js";
9
- import { useState as d } from "react";
10
- import { ArrowLeft as f, Flag as p, Loader2 as m, Minus as h, Trophy as g } from "lucide-react";
11
- import { useParams as _ } from "react-router-dom";
12
- import { jsx as v, jsxs as y } from "react/jsx-runtime";
13
- import { EmphasisPanel as b, GlassCard as x } from "@burdenoff/fe-libs/ui";
9
+ import { AvailabilityRsvpControl as d } from "../../components/availability/AvailabilityRsvpControl.js";
10
+ import { AvailabilityRosterPanel as f } from "../../components/availability/AvailabilityRosterPanel.js";
11
+ import { useState as p } from "react";
12
+ import { ArrowLeft as m, Flag as h, Loader2 as g, Minus as _, Trophy as v } from "lucide-react";
13
+ import { useParams as y } from "react-router-dom";
14
+ import { jsx as b, jsxs as x } from "react/jsx-runtime";
15
+ import { EmphasisPanel as S, GlassCard as C } from "@burdenoff/fe-libs/ui";
14
16
  //#region src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx
15
- function S() {
16
- let { id: S } = _(), { navigateTo: C } = e(), w = s(S), T = c(), [E, D] = d(""), [O, k] = d(""), [A, j] = d(null);
17
- if (w.isLoading) return /* @__PURE__ */ v(t, {
17
+ function w() {
18
+ let { id: w } = y(), { navigateTo: T } = e(), E = s(w), D = c(), [O, k] = p(""), [A, j] = p(""), [M, N] = p(null);
19
+ if (E.isLoading) return /* @__PURE__ */ b(t, {
18
20
  title: "Fixture",
19
- children: /* @__PURE__ */ v(o, { label: "Loading fixture…" })
21
+ children: /* @__PURE__ */ b(o, { label: "Loading fixture…" })
20
22
  });
21
- if (w.isError || !w.data) return /* @__PURE__ */ v(t, {
23
+ if (E.isError || !E.data) return /* @__PURE__ */ b(t, {
22
24
  title: "Fixture",
23
- children: /* @__PURE__ */ v(a, {
25
+ children: /* @__PURE__ */ b(a, {
24
26
  title: "Fixture not found",
25
27
  message: "This fixture does not exist or you do not have access to it.",
26
- onRetry: () => C("/fixtures")
28
+ onRetry: () => T("/fixtures")
27
29
  })
28
30
  });
29
- let M = w.data, N = M.homeTeam?.name ?? "Home", P = M.awayTeam?.name ?? (M.awayTeamId ? "Away" : "TBD"), F = !!M.awayTeamId && (M.status === "SCHEDULED" || M.status === "LIVE");
30
- return /* @__PURE__ */ y(t, {
31
- title: `${N} vs ${P}`,
32
- description: `${n[M.type]}${M.competition ? ` · ${M.competition}` : ""}`,
33
- actions: /* @__PURE__ */ y("button", {
31
+ let P = E.data, F = P.homeTeam?.name ?? "Home", I = P.awayTeam?.name ?? (P.awayTeamId ? "Away" : "TBD"), L = !!P.awayTeamId && (P.status === "SCHEDULED" || P.status === "LIVE");
32
+ return /* @__PURE__ */ x(t, {
33
+ title: `${F} vs ${I}`,
34
+ description: `${n[P.type]}${P.competition ? ` · ${P.competition}` : ""}`,
35
+ actions: /* @__PURE__ */ x("button", {
34
36
  type: "button",
35
- onClick: () => C("/fixtures"),
37
+ onClick: () => T("/fixtures"),
36
38
  className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
37
- children: [/* @__PURE__ */ v(f, { className: "h-4 w-4" }), " Back"]
39
+ children: [/* @__PURE__ */ b(m, { className: "h-4 w-4" }), " Back"]
38
40
  }),
39
41
  children: [
40
- /* @__PURE__ */ v(i, {
41
- fixture: M,
42
+ /* @__PURE__ */ b(i, {
43
+ fixture: P,
42
44
  variant: "hero",
43
45
  className: "mb-8"
44
46
  }),
45
- M.notes && /* @__PURE__ */ y(b, {
47
+ /* @__PURE__ */ x("div", {
48
+ className: "mb-8 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_2fr]",
49
+ children: [/* @__PURE__ */ b(d, {
50
+ teamId: P.homeTeamId,
51
+ fixtureId: P.id
52
+ }), /* @__PURE__ */ b(f, {
53
+ teamId: P.homeTeamId,
54
+ fixtureId: P.id
55
+ })]
56
+ }),
57
+ P.notes && /* @__PURE__ */ x(S, {
46
58
  className: "mb-8",
47
- children: [/* @__PURE__ */ v("h2", {
59
+ children: [/* @__PURE__ */ b("h2", {
48
60
  className: "mb-2 text-sm font-semibold text-text-primary",
49
61
  children: "Notes"
50
- }), /* @__PURE__ */ v("p", {
62
+ }), /* @__PURE__ */ b("p", {
51
63
  className: "text-sm text-text-secondary",
52
- children: M.notes
64
+ children: P.notes
53
65
  })]
54
66
  }),
55
- F && /* @__PURE__ */ y("section", {
67
+ L && /* @__PURE__ */ x("section", {
56
68
  className: "mb-8",
57
- children: [/* @__PURE__ */ y("h2", {
69
+ children: [/* @__PURE__ */ x("h2", {
58
70
  className: "mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary",
59
- children: [/* @__PURE__ */ v(p, { className: "h-5 w-5" }), " Record result"]
60
- }), /* @__PURE__ */ y(x, {
71
+ children: [/* @__PURE__ */ b(h, { className: "h-5 w-5" }), " Record result"]
72
+ }), /* @__PURE__ */ x(C, {
61
73
  treatment: "insight",
62
74
  className: "p-5 sm:p-6",
63
75
  children: [
64
- /* @__PURE__ */ y("p", {
76
+ /* @__PURE__ */ x("p", {
65
77
  className: "mb-5 text-sm text-text-secondary",
66
78
  children: [
67
79
  "Recording a final score marks this fixture ",
68
- /* @__PURE__ */ v("strong", { children: "Completed" }),
80
+ /* @__PURE__ */ b("strong", { children: "Completed" }),
69
81
  " and recomputes the competition standings for both teams."
70
82
  ]
71
83
  }),
72
- /* @__PURE__ */ y("form", {
84
+ /* @__PURE__ */ x("form", {
73
85
  onSubmit: async (e) => {
74
- if (e.preventDefault(), j(null), !S) return;
75
- let t = Number.parseInt(E, 10), n = Number.parseInt(O, 10);
86
+ if (e.preventDefault(), N(null), !w) return;
87
+ let t = Number.parseInt(O, 10), n = Number.parseInt(A, 10);
76
88
  if (Number.isNaN(t) || Number.isNaN(n)) {
77
- j("Both scores are required and must be numbers.");
89
+ N("Both scores are required and must be numbers.");
78
90
  return;
79
91
  }
80
92
  if (t < 0 || n < 0) {
81
- j("Scores cannot be negative.");
93
+ N("Scores cannot be negative.");
82
94
  return;
83
95
  }
84
96
  try {
85
- await T.mutateAsync({
86
- id: S,
97
+ await D.mutateAsync({
98
+ id: w,
87
99
  input: {
88
100
  homeScore: t,
89
101
  awayScore: n
90
102
  }
91
- }), D(""), k("");
103
+ }), k(""), j("");
92
104
  } catch (e) {
93
- j(e instanceof Error ? e.message : "Failed to record result.");
105
+ N(e instanceof Error ? e.message : "Failed to record result.");
94
106
  }
95
107
  },
96
- children: [/* @__PURE__ */ y("div", {
108
+ children: [/* @__PURE__ */ x("div", {
97
109
  className: "flex flex-wrap items-end justify-center gap-4 sm:gap-6",
98
110
  children: [
99
- /* @__PURE__ */ y("div", {
111
+ /* @__PURE__ */ x("div", {
100
112
  className: "flex items-end gap-3",
101
- children: [/* @__PURE__ */ y("div", {
113
+ children: [/* @__PURE__ */ x("div", {
102
114
  className: "flex flex-col items-center gap-2",
103
- children: [/* @__PURE__ */ v(r, {
104
- name: N,
105
- sport: M.homeTeam?.sport,
115
+ children: [/* @__PURE__ */ b(r, {
116
+ name: F,
117
+ sport: P.homeTeam?.sport,
106
118
  size: 40
107
- }), /* @__PURE__ */ v("span", {
119
+ }), /* @__PURE__ */ b("span", {
108
120
  className: "max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary",
109
- children: N
121
+ children: F
110
122
  })]
111
- }), /* @__PURE__ */ v(l, {
123
+ }), /* @__PURE__ */ b(l, {
112
124
  label: "Home",
113
125
  htmlFor: "homeScore",
114
126
  className: "w-24",
115
- children: /* @__PURE__ */ v("input", {
127
+ children: /* @__PURE__ */ b("input", {
116
128
  id: "homeScore",
117
129
  inputMode: "numeric",
118
130
  className: `${u} text-center text-lg font-bold tabular-nums`,
119
- value: E,
120
- onChange: (e) => D(e.target.value),
131
+ value: O,
132
+ onChange: (e) => k(e.target.value),
121
133
  placeholder: "0"
122
134
  })
123
135
  })]
124
136
  }),
125
- /* @__PURE__ */ v(h, {
137
+ /* @__PURE__ */ b(_, {
126
138
  className: "mb-3 h-5 w-5 shrink-0 text-text-muted",
127
139
  "aria-hidden": "true"
128
140
  }),
129
- /* @__PURE__ */ y("div", {
141
+ /* @__PURE__ */ x("div", {
130
142
  className: "flex items-end gap-3",
131
- children: [/* @__PURE__ */ v(l, {
143
+ children: [/* @__PURE__ */ b(l, {
132
144
  label: "Away",
133
145
  htmlFor: "awayScore",
134
146
  className: "w-24",
135
- children: /* @__PURE__ */ v("input", {
147
+ children: /* @__PURE__ */ b("input", {
136
148
  id: "awayScore",
137
149
  inputMode: "numeric",
138
150
  className: `${u} text-center text-lg font-bold tabular-nums`,
139
- value: O,
140
- onChange: (e) => k(e.target.value),
151
+ value: A,
152
+ onChange: (e) => j(e.target.value),
141
153
  placeholder: "0"
142
154
  })
143
- }), /* @__PURE__ */ y("div", {
155
+ }), /* @__PURE__ */ x("div", {
144
156
  className: "flex flex-col items-center gap-2",
145
- children: [/* @__PURE__ */ v(r, {
146
- name: P,
147
- sport: M.awayTeam?.sport,
157
+ children: [/* @__PURE__ */ b(r, {
158
+ name: I,
159
+ sport: P.awayTeam?.sport,
148
160
  size: 40
149
- }), /* @__PURE__ */ v("span", {
161
+ }), /* @__PURE__ */ b("span", {
150
162
  className: "max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary",
151
- children: P
163
+ children: I
152
164
  })]
153
165
  })]
154
166
  })
155
167
  ]
156
- }), /* @__PURE__ */ v("div", {
168
+ }), /* @__PURE__ */ b("div", {
157
169
  className: "mt-6 flex justify-center",
158
- children: /* @__PURE__ */ y("button", {
170
+ children: /* @__PURE__ */ x("button", {
159
171
  type: "submit",
160
- disabled: T.isPending,
172
+ disabled: D.isPending,
161
173
  className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-6 py-2.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60",
162
- children: [T.isPending ? /* @__PURE__ */ v(m, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ v(g, { className: "h-4 w-4" }), "Record result"]
174
+ children: [D.isPending ? /* @__PURE__ */ b(g, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ b(v, { className: "h-4 w-4" }), "Record result"]
163
175
  })
164
176
  })]
165
177
  }),
166
- A && /* @__PURE__ */ v("p", {
178
+ M && /* @__PURE__ */ b("p", {
167
179
  className: "mt-3 text-center text-sm text-status-error-text",
168
- children: A
180
+ children: M
169
181
  })
170
182
  ]
171
183
  })]
172
184
  }),
173
- M.competition && /* @__PURE__ */ y("button", {
185
+ P.competition && /* @__PURE__ */ x("button", {
174
186
  type: "button",
175
- onClick: () => C(`/standings?competition=${encodeURIComponent(M.competition ?? "")}`),
187
+ onClick: () => T(`/standings?competition=${encodeURIComponent(P.competition ?? "")}`),
176
188
  className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
177
189
  children: [
178
- /* @__PURE__ */ v(g, { className: "h-4 w-4" }),
190
+ /* @__PURE__ */ b(v, { className: "h-4 w-4" }),
179
191
  " View ",
180
- M.competition,
192
+ P.competition,
181
193
  " standings"
182
194
  ]
183
195
  })
@@ -185,6 +197,6 @@ function S() {
185
197
  });
186
198
  }
187
199
  //#endregion
188
- export { S as FixtureDetailPage };
200
+ export { w as FixtureDetailPage };
189
201
 
190
202
  //# sourceMappingURL=FixtureDetailPage.js.map