@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.
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/admin/AdminDashboardPage.js.map +1 -1
- package/dist/atheletebridge/admin/AdminRoot.js.map +1 -1
- package/dist/atheletebridge/components/AthleteSelect.js.map +1 -1
- package/dist/atheletebridge/components/Crest.js.map +1 -1
- package/dist/atheletebridge/components/CursorPager.js.map +1 -1
- package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -1
- package/dist/atheletebridge/components/EmptyState.js.map +1 -1
- package/dist/atheletebridge/components/FilterSelect.js.map +1 -1
- package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/MatchupRow.js.map +1 -1
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/PageLayout.js.map +1 -1
- package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
- package/dist/atheletebridge/components/RouteAnalytics.js.map +1 -1
- package/dist/atheletebridge/components/Scoreboard.js.map +1 -1
- package/dist/atheletebridge/components/SessionCard.js.map +1 -1
- package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -1
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/components/UserAvatar.js.map +1 -1
- package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -1
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts +9 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js +118 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts +9 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js +88 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js.map +1 -0
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/components/form.js.map +1 -1
- package/dist/atheletebridge/components/viz/BarChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/DonutChart.js +1 -1
- package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/LineChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -1
- package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -1
- package/dist/atheletebridge/components/viz/StatCard.js.map +1 -1
- package/dist/atheletebridge/components/viz/Timeline.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/format.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -1
- package/dist/atheletebridge/context/AthleteBridgeContext.js.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +18 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +71 -33
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/i18n.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +87 -75
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -1
- package/dist/atheletebridge/store/atheletebridgeStore.js.map +1 -1
- package/dist/atheletebridge/types/index.d.ts +27 -0
- package/dist/atheletebridge/types/index.d.ts.map +1 -1
- package/dist/atheletebridge/utils/cn.js.map +1 -1
- package/dist/atheletebridge/utils/datetime.js.map +1 -1
- package/dist/atheletebridge/utils/formatters.js.map +1 -1
- package/dist/atheletebridge/utils/identity.js.map +1 -1
- package/dist/atheletebridge/utils/navigation.js.map +1 -1
- package/dist/atheletebridge/utils/pageContext.js.map +1 -1
- package/dist/atheletebridge/utils/timeAgo.js.map +1 -1
- package/dist/atheletebridge/utils/url.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- 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;
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
|
16
|
-
let { id:
|
|
17
|
-
if (
|
|
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__ */
|
|
21
|
+
children: /* @__PURE__ */ b(o, { label: "Loading fixture…" })
|
|
20
22
|
});
|
|
21
|
-
if (
|
|
23
|
+
if (E.isError || !E.data) return /* @__PURE__ */ b(t, {
|
|
22
24
|
title: "Fixture",
|
|
23
|
-
children: /* @__PURE__ */
|
|
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: () =>
|
|
28
|
+
onRetry: () => T("/fixtures")
|
|
27
29
|
})
|
|
28
30
|
});
|
|
29
|
-
let
|
|
30
|
-
return /* @__PURE__ */
|
|
31
|
-
title: `${
|
|
32
|
-
description: `${n[
|
|
33
|
-
actions: /* @__PURE__ */
|
|
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: () =>
|
|
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__ */
|
|
39
|
+
children: [/* @__PURE__ */ b(m, { className: "h-4 w-4" }), " Back"]
|
|
38
40
|
}),
|
|
39
41
|
children: [
|
|
40
|
-
/* @__PURE__ */
|
|
41
|
-
fixture:
|
|
42
|
+
/* @__PURE__ */ b(i, {
|
|
43
|
+
fixture: P,
|
|
42
44
|
variant: "hero",
|
|
43
45
|
className: "mb-8"
|
|
44
46
|
}),
|
|
45
|
-
|
|
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__ */
|
|
59
|
+
children: [/* @__PURE__ */ b("h2", {
|
|
48
60
|
className: "mb-2 text-sm font-semibold text-text-primary",
|
|
49
61
|
children: "Notes"
|
|
50
|
-
}), /* @__PURE__ */
|
|
62
|
+
}), /* @__PURE__ */ b("p", {
|
|
51
63
|
className: "text-sm text-text-secondary",
|
|
52
|
-
children:
|
|
64
|
+
children: P.notes
|
|
53
65
|
})]
|
|
54
66
|
}),
|
|
55
|
-
|
|
67
|
+
L && /* @__PURE__ */ x("section", {
|
|
56
68
|
className: "mb-8",
|
|
57
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
60
|
-
}), /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
80
|
+
/* @__PURE__ */ b("strong", { children: "Completed" }),
|
|
69
81
|
" and recomputes the competition standings for both teams."
|
|
70
82
|
]
|
|
71
83
|
}),
|
|
72
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ x("form", {
|
|
73
85
|
onSubmit: async (e) => {
|
|
74
|
-
if (e.preventDefault(),
|
|
75
|
-
let t = Number.parseInt(
|
|
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
|
-
|
|
89
|
+
N("Both scores are required and must be numbers.");
|
|
78
90
|
return;
|
|
79
91
|
}
|
|
80
92
|
if (t < 0 || n < 0) {
|
|
81
|
-
|
|
93
|
+
N("Scores cannot be negative.");
|
|
82
94
|
return;
|
|
83
95
|
}
|
|
84
96
|
try {
|
|
85
|
-
await
|
|
86
|
-
id:
|
|
97
|
+
await D.mutateAsync({
|
|
98
|
+
id: w,
|
|
87
99
|
input: {
|
|
88
100
|
homeScore: t,
|
|
89
101
|
awayScore: n
|
|
90
102
|
}
|
|
91
|
-
}),
|
|
103
|
+
}), k(""), j("");
|
|
92
104
|
} catch (e) {
|
|
93
|
-
|
|
105
|
+
N(e instanceof Error ? e.message : "Failed to record result.");
|
|
94
106
|
}
|
|
95
107
|
},
|
|
96
|
-
children: [/* @__PURE__ */
|
|
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__ */
|
|
111
|
+
/* @__PURE__ */ x("div", {
|
|
100
112
|
className: "flex items-end gap-3",
|
|
101
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ x("div", {
|
|
102
114
|
className: "flex flex-col items-center gap-2",
|
|
103
|
-
children: [/* @__PURE__ */
|
|
104
|
-
name:
|
|
105
|
-
sport:
|
|
115
|
+
children: [/* @__PURE__ */ b(r, {
|
|
116
|
+
name: F,
|
|
117
|
+
sport: P.homeTeam?.sport,
|
|
106
118
|
size: 40
|
|
107
|
-
}), /* @__PURE__ */
|
|
119
|
+
}), /* @__PURE__ */ b("span", {
|
|
108
120
|
className: "max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary",
|
|
109
|
-
children:
|
|
121
|
+
children: F
|
|
110
122
|
})]
|
|
111
|
-
}), /* @__PURE__ */
|
|
123
|
+
}), /* @__PURE__ */ b(l, {
|
|
112
124
|
label: "Home",
|
|
113
125
|
htmlFor: "homeScore",
|
|
114
126
|
className: "w-24",
|
|
115
|
-
children: /* @__PURE__ */
|
|
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:
|
|
120
|
-
onChange: (e) =>
|
|
131
|
+
value: O,
|
|
132
|
+
onChange: (e) => k(e.target.value),
|
|
121
133
|
placeholder: "0"
|
|
122
134
|
})
|
|
123
135
|
})]
|
|
124
136
|
}),
|
|
125
|
-
/* @__PURE__ */
|
|
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__ */
|
|
141
|
+
/* @__PURE__ */ x("div", {
|
|
130
142
|
className: "flex items-end gap-3",
|
|
131
|
-
children: [/* @__PURE__ */
|
|
143
|
+
children: [/* @__PURE__ */ b(l, {
|
|
132
144
|
label: "Away",
|
|
133
145
|
htmlFor: "awayScore",
|
|
134
146
|
className: "w-24",
|
|
135
|
-
children: /* @__PURE__ */
|
|
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:
|
|
140
|
-
onChange: (e) =>
|
|
151
|
+
value: A,
|
|
152
|
+
onChange: (e) => j(e.target.value),
|
|
141
153
|
placeholder: "0"
|
|
142
154
|
})
|
|
143
|
-
}), /* @__PURE__ */
|
|
155
|
+
}), /* @__PURE__ */ x("div", {
|
|
144
156
|
className: "flex flex-col items-center gap-2",
|
|
145
|
-
children: [/* @__PURE__ */
|
|
146
|
-
name:
|
|
147
|
-
sport:
|
|
157
|
+
children: [/* @__PURE__ */ b(r, {
|
|
158
|
+
name: I,
|
|
159
|
+
sport: P.awayTeam?.sport,
|
|
148
160
|
size: 40
|
|
149
|
-
}), /* @__PURE__ */
|
|
161
|
+
}), /* @__PURE__ */ b("span", {
|
|
150
162
|
className: "max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary",
|
|
151
|
-
children:
|
|
163
|
+
children: I
|
|
152
164
|
})]
|
|
153
165
|
})]
|
|
154
166
|
})
|
|
155
167
|
]
|
|
156
|
-
}), /* @__PURE__ */
|
|
168
|
+
}), /* @__PURE__ */ b("div", {
|
|
157
169
|
className: "mt-6 flex justify-center",
|
|
158
|
-
children: /* @__PURE__ */
|
|
170
|
+
children: /* @__PURE__ */ x("button", {
|
|
159
171
|
type: "submit",
|
|
160
|
-
disabled:
|
|
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: [
|
|
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
|
-
|
|
178
|
+
M && /* @__PURE__ */ b("p", {
|
|
167
179
|
className: "mt-3 text-center text-sm text-status-error-text",
|
|
168
|
-
children:
|
|
180
|
+
children: M
|
|
169
181
|
})
|
|
170
182
|
]
|
|
171
183
|
})]
|
|
172
184
|
}),
|
|
173
|
-
|
|
185
|
+
P.competition && /* @__PURE__ */ x("button", {
|
|
174
186
|
type: "button",
|
|
175
|
-
onClick: () =>
|
|
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
|
|
190
|
+
/* @__PURE__ */ b(v, { className: "h-4 w-4" }),
|
|
179
191
|
" View ",
|
|
180
|
-
|
|
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 {
|
|
200
|
+
export { w as FixtureDetailPage };
|
|
189
201
|
|
|
190
202
|
//# sourceMappingURL=FixtureDetailPage.js.map
|