@burdenoff/microfe-atheletebridge 2026.805.1 → 2026.829.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/components/LoadingState.d.ts.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js +39 -32
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.d.ts.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js +45 -49
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- 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/DashboardPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js +56 -24
- package/dist/atheletebridge/pages/DashboardPage.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 +330 -174
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +110 -106
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js +80 -71
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js +244 -210
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.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 +526 -362
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +169 -143
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js +77 -72
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +104 -86
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js +54 -49
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js +286 -267
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js +185 -180
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js +70 -74
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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, tr: TranslateFn): {\n label: string;\n variant: 'default' | 'secondary' | 'destructive';\n} {\n if (ratio == null) {\n return { label: tr('atheletebridge.pages.analytics.acwr.noData', 'No data'), variant: 'secondary' };\n }\n if (ratio > 1.5) {\n return { label: tr('atheletebridge.pages.analytics.acwr.atRisk', 'At risk'), variant: 'destructive' };\n }\n if (ratio < 0.8 || ratio > 1.3) {\n return { label: tr('atheletebridge.pages.analytics.acwr.watch', 'Watch'), variant: 'secondary' };\n }\n return { label: tr('atheletebridge.pages.analytics.acwr.optimal', '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, tr('atheletebridge.rolePrefix.coach', 'Coach')),\n value: Number(entry.avgRating.toFixed(2)),\n })),\n [analytics.ratingByCoach, directory, tr]\n );\n\n const standingsColumns: ResponsiveTableColumn<AthStanding>[] = [\n {\n key: 'rank',\n header: tr('atheletebridge.pages.analytics.standings.rank', '#'),\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: tr('atheletebridge.pages.analytics.standings.team', 'Team'),\n priority: 'primary',\n cell: (row) => (\n <span className=\"flex items-center gap-2\">\n <Crest\n name={row.team?.name ?? tr('atheletebridge.pages.analytics.standings.teamFallback', '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 ?? tr('atheletebridge.pages.analytics.standings.teamFallback', 'Team')}</span>\n </span>\n ),\n },\n {\n key: 'played',\n header: tr('atheletebridge.pages.analytics.standings.played', '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: tr('atheletebridge.pages.analytics.standings.gd', '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: tr('atheletebridge.pages.analytics.standings.points', '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={tr('atheletebridge.pages.analytics.title', 'Coach analytics')}>\n <LoadingState label={tr('atheletebridge.pages.analytics.loading', 'Crunching coaching data…')} />\n </PageLayout>\n );\n }\n\n if (analytics.isError) {\n return (\n <PageLayout title={tr('atheletebridge.pages.analytics.title', 'Coach analytics')}>\n <ErrorState\n title={tr('atheletebridge.pages.analytics.errorTitle', 'Analytics unavailable')}\n message={tr('atheletebridge.pages.analytics.errorMessage', '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, tr);\n\n return (\n <PageLayout\n title={\n <span className=\"flex items-center gap-2\">\n <GradientText>{tr('atheletebridge.pages.analytics.title', 'Coach analytics')}</GradientText>\n <Badge variant=\"outline\">{tr('common.beta', 'Beta')}</Badge>\n </span>\n }\n description={tr('atheletebridge.pages.analytics.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={tr('atheletebridge.pages.analytics.emptyTitle', 'No coaching data yet')}\n description={tr('atheletebridge.pages.analytics.emptyDescription', '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={tr('atheletebridge.pages.analytics.totalSessions', '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\">{tr('atheletebridge.pages.analytics.avgRating', '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\">{tr('atheletebridge.pages.analytics.ratingOutOf', '/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\">{tr('atheletebridge.pages.analytics.noRatedSessions', 'No rated sessions yet')}</span>\n )}\n <span className=\"text-xs text-text-muted tabular-nums\">\n {analytics.ratedCount}{' '}\n {tr('atheletebridge.pages.analytics.ratedOf', 'rated of')}{' '}\n {analytics.totalSessions}\n </span>\n </GlassCard>\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={`${Math.round(analytics.completedPct)}%`}\n label={tr('atheletebridge.pages.analytics.sessionsCompleted', '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 {tr('atheletebridge.pages.analytics.statusOverviewTitle', 'Session status overview')}\n </h2>\n {statusSegments.length > 0 ? (\n <DonutChart\n segments={statusSegments}\n centerLabel={analytics.totalSessions}\n centerCaption={tr('atheletebridge.pages.analytics.sessionsCaption', 'sessions')}\n valueFormatter={(v) => v.toLocaleString()}\n />\n ) : (\n <p className=\"text-sm text-text-muted\">{tr('atheletebridge.pages.analytics.statusOverviewEmpty', '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 {tr('atheletebridge.pages.analytics.ratingByCoachTitle', '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, tr('atheletebridge.rolePrefix.coach', '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 {tr('atheletebridge.pages.analytics.ratingByCoachEmpty', '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\">{tr('atheletebridge.pages.analytics.weeklyLoadTitle', 'Weekly training load')}</h2>\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n type=\"button\"\n aria-label={tr('atheletebridge.pages.analytics.acwrAboutLabel', '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 {tr(\n 'atheletebridge.pages.analytics.acwrAbout',\n 'Total workout load per week (sum of real totalLoad). The badge is the acute:chronic workload ratio (last 7 days vs 28-day average) — a standard sports-science load indicator; 0.8–1.3 is the typical sweet spot.'\n )}\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={tr('atheletebridge.pages.analytics.trainingLoadLabel', '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 {tr('atheletebridge.pages.analytics.acuteLoad', '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 {tr('atheletebridge.pages.analytics.chronicLoad', '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 {tr('atheletebridge.pages.analytics.acwrAbbreviation', '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 {tr('atheletebridge.pages.analytics.weeklyLoadEmpty', '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\">{tr('atheletebridge.pages.analytics.sessionsPerWeekTitle', '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\">{tr('atheletebridge.pages.analytics.sessionsPerWeekEmpty', '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 {tr('atheletebridge.pages.analytics.standingsTitle', '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 {tr('atheletebridge.pages.analytics.viewFullStandings', '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, tr('atheletebridge.rolePrefix.athlete', '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, tr);\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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"}
|
|
@@ -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;AAkBH,wBAAgB,iBAAiB,gCAuLhC"}
|
|
@@ -1,202 +1,206 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
1
|
+
import { useAbTranslate as e } from "../../i18n.js";
|
|
2
|
+
import { useAthleteBridge as t } from "../../context/AthleteBridgeContext.js";
|
|
3
|
+
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
|
+
import { FIXTURE_TYPE_LABELS as r } from "../../components/domain.js";
|
|
5
|
+
import { Crest as i } from "../../components/Crest.js";
|
|
6
|
+
import { Scoreboard as a } from "../../components/Scoreboard.js";
|
|
7
|
+
import { ErrorState as o, LoadingState as s } from "../../components/LoadingState.js";
|
|
8
|
+
import { useFixture as c, useRecordFixtureResult as l } from "../../hooks/useAthleteBridgeApi.js";
|
|
9
|
+
import { FormField as u, fieldInputClass as d } from "../../components/form.js";
|
|
10
|
+
import { AvailabilityRsvpControl as f } from "../../components/availability/AvailabilityRsvpControl.js";
|
|
11
|
+
import { AvailabilityRosterPanel as p } from "../../components/availability/AvailabilityRosterPanel.js";
|
|
12
|
+
import { useState as m } from "react";
|
|
13
|
+
import { ArrowLeft as h, Flag as g, Loader2 as _, Minus as v, Trophy as y } from "lucide-react";
|
|
14
|
+
import { useParams as b } from "react-router";
|
|
15
|
+
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
16
|
+
import { EmphasisPanel as C, GlassCard as w } from "@burdenoff/fe-libs/ui";
|
|
16
17
|
//#region src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx
|
|
17
|
-
function
|
|
18
|
-
let { id:
|
|
19
|
-
if (
|
|
20
|
-
title: "Fixture",
|
|
21
|
-
children: /* @__PURE__ */
|
|
18
|
+
function T() {
|
|
19
|
+
let T = e(), { id: E } = b(), { navigateTo: D } = t(), O = c(E), k = l(), [A, j] = m(""), [M, N] = m(""), [P, F] = m(null);
|
|
20
|
+
if (O.isLoading) return /* @__PURE__ */ x(n, {
|
|
21
|
+
title: T("atheletebridge.pages.fixtureDetail.title", "Fixture"),
|
|
22
|
+
children: /* @__PURE__ */ x(s, { label: T("atheletebridge.pages.fixtureDetail.loading", "Loading fixture…") })
|
|
22
23
|
});
|
|
23
|
-
if (
|
|
24
|
-
title: "Fixture",
|
|
25
|
-
children: /* @__PURE__ */
|
|
26
|
-
title: "Fixture not found",
|
|
27
|
-
message: "This fixture does not exist or you do not have access to it.",
|
|
28
|
-
onRetry: () =>
|
|
24
|
+
if (O.isError || !O.data) return /* @__PURE__ */ x(n, {
|
|
25
|
+
title: T("atheletebridge.pages.fixtureDetail.title", "Fixture"),
|
|
26
|
+
children: /* @__PURE__ */ x(o, {
|
|
27
|
+
title: T("atheletebridge.pages.fixtureDetail.notFoundTitle", "Fixture not found"),
|
|
28
|
+
message: T("atheletebridge.pages.fixtureDetail.notFoundMessage", "This fixture does not exist or you do not have access to it."),
|
|
29
|
+
onRetry: () => D("/fixtures")
|
|
29
30
|
})
|
|
30
31
|
});
|
|
31
|
-
let
|
|
32
|
-
return /* @__PURE__ */
|
|
33
|
-
title: `${
|
|
34
|
-
description: `${
|
|
35
|
-
actions: /* @__PURE__ */
|
|
32
|
+
let I = O.data, L = I.homeTeam?.name ?? T("atheletebridge.pages.fixtureDetail.home", "Home"), R = I.awayTeam?.name ?? (I.awayTeamId ? T("atheletebridge.pages.fixtureDetail.away", "Away") : "TBD"), z = !!I.awayTeamId && (I.status === "SCHEDULED" || I.status === "LIVE");
|
|
33
|
+
return /* @__PURE__ */ S(n, {
|
|
34
|
+
title: `${L} vs ${R}`,
|
|
35
|
+
description: `${r[I.type]}${I.competition ? ` · ${I.competition}` : ""}`,
|
|
36
|
+
actions: /* @__PURE__ */ S("button", {
|
|
36
37
|
type: "button",
|
|
37
|
-
onClick: () =>
|
|
38
|
+
onClick: () => D("/fixtures"),
|
|
38
39
|
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",
|
|
39
|
-
children: [
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ x(h, { className: "h-4 w-4" }),
|
|
42
|
+
" ",
|
|
43
|
+
T("atheletebridge.pages.fixtureDetail.back", "Back")
|
|
44
|
+
]
|
|
40
45
|
}),
|
|
41
46
|
children: [
|
|
42
|
-
/* @__PURE__ */
|
|
43
|
-
fixture:
|
|
47
|
+
/* @__PURE__ */ x(a, {
|
|
48
|
+
fixture: I,
|
|
44
49
|
variant: "hero",
|
|
45
50
|
className: "mb-8"
|
|
46
51
|
}),
|
|
47
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ S("div", {
|
|
48
53
|
className: "mb-8 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_2fr]",
|
|
49
|
-
children: [/* @__PURE__ */
|
|
50
|
-
teamId:
|
|
51
|
-
fixtureId:
|
|
52
|
-
}), /* @__PURE__ */
|
|
53
|
-
teamId:
|
|
54
|
-
fixtureId:
|
|
54
|
+
children: [/* @__PURE__ */ x(f, {
|
|
55
|
+
teamId: I.homeTeamId,
|
|
56
|
+
fixtureId: I.id
|
|
57
|
+
}), /* @__PURE__ */ x(p, {
|
|
58
|
+
teamId: I.homeTeamId,
|
|
59
|
+
fixtureId: I.id
|
|
55
60
|
})]
|
|
56
61
|
}),
|
|
57
|
-
|
|
62
|
+
I.notes && /* @__PURE__ */ S(C, {
|
|
58
63
|
className: "mb-8",
|
|
59
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ x("h2", {
|
|
60
65
|
className: "mb-2 text-sm font-semibold text-text-primary",
|
|
61
|
-
children: "Notes"
|
|
62
|
-
}), /* @__PURE__ */
|
|
66
|
+
children: T("atheletebridge.pages.fixtureDetail.notes", "Notes")
|
|
67
|
+
}), /* @__PURE__ */ x("p", {
|
|
63
68
|
className: "text-sm text-text-secondary",
|
|
64
|
-
children:
|
|
69
|
+
children: I.notes
|
|
65
70
|
})]
|
|
66
71
|
}),
|
|
67
|
-
|
|
72
|
+
z && /* @__PURE__ */ S("section", {
|
|
68
73
|
className: "mb-8",
|
|
69
|
-
children: [/* @__PURE__ */
|
|
74
|
+
children: [/* @__PURE__ */ S("h2", {
|
|
70
75
|
className: "mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary",
|
|
71
|
-
children: [
|
|
72
|
-
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ x(g, { className: "h-5 w-5" }),
|
|
78
|
+
" ",
|
|
79
|
+
T("atheletebridge.pages.fixtureDetail.recordResult", "Record result")
|
|
80
|
+
]
|
|
81
|
+
}), /* @__PURE__ */ S(w, {
|
|
73
82
|
treatment: "insight",
|
|
74
83
|
className: "p-5 sm:p-6",
|
|
75
84
|
children: [
|
|
76
85
|
/* @__PURE__ */ x("p", {
|
|
77
86
|
className: "mb-5 text-sm text-text-secondary",
|
|
78
|
-
children:
|
|
79
|
-
"Recording a final score marks this fixture ",
|
|
80
|
-
/* @__PURE__ */ b("strong", { children: "Completed" }),
|
|
81
|
-
" and recomputes the competition standings for both teams."
|
|
82
|
-
]
|
|
87
|
+
children: T("atheletebridge.pages.fixtureDetail.recordResultDescription", "Recording a final score marks this fixture Completed and recomputes the competition standings for both teams.")
|
|
83
88
|
}),
|
|
84
|
-
/* @__PURE__ */
|
|
89
|
+
/* @__PURE__ */ S("form", {
|
|
85
90
|
onSubmit: async (e) => {
|
|
86
|
-
if (e.preventDefault(),
|
|
87
|
-
let t = Number.parseInt(
|
|
91
|
+
if (e.preventDefault(), F(null), !E) return;
|
|
92
|
+
let t = Number.parseInt(A, 10), n = Number.parseInt(M, 10);
|
|
88
93
|
if (Number.isNaN(t) || Number.isNaN(n)) {
|
|
89
|
-
|
|
94
|
+
F("Both scores are required and must be numbers.");
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
97
|
if (t < 0 || n < 0) {
|
|
93
|
-
|
|
98
|
+
F("Scores cannot be negative.");
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
96
101
|
try {
|
|
97
|
-
await
|
|
98
|
-
id:
|
|
102
|
+
await k.mutateAsync({
|
|
103
|
+
id: E,
|
|
99
104
|
input: {
|
|
100
105
|
homeScore: t,
|
|
101
106
|
awayScore: n
|
|
102
107
|
}
|
|
103
|
-
}),
|
|
108
|
+
}), j(""), N("");
|
|
104
109
|
} catch (e) {
|
|
105
|
-
|
|
110
|
+
F(e instanceof Error ? e.message : "Failed to record result.");
|
|
106
111
|
}
|
|
107
112
|
},
|
|
108
|
-
children: [/* @__PURE__ */
|
|
113
|
+
children: [/* @__PURE__ */ S("div", {
|
|
109
114
|
className: "flex flex-wrap items-end justify-center gap-4 sm:gap-6",
|
|
110
115
|
children: [
|
|
111
|
-
/* @__PURE__ */
|
|
116
|
+
/* @__PURE__ */ S("div", {
|
|
112
117
|
className: "flex items-end gap-3",
|
|
113
|
-
children: [/* @__PURE__ */
|
|
118
|
+
children: [/* @__PURE__ */ S("div", {
|
|
114
119
|
className: "flex flex-col items-center gap-2",
|
|
115
|
-
children: [/* @__PURE__ */
|
|
116
|
-
name:
|
|
117
|
-
sport:
|
|
120
|
+
children: [/* @__PURE__ */ x(i, {
|
|
121
|
+
name: L,
|
|
122
|
+
sport: I.homeTeam?.sport,
|
|
118
123
|
size: 40
|
|
119
|
-
}), /* @__PURE__ */
|
|
124
|
+
}), /* @__PURE__ */ x("span", {
|
|
120
125
|
className: "max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary",
|
|
121
|
-
children:
|
|
126
|
+
children: L
|
|
122
127
|
})]
|
|
123
|
-
}), /* @__PURE__ */
|
|
124
|
-
label: "Home",
|
|
128
|
+
}), /* @__PURE__ */ x(u, {
|
|
129
|
+
label: T("atheletebridge.pages.fixtureDetail.home", "Home"),
|
|
125
130
|
htmlFor: "homeScore",
|
|
126
131
|
className: "w-24",
|
|
127
|
-
children: /* @__PURE__ */
|
|
132
|
+
children: /* @__PURE__ */ x("input", {
|
|
128
133
|
id: "homeScore",
|
|
129
134
|
inputMode: "numeric",
|
|
130
|
-
className: `${
|
|
131
|
-
value:
|
|
132
|
-
onChange: (e) =>
|
|
135
|
+
className: `${d} text-center text-lg font-bold tabular-nums`,
|
|
136
|
+
value: A,
|
|
137
|
+
onChange: (e) => j(e.target.value),
|
|
133
138
|
placeholder: "0"
|
|
134
139
|
})
|
|
135
140
|
})]
|
|
136
141
|
}),
|
|
137
|
-
/* @__PURE__ */
|
|
142
|
+
/* @__PURE__ */ x(v, {
|
|
138
143
|
className: "mb-3 h-5 w-5 shrink-0 text-text-muted",
|
|
139
144
|
"aria-hidden": "true"
|
|
140
145
|
}),
|
|
141
|
-
/* @__PURE__ */
|
|
146
|
+
/* @__PURE__ */ S("div", {
|
|
142
147
|
className: "flex items-end gap-3",
|
|
143
|
-
children: [/* @__PURE__ */
|
|
144
|
-
label: "Away",
|
|
148
|
+
children: [/* @__PURE__ */ x(u, {
|
|
149
|
+
label: T("atheletebridge.pages.fixtureDetail.away", "Away"),
|
|
145
150
|
htmlFor: "awayScore",
|
|
146
151
|
className: "w-24",
|
|
147
|
-
children: /* @__PURE__ */
|
|
152
|
+
children: /* @__PURE__ */ x("input", {
|
|
148
153
|
id: "awayScore",
|
|
149
154
|
inputMode: "numeric",
|
|
150
|
-
className: `${
|
|
151
|
-
value:
|
|
152
|
-
onChange: (e) =>
|
|
155
|
+
className: `${d} text-center text-lg font-bold tabular-nums`,
|
|
156
|
+
value: M,
|
|
157
|
+
onChange: (e) => N(e.target.value),
|
|
153
158
|
placeholder: "0"
|
|
154
159
|
})
|
|
155
|
-
}), /* @__PURE__ */
|
|
160
|
+
}), /* @__PURE__ */ S("div", {
|
|
156
161
|
className: "flex flex-col items-center gap-2",
|
|
157
|
-
children: [/* @__PURE__ */
|
|
158
|
-
name:
|
|
159
|
-
sport:
|
|
162
|
+
children: [/* @__PURE__ */ x(i, {
|
|
163
|
+
name: R,
|
|
164
|
+
sport: I.awayTeam?.sport,
|
|
160
165
|
size: 40
|
|
161
|
-
}), /* @__PURE__ */
|
|
166
|
+
}), /* @__PURE__ */ x("span", {
|
|
162
167
|
className: "max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary",
|
|
163
|
-
children:
|
|
168
|
+
children: R
|
|
164
169
|
})]
|
|
165
170
|
})]
|
|
166
171
|
})
|
|
167
172
|
]
|
|
168
|
-
}), /* @__PURE__ */
|
|
173
|
+
}), /* @__PURE__ */ x("div", {
|
|
169
174
|
className: "mt-6 flex justify-center",
|
|
170
|
-
children: /* @__PURE__ */
|
|
175
|
+
children: /* @__PURE__ */ S("button", {
|
|
171
176
|
type: "submit",
|
|
172
|
-
disabled:
|
|
177
|
+
disabled: k.isPending,
|
|
173
178
|
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",
|
|
174
|
-
children: [
|
|
179
|
+
children: [k.isPending ? /* @__PURE__ */ x(_, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(y, { className: "h-4 w-4" }), T("atheletebridge.pages.fixtureDetail.recordResultAction", "Record result")]
|
|
175
180
|
})
|
|
176
181
|
})]
|
|
177
182
|
}),
|
|
178
|
-
|
|
183
|
+
P && /* @__PURE__ */ x("p", {
|
|
179
184
|
className: "mt-3 text-center text-sm text-status-error-text",
|
|
180
|
-
children:
|
|
185
|
+
children: P
|
|
181
186
|
})
|
|
182
187
|
]
|
|
183
188
|
})]
|
|
184
189
|
}),
|
|
185
|
-
|
|
190
|
+
I.competition && /* @__PURE__ */ S("button", {
|
|
186
191
|
type: "button",
|
|
187
|
-
onClick: () =>
|
|
192
|
+
onClick: () => D(`/standings?competition=${encodeURIComponent(I.competition ?? "")}`),
|
|
188
193
|
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",
|
|
189
194
|
children: [
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
" standings"
|
|
195
|
+
/* @__PURE__ */ x(y, { className: "h-4 w-4" }),
|
|
196
|
+
" ",
|
|
197
|
+
T("atheletebridge.pages.fixtureDetail.viewStandings", `View ${I.competition} standings`)
|
|
194
198
|
]
|
|
195
199
|
})
|
|
196
200
|
]
|
|
197
201
|
});
|
|
198
202
|
}
|
|
199
203
|
//#endregion
|
|
200
|
-
export {
|
|
204
|
+
export { T as FixtureDetailPage };
|
|
201
205
|
|
|
202
206
|
//# sourceMappingURL=FixtureDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixtureDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"sourcesContent":["/**\n * Fixture Detail\n * @module atheletebridge/pages/fixtures\n *\n * Reads a single fixture (athFixture) and lets the user record a final result\n * (athRecordFixtureResult) which marks it COMPLETED and recomputes standings.\n *\n * The header is a crest-vs-crest Scoreboard hero; the record-result panel shows\n * each team's crest beside its score input. Semantic tokens only, responsive.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Trophy, Loader2, Flag, Minus } from 'lucide-react';\nimport { GlassCard, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Scoreboard } from '../../components/Scoreboard';\nimport { Crest } from '../../components/Crest';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { AvailabilityRsvpControl } from '../../components/availability/AvailabilityRsvpControl';\nimport { AvailabilityRosterPanel } from '../../components/availability/AvailabilityRosterPanel';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixture, useRecordFixtureResult } from '../../hooks/useAthleteBridgeApi';\n\nexport function FixtureDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const fixture = useFixture(id);\n const recordResult = useRecordFixtureResult();\n\n const [homeScore, setHomeScore] = useState('');\n const [awayScore, setAwayScore] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (fixture.isLoading) {\n return (\n <PageLayout title=\"Fixture\">\n <LoadingState label=\"Loading fixture…\" />\n </PageLayout>\n );\n }\n\n if (fixture.isError || !fixture.data) {\n return (\n <PageLayout title=\"Fixture\">\n <ErrorState\n title=\"Fixture not found\"\n message=\"This fixture does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/fixtures')}\n />\n </PageLayout>\n );\n }\n\n const f = fixture.data;\n const homeName = f.homeTeam?.name ?? 'Home';\n const awayName = f.awayTeam?.name ?? (f.awayTeamId ? 'Away' : 'TBD');\n const canRecordResult =\n Boolean(f.awayTeamId) && (f.status === 'SCHEDULED' || f.status === 'LIVE');\n\n const handleRecord = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n const home = Number.parseInt(homeScore, 10);\n const away = Number.parseInt(awayScore, 10);\n if (Number.isNaN(home) || Number.isNaN(away)) {\n setError('Both scores are required and must be numbers.');\n return;\n }\n if (home < 0 || away < 0) {\n setError('Scores cannot be negative.');\n return;\n }\n try {\n await recordResult.mutateAsync({ id, input: { homeScore: home, awayScore: away } });\n setHomeScore('');\n setAwayScore('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record result.');\n }\n };\n\n return (\n <PageLayout\n title={`${homeName} vs ${awayName}`}\n description={`${FIXTURE_TYPE_LABELS[f.type]}${f.competition ? ` · ${f.competition}` : ''}`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n 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\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n {/* Crest-vs-crest hero scoreboard */}\n <Scoreboard fixture={f} variant=\"hero\" className=\"mb-8\" />\n\n {/* Availability / RSVP (BOFF-4134) — athlete's own response + coach roster view */}\n <div className=\"mb-8 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_2fr]\">\n <AvailabilityRsvpControl teamId={f.homeTeamId} fixtureId={f.id} />\n <AvailabilityRosterPanel teamId={f.homeTeamId} fixtureId={f.id} />\n </div>\n\n {f.notes && (\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">Notes</h2>\n <p className=\"text-sm text-text-secondary\">{f.notes}</p>\n </EmphasisPanel>\n )}\n\n {/* Record result */}\n {canRecordResult && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Flag className=\"h-5 w-5\" /> Record result\n </h2>\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <p className=\"mb-5 text-sm text-text-secondary\">\n Recording a final score marks this fixture <strong>Completed</strong> and recomputes\n the competition standings for both teams.\n </p>\n <form onSubmit={handleRecord}>\n <div className=\"flex flex-wrap items-end justify-center gap-4 sm:gap-6\">\n <div className=\"flex items-end gap-3\">\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={homeName} sport={f.homeTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {homeName}\n </span>\n </div>\n <FormField label=\"Home\" htmlFor=\"homeScore\" className=\"w-24\">\n <input\n id=\"homeScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={homeScore}\n onChange={(e) => setHomeScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n </div>\n\n <Minus className=\"mb-3 h-5 w-5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n\n <div className=\"flex items-end gap-3\">\n <FormField label=\"Away\" htmlFor=\"awayScore\" className=\"w-24\">\n <input\n id=\"awayScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={awayScore}\n onChange={(e) => setAwayScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={awayName} sport={f.awayTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {awayName}\n </span>\n </div>\n </div>\n </div>\n\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"submit\"\n disabled={recordResult.isPending}\n 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\"\n >\n {recordResult.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"h-4 w-4\" />\n )}\n Record result\n </button>\n </div>\n </form>\n {error && <p className=\"mt-3 text-center text-sm text-status-error-text\">{error}</p>}\n </GlassCard>\n </section>\n )}\n\n {f.competition && (\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(`/standings?competition=${encodeURIComponent(f.competition ?? '')}`)\n }\n 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\"\n >\n <Trophy className=\"h-4 w-4\" /> View {f.competition} standings\n </button>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FixtureDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureDetailPage.tsx"],"sourcesContent":["/**\n * Fixture Detail\n * @module atheletebridge/pages/fixtures\n *\n * Reads a single fixture (athFixture) and lets the user record a final result\n * (athRecordFixtureResult) which marks it COMPLETED and recomputes standings.\n *\n * The header is a crest-vs-crest Scoreboard hero; the record-result panel shows\n * each team's crest beside its score input. Semantic tokens only, responsive.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Trophy, Loader2, Flag, Minus } from 'lucide-react';\nimport { GlassCard, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Scoreboard } from '../../components/Scoreboard';\nimport { Crest } from '../../components/Crest';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { AvailabilityRsvpControl } from '../../components/availability/AvailabilityRsvpControl';\nimport { AvailabilityRosterPanel } from '../../components/availability/AvailabilityRosterPanel';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useFixture, useRecordFixtureResult } from '../../hooks/useAthleteBridgeApi';\nimport { useAbTranslate } from '../../i18n';\n\nexport function FixtureDetailPage() {\n const tr = useAbTranslate();\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const fixture = useFixture(id);\n const recordResult = useRecordFixtureResult();\n\n const [homeScore, setHomeScore] = useState('');\n const [awayScore, setAwayScore] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (fixture.isLoading) {\n return (\n <PageLayout title={tr('atheletebridge.pages.fixtureDetail.title', 'Fixture')}>\n <LoadingState label={tr('atheletebridge.pages.fixtureDetail.loading', 'Loading fixture…')} />\n </PageLayout>\n );\n }\n\n if (fixture.isError || !fixture.data) {\n return (\n <PageLayout title={tr('atheletebridge.pages.fixtureDetail.title', 'Fixture')}>\n <ErrorState\n title={tr('atheletebridge.pages.fixtureDetail.notFoundTitle', 'Fixture not found')}\n message={tr(\n 'atheletebridge.pages.fixtureDetail.notFoundMessage',\n 'This fixture does not exist or you do not have access to it.'\n )}\n onRetry={() => navigateTo('/fixtures')}\n />\n </PageLayout>\n );\n }\n\n const f = fixture.data;\n const homeName = f.homeTeam?.name ?? tr('atheletebridge.pages.fixtureDetail.home', 'Home');\n const awayName = f.awayTeam?.name ?? (f.awayTeamId ? tr('atheletebridge.pages.fixtureDetail.away', 'Away') : 'TBD');\n const canRecordResult =\n Boolean(f.awayTeamId) && (f.status === 'SCHEDULED' || f.status === 'LIVE');\n\n const handleRecord = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n const home = Number.parseInt(homeScore, 10);\n const away = Number.parseInt(awayScore, 10);\n if (Number.isNaN(home) || Number.isNaN(away)) {\n setError('Both scores are required and must be numbers.');\n return;\n }\n if (home < 0 || away < 0) {\n setError('Scores cannot be negative.');\n return;\n }\n try {\n await recordResult.mutateAsync({ id, input: { homeScore: home, awayScore: away } });\n setHomeScore('');\n setAwayScore('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record result.');\n }\n };\n\n return (\n <PageLayout\n title={`${homeName} vs ${awayName}`}\n description={`${FIXTURE_TYPE_LABELS[f.type]}${f.competition ? ` · ${f.competition}` : ''}`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n 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\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtureDetail.back', 'Back')}\n </button>\n }\n >\n {/* Crest-vs-crest hero scoreboard */}\n <Scoreboard fixture={f} variant=\"hero\" className=\"mb-8\" />\n\n {/* Availability / RSVP (BOFF-4134) — athlete's own response + coach roster view */}\n <div className=\"mb-8 grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_2fr]\">\n <AvailabilityRsvpControl teamId={f.homeTeamId} fixtureId={f.id} />\n <AvailabilityRosterPanel teamId={f.homeTeamId} fixtureId={f.id} />\n </div>\n\n {f.notes && (\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">{tr('atheletebridge.pages.fixtureDetail.notes', 'Notes')}</h2>\n <p className=\"text-sm text-text-secondary\">{f.notes}</p>\n </EmphasisPanel>\n )}\n\n {/* Record result */}\n {canRecordResult && (\n <section className=\"mb-8\">\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Flag className=\"h-5 w-5\" /> {tr('atheletebridge.pages.fixtureDetail.recordResult', 'Record result')}\n </h2>\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <p className=\"mb-5 text-sm text-text-secondary\">\n {tr(\n 'atheletebridge.pages.fixtureDetail.recordResultDescription',\n 'Recording a final score marks this fixture Completed and recomputes the competition standings for both teams.'\n )}\n </p>\n <form onSubmit={handleRecord}>\n <div className=\"flex flex-wrap items-end justify-center gap-4 sm:gap-6\">\n <div className=\"flex items-end gap-3\">\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={homeName} sport={f.homeTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {homeName}\n </span>\n </div>\n <FormField label={tr('atheletebridge.pages.fixtureDetail.home', 'Home')} htmlFor=\"homeScore\" className=\"w-24\">\n <input\n id=\"homeScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={homeScore}\n onChange={(e) => setHomeScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n </div>\n\n <Minus className=\"mb-3 h-5 w-5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n\n <div className=\"flex items-end gap-3\">\n <FormField label={tr('atheletebridge.pages.fixtureDetail.away', 'Away')} htmlFor=\"awayScore\" className=\"w-24\">\n <input\n id=\"awayScore\"\n inputMode=\"numeric\"\n className={`${fieldInputClass} text-center text-lg font-bold tabular-nums`}\n value={awayScore}\n onChange={(e) => setAwayScore(e.target.value)}\n placeholder=\"0\"\n />\n </FormField>\n <div className=\"flex flex-col items-center gap-2\">\n <Crest name={awayName} sport={f.awayTeam?.sport} size={40} />\n <span className=\"max-w-[8rem] truncate text-center text-xs font-medium text-text-secondary\">\n {awayName}\n </span>\n </div>\n </div>\n </div>\n\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"submit\"\n disabled={recordResult.isPending}\n 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\"\n >\n {recordResult.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.fixtureDetail.recordResultAction', 'Record result')}\n </button>\n </div>\n </form>\n {error && <p className=\"mt-3 text-center text-sm text-status-error-text\">{error}</p>}\n </GlassCard>\n </section>\n )}\n\n {f.competition && (\n <button\n type=\"button\"\n onClick={() =>\n navigateTo(`/standings?competition=${encodeURIComponent(f.competition ?? '')}`)\n }\n 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\"\n >\n <Trophy className=\"h-4 w-4\" /> {tr('atheletebridge.pages.fixtureDetail.viewStandings', `View ${f.competition} standings`)}\n </button>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FixturesListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"FixturesListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/fixtures/FixturesListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8BH,wBAAgB,gBAAgB,gCA0I/B"}
|