@burdenoff/microfe-atheletebridge 2026.801.3 → 2026.827.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/hooks/index.d.ts +1 -1
- package/dist/atheletebridge/hooks/index.d.ts.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +41 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +58 -28
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.d.ts +6 -2
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js +284 -130
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.d.ts +7 -0
- package/dist/atheletebridge/pages/performance/PerformancePage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js +392 -277
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/package.json +2 -2
|
@@ -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, a per-athlete acute:chronic training-load table, and a\n * 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 — both\n * workspace-wide and, in the per-athlete table, grouped by each log's real\n * `athleteId`. Where an athlete's workouts carry no recorded load the table\n * says so rather than showing a fabricated zero.\n * Semantic tokens only; responsive.\n */\n\nimport { useMemo } from 'react';\nimport { Activity, BarChart3, CheckCircle2, Info, ListChecks, Star, Users } 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 { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useCoachAnalytics,\n useUserDirectory,\n type AthleteTrainingLoad,\n type UserDirectory,\n} 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/**\n * Short person display: the resolved directory name, else a monogram-friendly\n * `<role> <id fragment>` so an unresolvable id still reads as a person rather\n * than as a raw uuid.\n */\nfunction shortName(name: string | undefined, id: string, rolePrefix: string): string {\n if (name && name.trim()) {\n return name;\n }\n return `${rolePrefix} ${id.slice(0, 4)}`;\n}\n\n/** Whole-number load display; loads are arbitrary units, so no decimals. */\nconst fmtLoad = (value: number): string => Math.round(value).toLocaleString();\n\n/**\n * A load bar scaled against the heaviest load in the table, so acute and\n * chronic columns stay visually comparable to each other and across rows.\n * Purely decorative — the number beside it is the fact.\n */\nfunction LoadBar({ value, max }: { value: number; max: number }) {\n const pct = max > 0 ? Math.round((value / max) * 100) : 0;\n return (\n <span className=\"block h-1.5 w-16 overflow-hidden rounded-full bg-bg-sunken\" aria-hidden=\"true\">\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(value > 0 ? 4 : 0, Math.min(100, pct))}%` }}\n />\n </span>\n );\n}\n\nexport function CoachAnalyticsPage() {\n const tr = useAbTranslate();\n const { navigateTo } = useAthleteBridge();\n const analytics = useCoachAnalytics();\n // One directory round-trip for both surfaces — `useUsersByIds` dedupes/sorts.\n const directoryIds = useMemo(\n () => [...analytics.coachIds, ...analytics.athleteIds],\n [analytics.coachIds, analytics.athleteIds]\n );\n const directory = useUserDirectory(directoryIds);\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: shortName(directory.nameOf(entry.coachId), entry.coachId, 'Coach'),\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 =\n analytics.totalSessions > 0 ||\n analytics.standings.length > 0 ||\n analytics.loadByAthlete.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 {shortName(directory.nameOf(entry.coachId), entry.coachId, 'Coach')}\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 {/* Training load by athlete — the workspace ACWR, broken out per athlete */}\n <TrainingLoadByAthlete\n rows={analytics.loadByAthlete}\n directory={directory}\n truncated={analytics.loadSampleTruncated}\n tr={tr}\n />\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\ninterface TrainingLoadByAthleteProps {\n rows: AthleteTrainingLoad[];\n directory: UserDirectory;\n /** The sampled workout page did not reach back 28 days — loads under-count. */\n truncated: boolean;\n tr: TranslateFn;\n}\n\n/**\n * \"Training load by athlete\" — the same acute:chronic maths the workspace-wide\n * ACWR badge already uses, broken out per athlete from each workout log's own\n * `athleteId` + `totalLoad` + `performedAt`.\n *\n * Nothing here is synthesised: an athlete whose 28-day workouts carry no\n * `totalLoad` renders as em-dashes and the shared `acwrBand(null)` \"No data\"\n * badge rather than as a real zero, and when the sampled page stops short of\n * 28 days the section says so instead of presenting a floor as a total.\n */\nfunction TrainingLoadByAthlete({ rows, directory, truncated, tr }: TrainingLoadByAthleteProps) {\n const maxLoad = useMemo(\n () => rows.reduce((max, row) => Math.max(max, row.acute, row.chronic), 0),\n [rows]\n );\n\n const columns: ResponsiveTableColumn<AthleteTrainingLoad>[] = useMemo(\n () => [\n {\n key: 'athlete',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.athlete', 'Athlete'),\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <UserAvatar name={directory.nameOf(row.athleteId)} id={row.athleteId} size=\"xs\" />\n <span className=\"min-w-0\">\n <span className=\"block truncate font-medium text-text-primary\">\n {shortName(directory.nameOf(row.athleteId), row.athleteId, 'Athlete')}\n </span>\n <span className=\"block text-xs tabular-nums text-text-muted\">\n {row.workoutCount}{' '}\n {tr('atheletebridge.pages.analytics.loadByAthlete.workouts28d', 'workouts · 28d')}\n </span>\n </span>\n </span>\n ),\n },\n {\n key: 'acute',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.acute', 'Acute load (7d)'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.acute', 'Acute load (7d)'),\n cell: (row) =>\n row.loadedCount === 0 ? (\n <span className=\"text-text-muted\">—</span>\n ) : (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-14 text-right font-medium tabular-nums text-text-primary\">\n {fmtLoad(row.acute)}\n </span>\n <LoadBar value={row.acute} max={maxLoad} />\n </span>\n ),\n },\n {\n key: 'chronic',\n header: tr(\n 'atheletebridge.pages.analytics.loadByAthlete.chronic',\n 'Chronic load (28d avg)'\n ),\n priority: 'secondary',\n mobileLabel: tr(\n 'atheletebridge.pages.analytics.loadByAthlete.chronic',\n 'Chronic load (28d avg)'\n ),\n cell: (row) =>\n row.loadedCount === 0 ? (\n <span className=\"text-text-muted\">—</span>\n ) : (\n <span className=\"flex items-center gap-2\">\n <span className=\"w-14 text-right font-medium tabular-nums text-text-primary\">\n {fmtLoad(row.chronic)}\n </span>\n <LoadBar value={row.chronic} max={maxLoad} />\n </span>\n ),\n },\n {\n key: 'ratio',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.ratio', 'Load ratio'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.ratio', 'Load ratio'),\n cell: (row) =>\n row.ratio != null ? (\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {row.ratio.toFixed(2)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'band',\n header: tr('atheletebridge.pages.analytics.loadByAthlete.status', 'Status'),\n priority: 'primary',\n mobileLabel: tr('atheletebridge.pages.analytics.loadByAthlete.status', 'Status'),\n cell: (row) => {\n const rowBand = acwrBand(row.ratio);\n return <Badge variant={rowBand.variant}>{rowBand.label}</Badge>;\n },\n },\n ],\n [directory, maxLoad, tr]\n );\n\n return (\n <section>\n <div className=\"mb-4 flex flex-wrap items-center gap-2\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Users className=\"h-5 w-5 text-text-muted\" />\n {tr('atheletebridge.pages.analytics.loadByAthlete.title', 'Training load by athlete')}\n </h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={tr(\n 'atheletebridge.pages.analytics.loadByAthlete.aboutLabel',\n 'About per-athlete training load'\n )}\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 {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.about',\n 'Each athlete’s acute:chronic workload ratio — their last 7 days of logged workout load against their 28-day weekly average. 0.8–1.3 is the typical sweet spot; above 1.5 is a spike worth reviewing.'\n )}\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n </div>\n\n {rows.length === 0 ? (\n <GlassCard treatment=\"insight\" className=\"p-6\">\n <p className=\"text-sm text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.empty',\n 'No workouts logged in the last 28 days. Log workouts with a total load to compare athletes.'\n )}\n </p>\n </GlassCard>\n ) : (\n <>\n <GlassCard treatment=\"insight\" className=\"p-2 sm:p-4\">\n <ResponsiveTable\n columns={columns}\n data={rows}\n rowKey={(row) => row.athleteId}\n caption={tr(\n 'atheletebridge.pages.analytics.loadByAthlete.caption',\n 'Acute and chronic training load per athlete over the last 28 days'\n )}\n />\n </GlassCard>\n {truncated ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.analytics.loadByAthlete.truncated',\n 'Computed from the most recent page of workout logs, which does not reach back a full 28 days — these loads are a floor, not a total.'\n )}\n </p>\n ) : null}\n </>\n )}\n </section>\n );\n}\n"],"mappings":"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"}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* from the real time-series (nothing fabricated). Workout history renders as a
|
|
14
14
|
* responsive table that reflows to cards on mobile, with a thumb-reachable
|
|
15
15
|
* sticky "Log workout" action.
|
|
16
|
+
*
|
|
17
|
+
* A rolling date-range window ("Last 7 / 28 / 90 days", or All time) scopes
|
|
18
|
+
* everything below it: which readings a tile is built from, what its delta is
|
|
19
|
+
* measured against ("vs previous 7 days" really means the preceding 7 days,
|
|
20
|
+
* not simply the preceding reading), and which workouts the history lists.
|
|
21
|
+
* The window is applied client-side — see WINDOW_FETCH_LIMIT for why, and for
|
|
22
|
+
* the coverage check that stops a truncated page reading as a complete one.
|
|
16
23
|
*/
|
|
17
24
|
export declare function PerformancePage(): import("react").JSX.Element;
|
|
18
25
|
//# sourceMappingURL=PerformancePage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PerformancePage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"PerformancePage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAuNH,wBAAgB,eAAe,gCAkC9B"}
|