@burdenoff/microfe-atheletebridge 2026.706.1 → 2026.706.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"MatchupRow.js","names":[],"sources":["../../../src/atheletebridge/components/MatchupRow.tsx"],"sourcesContent":["/**\n * MatchupRow\n * @module atheletebridge/components\n *\n * A compact single-line crest-vs-crest fixture row for the dashboard \"Upcoming\n * fixtures\" feed and team-detail fixture lists. Reuses the Scoreboard building\n * blocks (crest sides + score/vs centerpiece). Semantic tokens only.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { CalendarDays, ChevronRight, Trophy } from 'lucide-react';\nimport { Badge } from '@burdenoff/fe-libs/ui';\nimport { MatchupSide, ScoreCenter, resolveMatchup } from './Scoreboard';\nimport { StatusBadge } from './StatusBadge';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthFixture } from '../types';\n\nexport interface MatchupRowProps {\n fixture: AthFixture;\n onClick?: () => void;\n /**\n * Right-hand rail content. Defaults to date + competition + status. Pass\n * `false` to hide the rail entirely, or a node to override it.\n */\n rail?: ReactNode | false;\n className?: string;\n}\n\nconst DefaultRail: FC<{ fixture: AthFixture }> = ({ fixture }) => (\n <div className=\"flex flex-col items-end gap-1 text-right\">\n <span className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <CalendarDays className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n {formatDate(fixture.scheduledAt)}\n </span>\n <div className=\"flex items-center gap-1.5\">\n {fixture.competition ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n {fixture.type === 'CUP' || fixture.type === 'TOURNAMENT' ? (\n <Trophy className=\"h-3 w-3\" aria-hidden=\"true\" />\n ) : null}\n {fixture.competition}\n </Badge>\n ) : null}\n <StatusBadge status={fixture.status} />\n </div>\n </div>\n);\n\n/** Compact crest-vs-crest fixture row (dashboard / team-detail feeds). */\nexport const MatchupRow: FC<MatchupRowProps> = ({ fixture, onClick, rail, className }) => {\n const matchup = resolveMatchup(fixture);\n const clickable = Boolean(onClick);\n\n const railNode =\n rail === false ? null : rail !== undefined ? rail : <DefaultRail fixture={fixture} />;\n\n // When there's no rail to carry the date, surface the kickoff under the \"vs\".\n const timeLabel = railNode == null && !matchup.hasScore ? formatDate(fixture.scheduledAt) : null;\n\n const content = (\n <>\n <div className=\"grid flex-1 grid-cols-[1fr_auto_1fr] items-center gap-2 sm:gap-3\">\n <MatchupSide\n name={matchup.homeName}\n sport={matchup.homeSport}\n align=\"end\"\n won={matchup.homeWon}\n dimmed={matchup.awayWon}\n crestSize={36}\n />\n <ScoreCenter matchup={matchup} time={timeLabel} size=\"sm\" />\n <MatchupSide\n name={matchup.awayName}\n sport={matchup.awaySport}\n align=\"start\"\n won={matchup.awayWon}\n dimmed={matchup.homeWon}\n crestSize={36}\n />\n </div>\n {railNode ? (\n <div className=\"hidden items-center gap-3 border-l border-border-seam pl-3 sm:flex md:pl-4\">\n {railNode}\n {clickable ? (\n <ChevronRight className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n ) : null}\n </div>\n ) : null}\n </>\n );\n\n if (clickable) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n 'flex w-full items-center gap-3 rounded-[var(--radius-card)] px-1 py-3 text-left transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] md:gap-4',\n className\n )}\n >\n {content}\n </button>\n );\n }\n\n return <div className={cn('flex w-full items-center gap-3 py-3 md:gap-4', className)}>{content}</div>;\n};\n"],"mappings":";;;;;;;;AA6BA,IAAM,KAA4C,EAAE,iBAClD,kBAAC,OAAD;CAAK,WAAU;WAAf,CACE,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD;GAAc,WAAU;GAAuB,eAAY;GAAS,CAAA,EACnE,EAAW,EAAQ,YAAY,CAC3B;KACP,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAQ,cACP,kBAAC,GAAD;GAAO,SAAQ;GAAY,WAAU;aAArC,CACG,EAAQ,SAAS,SAAS,EAAQ,SAAS,eAC1C,kBAAC,GAAD;IAAQ,WAAU;IAAU,eAAY;IAAS,CAAA,GAC/C,MACH,EAAQ,YACH;OACN,MACJ,kBAAC,GAAD,EAAa,QAAQ,EAAQ,QAAU,CAAA,CACnC;IACF;IAIK,KAAmC,EAAE,YAAS,YAAS,SAAM,mBAAgB;CACxF,IAAM,IAAU,EAAe,EAAQ,EACjC,IAAY,EAAQ,GAEpB,IACJ,MAAS,KAAQ,OAAO,MAAS,KAAA,IAAmB,kBAAC,GAAD,EAAsB,YAAW,CAAA,GAAxC,GAGzC,IAAY,KAAY,QAAQ,CAAC,EAAQ,WAAW,EAAW,EAAQ,YAAY,GAAG,MAEtF,IACJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,MAAM,EAAQ;IACd,OAAO,EAAQ;IACf,OAAM;IACN,KAAK,EAAQ;IACb,QAAQ,EAAQ;IAChB,WAAW;IACX,CAAA;GACF,kBAAC,GAAD;IAAsB;IAAS,MAAM;IAAW,MAAK;IAAO,CAAA;GAC5D,kBAAC,GAAD;IACE,MAAM,EAAQ;IACd,OAAO,EAAQ;IACf,OAAM;IACN,KAAK,EAAQ;IACb,QAAQ,EAAQ;IAChB,WAAW;IACX,CAAA;GACE;KACL,IACC,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,GACA,IACC,kBAAC,GAAD;GAAc,WAAU;GAAmC,eAAY;GAAS,CAAA,GAC9E,KACA;MACJ,KACH,EAAA,CAAA;AAkBL,QAfI,IAEA,kBAAC,UAAD;EACE,MAAK;EACI;EACT,WAAW,EACT,8NACA,EACD;YAEA;EACM,CAAA,GAIN,kBAAC,OAAD;EAAK,WAAW,EAAG,gDAAgD,EAAU;YAAG;EAAc,CAAA"}
1
+ {"version":3,"file":"MatchupRow.js","names":[],"sources":["../../../src/atheletebridge/components/MatchupRow.tsx"],"sourcesContent":["/**\n * MatchupRow\n * @module atheletebridge/components\n *\n * A compact single-line crest-vs-crest fixture row for the dashboard \"Upcoming\n * fixtures\" feed and team-detail fixture lists. Reuses the Scoreboard building\n * blocks (crest sides + score/vs centerpiece). Semantic tokens only.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { CalendarDays, ChevronRight, Trophy } from 'lucide-react';\nimport { Badge } from '@burdenoff/fe-libs/ui';\nimport { MatchupSide, ScoreCenter, resolveMatchup } from './Scoreboard';\nimport { StatusBadge } from './StatusBadge';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthFixture } from '../types';\n\nexport interface MatchupRowProps {\n fixture: AthFixture;\n onClick?: () => void;\n /**\n * Right-hand rail content. Defaults to date + competition + status. Pass\n * `false` to hide the rail entirely, or a node to override it.\n */\n rail?: ReactNode | false;\n className?: string;\n}\n\nconst DefaultRail: FC<{ fixture: AthFixture }> = ({ fixture }) => (\n <div className=\"flex flex-col items-end gap-1 text-right\">\n <span className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <CalendarDays className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n {formatDate(fixture.scheduledAt)}\n </span>\n <div className=\"flex items-center gap-1.5\">\n {fixture.competition ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n {fixture.type === 'CUP' || fixture.type === 'TOURNAMENT' ? (\n <Trophy className=\"h-3 w-3\" aria-hidden=\"true\" />\n ) : null}\n {fixture.competition}\n </Badge>\n ) : null}\n <StatusBadge status={fixture.status} />\n </div>\n </div>\n);\n\n/** Compact crest-vs-crest fixture row (dashboard / team-detail feeds). */\nexport const MatchupRow: FC<MatchupRowProps> = ({ fixture, onClick, rail, className }) => {\n const matchup = resolveMatchup(fixture);\n const clickable = Boolean(onClick);\n\n const railNode =\n rail === false ? null : rail !== undefined ? rail : <DefaultRail fixture={fixture} />;\n\n // When there's no rail to carry the date, surface the kickoff under the \"vs\".\n const timeLabel = railNode == null && !matchup.hasScore ? formatDate(fixture.scheduledAt) : null;\n\n const content = (\n <>\n <div className=\"grid flex-1 grid-cols-[1fr_auto_1fr] items-center gap-2 sm:gap-3\">\n <MatchupSide\n name={matchup.homeName}\n sport={matchup.homeSport}\n align=\"end\"\n won={matchup.homeWon}\n dimmed={matchup.awayWon}\n crestSize={36}\n />\n <ScoreCenter matchup={matchup} time={timeLabel} size=\"sm\" />\n <MatchupSide\n name={matchup.awayName}\n sport={matchup.awaySport}\n align=\"start\"\n won={matchup.awayWon}\n dimmed={matchup.homeWon}\n crestSize={36}\n />\n </div>\n {railNode ? (\n <div className=\"hidden items-center gap-3 border-l border-border-seam pl-3 sm:flex md:pl-4\">\n {railNode}\n {clickable ? (\n <ChevronRight className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n ) : null}\n </div>\n ) : null}\n </>\n );\n\n if (clickable) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n 'flex w-full items-center gap-3 rounded-[var(--radius-card)] px-1 py-3 text-left transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] md:gap-4',\n className\n )}\n >\n {content}\n </button>\n );\n }\n\n return (\n <div className={cn('flex w-full items-center gap-3 py-3 md:gap-4', className)}>{content}</div>\n );\n};\n"],"mappings":";;;;;;;;AA6BA,IAAM,KAA4C,EAAE,iBAClD,kBAAC,OAAD;CAAK,WAAU;WAAf,CACE,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,GAAD;GAAc,WAAU;GAAuB,eAAY;GAAS,CAAA,EACnE,EAAW,EAAQ,YAAY,CAC3B;KACP,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAQ,cACP,kBAAC,GAAD;GAAO,SAAQ;GAAY,WAAU;aAArC,CACG,EAAQ,SAAS,SAAS,EAAQ,SAAS,eAC1C,kBAAC,GAAD;IAAQ,WAAU;IAAU,eAAY;IAAS,CAAA,GAC/C,MACH,EAAQ,YACH;OACN,MACJ,kBAAC,GAAD,EAAa,QAAQ,EAAQ,QAAU,CAAA,CACnC;IACF;IAIK,KAAmC,EAAE,YAAS,YAAS,SAAM,mBAAgB;CACxF,IAAM,IAAU,EAAe,EAAQ,EACjC,IAAY,EAAQ,GAEpB,IACJ,MAAS,KAAQ,OAAO,MAAS,KAAA,IAAmB,kBAAC,GAAD,EAAsB,YAAW,CAAA,GAAxC,GAGzC,IAAY,KAAY,QAAQ,CAAC,EAAQ,WAAW,EAAW,EAAQ,YAAY,GAAG,MAEtF,IACJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,MAAM,EAAQ;IACd,OAAO,EAAQ;IACf,OAAM;IACN,KAAK,EAAQ;IACb,QAAQ,EAAQ;IAChB,WAAW;IACX,CAAA;GACF,kBAAC,GAAD;IAAsB;IAAS,MAAM;IAAW,MAAK;IAAO,CAAA;GAC5D,kBAAC,GAAD;IACE,MAAM,EAAQ;IACd,OAAO,EAAQ;IACf,OAAM;IACN,KAAK,EAAQ;IACb,QAAQ,EAAQ;IAChB,WAAW;IACX,CAAA;GACE;KACL,IACC,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,GACA,IACC,kBAAC,GAAD;GAAc,WAAU;GAAmC,eAAY;GAAS,CAAA,GAC9E,KACA;MACJ,KACH,EAAA,CAAA;AAkBL,QAfI,IAEA,kBAAC,UAAD;EACE,MAAK;EACI;EACT,WAAW,EACT,8NACA,EACD;YAEA;EACM,CAAA,GAKX,kBAAC,OAAD;EAAK,WAAW,EAAG,gDAAgD,EAAU;YAAG;EAAc,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../src/atheletebridge/components/MetricsCard.tsx"],"sourcesContent":["/**\n * Metrics Card Component\n * @module atheletebridge/components\n *\n * Boff UI 2.0: KPI tiles use the glassmorphic `GlassCard` \"metric\" treatment\n * (accent wash + seam border + soft pop-lift). The hero metric per page can\n * opt into the brighter `glass` treatment with an accent glow via `featured`.\n *\n * The original API (`icon`, `value`, `label`, `featured`, `className`) is\n * unchanged. Optional props enrich the tile toward the mock's KPI style: a teal\n * `tone`, a signed `delta` pill, and a footer `sparkline` trend — all opt-in, so\n * existing call sites render exactly as before. Numerals are tabular.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { ArrowDownRight, ArrowUpRight } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Sparkline } from './viz';\nimport { cn } from '../utils/cn';\n\ntype MetricTone = 'default' | 'accent' | 'success';\n\ninterface MetricsCardProps {\n icon: ReactNode;\n value: string | number;\n label: string;\n /** Render as the single featured hero metric (glass treatment + glow). */\n featured?: boolean;\n /** Value color: `default` (primary), `accent` (teal), or `success` (green). */\n tone?: MetricTone;\n /** Signed change; positive is good unless `invertDelta`. Renders a pill. */\n delta?: number;\n /** Unit appended to the delta (default \"%\"). */\n deltaSuffix?: string;\n /** Context line beside the delta, e.g. \"vs last month\". */\n deltaLabel?: string;\n /** When true (e.g. injuries), a negative delta is the good outcome. */\n invertDelta?: boolean;\n /** Optional trend rendered as a footer sparkline. */\n sparkline?: number[];\n className?: string;\n}\n\nconst TONE_CLASS: Record<MetricTone, string> = {\n default: 'text-text-primary',\n accent: 'text-[var(--color-accent-teal)]',\n success: 'text-status-success-text',\n};\n\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n featured = false,\n tone = 'default',\n delta,\n deltaSuffix = '%',\n deltaLabel,\n invertDelta = false,\n sparkline,\n className,\n}) => {\n const hasDelta = typeof delta === 'number';\n const isNeutral = (delta ?? 0) === 0;\n const positiveIsGood = invertDelta ? (delta ?? 0) < 0 : (delta ?? 0) > 0;\n const rising = (delta ?? 0) > 0;\n const deltaTone = isNeutral\n ? 'text-text-muted'\n : positiveIsGood\n ? 'text-status-success-text'\n : 'text-status-error-text';\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('p-6', className)}\n >\n <div className=\"relative text-text-secondary\">{icon}</div>\n <div className=\"relative mt-3\">\n <p className={cn('text-2xl font-bold tabular-nums', TONE_CLASS[tone])}>{value}</p>\n <p className=\"mt-1 text-sm text-text-secondary\">{label}</p>\n </div>\n\n {hasDelta ? (\n <div className=\"relative mt-2 flex items-center gap-1.5 text-sm\">\n <span className={cn('inline-flex items-center gap-0.5 font-medium tabular-nums', deltaTone)}>\n {isNeutral ? null : rising ? (\n <ArrowUpRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n ) : (\n <ArrowDownRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n )}\n {rising ? '+' : ''}\n {delta}\n {deltaSuffix}\n </span>\n {deltaLabel ? <span className=\"text-text-muted\">{deltaLabel}</span> : null}\n </div>\n ) : null}\n\n {sparkline && sparkline.length > 1 ? (\n <div className=\"relative mt-3\">\n <Sparkline data={sparkline} variant=\"area\" label={`${label} trend`} height={32} />\n </div>\n ) : null}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;AA2CA,IAAM,IAAyC;CAC7C,SAAS;CACT,QAAQ;CACR,SAAS;CACV,EAEY,KAAqC,EAChD,SACA,UACA,UACA,cAAW,IACX,UAAO,WACP,UACA,iBAAc,KACd,eACA,iBAAc,IACd,cACA,mBACI;CACJ,IAAM,IAAW,OAAO,KAAU,UAC5B,KAAa,KAAS,OAAO,GAC7B,IAAiB,KAAe,KAAS,KAAK,KAAK,KAAS,KAAK,GACjE,KAAU,KAAS,KAAK,GACxB,IAAY,IACd,oBACA,IACE,6BACA;AAEN,QACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,OAAO,EAAU;YAHjC;GAKE,kBAAC,OAAD;IAAK,WAAU;cAAgC;IAAW,CAAA;GAC1D,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAW,EAAG,mCAAmC,EAAW,GAAM;eAAG;KAAU,CAAA,EAClF,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAU,CAAA,CACvD;;GAEL,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAW,EAAG,6DAA6D,EAAU;eAA3F;MACG,IAAY,OACX,EADkB,IACjB,IAEA,GAFD;OAAc,eAAY;OAAO,WAAU;OAAY,CAEE;MAE1D,IAAS,MAAM;MACf;MACA;MACI;QACN,IAAa,kBAAC,QAAD;KAAM,WAAU;eAAmB;KAAkB,CAAA,GAAG,KAClE;QACJ;GAEH,KAAa,EAAU,SAAS,IAC/B,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,MAAM;KAAW,SAAQ;KAAO,OAAO,GAAG,EAAM;KAAS,QAAQ;KAAM,CAAA;IAC9E,CAAA,GACJ;GACM"}
1
+ {"version":3,"file":"MetricsCard.js","names":[],"sources":["../../../src/atheletebridge/components/MetricsCard.tsx"],"sourcesContent":["/**\n * Metrics Card Component\n * @module atheletebridge/components\n *\n * Boff UI 2.0: KPI tiles use the glassmorphic `GlassCard` \"metric\" treatment\n * (accent wash + seam border + soft pop-lift). The hero metric per page can\n * opt into the brighter `glass` treatment with an accent glow via `featured`.\n *\n * The original API (`icon`, `value`, `label`, `featured`, `className`) is\n * unchanged. Optional props enrich the tile toward the mock's KPI style: a teal\n * `tone`, a signed `delta` pill, and a footer `sparkline` trend — all opt-in, so\n * existing call sites render exactly as before. Numerals are tabular.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { ArrowDownRight, ArrowUpRight } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Sparkline } from './viz';\nimport { cn } from '../utils/cn';\n\ntype MetricTone = 'default' | 'accent' | 'success';\n\ninterface MetricsCardProps {\n icon: ReactNode;\n value: string | number;\n label: string;\n /** Render as the single featured hero metric (glass treatment + glow). */\n featured?: boolean;\n /** Value color: `default` (primary), `accent` (teal), or `success` (green). */\n tone?: MetricTone;\n /** Signed change; positive is good unless `invertDelta`. Renders a pill. */\n delta?: number;\n /** Unit appended to the delta (default \"%\"). */\n deltaSuffix?: string;\n /** Context line beside the delta, e.g. \"vs last month\". */\n deltaLabel?: string;\n /** When true (e.g. injuries), a negative delta is the good outcome. */\n invertDelta?: boolean;\n /** Optional trend rendered as a footer sparkline. */\n sparkline?: number[];\n className?: string;\n}\n\nconst TONE_CLASS: Record<MetricTone, string> = {\n default: 'text-text-primary',\n accent: 'text-[var(--color-accent-teal)]',\n success: 'text-status-success-text',\n};\n\nexport const MetricsCard: FC<MetricsCardProps> = ({\n icon,\n value,\n label,\n featured = false,\n tone = 'default',\n delta,\n deltaSuffix = '%',\n deltaLabel,\n invertDelta = false,\n sparkline,\n className,\n}) => {\n const hasDelta = typeof delta === 'number';\n const isNeutral = (delta ?? 0) === 0;\n const positiveIsGood = invertDelta ? (delta ?? 0) < 0 : (delta ?? 0) > 0;\n const rising = (delta ?? 0) > 0;\n const deltaTone = isNeutral\n ? 'text-text-muted'\n : positiveIsGood\n ? 'text-status-success-text'\n : 'text-status-error-text';\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('p-6', className)}\n >\n <div className=\"relative text-text-secondary\">{icon}</div>\n <div className=\"relative mt-3\">\n <p className={cn('text-2xl font-bold tabular-nums', TONE_CLASS[tone])}>{value}</p>\n <p className=\"mt-1 text-sm text-text-secondary\">{label}</p>\n </div>\n\n {hasDelta ? (\n <div className=\"relative mt-2 flex items-center gap-1.5 text-sm\">\n <span\n className={cn('inline-flex items-center gap-0.5 font-medium tabular-nums', deltaTone)}\n >\n {isNeutral ? null : rising ? (\n <ArrowUpRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n ) : (\n <ArrowDownRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n )}\n {rising ? '+' : ''}\n {delta}\n {deltaSuffix}\n </span>\n {deltaLabel ? <span className=\"text-text-muted\">{deltaLabel}</span> : null}\n </div>\n ) : null}\n\n {sparkline && sparkline.length > 1 ? (\n <div className=\"relative mt-3\">\n <Sparkline data={sparkline} variant=\"area\" label={`${label} trend`} height={32} />\n </div>\n ) : null}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;AA2CA,IAAM,IAAyC;CAC7C,SAAS;CACT,QAAQ;CACR,SAAS;CACV,EAEY,KAAqC,EAChD,SACA,UACA,UACA,cAAW,IACX,UAAO,WACP,UACA,iBAAc,KACd,eACA,iBAAc,IACd,cACA,mBACI;CACJ,IAAM,IAAW,OAAO,KAAU,UAC5B,KAAa,KAAS,OAAO,GAC7B,IAAiB,KAAe,KAAS,KAAK,KAAK,KAAS,KAAK,GACjE,KAAU,KAAS,KAAK,GACxB,IAAY,IACd,oBACA,IACE,6BACA;AAEN,QACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,OAAO,EAAU;YAHjC;GAKE,kBAAC,OAAD;IAAK,WAAU;cAAgC;IAAW,CAAA;GAC1D,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAW,EAAG,mCAAmC,EAAW,GAAM;eAAG;KAAU,CAAA,EAClF,kBAAC,KAAD;KAAG,WAAU;eAAoC;KAAU,CAAA,CACvD;;GAEL,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KACE,WAAW,EAAG,6DAA6D,EAAU;eADvF;MAGG,IAAY,OACX,EADkB,IACjB,IAEA,GAFD;OAAc,eAAY;OAAO,WAAU;OAAY,CAEE;MAE1D,IAAS,MAAM;MACf;MACA;MACI;QACN,IAAa,kBAAC,QAAD;KAAM,WAAU;eAAmB;KAAkB,CAAA,GAAG,KAClE;QACJ;GAEH,KAAa,EAAU,SAAS,IAC/B,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,MAAM;KAAW,SAAQ;KAAO,OAAO,GAAG,EAAM;KAAS,QAAQ;KAAM,CAAA;IAC9E,CAAA,GACJ;GACM"}
@@ -1 +1 @@
1
- {"version":3,"file":"Scoreboard.js","names":[],"sources":["../../../src/atheletebridge/components/Scoreboard.tsx"],"sourcesContent":["/**\n * Scoreboard\n * @module atheletebridge/components\n *\n * Crest-vs-crest fixture presentation. `Scoreboard` renders the full match card\n * (competition strip → crest / score / crest → venue) used by the fixtures list\n * and fixture detail; the compact single-line `MatchupRow` (separate file)\n * reuses the same building blocks for the dashboard + team-detail feeds.\n *\n * All colors are semantic tokens. Winner side is tinted with\n * status-success-text, loser muted — driven purely by the real score.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { CalendarClock, MapPin, Trophy } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, FIXTURE_TYPE_LABELS } from './domain';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthFixture, AthSport } from '../types';\n\n// ── Shared matchup model ─────────────────────────────────────────────────────\n\nexport interface Matchup {\n homeName: string;\n awayName: string;\n homeSport?: AthSport;\n awaySport?: AthSport;\n homeScore: number | null;\n awayScore: number | null;\n hasScore: boolean;\n homeWon: boolean;\n awayWon: boolean;\n}\n\n/** Derive the display matchup (names, scores, winner) from a fixture. */\nexport function resolveMatchup(fixture: AthFixture): Matchup {\n const homeName = fixture.homeTeam?.name ?? 'Home';\n const awayName = fixture.awayTeam?.name ?? (fixture.awayTeamId ? 'Away' : 'TBD');\n const hasScore = fixture.homeScore !== null && fixture.awayScore !== null;\n const homeScore = fixture.homeScore;\n const awayScore = fixture.awayScore;\n const homeWon = hasScore && (homeScore ?? 0) > (awayScore ?? 0);\n const awayWon = hasScore && (awayScore ?? 0) > (homeScore ?? 0);\n return {\n homeName,\n awayName,\n homeSport: fixture.homeTeam?.sport,\n awaySport: fixture.awayTeam?.sport,\n homeScore,\n awayScore,\n hasScore,\n homeWon,\n awayWon,\n };\n}\n\n// ── One team flank (crest + name) ────────────────────────────────────────────\n\nexport interface MatchupSideProps {\n name: string;\n sport?: AthSport;\n align?: 'start' | 'end';\n /** Winner emphasis — brightens the name. */\n won?: boolean;\n /** Loser de-emphasis when the fixture has a score. */\n dimmed?: boolean;\n crestSize?: number;\n /** Stack crest above name (hero/card) instead of inline (row). */\n stacked?: boolean;\n className?: string;\n}\n\nexport const MatchupSide: FC<MatchupSideProps> = ({\n name,\n sport,\n align = 'start',\n won = false,\n dimmed = false,\n crestSize = 40,\n stacked = false,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-w-0 items-center gap-3',\n stacked ? 'flex-col text-center' : align === 'end' ? 'flex-row-reverse text-right' : 'text-left',\n className\n )}\n >\n <Crest name={name} sport={sport} size={crestSize} />\n <span\n className={cn(\n 'min-w-0 truncate font-semibold',\n stacked ? 'max-w-[10rem] text-sm' : 'text-sm',\n won ? 'text-status-success-text' : dimmed ? 'text-text-muted' : 'text-text-primary'\n )}\n >\n {name}\n </span>\n </div>\n);\n\n// ── Centerpiece (score or vs + kickoff) ──────────────────────────────────────\n\nexport interface ScoreCenterProps {\n matchup: Matchup;\n time?: ReactNode;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nconst SCORE_SIZE = {\n sm: 'text-lg',\n md: 'text-2xl',\n lg: 'text-4xl sm:text-5xl',\n} as const;\n\nexport const ScoreCenter: FC<ScoreCenterProps> = ({ matchup, time, size = 'md', className }) => {\n if (!matchup.hasScore) {\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span className=\"rounded-lg bg-bg-sunken px-3 py-1 text-xs font-bold uppercase tracking-wide text-text-secondary\">\n vs\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n }\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span\n className={cn(\n 'flex items-center gap-2 font-bold leading-none tabular-nums',\n SCORE_SIZE[size]\n )}\n >\n <span className={matchup.homeWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.homeScore}\n </span>\n <span className=\"text-text-muted\">–</span>\n <span className={matchup.awayWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.awayScore}\n </span>\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n};\n\n// ── Competition eyebrow ──────────────────────────────────────────────────────\n\nconst CompetitionStrip: FC<{ fixture: AthFixture; center?: boolean }> = ({\n fixture,\n center = false,\n}) => {\n const SportIcon = fixture.homeTeam?.sport ? SPORT_ICONS[fixture.homeTeam.sport] : Trophy;\n const parts = [fixture.competition, fixture.round].filter(Boolean).join(' · ');\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary',\n center && 'justify-center'\n )}\n >\n <SportIcon className=\"h-3.5 w-3.5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <span className=\"truncate\">{parts || FIXTURE_TYPE_LABELS[fixture.type]}</span>\n </span>\n );\n};\n\n// ── Full match card / hero ───────────────────────────────────────────────────\n\nexport interface ScoreboardProps {\n fixture: AthFixture;\n onClick?: () => void;\n /** `card` = fixtures-list tile; `hero` = large fixture-detail header. */\n variant?: 'card' | 'hero';\n className?: string;\n}\n\n/** Full crest-vs-crest match card (list tile or detail hero). */\nexport const Scoreboard: FC<ScoreboardProps> = ({\n fixture,\n onClick,\n variant = 'card',\n className,\n}) => {\n const matchup = resolveMatchup(fixture);\n const isHero = variant === 'hero';\n const timeLabel = matchup.hasScore ? null : formatDate(fixture.scheduledAt);\n\n const clickable = Boolean(onClick);\n\n return (\n <GlassCard\n treatment={isHero ? 'glass' : 'metric'}\n glow={isHero}\n role={clickable ? 'button' : undefined}\n tabIndex={clickable ? 0 : undefined}\n onClick={onClick}\n onKeyDown={\n clickable\n ? (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }\n : undefined\n }\n className={cn(\n isHero ? 'p-6 sm:p-8' : 'p-5',\n clickable &&\n 'cursor-pointer transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <div className={cn('flex items-center justify-between gap-3', isHero && 'justify-center')}>\n <CompetitionStrip fixture={fixture} center={isHero} />\n {!isHero ? <StatusBadge status={fixture.status} /> : null}\n </div>\n\n <div\n className={cn(\n 'mt-4 grid items-center gap-3',\n 'grid-cols-[1fr_auto_1fr]',\n isHero && 'mt-6 gap-4 sm:gap-8'\n )}\n >\n <MatchupSide\n name={matchup.homeName}\n sport={matchup.homeSport}\n align={isHero ? undefined : 'end'}\n stacked={isHero}\n won={matchup.homeWon}\n dimmed={matchup.awayWon}\n crestSize={isHero ? 72 : 40}\n />\n <ScoreCenter matchup={matchup} time={timeLabel} size={isHero ? 'lg' : 'md'} />\n <MatchupSide\n name={matchup.awayName}\n sport={matchup.awaySport}\n align={isHero ? undefined : 'start'}\n stacked={isHero}\n won={matchup.awayWon}\n dimmed={matchup.homeWon}\n crestSize={isHero ? 72 : 40}\n />\n </div>\n\n {isHero ? (\n <div className=\"mt-5 flex justify-center\">\n <StatusBadge status={fixture.status} />\n </div>\n ) : null}\n\n <div\n className={cn(\n 'mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary',\n isHero && 'mt-6 justify-center'\n )}\n >\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatDate(fixture.scheduledAt)}\n </span>\n {fixture.venue ? (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {fixture.venue}\n </span>\n ) : null}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"Scoreboard.js","names":[],"sources":["../../../src/atheletebridge/components/Scoreboard.tsx"],"sourcesContent":["/**\n * Scoreboard\n * @module atheletebridge/components\n *\n * Crest-vs-crest fixture presentation. `Scoreboard` renders the full match card\n * (competition strip → crest / score / crest → venue) used by the fixtures list\n * and fixture detail; the compact single-line `MatchupRow` (separate file)\n * reuses the same building blocks for the dashboard + team-detail feeds.\n *\n * All colors are semantic tokens. Winner side is tinted with\n * status-success-text, loser muted — driven purely by the real score.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { CalendarClock, MapPin, Trophy } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { Crest } from './Crest';\nimport { SPORT_ICONS, FIXTURE_TYPE_LABELS } from './domain';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthFixture, AthSport } from '../types';\n\n// ── Shared matchup model ─────────────────────────────────────────────────────\n\nexport interface Matchup {\n homeName: string;\n awayName: string;\n homeSport?: AthSport;\n awaySport?: AthSport;\n homeScore: number | null;\n awayScore: number | null;\n hasScore: boolean;\n homeWon: boolean;\n awayWon: boolean;\n}\n\n/** Derive the display matchup (names, scores, winner) from a fixture. */\nexport function resolveMatchup(fixture: AthFixture): Matchup {\n const homeName = fixture.homeTeam?.name ?? 'Home';\n const awayName = fixture.awayTeam?.name ?? (fixture.awayTeamId ? 'Away' : 'TBD');\n const hasScore = fixture.homeScore !== null && fixture.awayScore !== null;\n const homeScore = fixture.homeScore;\n const awayScore = fixture.awayScore;\n const homeWon = hasScore && (homeScore ?? 0) > (awayScore ?? 0);\n const awayWon = hasScore && (awayScore ?? 0) > (homeScore ?? 0);\n return {\n homeName,\n awayName,\n homeSport: fixture.homeTeam?.sport,\n awaySport: fixture.awayTeam?.sport,\n homeScore,\n awayScore,\n hasScore,\n homeWon,\n awayWon,\n };\n}\n\n// ── One team flank (crest + name) ────────────────────────────────────────────\n\nexport interface MatchupSideProps {\n name: string;\n sport?: AthSport;\n align?: 'start' | 'end';\n /** Winner emphasis — brightens the name. */\n won?: boolean;\n /** Loser de-emphasis when the fixture has a score. */\n dimmed?: boolean;\n crestSize?: number;\n /** Stack crest above name (hero/card) instead of inline (row). */\n stacked?: boolean;\n className?: string;\n}\n\nexport const MatchupSide: FC<MatchupSideProps> = ({\n name,\n sport,\n align = 'start',\n won = false,\n dimmed = false,\n crestSize = 40,\n stacked = false,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-w-0 items-center gap-3',\n stacked\n ? 'flex-col text-center'\n : align === 'end'\n ? 'flex-row-reverse text-right'\n : 'text-left',\n className\n )}\n >\n <Crest name={name} sport={sport} size={crestSize} />\n <span\n className={cn(\n 'min-w-0 truncate font-semibold',\n stacked ? 'max-w-[10rem] text-sm' : 'text-sm',\n won ? 'text-status-success-text' : dimmed ? 'text-text-muted' : 'text-text-primary'\n )}\n >\n {name}\n </span>\n </div>\n);\n\n// ── Centerpiece (score or vs + kickoff) ──────────────────────────────────────\n\nexport interface ScoreCenterProps {\n matchup: Matchup;\n time?: ReactNode;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nconst SCORE_SIZE = {\n sm: 'text-lg',\n md: 'text-2xl',\n lg: 'text-4xl sm:text-5xl',\n} as const;\n\nexport const ScoreCenter: FC<ScoreCenterProps> = ({ matchup, time, size = 'md', className }) => {\n if (!matchup.hasScore) {\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span className=\"rounded-lg bg-bg-sunken px-3 py-1 text-xs font-bold uppercase tracking-wide text-text-secondary\">\n vs\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n }\n return (\n <div className={cn('flex flex-col items-center gap-0.5', className)}>\n <span\n className={cn(\n 'flex items-center gap-2 font-bold leading-none tabular-nums',\n SCORE_SIZE[size]\n )}\n >\n <span className={matchup.homeWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.homeScore}\n </span>\n <span className=\"text-text-muted\">–</span>\n <span className={matchup.awayWon ? 'text-status-success-text' : 'text-text-primary'}>\n {matchup.awayScore}\n </span>\n </span>\n {time ? <span className=\"text-xs text-text-secondary\">{time}</span> : null}\n </div>\n );\n};\n\n// ── Competition eyebrow ──────────────────────────────────────────────────────\n\nconst CompetitionStrip: FC<{ fixture: AthFixture; center?: boolean }> = ({\n fixture,\n center = false,\n}) => {\n const SportIcon = fixture.homeTeam?.sport ? SPORT_ICONS[fixture.homeTeam.sport] : Trophy;\n const parts = [fixture.competition, fixture.round].filter(Boolean).join(' · ');\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary',\n center && 'justify-center'\n )}\n >\n <SportIcon className=\"h-3.5 w-3.5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <span className=\"truncate\">{parts || FIXTURE_TYPE_LABELS[fixture.type]}</span>\n </span>\n );\n};\n\n// ── Full match card / hero ───────────────────────────────────────────────────\n\nexport interface ScoreboardProps {\n fixture: AthFixture;\n onClick?: () => void;\n /** `card` = fixtures-list tile; `hero` = large fixture-detail header. */\n variant?: 'card' | 'hero';\n className?: string;\n}\n\n/** Full crest-vs-crest match card (list tile or detail hero). */\nexport const Scoreboard: FC<ScoreboardProps> = ({\n fixture,\n onClick,\n variant = 'card',\n className,\n}) => {\n const matchup = resolveMatchup(fixture);\n const isHero = variant === 'hero';\n const timeLabel = matchup.hasScore ? null : formatDate(fixture.scheduledAt);\n\n const clickable = Boolean(onClick);\n\n return (\n <GlassCard\n treatment={isHero ? 'glass' : 'metric'}\n glow={isHero}\n role={clickable ? 'button' : undefined}\n tabIndex={clickable ? 0 : undefined}\n onClick={onClick}\n onKeyDown={\n clickable\n ? (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }\n : undefined\n }\n className={cn(\n isHero ? 'p-6 sm:p-8' : 'p-5',\n clickable &&\n 'cursor-pointer transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <div className={cn('flex items-center justify-between gap-3', isHero && 'justify-center')}>\n <CompetitionStrip fixture={fixture} center={isHero} />\n {!isHero ? <StatusBadge status={fixture.status} /> : null}\n </div>\n\n <div\n className={cn(\n 'mt-4 grid items-center gap-3',\n 'grid-cols-[1fr_auto_1fr]',\n isHero && 'mt-6 gap-4 sm:gap-8'\n )}\n >\n <MatchupSide\n name={matchup.homeName}\n sport={matchup.homeSport}\n align={isHero ? undefined : 'end'}\n stacked={isHero}\n won={matchup.homeWon}\n dimmed={matchup.awayWon}\n crestSize={isHero ? 72 : 40}\n />\n <ScoreCenter matchup={matchup} time={timeLabel} size={isHero ? 'lg' : 'md'} />\n <MatchupSide\n name={matchup.awayName}\n sport={matchup.awaySport}\n align={isHero ? undefined : 'start'}\n stacked={isHero}\n won={matchup.awayWon}\n dimmed={matchup.homeWon}\n crestSize={isHero ? 72 : 40}\n />\n </div>\n\n {isHero ? (\n <div className=\"mt-5 flex justify-center\">\n <StatusBadge status={fixture.status} />\n </div>\n ) : null}\n\n <div\n className={cn(\n 'mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary',\n isHero && 'mt-6 justify-center'\n )}\n >\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {formatDate(fixture.scheduledAt)}\n </span>\n {fixture.venue ? (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {fixture.venue}\n </span>\n ) : null}\n </div>\n </GlassCard>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SessionCard.js","names":[],"sources":["../../../src/atheletebridge/components/SessionCard.tsx"],"sourcesContent":["/**\n * Session Card\n * @module atheletebridge/components\n *\n * A coaching-session tile showing the athlete identity (monogram avatar +\n * resolved name when available), session type, schedule, and a colored status\n * badge (Requested amber / Confirmed blue / Completed green / Cancelled grey).\n * A stored rating renders as read-only stars. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { CalendarClock, Clock, Video, MapPin } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { UserAvatar } from './UserAvatar';\nimport { RatingStars } from './viz';\nimport { SESSION_TYPE_LABELS } from './domain';\nimport { formatDate, formatDuration } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSession } from '../types';\n\ninterface SessionCardProps {\n session: AthSession;\n onClick?: () => void;\n /** Resolved athlete display name (else a monogram from the id is shown). */\n athleteName?: string | null;\n /** Resolved coach display name (optional sub-label). */\n coachName?: string | null;\n}\n\nexport const SessionCard: FC<SessionCardProps> = ({\n session,\n onClick,\n athleteName,\n coachName,\n}) => (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <UserAvatar name={athleteName} id={session.athleteId} size=\"md\" />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{session.title}</h3>\n <p className=\"truncate text-sm text-text-secondary\">\n {athleteName?.trim() || 'Athlete'} · {SESSION_TYPE_LABELS[session.type]}\n {coachName?.trim() ? ` · ${coachName.trim()}` : ''}\n </p>\n </div>\n </div>\n <StatusBadge status={session.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatDate(session.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatDuration(session.durationMin)}\n </span>\n {session.type === 'IN_PERSON' && session.location && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {session.location}\n </span>\n )}\n {session.type !== 'IN_PERSON' && session.meetingUrl && (\n <span className=\"flex items-center gap-1\">\n <Video className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> Online\n </span>\n )}\n {session.rating !== null && (\n <RatingStars value={session.rating} size={14} showValue />\n )}\n </div>\n </GlassCard>\n);\n"],"mappings":";;;;;;;;;;;AA8BA,IAAa,KAAqC,EAChD,YACA,YACA,gBACA,mBAEA,kBAAC,GAAD;CACE,WAAU;CACV,MAAK;CACL,UAAU;CACD;CACT,YAAY,MAAM;AAChB,GAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,KAAW;;CAGf,WAAW,EACT,2KACD;WAbH,CAeE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAY,MAAM;IAAa,IAAI,EAAQ;IAAW,MAAK;IAAO,CAAA,EAClE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsD,EAAQ;KAAW,CAAA,EACvF,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,GAAa,MAAM,IAAI;MAAU;MAAI,EAAoB,EAAQ;MACjE,GAAW,MAAM,GAAG,MAAM,EAAU,MAAM,KAAK;MAC9C;OACA;MACF;MACN,kBAAC,GAAD,EAAa,QAAQ,EAAQ,QAAU,CAAA,CACnC;KAEN,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAe,WAAU;MAAc,eAAY;MAAS,CAAA;;KAAE,EAAW,EAAQ,YAAY;KACxF;;GACP,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAO,WAAU;MAAc,eAAY;MAAS,CAAA;;KAAE,EAAe,EAAQ,YAAY;KACpF;;GACN,EAAQ,SAAS,eAAe,EAAQ,YACvC,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAQ,WAAU;MAAuB,eAAY;MAAS,CAAA;;KAAE,EAAQ;KACnE;;GAER,EAAQ,SAAS,eAAe,EAAQ,cACvC,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,GAAD;KAAO,WAAU;KAAc,eAAY;KAAS,CAAA,EAAA,UAC/C;;GAER,EAAQ,WAAW,QAClB,kBAAC,GAAD;IAAa,OAAO,EAAQ;IAAQ,MAAM;IAAI,WAAA;IAAY,CAAA;GAExD;IACI"}
1
+ {"version":3,"file":"SessionCard.js","names":[],"sources":["../../../src/atheletebridge/components/SessionCard.tsx"],"sourcesContent":["/**\n * Session Card\n * @module atheletebridge/components\n *\n * A coaching-session tile showing the athlete identity (monogram avatar +\n * resolved name when available), session type, schedule, and a colored status\n * badge (Requested amber / Confirmed blue / Completed green / Cancelled grey).\n * A stored rating renders as read-only stars. Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { CalendarClock, Clock, Video, MapPin } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { UserAvatar } from './UserAvatar';\nimport { RatingStars } from './viz';\nimport { SESSION_TYPE_LABELS } from './domain';\nimport { formatDate, formatDuration } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSession } from '../types';\n\ninterface SessionCardProps {\n session: AthSession;\n onClick?: () => void;\n /** Resolved athlete display name (else a monogram from the id is shown). */\n athleteName?: string | null;\n /** Resolved coach display name (optional sub-label). */\n coachName?: string | null;\n}\n\nexport const SessionCard: FC<SessionCardProps> = ({ session, onClick, athleteName, coachName }) => (\n <GlassCard\n treatment=\"metric\"\n role=\"button\"\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n className={cn(\n 'cursor-pointer p-5 transition-transform duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <UserAvatar name={athleteName} id={session.athleteId} size=\"md\" />\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary\">{session.title}</h3>\n <p className=\"truncate text-sm text-text-secondary\">\n {athleteName?.trim() || 'Athlete'} · {SESSION_TYPE_LABELS[session.type]}\n {coachName?.trim() ? ` · ${coachName.trim()}` : ''}\n </p>\n </div>\n </div>\n <StatusBadge status={session.status} />\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />{' '}\n {formatDate(session.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> {formatDuration(session.durationMin)}\n </span>\n {session.type === 'IN_PERSON' && session.location && (\n <span className=\"flex items-center gap-1 truncate\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" /> {session.location}\n </span>\n )}\n {session.type !== 'IN_PERSON' && session.meetingUrl && (\n <span className=\"flex items-center gap-1\">\n <Video className=\"h-3.5 w-3.5\" aria-hidden=\"true\" /> Online\n </span>\n )}\n {session.rating !== null && <RatingStars value={session.rating} size={14} showValue />}\n </div>\n </GlassCard>\n);\n"],"mappings":";;;;;;;;;;;AA8BA,IAAa,KAAqC,EAAE,YAAS,YAAS,gBAAa,mBACjF,kBAAC,GAAD;CACE,WAAU;CACV,MAAK;CACL,UAAU;CACD;CACT,YAAY,MAAM;AAChB,GAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,KAAW;;CAGf,WAAW,EACT,2KACD;WAbH,CAeE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAY,MAAM;IAAa,IAAI,EAAQ;IAAW,MAAK;IAAO,CAAA,EAClE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAsD,EAAQ;KAAW,CAAA,EACvF,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,GAAa,MAAM,IAAI;MAAU;MAAI,EAAoB,EAAQ;MACjE,GAAW,MAAM,GAAG,MAAM,EAAU,MAAM,KAAK;MAC9C;OACA;MACF;MACN,kBAAC,GAAD,EAAa,QAAQ,EAAQ,QAAU,CAAA,CACnC;KAEN,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAe,WAAU;MAAc,eAAY;MAAS,CAAA;KAAC;KAC5D,EAAW,EAAQ,YAAY;KAC3B;;GACP,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAO,WAAU;MAAc,eAAY;MAAS,CAAA;;KAAE,EAAe,EAAQ,YAAY;KACpF;;GACN,EAAQ,SAAS,eAAe,EAAQ,YACvC,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAQ,WAAU;MAAuB,eAAY;MAAS,CAAA;;KAAE,EAAQ;KACnE;;GAER,EAAQ,SAAS,eAAe,EAAQ,cACvC,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,GAAD;KAAO,WAAU;KAAc,eAAY;KAAS,CAAA,EAAA,UAC/C;;GAER,EAAQ,WAAW,QAAQ,kBAAC,GAAD;IAAa,OAAO,EAAQ;IAAQ,MAAM;IAAI,WAAA;IAAY,CAAA;GAClF;IACI"}
@@ -1 +1 @@
1
- {"version":3,"file":"SessionStatusTimeline.js","names":[],"sources":["../../../src/atheletebridge/components/SessionStatusTimeline.tsx"],"sourcesContent":["/**\n * SessionStatusTimeline\n * @module atheletebridge/components\n *\n * A horizontal lifecycle stepper for a coaching session:\n * Requested → Confirmed → Completed\n * with a distinct terminal variant when the session was Cancelled / No-show.\n * The happy path reuses the fe-libs Stepper; timestamps are shown ONLY where the\n * schema actually has them (createdAt for Requested, completedAt for Completed)\n * — Confirmed has no dedicated timestamp field, so none is fabricated.\n * Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Check, X } from 'lucide-react';\nimport { Stepper, type StepperStep } from '@burdenoff/fe-libs/ui';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSessionStatus } from '../types';\n\nexport interface SessionStatusTimelineProps {\n status: AthSessionStatus;\n /** Requested-at timestamp (session.createdAt). */\n createdAt?: string | null;\n /** Completed-at timestamp (session.completedAt). */\n completedAt?: string | null;\n className?: string;\n}\n\nconst shortWhen = (iso: string | null | undefined): string | undefined => {\n if (!iso) return undefined;\n return formatDate(iso, { weekday: 'short', year: undefined });\n};\n\n/** Lifecycle stepper for a coaching session (with cancelled/no-show terminal). */\nexport const SessionStatusTimeline: FC<SessionStatusTimelineProps> = ({\n status,\n createdAt,\n completedAt,\n className,\n}) => {\n // ── Terminal (cancelled / no-show) ─────────────────────────────────────────\n if (status === 'CANCELLED' || status === 'NO_SHOW') {\n const terminalLabel = status === 'CANCELLED' ? 'Cancelled' : 'No-show';\n return (\n <ol\n className={cn('flex items-start gap-2', className)}\n aria-label=\"Session lifecycle\"\n >\n <li className=\"flex flex-1 items-center gap-3\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-action-primary-bg bg-action-primary-bg text-action-primary-text\"\n >\n <Check className=\"size-4\" />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">Requested</span>\n {shortWhen(createdAt) ? (\n <span className=\"block text-xs text-text-muted\">{shortWhen(createdAt)}</span>\n ) : null}\n </span>\n <span\n aria-hidden=\"true\"\n className=\"h-px flex-1 bg-status-error-border\"\n />\n </li>\n <li className=\"flex items-center gap-3\" aria-current=\"step\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-status-error-border bg-status-error-bg-subtle text-status-error-text\"\n >\n <X className=\"size-4\" />\n </span>\n <span className=\"text-sm font-medium text-status-error-text\">{terminalLabel}</span>\n </li>\n </ol>\n );\n }\n\n // ── Happy path ─────────────────────────────────────────────────────────────\n const steps: StepperStep[] = [\n { key: 'requested', label: 'Requested', description: shortWhen(createdAt) },\n { key: 'confirmed', label: 'Confirmed' },\n { key: 'completed', label: 'Completed', description: shortWhen(completedAt) },\n ];\n\n // active = index of the in-progress step; past `steps.length` marks all done.\n const active = status === 'REQUESTED' ? 0 : status === 'CONFIRMED' ? 1 : steps.length;\n\n return <Stepper steps={steps} active={active} className={className} />;\n};\n"],"mappings":";;;;;;AA6BA,IAAM,KAAa,MAAuD;AACnE,OACL,QAAO,EAAW,GAAK;EAAE,SAAS;EAAS,MAAM,KAAA;EAAW,CAAC;GAIlD,KAAyD,EACpE,WACA,cACA,gBACA,mBACI;AAEJ,KAAI,MAAW,eAAe,MAAW,WAAW;EAClD,IAAM,IAAgB,MAAW,cAAc,cAAc;AAC7D,SACE,kBAAC,MAAD;GACE,WAAW,EAAG,0BAA0B,EAAU;GAClD,cAAW;aAFb,CAIE,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;gBAEV,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MACvB,CAAA;KACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAA8C;OAAgB,CAAA,EAC7E,EAAU,EAAU,GACnB,kBAAC,QAAD;OAAM,WAAU;iBAAiC,EAAU,EAAU;OAAQ,CAAA,GAC3E,KACC;;KACP,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;MACV,CAAA;KACC;OACL,kBAAC,MAAD;IAAI,WAAU;IAA0B,gBAAa;cAArD,CACE,kBAAC,QAAD;KACE,eAAY;KACZ,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KACnB,CAAA,EACP,kBAAC,QAAD;KAAM,WAAU;eAA8C;KAAqB,CAAA,CAChF;MACF;;;CAKT,IAAM,IAAuB;EAC3B;GAAE,KAAK;GAAa,OAAO;GAAa,aAAa,EAAU,EAAU;GAAE;EAC3E;GAAE,KAAK;GAAa,OAAO;GAAa;EACxC;GAAE,KAAK;GAAa,OAAO;GAAa,aAAa,EAAU,EAAY;GAAE;EAC9E;AAKD,QAAO,kBAAC,GAAD;EAAgB;EAAe,QAFvB,MAAW,cAAc,IAAI,MAAW,cAAc,IAAI,EAAM;EAEtB;EAAa,CAAA"}
1
+ {"version":3,"file":"SessionStatusTimeline.js","names":[],"sources":["../../../src/atheletebridge/components/SessionStatusTimeline.tsx"],"sourcesContent":["/**\n * SessionStatusTimeline\n * @module atheletebridge/components\n *\n * A horizontal lifecycle stepper for a coaching session:\n * Requested → Confirmed → Completed\n * with a distinct terminal variant when the session was Cancelled / No-show.\n * The happy path reuses the fe-libs Stepper; timestamps are shown ONLY where the\n * schema actually has them (createdAt for Requested, completedAt for Completed)\n * — Confirmed has no dedicated timestamp field, so none is fabricated.\n * Semantic tokens only.\n */\n\nimport type { FC } from 'react';\nimport { Check, X } from 'lucide-react';\nimport { Stepper, type StepperStep } from '@burdenoff/fe-libs/ui';\nimport { formatDate } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { AthSessionStatus } from '../types';\n\nexport interface SessionStatusTimelineProps {\n status: AthSessionStatus;\n /** Requested-at timestamp (session.createdAt). */\n createdAt?: string | null;\n /** Completed-at timestamp (session.completedAt). */\n completedAt?: string | null;\n className?: string;\n}\n\nconst shortWhen = (iso: string | null | undefined): string | undefined => {\n if (!iso) return undefined;\n return formatDate(iso, { weekday: 'short', year: undefined });\n};\n\n/** Lifecycle stepper for a coaching session (with cancelled/no-show terminal). */\nexport const SessionStatusTimeline: FC<SessionStatusTimelineProps> = ({\n status,\n createdAt,\n completedAt,\n className,\n}) => {\n // ── Terminal (cancelled / no-show) ─────────────────────────────────────────\n if (status === 'CANCELLED' || status === 'NO_SHOW') {\n const terminalLabel = status === 'CANCELLED' ? 'Cancelled' : 'No-show';\n return (\n <ol className={cn('flex items-start gap-2', className)} aria-label=\"Session lifecycle\">\n <li className=\"flex flex-1 items-center gap-3\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-action-primary-bg bg-action-primary-bg text-action-primary-text\"\n >\n <Check className=\"size-4\" />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">Requested</span>\n {shortWhen(createdAt) ? (\n <span className=\"block text-xs text-text-muted\">{shortWhen(createdAt)}</span>\n ) : null}\n </span>\n <span aria-hidden=\"true\" className=\"h-px flex-1 bg-status-error-border\" />\n </li>\n <li className=\"flex items-center gap-3\" aria-current=\"step\">\n <span\n aria-hidden=\"true\"\n className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-status-error-border bg-status-error-bg-subtle text-status-error-text\"\n >\n <X className=\"size-4\" />\n </span>\n <span className=\"text-sm font-medium text-status-error-text\">{terminalLabel}</span>\n </li>\n </ol>\n );\n }\n\n // ── Happy path ─────────────────────────────────────────────────────────────\n const steps: StepperStep[] = [\n { key: 'requested', label: 'Requested', description: shortWhen(createdAt) },\n { key: 'confirmed', label: 'Confirmed' },\n { key: 'completed', label: 'Completed', description: shortWhen(completedAt) },\n ];\n\n // active = index of the in-progress step; past `steps.length` marks all done.\n const active = status === 'REQUESTED' ? 0 : status === 'CONFIRMED' ? 1 : steps.length;\n\n return <Stepper steps={steps} active={active} className={className} />;\n};\n"],"mappings":";;;;;;AA6BA,IAAM,KAAa,MAAuD;AACnE,OACL,QAAO,EAAW,GAAK;EAAE,SAAS;EAAS,MAAM,KAAA;EAAW,CAAC;GAIlD,KAAyD,EACpE,WACA,cACA,gBACA,mBACI;AAEJ,KAAI,MAAW,eAAe,MAAW,WAAW;EAClD,IAAM,IAAgB,MAAW,cAAc,cAAc;AAC7D,SACE,kBAAC,MAAD;GAAI,WAAW,EAAG,0BAA0B,EAAU;GAAE,cAAW;aAAnE,CACE,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;gBAEV,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MACvB,CAAA;KACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,QAAD;OAAM,WAAU;iBAA8C;OAAgB,CAAA,EAC7E,EAAU,EAAU,GACnB,kBAAC,QAAD;OAAM,WAAU;iBAAiC,EAAU,EAAU;OAAQ,CAAA,GAC3E,KACC;;KACP,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAU;MAAuC,CAAA;KACvE;OACL,kBAAC,MAAD;IAAI,WAAU;IAA0B,gBAAa;cAArD,CACE,kBAAC,QAAD;KACE,eAAY;KACZ,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KACnB,CAAA,EACP,kBAAC,QAAD;KAAM,WAAU;eAA8C;KAAqB,CAAA,CAChF;MACF;;;CAKT,IAAM,IAAuB;EAC3B;GAAE,KAAK;GAAa,OAAO;GAAa,aAAa,EAAU,EAAU;GAAE;EAC3E;GAAE,KAAK;GAAa,OAAO;GAAa;EACxC;GAAE,KAAK;GAAa,OAAO;GAAa,aAAa,EAAU,EAAY;GAAE;EAC9E;AAKD,QAAO,kBAAC,GAAD;EAAgB;EAAe,QAFvB,MAAW,cAAc,IAAI,MAAW,cAAc,IAAI,EAAM;EAEtB;EAAa,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/atheletebridge/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module atheletebridge/components\n *\n * A colored badge based on a domain status value, with semantic-token color\n * mapping for the AthleteBridge lifecycle states (teams, members, fixtures,\n * programs, sessions, workouts). Uses semantic tokens only — never raw colors.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\ninterface StatusBadgeProps {\n status: string;\n /**\n * Force a specific variant, overriding the auto status→color mapping. Useful\n * where the same status string reads differently by context (e.g. a cancelled\n * coaching session shown grey rather than the default destructive red).\n */\n variant?: BadgeVariant;\n className?: string;\n}\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / in-progress condition get a subtle,\n// opacity-only glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['live', 'active', 'confirmed', 'in_progress']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Team status\n forming: 'warning',\n active: 'success',\n inactive: 'secondary',\n archived: 'secondary',\n\n // Member status\n trial: 'info',\n injured: 'warning',\n suspended: 'destructive',\n released: 'secondary',\n\n // Fixture status\n scheduled: 'info',\n live: 'success',\n completed: 'success',\n postponed: 'warning',\n cancelled: 'destructive',\n\n // Program status\n draft: 'secondary',\n published: 'success',\n\n // Session status\n requested: 'warning',\n confirmed: 'info',\n no_show: 'destructive',\n\n // Workout status\n planned: 'info',\n skipped: 'secondary',\n};\n\nfunction formatStatusLabel(status: string): string {\n return status\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, variant: variantOverride, className }) => {\n const key = status.toLowerCase();\n const variant = variantOverride ?? statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatStatusLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;AAyBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;CACV,EAIK,IAAe,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAa;CAAc,CAAC,EAEtE,IAAiD;CAErD,SAAS;CACT,QAAQ;CACR,UAAU;CACV,UAAU;CAGV,OAAO;CACP,SAAS;CACT,WAAW;CACX,UAAU;CAGV,WAAW;CACX,MAAM;CACN,WAAW;CACX,WAAW;CACX,WAAW;CAGX,OAAO;CACP,WAAW;CAGX,WAAW;CACX,WAAW;CACX,SAAS;CAGT,SAAS;CACT,SAAS;CACV;AAED,SAAS,EAAkB,GAAwB;AACjD,QAAO,EACJ,QAAQ,MAAM,IAAI,CAClB,aAAa,CACb,QAAQ,UAAU,MAAS,EAAK,aAAa,CAAC;;AAGnD,IAAa,KAAqC,EAAE,WAAQ,SAAS,GAAiB,mBAAgB;CACpG,IAAM,IAAM,EAAO,aAAa,EAC1B,IAAU,KAAmB,EAAiB,MAAQ,aACtD,IAAS,EAAa,IAAI,EAAI;AAEpC,QACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,EACD;YALH,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,EAEH,EAAkB,EAAO,CACrB"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/atheletebridge/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module atheletebridge/components\n *\n * A colored badge based on a domain status value, with semantic-token color\n * mapping for the AthleteBridge lifecycle states (teams, members, fixtures,\n * programs, sessions, workouts). Uses semantic tokens only — never raw colors.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\ninterface StatusBadgeProps {\n status: string;\n /**\n * Force a specific variant, overriding the auto status→color mapping. Useful\n * where the same status string reads differently by context (e.g. a cancelled\n * coaching session shown grey rather than the default destructive red).\n */\n variant?: BadgeVariant;\n className?: string;\n}\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-[var(--color-accent-soft)] text-text-primary',\n outline: 'border border-border-seam text-text-primary',\n};\n\n// Statuses that represent a \"live\" / in-progress condition get a subtle,\n// opacity-only glow pulse (reduced-motion safe via motion-safe:).\nconst liveStatuses = new Set(['live', 'active', 'confirmed', 'in_progress']);\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Team status\n forming: 'warning',\n active: 'success',\n inactive: 'secondary',\n archived: 'secondary',\n\n // Member status\n trial: 'info',\n injured: 'warning',\n suspended: 'destructive',\n released: 'secondary',\n\n // Fixture status\n scheduled: 'info',\n live: 'success',\n completed: 'success',\n postponed: 'warning',\n cancelled: 'destructive',\n\n // Program status\n draft: 'secondary',\n published: 'success',\n\n // Session status\n requested: 'warning',\n confirmed: 'info',\n no_show: 'destructive',\n\n // Workout status\n planned: 'info',\n skipped: 'secondary',\n};\n\nfunction formatStatusLabel(status: string): string {\n return status\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({\n status,\n variant: variantOverride,\n className,\n}) => {\n const key = status.toLowerCase();\n const variant = variantOverride ?? statusVariantMap[key] ?? 'secondary';\n const isLive = liveStatuses.has(key);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n {isLive && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {formatStatusLabel(status)}\n </span>\n );\n};\n"],"mappings":";;;AAyBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;CACV,EAIK,IAAe,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAa;CAAc,CAAC,EAEtE,IAAiD;CAErD,SAAS;CACT,QAAQ;CACR,UAAU;CACV,UAAU;CAGV,OAAO;CACP,SAAS;CACT,WAAW;CACX,UAAU;CAGV,WAAW;CACX,MAAM;CACN,WAAW;CACX,WAAW;CACX,WAAW;CAGX,OAAO;CACP,WAAW;CAGX,WAAW;CACX,WAAW;CACX,SAAS;CAGT,SAAS;CACT,SAAS;CACV;AAED,SAAS,EAAkB,GAAwB;AACjD,QAAO,EACJ,QAAQ,MAAM,IAAI,CAClB,aAAa,CACb,QAAQ,UAAU,MAAS,EAAK,aAAa,CAAC;;AAGnD,IAAa,KAAqC,EAChD,WACA,SAAS,GACT,mBACI;CACJ,IAAM,IAAM,EAAO,aAAa,EAC1B,IAAU,KAAmB,EAAiB,MAAQ,aACtD,IAAS,EAAa,IAAI,EAAI;AAEpC,QACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,EACD;YALH,CAOG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,EAEH,EAAkB,EAAO,CACrB"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAthleteBridgeApi.js","names":[],"sources":["../../../src/atheletebridge/hooks/useAthleteBridgeApi.ts"],"sourcesContent":["/**\n * AthleteBridge API hooks\n * @module atheletebridge/hooks\n *\n * Thin react-query wrappers over the real `wspace-atheletebridge-svc` federated\n * GraphQL contract, reached through the workspace gateway with the workspace\n * dual-token. Every query/mutation here maps 1:1 to an `ath*` root field in the\n * service's `.hive-schema.graphql`. NO MOCKS — all data is live.\n */\n\nimport { useCallback, useMemo } from 'react';\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { graphqlFetch, clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useAthleteBridge } from '../context';\nimport type {\n AthCompleteSessionInput,\n AthCreateFixtureInput,\n AthCreateProgramInput,\n AthCreateTeamInput,\n AthAddMemberInput,\n AthAddDrillInput,\n AthBookSessionInput,\n AthDrill,\n AthFixture,\n AthFixtureConnection,\n AthFixtureStatus,\n AthLogWorkoutInput,\n AthMember,\n AthMemberStatus,\n AthPerformanceMetric,\n AthProgram,\n AthProgramConnection,\n AthProgramLevel,\n AthProgramStatus,\n AthRecordMetricInput,\n AthRecordResultInput,\n AthSession,\n AthSessionConnection,\n AthSessionStatus,\n AthSport,\n AthStanding,\n AthTeam,\n AthTeamConnection,\n AthTeamStatus,\n AthUpdateProgramInput,\n AthUpdateTeamInput,\n AthWorkoutLog,\n AthWorkoutLogConnection,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Low-level query wrapper\n// ============================================================================\n\nconst athQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n\n // This MFE bypasses Apollo, so fe-libs' short-lived graphqlFetch read cache\n // would otherwise serve pre-write data to the very next refetch issued by a\n // mutation's onSuccess invalidation. Clear it after any successful mutation so\n // every downstream read is fresh (fleet-wide stale-read-after-write fix).\n if (query.trimStart().startsWith('mutation')) {\n clearGraphqlFetchResponseCache();\n }\n\n return response.data;\n};\n\n// Global-gateway read wrapper — the workspace-scoped MFE reaches the tenant user\n// directory (for real user pickers) through the global gateway.\nconst athGlobalQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'global',\n query,\n variables,\n suppressGlobalErrorEvent: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n\n return response.data;\n};\n\n// ============================================================================\n// GraphQL fragments (inline strings — exact fields from the svc schema)\n// ============================================================================\n\nconst TEAM_FIELDS = `\n id workspaceId name sport status description ageGroup season homeVenue\n headCoachId tags createdAt updatedAt\n`;\n\nconst MEMBER_FIELDS = `\n id workspaceId teamId userId displayName role status position jerseyNumber\n height weight dominantSide notes joinedAt createdAt updatedAt\n`;\n\nconst FIXTURE_FIELDS = `\n id workspaceId type status competition round venue homeTeamId awayTeamId\n homeScore awayScore scheduledAt kickoffAt notes createdAt updatedAt\n homeTeam { id name sport }\n awayTeam { id name sport }\n`;\n\nconst STANDING_FIELDS = `\n id workspaceId competition teamId played won drawn lost goalsFor goalsAgainst\n points rank createdAt updatedAt\n team { id name sport }\n`;\n\nconst DRILL_FIELDS = `\n id workspaceId programId name description weekNumber orderIndex sets reps\n durationSec restSec createdAt updatedAt\n video { id }\n`;\n\nconst PROGRAM_FIELDS = `\n id workspaceId coachId name sport level status summary weeks isTemplate tags\n createdAt updatedAt\n`;\n\nconst SESSION_FIELDS = `\n id workspaceId coachId athleteId programId title type status location\n meetingUrl scheduledAt durationMin completedAt feedback rating\n createdAt updatedAt\n`;\n\nconst WORKOUT_LOG_FIELDS = `\n id workspaceId athleteId programId title status performedAt durationMin rpe\n totalLoad metrics notes createdAt updatedAt\n`;\n\nconst METRIC_FIELDS = `\n id workspaceId athleteId type label value unit source recordedAt\n createdAt updatedAt\n`;\n\n// ============================================================================\n// Teams\n// ============================================================================\n\nexport interface TeamFilters {\n sport?: AthSport;\n status?: AthTeamStatus;\n pagination?: PaginationInput;\n}\n\nexport const useTeams = (filters: TeamFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'teams', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListTeams: AthTeamConnection }>(\n `query AthListTeams($sport: AthSport, $status: AthTeamStatus, $pagination: PaginationInput) {\n athListTeams(sport: $sport, status: $status, pagination: $pagination) {\n items { ${TEAM_FIELDS} members { id } }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n sport: filters.sport,\n status: filters.status,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListTeams;\n },\n });\n};\n\nexport const useTeam = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'team', id],\n queryFn: async () => {\n const result = await athQuery<{ athTeam: AthTeam | null }>(\n `query AthTeam($id: ID!) {\n athTeam(id: $id) {\n ${TEAM_FIELDS}\n members { ${MEMBER_FIELDS} }\n fixtures { ${FIXTURE_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athTeam;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useTeamMembers = (teamId: string | undefined, status?: AthMemberStatus) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'members', teamId, status],\n queryFn: async () => {\n const result = await athQuery<{ athListMembers: AthMember[] }>(\n `query AthListMembers($teamId: ID!, $status: AthMemberStatus) {\n athListMembers(teamId: $teamId, status: $status) { ${MEMBER_FIELDS} }\n }`,\n { teamId, status },\n apiGatewayUrl\n );\n return result.athListMembers;\n },\n enabled: Boolean(teamId),\n });\n};\n\nexport const useCreateTeam = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthCreateTeamInput) => {\n const result = await athQuery<{ athCreateTeam: AthTeam }>(\n `mutation AthCreateTeam($input: AthCreateTeamInput!) {\n athCreateTeam(input: $input) { ${TEAM_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athCreateTeam;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'teams'] });\n },\n });\n};\n\nexport const useUpdateTeam = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: AthUpdateTeamInput }) => {\n const result = await athQuery<{ athUpdateTeam: AthTeam }>(\n `mutation AthUpdateTeam($id: ID!, $input: AthUpdateTeamInput!) {\n athUpdateTeam(id: $id, input: $input) { ${TEAM_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athUpdateTeam;\n },\n onSuccess: (team) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'teams'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'team', team.id] });\n },\n });\n};\n\nexport const useArchiveTeam = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athArchiveTeam: boolean }>(\n `mutation AthArchiveTeam($id: ID!) { athArchiveTeam(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athArchiveTeam;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'teams'] });\n },\n });\n};\n\nexport const useAddMember = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthAddMemberInput) => {\n const result = await athQuery<{ athAddMember: AthMember }>(\n `mutation AthAddMember($input: AthAddMemberInput!) {\n athAddMember(input: $input) { ${MEMBER_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athAddMember;\n },\n onSuccess: (member) => {\n void queryClient.invalidateQueries({\n queryKey: ['atheletebridge', 'members', member.teamId],\n });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'team', member.teamId] });\n },\n });\n};\n\nexport const useRemoveMember = (teamId?: string) => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athRemoveMember: boolean }>(\n `mutation AthRemoveMember($id: ID!) { athRemoveMember(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athRemoveMember;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'members', teamId] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'team', teamId] });\n },\n });\n};\n\n// ============================================================================\n// Fixtures & standings\n// ============================================================================\n\nexport interface FixtureFilters {\n status?: AthFixtureStatus;\n teamId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useFixtures = (filters: FixtureFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'fixtures', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListFixtures: AthFixtureConnection }>(\n `query AthListFixtures($status: AthFixtureStatus, $teamId: ID, $pagination: PaginationInput) {\n athListFixtures(status: $status, teamId: $teamId, pagination: $pagination) {\n items { ${FIXTURE_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n teamId: filters.teamId,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListFixtures;\n },\n });\n};\n\nexport const useFixture = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'fixture', id],\n queryFn: async () => {\n const result = await athQuery<{ athFixture: AthFixture | null }>(\n `query AthFixture($id: ID!) {\n athFixture(id: $id) { ${FIXTURE_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athFixture;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useStandings = (competition: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'standings', competition],\n queryFn: async () => {\n const result = await athQuery<{ athStandings: AthStanding[] }>(\n `query AthStandings($competition: String!) {\n athStandings(competition: $competition) { ${STANDING_FIELDS} }\n }`,\n { competition },\n apiGatewayUrl\n );\n return result.athStandings;\n },\n enabled: Boolean(competition),\n });\n};\n\nexport const useScheduleFixture = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthCreateFixtureInput) => {\n const result = await athQuery<{ athScheduleFixture: AthFixture }>(\n `mutation AthScheduleFixture($input: AthCreateFixtureInput!) {\n athScheduleFixture(input: $input) { ${FIXTURE_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athScheduleFixture;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'fixtures'] });\n },\n });\n};\n\nexport const useRecordFixtureResult = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: AthRecordResultInput }) => {\n const result = await athQuery<{ athRecordFixtureResult: AthFixture }>(\n `mutation AthRecordFixtureResult($id: ID!, $input: AthRecordResultInput!) {\n athRecordFixtureResult(id: $id, input: $input) { ${FIXTURE_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athRecordFixtureResult;\n },\n onSuccess: (fixture) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'fixtures'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'fixture', fixture.id] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'standings'] });\n },\n });\n};\n\n// ============================================================================\n// Programs & drills\n// ============================================================================\n\nexport interface ProgramFilters {\n sport?: AthSport;\n level?: AthProgramLevel;\n status?: AthProgramStatus;\n pagination?: PaginationInput;\n}\n\nexport const usePrograms = (filters: ProgramFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'programs', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListPrograms: AthProgramConnection }>(\n `query AthListPrograms($sport: AthSport, $level: AthProgramLevel, $status: AthProgramStatus, $pagination: PaginationInput) {\n athListPrograms(sport: $sport, level: $level, status: $status, pagination: $pagination) {\n items { ${PROGRAM_FIELDS} drills { id } }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n sport: filters.sport,\n level: filters.level,\n status: filters.status,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListPrograms;\n },\n });\n};\n\nexport const useProgram = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'program', id],\n queryFn: async () => {\n const result = await athQuery<{ athProgram: AthProgram | null }>(\n `query AthProgram($id: ID!) {\n athProgram(id: $id) {\n ${PROGRAM_FIELDS}\n drills { ${DRILL_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athProgram;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useCreateProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthCreateProgramInput) => {\n const result = await athQuery<{ athCreateProgram: AthProgram }>(\n `mutation AthCreateProgram($input: AthCreateProgramInput!) {\n athCreateProgram(input: $input) { ${PROGRAM_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athCreateProgram;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n },\n });\n};\n\nexport const useUpdateProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: AthUpdateProgramInput }) => {\n const result = await athQuery<{ athUpdateProgram: AthProgram }>(\n `mutation AthUpdateProgram($id: ID!, $input: AthUpdateProgramInput!) {\n athUpdateProgram(id: $id, input: $input) { ${PROGRAM_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athUpdateProgram;\n },\n onSuccess: (program) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'program', program.id] });\n },\n });\n};\n\nexport const useTransitionProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, status }: { id: string; status: AthProgramStatus }) => {\n const result = await athQuery<{ athTransitionProgram: AthProgram }>(\n `mutation AthTransitionProgram($id: ID!, $status: AthProgramStatus!) {\n athTransitionProgram(id: $id, status: $status) { ${PROGRAM_FIELDS} }\n }`,\n { id, status },\n apiGatewayUrl\n );\n return result.athTransitionProgram;\n },\n onSuccess: (program) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'program', program.id] });\n },\n });\n};\n\nexport const useArchiveProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athArchiveProgram: boolean }>(\n `mutation AthArchiveProgram($id: ID!) { athArchiveProgram(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athArchiveProgram;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n },\n });\n};\n\nexport const useAddDrill = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthAddDrillInput) => {\n const result = await athQuery<{ athAddDrill: AthDrill }>(\n `mutation AthAddDrill($input: AthAddDrillInput!) {\n athAddDrill(input: $input) { ${DRILL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athAddDrill;\n },\n onSuccess: (drill) => {\n void queryClient.invalidateQueries({\n queryKey: ['atheletebridge', 'program', drill.programId],\n });\n },\n });\n};\n\nexport const useRemoveDrill = (programId?: string) => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athRemoveDrill: boolean }>(\n `mutation AthRemoveDrill($id: ID!) { athRemoveDrill(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athRemoveDrill;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'program', programId] });\n },\n });\n};\n\n// ============================================================================\n// Coaching sessions\n// ============================================================================\n\nexport interface SessionFilters {\n athleteId?: string;\n coachId?: string;\n status?: AthSessionStatus;\n pagination?: PaginationInput;\n}\n\nexport const useSessions = (filters: SessionFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'sessions', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListSessions: AthSessionConnection }>(\n `query AthListSessions($athleteId: ID, $coachId: ID, $status: AthSessionStatus, $pagination: PaginationInput) {\n athListSessions(athleteId: $athleteId, coachId: $coachId, status: $status, pagination: $pagination) {\n items { ${SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n athleteId: filters.athleteId,\n coachId: filters.coachId,\n status: filters.status,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListSessions;\n },\n });\n};\n\nexport const useSession = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'session', id],\n queryFn: async () => {\n const result = await athQuery<{ athSession: AthSession | null }>(\n `query AthSession($id: ID!) {\n athSession(id: $id) { ${SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athSession;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useBookSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthBookSessionInput) => {\n const result = await athQuery<{ athBookSession: AthSession }>(\n `mutation AthBookSession($input: AthBookSessionInput!) {\n athBookSession(input: $input) { ${SESSION_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athBookSession;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n },\n });\n};\n\nexport const useConfirmSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athConfirmSession: AthSession }>(\n `mutation AthConfirmSession($id: ID!) {\n athConfirmSession(id: $id) { ${SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athConfirmSession;\n },\n onSuccess: (session) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'session', session.id] });\n },\n });\n};\n\nexport const useCompleteSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input?: AthCompleteSessionInput }) => {\n const result = await athQuery<{ athCompleteSession: AthSession }>(\n `mutation AthCompleteSession($id: ID!, $input: AthCompleteSessionInput) {\n athCompleteSession(id: $id, input: $input) { ${SESSION_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athCompleteSession;\n },\n onSuccess: (session) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'session', session.id] });\n },\n });\n};\n\nexport const useCancelSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athCancelSession: AthSession }>(\n `mutation AthCancelSession($id: ID!) {\n athCancelSession(id: $id) { ${SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athCancelSession;\n },\n onSuccess: (session) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'session', session.id] });\n },\n });\n};\n\n// ============================================================================\n// Workout logs & performance metrics\n// ============================================================================\n\nexport interface WorkoutLogFilters {\n athleteId?: string;\n programId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useWorkoutLogs = (filters: WorkoutLogFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'workout-logs', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListWorkoutLogs: AthWorkoutLogConnection }>(\n `query AthListWorkoutLogs($athleteId: ID, $programId: ID, $pagination: PaginationInput) {\n athListWorkoutLogs(athleteId: $athleteId, programId: $programId, pagination: $pagination) {\n items { ${WORKOUT_LOG_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n athleteId: filters.athleteId,\n programId: filters.programId,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListWorkoutLogs;\n },\n });\n};\n\nexport const usePerformanceMetrics = (athleteId: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'metrics', athleteId],\n queryFn: async () => {\n const result = await athQuery<{ athPerformanceMetrics: AthPerformanceMetric[] }>(\n `query AthPerformanceMetrics($athleteId: ID!) {\n athPerformanceMetrics(athleteId: $athleteId) { ${METRIC_FIELDS} }\n }`,\n { athleteId },\n apiGatewayUrl\n );\n return result.athPerformanceMetrics;\n },\n enabled: Boolean(athleteId),\n });\n};\n\nexport const useLogWorkout = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthLogWorkoutInput) => {\n const result = await athQuery<{ athLogWorkout: AthWorkoutLog }>(\n `mutation AthLogWorkout($input: AthLogWorkoutInput!) {\n athLogWorkout(input: $input) { ${WORKOUT_LOG_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athLogWorkout;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'workout-logs'] });\n },\n });\n};\n\nexport const useRecordMetric = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthRecordMetricInput) => {\n const result = await athQuery<{ athRecordMetric: AthPerformanceMetric }>(\n `mutation AthRecordMetric($input: AthRecordMetricInput!) {\n athRecordMetric(input: $input) { ${METRIC_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athRecordMetric;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'metrics'] });\n },\n });\n};\n\n// ============================================================================\n// Workspace files — drill demo videos live in wspace-files-svc, reached through\n// the same workspace gateway. Blobs never touch the atheletebridge subgraph.\n// ============================================================================\n\nconst fileExtensionOf = (fileName: string): string => {\n const dot = fileName.lastIndexOf('.');\n return dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n};\n\n/**\n * Upload a file to wspace-files-svc via the standard three-step flow\n * (initiateUpload → presigned PUT → completeUpload) and resolve the new file id,\n * ready to pass as `AthAddDrillInput.videoFileId`.\n */\nexport const useUploadFile = () => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useCallback(\n async (file: File): Promise<string> => {\n const initiated = await athQuery<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation AthInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) { uploadUrl session { id } }\n }`,\n {\n input: {\n fileName: file.name,\n extension: fileExtensionOf(file.name),\n fileSize: file.size,\n mimeType: file.type || 'application/octet-stream',\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n\n const uploadUrl = initiated.initiateUpload.uploadUrl;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('Upload could not be initialised.');\n }\n\n const put = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type || 'application/octet-stream' },\n });\n if (!put.ok) {\n throw new Error(`Storage rejected the upload (HTTP ${put.status}).`);\n }\n\n const completed = await athQuery<{ completeUpload: { fileId: string } }>(\n `mutation AthCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) { fileId }\n }`,\n { input: { sessionId } },\n apiGatewayUrl\n );\n return completed.completeUpload.fileId;\n },\n [apiGatewayUrl]\n );\n};\n\nexport interface WorkspaceFileRef {\n id: string;\n name: string;\n downloadUrl: string | null;\n}\n\n/**\n * Resolve a wspace-files File (a drill's demo video) by id, on demand. Kept as a\n * separate lazy read so a missing/expired blob can never break the program page.\n */\nexport const useFile = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'file', id],\n queryFn: async () => {\n const result = await athQuery<{ getFile: WorkspaceFileRef | null }>(\n `query AthGetFile($id: ID!) {\n getFile(id: $id) { id name downloadUrl }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.getFile;\n },\n enabled: Boolean(id),\n staleTime: 60_000,\n });\n};\n\n// ============================================================================\n// User directory (global gateway) — resolve real users for team member add so a\n// coach never has to paste a raw UUID.\n// ============================================================================\n\nexport interface DirectoryUser {\n id: string;\n name: string | null;\n firstName: string | null;\n lastName: string | null;\n email: string;\n username: string;\n}\n\n/** Best display name for a directory user (name → first+last → username → email). */\nexport const directoryUserName = (user: DirectoryUser): string => {\n if (user.name?.trim()) return user.name.trim();\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n if (full) return full;\n if (user.username?.trim()) return user.username.trim();\n return user.email;\n};\n\n/**\n * Search the tenant user directory by name/email. Enabled once the term is at\n * least two characters; failures are surfaced to the caller (which offers a\n * manual-id fallback) rather than as a global error toast.\n */\nexport const useUserSearch = (search: string) => {\n const term = search.trim();\n return useQuery({\n queryKey: ['atheletebridge', 'user-search', term],\n queryFn: async () => {\n const result = await athGlobalQuery<{\n users: { users: DirectoryUser[]; hasMore: boolean };\n }>(\n `query AthUserSearch($filter: UserFilterInput, $pagination: PaginationInput) {\n users(filter: $filter, pagination: $pagination) {\n users { id name firstName lastName email username }\n hasMore\n }\n }`,\n { filter: { search: term }, pagination: { limit: 8, offset: 0 } }\n );\n return result.users.users;\n },\n enabled: term.length >= 2,\n staleTime: 30_000,\n retry: false,\n });\n};\n\n// ============================================================================\n// User id resolution (global gateway) — resolve a set of user ids to real\n// names/emails for identity chips (coach/athlete), mirroring the UserSearchSelect\n// directory pattern. Batched into one aliased request, cached, and RBAC-safe:\n// on any error it degrades to an empty map so callers fall back to id-seeded\n// monograms rather than surfacing an error toast.\n// ============================================================================\n\nexport interface ResolvedUser {\n id: string;\n name: string;\n email: string | null;\n}\n\n/**\n * Resolve the given user ids to `{ id, name, email }` via the global `user(id)`\n * query (one aliased round-trip). Unknown / forbidden ids are simply absent from\n * the returned map.\n */\nexport const useUsersByIds = (ids: ReadonlyArray<string | null | undefined>) => {\n const unique = useMemo(\n () =>\n Array.from(new Set(ids.filter((value): value is string => Boolean(value && value.trim())))).sort(),\n [ids]\n );\n\n return useQuery({\n queryKey: ['atheletebridge', 'users-by-id', unique],\n queryFn: async (): Promise<Record<string, ResolvedUser>> => {\n if (unique.length === 0) {\n return {};\n }\n const varDefs = unique.map((_, i) => `$id${i}: ID!`).join(', ');\n const selections = unique\n .map((_, i) => `u${i}: user(id: $id${i}) { id name firstName lastName email username }`)\n .join('\\n');\n const variables: Record<string, string> = {};\n unique.forEach((id, i) => {\n variables[`id${i}`] = id;\n });\n\n // Read tolerantly: don't throw on partial/RBAC errors — keep whatever\n // aliases resolved so a single forbidden id can't blank the whole feed.\n const response = await graphqlFetch<Record<string, DirectoryUser | null>>({\n gateway: 'global',\n query: `query AthUsersByIds(${varDefs}) {\\n${selections}\\n}`,\n variables,\n suppressGlobalErrorEvent: true,\n });\n\n const data = response.data ?? {};\n const map: Record<string, ResolvedUser> = {};\n for (const value of Object.values(data)) {\n if (value && value.id) {\n map[value.id] = {\n id: value.id,\n name: directoryUserName(value),\n email: value.email ?? null,\n };\n }\n }\n return map;\n },\n enabled: unique.length > 0,\n staleTime: 5 * 60_000,\n retry: false,\n });\n};\n\nexport interface UserDirectory {\n byId: Record<string, ResolvedUser>;\n isLoading: boolean;\n isError: boolean;\n /** Resolved display name for an id, or undefined while unknown. */\n nameOf: (id: string | null | undefined) => string | undefined;\n /** Resolved email for an id, or undefined while unknown. */\n emailOf: (id: string | null | undefined) => string | null | undefined;\n}\n\n/**\n * Convenience wrapper over {@link useUsersByIds} returning name/email lookup\n * helpers, so identity components can resolve ids without touching the query.\n */\nexport const useUserDirectory = (ids: ReadonlyArray<string | null | undefined>): UserDirectory => {\n const query = useUsersByIds(ids);\n const byId = query.data ?? {};\n return {\n byId,\n isLoading: query.isLoading,\n isError: query.isError,\n nameOf: (id) => (id ? byId[id]?.name : undefined),\n emailOf: (id) => (id ? byId[id]?.email : undefined),\n };\n};\n\n// ============================================================================\n// Recent activity — a unified, time-ordered feed derived PURELY from records\n// already loaded on the page (teams/fixtures/sessions/programs). No extra\n// network, no fabricated events: every item maps to a real timestamp on a real\n// row.\n// ============================================================================\n\nexport type ActivityType =\n | 'team-created'\n | 'member-added'\n | 'fixture-updated'\n | 'program-published'\n | 'session-completed';\n\nexport interface ActivityItem {\n id: string;\n type: ActivityType;\n title: string;\n subtitle?: string;\n /** ISO timestamp the event occurred at. */\n at: string;\n entityId: string;\n entityKind: 'team' | 'fixture' | 'session' | 'program';\n}\n\nexport interface RecentActivityInput {\n teams?: AthTeam[];\n fixtures?: AthFixture[];\n sessions?: AthSession[];\n programs?: AthProgram[];\n /** Max items to return (newest first). Default 8. */\n limit?: number;\n}\n\n/** Derive a typed, time-ordered activity feed from already-loaded records. */\nexport const useRecentActivity = ({\n teams = [],\n fixtures = [],\n sessions = [],\n programs = [],\n limit = 8,\n}: RecentActivityInput): ActivityItem[] =>\n useMemo(() => {\n const items: ActivityItem[] = [];\n\n for (const team of teams) {\n items.push({\n id: `team-${team.id}`,\n type: 'team-created',\n title: `Team ${team.name} created`,\n at: team.createdAt,\n entityId: team.id,\n entityKind: 'team',\n });\n for (const member of team.members ?? []) {\n items.push({\n id: `member-${member.id}`,\n type: 'member-added',\n title: `${member.displayName} added to ${team.name}`,\n at: member.joinedAt ?? member.createdAt,\n entityId: team.id,\n entityKind: 'team',\n });\n }\n }\n\n for (const program of programs) {\n if (program.status === 'PUBLISHED') {\n items.push({\n id: `program-${program.id}`,\n type: 'program-published',\n title: `${program.name} published`,\n at: program.updatedAt,\n entityId: program.id,\n entityKind: 'program',\n });\n }\n }\n\n for (const fixture of fixtures) {\n const home = fixture.homeTeam?.name ?? 'Home';\n const away = fixture.awayTeam?.name ?? (fixture.awayTeamId ? 'Away' : 'TBD');\n const hasScore = fixture.homeScore !== null && fixture.awayScore !== null;\n items.push({\n id: `fixture-${fixture.id}`,\n type: 'fixture-updated',\n title: hasScore\n ? `${home} ${fixture.homeScore}–${fixture.awayScore} ${away}`\n : `Fixture ${home} vs ${away} updated`,\n subtitle: fixture.competition ?? undefined,\n at: fixture.updatedAt,\n entityId: fixture.id,\n entityKind: 'fixture',\n });\n }\n\n for (const session of sessions) {\n if (session.status === 'COMPLETED') {\n items.push({\n id: `session-${session.id}`,\n type: 'session-completed',\n title: `Session \"${session.title}\" completed`,\n subtitle: session.rating !== null ? `Rated ${session.rating}/5` : undefined,\n at: session.completedAt ?? session.updatedAt,\n entityId: session.id,\n entityKind: 'session',\n });\n }\n }\n\n return items\n .filter((item) => Boolean(item.at) && !Number.isNaN(new Date(item.at).getTime()))\n .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime())\n .slice(0, limit);\n }, [teams, fixtures, sessions, programs, limit]);\n\n// ============================================================================\n// Athlete profile — compose a team member with that athlete's real training\n// data (performance metrics + workout logs + coaching sessions), keyed off the\n// member's userId (the athleteId used across the training tables).\n// ============================================================================\n\nexport interface AthleteProfile {\n /** The resolved team member, or null while loading / not found. */\n member: AthMember | null;\n /** The user id used across sessions / metrics / workouts. */\n athleteId: string | undefined;\n metrics: AthPerformanceMetric[];\n workouts: AthWorkoutLog[];\n sessions: AthSession[];\n isLoading: boolean;\n isError: boolean;\n}\n\n/** Compose a member + their metrics, workout logs and sessions into a profile. */\nexport const useAthleteProfile = (\n teamId: string | undefined,\n memberId: string | undefined\n): AthleteProfile => {\n const membersQuery = useTeamMembers(teamId);\n const member = useMemo(\n () => (membersQuery.data ?? []).find((m) => m.id === memberId) ?? null,\n [membersQuery.data, memberId]\n );\n const athleteId = member?.userId;\n\n const metricsQuery = usePerformanceMetrics(athleteId);\n const workoutsQuery = useWorkoutLogs({ athleteId, pagination: { limit: 100 } });\n const sessionsQuery = useSessions({ athleteId, pagination: { limit: 100 } });\n\n return {\n member,\n athleteId,\n metrics: metricsQuery.data ?? [],\n workouts: workoutsQuery.data?.items ?? [],\n sessions: sessionsQuery.data?.items ?? [],\n isLoading:\n membersQuery.isLoading ||\n (Boolean(athleteId) &&\n (metricsQuery.isLoading || workoutsQuery.isLoading || sessionsQuery.isLoading)),\n isError:\n membersQuery.isError ||\n metricsQuery.isError ||\n workoutsQuery.isError ||\n sessionsQuery.isError,\n };\n};\n\n// ============================================================================\n// Coach analytics — honest aggregates over real sessions / workouts / fixtures\n// / standings. No fabricated wellness or injury data.\n// ============================================================================\n\nexport interface SessionStatusCount {\n status: AthSessionStatus;\n count: number;\n}\n\nexport interface RatingByCoach {\n coachId: string;\n avgRating: number;\n count: number;\n}\n\nexport interface AnalyticsTrendPoint {\n label: string;\n value: number;\n}\n\nexport interface CoachAnalytics {\n totalSessions: number;\n ratedCount: number;\n /** Mean of session.rating over rated sessions, or null when none rated. */\n avgRating: number | null;\n completedCount: number;\n completedPct: number;\n sessionsByStatus: SessionStatusCount[];\n ratingByCoach: RatingByCoach[];\n coachIds: string[];\n /** Acute (last 7d) vs chronic (avg weekly over 28d) workout load + ratio. */\n trainingLoad: { acute: number; chronic: number; ratio: number | null };\n /** Weekly total workout load, oldest → newest (from real totalLoad). */\n loadTrend: AnalyticsTrendPoint[];\n /** Weekly session count, oldest → newest. */\n sessionTrend: AnalyticsTrendPoint[];\n /** Standings snapshot for the resolved competition. */\n standings: AthStanding[];\n competition: string | null;\n isLoading: boolean;\n isError: boolean;\n}\n\nexport interface CoachAnalyticsOptions {\n /** Competition for the standings snapshot; else the most-used one in fixtures. */\n competition?: string;\n /** Rows to sample from each list query. Default 200. */\n sampleLimit?: number;\n}\n\n/** Bucket rows into `weeks` weekly totals ending at \"now\" (oldest → newest). */\nfunction weeklyTrend<T>(\n rows: readonly T[],\n getDate: (row: T) => string,\n getValue: (row: T) => number,\n weeks = 8\n): AnalyticsTrendPoint[] {\n const MS_WEEK = 7 * 86_400_000;\n const now = Date.now();\n const buckets = Array.from({ length: weeks }, () => 0);\n for (const row of rows) {\n const time = new Date(getDate(row)).getTime();\n if (Number.isNaN(time)) {\n continue;\n }\n const age = now - time;\n if (age < 0) {\n continue;\n }\n const weeksAgo = Math.floor(age / MS_WEEK);\n if (weeksAgo >= 0 && weeksAgo < weeks) {\n buckets[weeks - 1 - weeksAgo] += getValue(row);\n }\n }\n return buckets.map((value, i) => {\n const ago = weeks - 1 - i;\n return { label: ago === 0 ? 'Now' : `${ago}w`, value };\n });\n}\n\n/** Aggregate real coaching data for the analytics dashboard. */\nexport const useCoachAnalytics = (options: CoachAnalyticsOptions = {}): CoachAnalytics => {\n const { competition: competitionOpt, sampleLimit = 200 } = options;\n\n const sessionsQuery = useSessions({ pagination: { limit: sampleLimit } });\n const workoutsQuery = useWorkoutLogs({ pagination: { limit: sampleLimit } });\n const fixturesQuery = useFixtures({ pagination: { limit: sampleLimit } });\n\n const sessions = useMemo(() => sessionsQuery.data?.items ?? [], [sessionsQuery.data]);\n const workouts = useMemo(() => workoutsQuery.data?.items ?? [], [workoutsQuery.data]);\n const fixtures = useMemo(() => fixturesQuery.data?.items ?? [], [fixturesQuery.data]);\n\n // Resolve the competition for the standings snapshot.\n const competition = useMemo(() => {\n if (competitionOpt) {\n return competitionOpt;\n }\n const counts = new Map<string, number>();\n for (const fixture of fixtures) {\n if (fixture.competition) {\n counts.set(fixture.competition, (counts.get(fixture.competition) ?? 0) + 1);\n }\n }\n let top: string | null = null;\n let best = 0;\n for (const [comp, n] of counts) {\n if (n > best) {\n best = n;\n top = comp;\n }\n }\n return top;\n }, [competitionOpt, fixtures]);\n\n const standingsQuery = useStandings(competition ?? undefined);\n\n return useMemo<CoachAnalytics>(() => {\n const totalSessions = sessions.length;\n const rated = sessions.filter((s) => s.rating !== null);\n const ratedCount = rated.length;\n const avgRating =\n ratedCount > 0 ? rated.reduce((acc, s) => acc + (s.rating ?? 0), 0) / ratedCount : null;\n const completedCount = sessions.filter((s) => s.status === 'COMPLETED').length;\n const completedPct = totalSessions > 0 ? (completedCount / totalSessions) * 100 : 0;\n\n const statusOrder: AthSessionStatus[] = [\n 'REQUESTED',\n 'CONFIRMED',\n 'COMPLETED',\n 'CANCELLED',\n 'NO_SHOW',\n ];\n const statusCounts = new Map<AthSessionStatus, number>();\n for (const s of sessions) {\n statusCounts.set(s.status, (statusCounts.get(s.status) ?? 0) + 1);\n }\n const sessionsByStatus = statusOrder\n .map((status) => ({ status, count: statusCounts.get(status) ?? 0 }))\n .filter((entry) => entry.count > 0);\n\n const byCoach = new Map<string, { sum: number; count: number }>();\n for (const s of rated) {\n const entry = byCoach.get(s.coachId) ?? { sum: 0, count: 0 };\n entry.sum += s.rating ?? 0;\n entry.count += 1;\n byCoach.set(s.coachId, entry);\n }\n const ratingByCoach = Array.from(byCoach.entries())\n .map(([coachId, { sum, count }]) => ({ coachId, avgRating: sum / count, count }))\n .sort((a, b) => b.avgRating - a.avgRating);\n\n const coachIds = Array.from(new Set(sessions.map((s) => s.coachId)));\n\n // Training load (ACWR) from real workout totalLoad.\n const now = Date.now();\n const DAY = 86_400_000;\n let acute = 0;\n let chronic28 = 0;\n for (const w of workouts) {\n const load = w.totalLoad ?? 0;\n const age = now - new Date(w.performedAt).getTime();\n if (Number.isNaN(age)) {\n continue;\n }\n if (age <= 7 * DAY) {\n acute += load;\n }\n if (age <= 28 * DAY) {\n chronic28 += load;\n }\n }\n const chronic = chronic28 / 4;\n const ratio = chronic > 0 ? acute / chronic : null;\n\n const loadTrend = weeklyTrend(\n workouts,\n (w) => w.performedAt,\n (w) => w.totalLoad ?? 0\n );\n const sessionTrend = weeklyTrend(\n sessions,\n (s) => s.scheduledAt,\n () => 1\n );\n\n return {\n totalSessions,\n ratedCount,\n avgRating,\n completedCount,\n completedPct,\n sessionsByStatus,\n ratingByCoach,\n coachIds,\n trainingLoad: { acute, chronic, ratio },\n loadTrend,\n sessionTrend,\n standings: standingsQuery.data ?? [],\n competition,\n isLoading:\n sessionsQuery.isLoading ||\n workoutsQuery.isLoading ||\n fixturesQuery.isLoading ||\n (Boolean(competition) && standingsQuery.isLoading),\n isError:\n sessionsQuery.isError ||\n workoutsQuery.isError ||\n fixturesQuery.isError ||\n standingsQuery.isError,\n };\n }, [\n sessions,\n workouts,\n competition,\n standingsQuery.data,\n standingsQuery.isLoading,\n standingsQuery.isError,\n sessionsQuery.isLoading,\n sessionsQuery.isError,\n workoutsQuery.isLoading,\n workoutsQuery.isError,\n fixturesQuery.isLoading,\n fixturesQuery.isError,\n ]);\n};\n\n// ============================================================================\n// Health probe\n// ============================================================================\n\nexport const useHealth = () => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'health'],\n queryFn: async () => {\n const result = await athQuery<{ athHealth: string }>(\n `query AthHealth { athHealth }`,\n {},\n apiGatewayUrl\n );\n return result.athHealth;\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAthleteBridgeApi.js","names":[],"sources":["../../../src/atheletebridge/hooks/useAthleteBridgeApi.ts"],"sourcesContent":["/**\n * AthleteBridge API hooks\n * @module atheletebridge/hooks\n *\n * Thin react-query wrappers over the real `wspace-atheletebridge-svc` federated\n * GraphQL contract, reached through the workspace gateway with the workspace\n * dual-token. Every query/mutation here maps 1:1 to an `ath*` root field in the\n * service's `.hive-schema.graphql`. NO MOCKS — all data is live.\n */\n\nimport { useCallback, useMemo } from 'react';\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { graphqlFetch, clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useAthleteBridge } from '../context';\nimport type {\n AthCompleteSessionInput,\n AthCreateFixtureInput,\n AthCreateProgramInput,\n AthCreateTeamInput,\n AthAddMemberInput,\n AthAddDrillInput,\n AthBookSessionInput,\n AthDrill,\n AthFixture,\n AthFixtureConnection,\n AthFixtureStatus,\n AthLogWorkoutInput,\n AthMember,\n AthMemberStatus,\n AthPerformanceMetric,\n AthProgram,\n AthProgramConnection,\n AthProgramLevel,\n AthProgramStatus,\n AthRecordMetricInput,\n AthRecordResultInput,\n AthSession,\n AthSessionConnection,\n AthSessionStatus,\n AthSport,\n AthStanding,\n AthTeam,\n AthTeamConnection,\n AthTeamStatus,\n AthUpdateProgramInput,\n AthUpdateTeamInput,\n AthWorkoutLog,\n AthWorkoutLogConnection,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Low-level query wrapper\n// ============================================================================\n\nconst athQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n\n // This MFE bypasses Apollo, so fe-libs' short-lived graphqlFetch read cache\n // would otherwise serve pre-write data to the very next refetch issued by a\n // mutation's onSuccess invalidation. Clear it after any successful mutation so\n // every downstream read is fresh (fleet-wide stale-read-after-write fix).\n if (query.trimStart().startsWith('mutation')) {\n clearGraphqlFetchResponseCache();\n }\n\n return response.data;\n};\n\n// Global-gateway read wrapper — the workspace-scoped MFE reaches the tenant user\n// directory (for real user pickers) through the global gateway.\nconst athGlobalQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'global',\n query,\n variables,\n suppressGlobalErrorEvent: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n\n return response.data;\n};\n\n// ============================================================================\n// GraphQL fragments (inline strings — exact fields from the svc schema)\n// ============================================================================\n\nconst TEAM_FIELDS = `\n id workspaceId name sport status description ageGroup season homeVenue\n headCoachId tags createdAt updatedAt\n`;\n\nconst MEMBER_FIELDS = `\n id workspaceId teamId userId displayName role status position jerseyNumber\n height weight dominantSide notes joinedAt createdAt updatedAt\n`;\n\nconst FIXTURE_FIELDS = `\n id workspaceId type status competition round venue homeTeamId awayTeamId\n homeScore awayScore scheduledAt kickoffAt notes createdAt updatedAt\n homeTeam { id name sport }\n awayTeam { id name sport }\n`;\n\nconst STANDING_FIELDS = `\n id workspaceId competition teamId played won drawn lost goalsFor goalsAgainst\n points rank createdAt updatedAt\n team { id name sport }\n`;\n\nconst DRILL_FIELDS = `\n id workspaceId programId name description weekNumber orderIndex sets reps\n durationSec restSec createdAt updatedAt\n video { id }\n`;\n\nconst PROGRAM_FIELDS = `\n id workspaceId coachId name sport level status summary weeks isTemplate tags\n createdAt updatedAt\n`;\n\nconst SESSION_FIELDS = `\n id workspaceId coachId athleteId programId title type status location\n meetingUrl scheduledAt durationMin completedAt feedback rating\n createdAt updatedAt\n`;\n\nconst WORKOUT_LOG_FIELDS = `\n id workspaceId athleteId programId title status performedAt durationMin rpe\n totalLoad metrics notes createdAt updatedAt\n`;\n\nconst METRIC_FIELDS = `\n id workspaceId athleteId type label value unit source recordedAt\n createdAt updatedAt\n`;\n\n// ============================================================================\n// Teams\n// ============================================================================\n\nexport interface TeamFilters {\n sport?: AthSport;\n status?: AthTeamStatus;\n pagination?: PaginationInput;\n}\n\nexport const useTeams = (filters: TeamFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'teams', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListTeams: AthTeamConnection }>(\n `query AthListTeams($sport: AthSport, $status: AthTeamStatus, $pagination: PaginationInput) {\n athListTeams(sport: $sport, status: $status, pagination: $pagination) {\n items { ${TEAM_FIELDS} members { id } }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n sport: filters.sport,\n status: filters.status,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListTeams;\n },\n });\n};\n\nexport const useTeam = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'team', id],\n queryFn: async () => {\n const result = await athQuery<{ athTeam: AthTeam | null }>(\n `query AthTeam($id: ID!) {\n athTeam(id: $id) {\n ${TEAM_FIELDS}\n members { ${MEMBER_FIELDS} }\n fixtures { ${FIXTURE_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athTeam;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useTeamMembers = (teamId: string | undefined, status?: AthMemberStatus) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'members', teamId, status],\n queryFn: async () => {\n const result = await athQuery<{ athListMembers: AthMember[] }>(\n `query AthListMembers($teamId: ID!, $status: AthMemberStatus) {\n athListMembers(teamId: $teamId, status: $status) { ${MEMBER_FIELDS} }\n }`,\n { teamId, status },\n apiGatewayUrl\n );\n return result.athListMembers;\n },\n enabled: Boolean(teamId),\n });\n};\n\nexport const useCreateTeam = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthCreateTeamInput) => {\n const result = await athQuery<{ athCreateTeam: AthTeam }>(\n `mutation AthCreateTeam($input: AthCreateTeamInput!) {\n athCreateTeam(input: $input) { ${TEAM_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athCreateTeam;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'teams'] });\n },\n });\n};\n\nexport const useUpdateTeam = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: AthUpdateTeamInput }) => {\n const result = await athQuery<{ athUpdateTeam: AthTeam }>(\n `mutation AthUpdateTeam($id: ID!, $input: AthUpdateTeamInput!) {\n athUpdateTeam(id: $id, input: $input) { ${TEAM_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athUpdateTeam;\n },\n onSuccess: (team) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'teams'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'team', team.id] });\n },\n });\n};\n\nexport const useArchiveTeam = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athArchiveTeam: boolean }>(\n `mutation AthArchiveTeam($id: ID!) { athArchiveTeam(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athArchiveTeam;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'teams'] });\n },\n });\n};\n\nexport const useAddMember = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthAddMemberInput) => {\n const result = await athQuery<{ athAddMember: AthMember }>(\n `mutation AthAddMember($input: AthAddMemberInput!) {\n athAddMember(input: $input) { ${MEMBER_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athAddMember;\n },\n onSuccess: (member) => {\n void queryClient.invalidateQueries({\n queryKey: ['atheletebridge', 'members', member.teamId],\n });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'team', member.teamId] });\n },\n });\n};\n\nexport const useRemoveMember = (teamId?: string) => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athRemoveMember: boolean }>(\n `mutation AthRemoveMember($id: ID!) { athRemoveMember(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athRemoveMember;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'members', teamId] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'team', teamId] });\n },\n });\n};\n\n// ============================================================================\n// Fixtures & standings\n// ============================================================================\n\nexport interface FixtureFilters {\n status?: AthFixtureStatus;\n teamId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useFixtures = (filters: FixtureFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'fixtures', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListFixtures: AthFixtureConnection }>(\n `query AthListFixtures($status: AthFixtureStatus, $teamId: ID, $pagination: PaginationInput) {\n athListFixtures(status: $status, teamId: $teamId, pagination: $pagination) {\n items { ${FIXTURE_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n status: filters.status,\n teamId: filters.teamId,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListFixtures;\n },\n });\n};\n\nexport const useFixture = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'fixture', id],\n queryFn: async () => {\n const result = await athQuery<{ athFixture: AthFixture | null }>(\n `query AthFixture($id: ID!) {\n athFixture(id: $id) { ${FIXTURE_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athFixture;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useStandings = (competition: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'standings', competition],\n queryFn: async () => {\n const result = await athQuery<{ athStandings: AthStanding[] }>(\n `query AthStandings($competition: String!) {\n athStandings(competition: $competition) { ${STANDING_FIELDS} }\n }`,\n { competition },\n apiGatewayUrl\n );\n return result.athStandings;\n },\n enabled: Boolean(competition),\n });\n};\n\nexport const useScheduleFixture = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthCreateFixtureInput) => {\n const result = await athQuery<{ athScheduleFixture: AthFixture }>(\n `mutation AthScheduleFixture($input: AthCreateFixtureInput!) {\n athScheduleFixture(input: $input) { ${FIXTURE_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athScheduleFixture;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'fixtures'] });\n },\n });\n};\n\nexport const useRecordFixtureResult = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: AthRecordResultInput }) => {\n const result = await athQuery<{ athRecordFixtureResult: AthFixture }>(\n `mutation AthRecordFixtureResult($id: ID!, $input: AthRecordResultInput!) {\n athRecordFixtureResult(id: $id, input: $input) { ${FIXTURE_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athRecordFixtureResult;\n },\n onSuccess: (fixture) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'fixtures'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'fixture', fixture.id] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'standings'] });\n },\n });\n};\n\n// ============================================================================\n// Programs & drills\n// ============================================================================\n\nexport interface ProgramFilters {\n sport?: AthSport;\n level?: AthProgramLevel;\n status?: AthProgramStatus;\n pagination?: PaginationInput;\n}\n\nexport const usePrograms = (filters: ProgramFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'programs', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListPrograms: AthProgramConnection }>(\n `query AthListPrograms($sport: AthSport, $level: AthProgramLevel, $status: AthProgramStatus, $pagination: PaginationInput) {\n athListPrograms(sport: $sport, level: $level, status: $status, pagination: $pagination) {\n items { ${PROGRAM_FIELDS} drills { id } }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n sport: filters.sport,\n level: filters.level,\n status: filters.status,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListPrograms;\n },\n });\n};\n\nexport const useProgram = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'program', id],\n queryFn: async () => {\n const result = await athQuery<{ athProgram: AthProgram | null }>(\n `query AthProgram($id: ID!) {\n athProgram(id: $id) {\n ${PROGRAM_FIELDS}\n drills { ${DRILL_FIELDS} }\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athProgram;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useCreateProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthCreateProgramInput) => {\n const result = await athQuery<{ athCreateProgram: AthProgram }>(\n `mutation AthCreateProgram($input: AthCreateProgramInput!) {\n athCreateProgram(input: $input) { ${PROGRAM_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athCreateProgram;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n },\n });\n};\n\nexport const useUpdateProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input: AthUpdateProgramInput }) => {\n const result = await athQuery<{ athUpdateProgram: AthProgram }>(\n `mutation AthUpdateProgram($id: ID!, $input: AthUpdateProgramInput!) {\n athUpdateProgram(id: $id, input: $input) { ${PROGRAM_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athUpdateProgram;\n },\n onSuccess: (program) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'program', program.id] });\n },\n });\n};\n\nexport const useTransitionProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, status }: { id: string; status: AthProgramStatus }) => {\n const result = await athQuery<{ athTransitionProgram: AthProgram }>(\n `mutation AthTransitionProgram($id: ID!, $status: AthProgramStatus!) {\n athTransitionProgram(id: $id, status: $status) { ${PROGRAM_FIELDS} }\n }`,\n { id, status },\n apiGatewayUrl\n );\n return result.athTransitionProgram;\n },\n onSuccess: (program) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'program', program.id] });\n },\n });\n};\n\nexport const useArchiveProgram = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athArchiveProgram: boolean }>(\n `mutation AthArchiveProgram($id: ID!) { athArchiveProgram(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athArchiveProgram;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'programs'] });\n },\n });\n};\n\nexport const useAddDrill = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthAddDrillInput) => {\n const result = await athQuery<{ athAddDrill: AthDrill }>(\n `mutation AthAddDrill($input: AthAddDrillInput!) {\n athAddDrill(input: $input) { ${DRILL_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athAddDrill;\n },\n onSuccess: (drill) => {\n void queryClient.invalidateQueries({\n queryKey: ['atheletebridge', 'program', drill.programId],\n });\n },\n });\n};\n\nexport const useRemoveDrill = (programId?: string) => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athRemoveDrill: boolean }>(\n `mutation AthRemoveDrill($id: ID!) { athRemoveDrill(id: $id) }`,\n { id },\n apiGatewayUrl\n );\n return result.athRemoveDrill;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'program', programId] });\n },\n });\n};\n\n// ============================================================================\n// Coaching sessions\n// ============================================================================\n\nexport interface SessionFilters {\n athleteId?: string;\n coachId?: string;\n status?: AthSessionStatus;\n pagination?: PaginationInput;\n}\n\nexport const useSessions = (filters: SessionFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'sessions', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListSessions: AthSessionConnection }>(\n `query AthListSessions($athleteId: ID, $coachId: ID, $status: AthSessionStatus, $pagination: PaginationInput) {\n athListSessions(athleteId: $athleteId, coachId: $coachId, status: $status, pagination: $pagination) {\n items { ${SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n athleteId: filters.athleteId,\n coachId: filters.coachId,\n status: filters.status,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListSessions;\n },\n });\n};\n\nexport const useSession = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'session', id],\n queryFn: async () => {\n const result = await athQuery<{ athSession: AthSession | null }>(\n `query AthSession($id: ID!) {\n athSession(id: $id) { ${SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athSession;\n },\n enabled: Boolean(id),\n });\n};\n\nexport const useBookSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthBookSessionInput) => {\n const result = await athQuery<{ athBookSession: AthSession }>(\n `mutation AthBookSession($input: AthBookSessionInput!) {\n athBookSession(input: $input) { ${SESSION_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athBookSession;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n },\n });\n};\n\nexport const useConfirmSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athConfirmSession: AthSession }>(\n `mutation AthConfirmSession($id: ID!) {\n athConfirmSession(id: $id) { ${SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athConfirmSession;\n },\n onSuccess: (session) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'session', session.id] });\n },\n });\n};\n\nexport const useCompleteSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async ({ id, input }: { id: string; input?: AthCompleteSessionInput }) => {\n const result = await athQuery<{ athCompleteSession: AthSession }>(\n `mutation AthCompleteSession($id: ID!, $input: AthCompleteSessionInput) {\n athCompleteSession(id: $id, input: $input) { ${SESSION_FIELDS} }\n }`,\n { id, input },\n apiGatewayUrl\n );\n return result.athCompleteSession;\n },\n onSuccess: (session) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'session', session.id] });\n },\n });\n};\n\nexport const useCancelSession = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await athQuery<{ athCancelSession: AthSession }>(\n `mutation AthCancelSession($id: ID!) {\n athCancelSession(id: $id) { ${SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.athCancelSession;\n },\n onSuccess: (session) => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'sessions'] });\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'session', session.id] });\n },\n });\n};\n\n// ============================================================================\n// Workout logs & performance metrics\n// ============================================================================\n\nexport interface WorkoutLogFilters {\n athleteId?: string;\n programId?: string;\n pagination?: PaginationInput;\n}\n\nexport const useWorkoutLogs = (filters: WorkoutLogFilters = {}) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'workout-logs', filters],\n queryFn: async () => {\n const result = await athQuery<{ athListWorkoutLogs: AthWorkoutLogConnection }>(\n `query AthListWorkoutLogs($athleteId: ID, $programId: ID, $pagination: PaginationInput) {\n athListWorkoutLogs(athleteId: $athleteId, programId: $programId, pagination: $pagination) {\n items { ${WORKOUT_LOG_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n {\n athleteId: filters.athleteId,\n programId: filters.programId,\n pagination: filters.pagination,\n },\n apiGatewayUrl\n );\n return result.athListWorkoutLogs;\n },\n });\n};\n\nexport const usePerformanceMetrics = (athleteId: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'metrics', athleteId],\n queryFn: async () => {\n const result = await athQuery<{ athPerformanceMetrics: AthPerformanceMetric[] }>(\n `query AthPerformanceMetrics($athleteId: ID!) {\n athPerformanceMetrics(athleteId: $athleteId) { ${METRIC_FIELDS} }\n }`,\n { athleteId },\n apiGatewayUrl\n );\n return result.athPerformanceMetrics;\n },\n enabled: Boolean(athleteId),\n });\n};\n\nexport const useLogWorkout = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthLogWorkoutInput) => {\n const result = await athQuery<{ athLogWorkout: AthWorkoutLog }>(\n `mutation AthLogWorkout($input: AthLogWorkoutInput!) {\n athLogWorkout(input: $input) { ${WORKOUT_LOG_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athLogWorkout;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'workout-logs'] });\n },\n });\n};\n\nexport const useRecordMetric = () => {\n const queryClient = useQueryClient();\n const { apiGatewayUrl } = useAthleteBridge();\n return useMutation({\n mutationFn: async (input: AthRecordMetricInput) => {\n const result = await athQuery<{ athRecordMetric: AthPerformanceMetric }>(\n `mutation AthRecordMetric($input: AthRecordMetricInput!) {\n athRecordMetric(input: $input) { ${METRIC_FIELDS} }\n }`,\n { input },\n apiGatewayUrl\n );\n return result.athRecordMetric;\n },\n onSuccess: () => {\n void queryClient.invalidateQueries({ queryKey: ['atheletebridge', 'metrics'] });\n },\n });\n};\n\n// ============================================================================\n// Workspace files — drill demo videos live in wspace-files-svc, reached through\n// the same workspace gateway. Blobs never touch the atheletebridge subgraph.\n// ============================================================================\n\nconst fileExtensionOf = (fileName: string): string => {\n const dot = fileName.lastIndexOf('.');\n return dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n};\n\n/**\n * Upload a file to wspace-files-svc via the standard three-step flow\n * (initiateUpload → presigned PUT → completeUpload) and resolve the new file id,\n * ready to pass as `AthAddDrillInput.videoFileId`.\n */\nexport const useUploadFile = () => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useCallback(\n async (file: File): Promise<string> => {\n const initiated = await athQuery<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation AthInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) { uploadUrl session { id } }\n }`,\n {\n input: {\n fileName: file.name,\n extension: fileExtensionOf(file.name),\n fileSize: file.size,\n mimeType: file.type || 'application/octet-stream',\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n\n const uploadUrl = initiated.initiateUpload.uploadUrl;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('Upload could not be initialised.');\n }\n\n const put = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type || 'application/octet-stream' },\n });\n if (!put.ok) {\n throw new Error(`Storage rejected the upload (HTTP ${put.status}).`);\n }\n\n const completed = await athQuery<{ completeUpload: { fileId: string } }>(\n `mutation AthCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) { fileId }\n }`,\n { input: { sessionId } },\n apiGatewayUrl\n );\n return completed.completeUpload.fileId;\n },\n [apiGatewayUrl]\n );\n};\n\nexport interface WorkspaceFileRef {\n id: string;\n name: string;\n downloadUrl: string | null;\n}\n\n/**\n * Resolve a wspace-files File (a drill's demo video) by id, on demand. Kept as a\n * separate lazy read so a missing/expired blob can never break the program page.\n */\nexport const useFile = (id: string | undefined) => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'file', id],\n queryFn: async () => {\n const result = await athQuery<{ getFile: WorkspaceFileRef | null }>(\n `query AthGetFile($id: ID!) {\n getFile(id: $id) { id name downloadUrl }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.getFile;\n },\n enabled: Boolean(id),\n staleTime: 60_000,\n });\n};\n\n// ============================================================================\n// User directory (global gateway) — resolve real users for team member add so a\n// coach never has to paste a raw UUID.\n// ============================================================================\n\nexport interface DirectoryUser {\n id: string;\n name: string | null;\n firstName: string | null;\n lastName: string | null;\n email: string;\n username: string;\n}\n\n/** Best display name for a directory user (name → first+last → username → email). */\nexport const directoryUserName = (user: DirectoryUser): string => {\n if (user.name?.trim()) return user.name.trim();\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n if (full) return full;\n if (user.username?.trim()) return user.username.trim();\n return user.email;\n};\n\n/**\n * Search the tenant user directory by name/email. Enabled once the term is at\n * least two characters; failures are surfaced to the caller (which offers a\n * manual-id fallback) rather than as a global error toast.\n */\nexport const useUserSearch = (search: string) => {\n const term = search.trim();\n return useQuery({\n queryKey: ['atheletebridge', 'user-search', term],\n queryFn: async () => {\n const result = await athGlobalQuery<{\n users: { users: DirectoryUser[]; hasMore: boolean };\n }>(\n `query AthUserSearch($filter: UserFilterInput, $pagination: PaginationInput) {\n users(filter: $filter, pagination: $pagination) {\n users { id name firstName lastName email username }\n hasMore\n }\n }`,\n { filter: { search: term }, pagination: { limit: 8, offset: 0 } }\n );\n return result.users.users;\n },\n enabled: term.length >= 2,\n staleTime: 30_000,\n retry: false,\n });\n};\n\n// ============================================================================\n// User id resolution (global gateway) — resolve a set of user ids to real\n// names/emails for identity chips (coach/athlete), mirroring the UserSearchSelect\n// directory pattern. Batched into one aliased request, cached, and RBAC-safe:\n// on any error it degrades to an empty map so callers fall back to id-seeded\n// monograms rather than surfacing an error toast.\n// ============================================================================\n\nexport interface ResolvedUser {\n id: string;\n name: string;\n email: string | null;\n}\n\n/**\n * Resolve the given user ids to `{ id, name, email }` via the global `user(id)`\n * query (one aliased round-trip). Unknown / forbidden ids are simply absent from\n * the returned map.\n */\nexport const useUsersByIds = (ids: ReadonlyArray<string | null | undefined>) => {\n const unique = useMemo(\n () =>\n Array.from(\n new Set(ids.filter((value): value is string => Boolean(value && value.trim())))\n ).sort(),\n [ids]\n );\n\n return useQuery({\n queryKey: ['atheletebridge', 'users-by-id', unique],\n queryFn: async (): Promise<Record<string, ResolvedUser>> => {\n if (unique.length === 0) {\n return {};\n }\n const varDefs = unique.map((_, i) => `$id${i}: ID!`).join(', ');\n const selections = unique\n .map((_, i) => `u${i}: user(id: $id${i}) { id name firstName lastName email username }`)\n .join('\\n');\n const variables: Record<string, string> = {};\n unique.forEach((id, i) => {\n variables[`id${i}`] = id;\n });\n\n // Read tolerantly: don't throw on partial/RBAC errors — keep whatever\n // aliases resolved so a single forbidden id can't blank the whole feed.\n const response = await graphqlFetch<Record<string, DirectoryUser | null>>({\n gateway: 'global',\n query: `query AthUsersByIds(${varDefs}) {\\n${selections}\\n}`,\n variables,\n suppressGlobalErrorEvent: true,\n });\n\n const data = response.data ?? {};\n const map: Record<string, ResolvedUser> = {};\n for (const value of Object.values(data)) {\n if (value && value.id) {\n map[value.id] = {\n id: value.id,\n name: directoryUserName(value),\n email: value.email ?? null,\n };\n }\n }\n return map;\n },\n enabled: unique.length > 0,\n staleTime: 5 * 60_000,\n retry: false,\n });\n};\n\nexport interface UserDirectory {\n byId: Record<string, ResolvedUser>;\n isLoading: boolean;\n isError: boolean;\n /** Resolved display name for an id, or undefined while unknown. */\n nameOf: (id: string | null | undefined) => string | undefined;\n /** Resolved email for an id, or undefined while unknown. */\n emailOf: (id: string | null | undefined) => string | null | undefined;\n}\n\n/**\n * Convenience wrapper over {@link useUsersByIds} returning name/email lookup\n * helpers, so identity components can resolve ids without touching the query.\n */\nexport const useUserDirectory = (ids: ReadonlyArray<string | null | undefined>): UserDirectory => {\n const query = useUsersByIds(ids);\n const byId = query.data ?? {};\n return {\n byId,\n isLoading: query.isLoading,\n isError: query.isError,\n nameOf: (id) => (id ? byId[id]?.name : undefined),\n emailOf: (id) => (id ? byId[id]?.email : undefined),\n };\n};\n\n// ============================================================================\n// Recent activity — a unified, time-ordered feed derived PURELY from records\n// already loaded on the page (teams/fixtures/sessions/programs). No extra\n// network, no fabricated events: every item maps to a real timestamp on a real\n// row.\n// ============================================================================\n\nexport type ActivityType =\n | 'team-created'\n | 'member-added'\n | 'fixture-updated'\n | 'program-published'\n | 'session-completed';\n\nexport interface ActivityItem {\n id: string;\n type: ActivityType;\n title: string;\n subtitle?: string;\n /** ISO timestamp the event occurred at. */\n at: string;\n entityId: string;\n entityKind: 'team' | 'fixture' | 'session' | 'program';\n}\n\nexport interface RecentActivityInput {\n teams?: AthTeam[];\n fixtures?: AthFixture[];\n sessions?: AthSession[];\n programs?: AthProgram[];\n /** Max items to return (newest first). Default 8. */\n limit?: number;\n}\n\n/** Derive a typed, time-ordered activity feed from already-loaded records. */\nexport const useRecentActivity = ({\n teams = [],\n fixtures = [],\n sessions = [],\n programs = [],\n limit = 8,\n}: RecentActivityInput): ActivityItem[] =>\n useMemo(() => {\n const items: ActivityItem[] = [];\n\n for (const team of teams) {\n items.push({\n id: `team-${team.id}`,\n type: 'team-created',\n title: `Team ${team.name} created`,\n at: team.createdAt,\n entityId: team.id,\n entityKind: 'team',\n });\n for (const member of team.members ?? []) {\n items.push({\n id: `member-${member.id}`,\n type: 'member-added',\n title: `${member.displayName} added to ${team.name}`,\n at: member.joinedAt ?? member.createdAt,\n entityId: team.id,\n entityKind: 'team',\n });\n }\n }\n\n for (const program of programs) {\n if (program.status === 'PUBLISHED') {\n items.push({\n id: `program-${program.id}`,\n type: 'program-published',\n title: `${program.name} published`,\n at: program.updatedAt,\n entityId: program.id,\n entityKind: 'program',\n });\n }\n }\n\n for (const fixture of fixtures) {\n const home = fixture.homeTeam?.name ?? 'Home';\n const away = fixture.awayTeam?.name ?? (fixture.awayTeamId ? 'Away' : 'TBD');\n const hasScore = fixture.homeScore !== null && fixture.awayScore !== null;\n items.push({\n id: `fixture-${fixture.id}`,\n type: 'fixture-updated',\n title: hasScore\n ? `${home} ${fixture.homeScore}–${fixture.awayScore} ${away}`\n : `Fixture ${home} vs ${away} updated`,\n subtitle: fixture.competition ?? undefined,\n at: fixture.updatedAt,\n entityId: fixture.id,\n entityKind: 'fixture',\n });\n }\n\n for (const session of sessions) {\n if (session.status === 'COMPLETED') {\n items.push({\n id: `session-${session.id}`,\n type: 'session-completed',\n title: `Session \"${session.title}\" completed`,\n subtitle: session.rating !== null ? `Rated ${session.rating}/5` : undefined,\n at: session.completedAt ?? session.updatedAt,\n entityId: session.id,\n entityKind: 'session',\n });\n }\n }\n\n return items\n .filter((item) => Boolean(item.at) && !Number.isNaN(new Date(item.at).getTime()))\n .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime())\n .slice(0, limit);\n }, [teams, fixtures, sessions, programs, limit]);\n\n// ============================================================================\n// Athlete profile — compose a team member with that athlete's real training\n// data (performance metrics + workout logs + coaching sessions), keyed off the\n// member's userId (the athleteId used across the training tables).\n// ============================================================================\n\nexport interface AthleteProfile {\n /** The resolved team member, or null while loading / not found. */\n member: AthMember | null;\n /** The user id used across sessions / metrics / workouts. */\n athleteId: string | undefined;\n metrics: AthPerformanceMetric[];\n workouts: AthWorkoutLog[];\n sessions: AthSession[];\n isLoading: boolean;\n isError: boolean;\n}\n\n/** Compose a member + their metrics, workout logs and sessions into a profile. */\nexport const useAthleteProfile = (\n teamId: string | undefined,\n memberId: string | undefined\n): AthleteProfile => {\n const membersQuery = useTeamMembers(teamId);\n const member = useMemo(\n () => (membersQuery.data ?? []).find((m) => m.id === memberId) ?? null,\n [membersQuery.data, memberId]\n );\n const athleteId = member?.userId;\n\n const metricsQuery = usePerformanceMetrics(athleteId);\n const workoutsQuery = useWorkoutLogs({ athleteId, pagination: { limit: 100 } });\n const sessionsQuery = useSessions({ athleteId, pagination: { limit: 100 } });\n\n return {\n member,\n athleteId,\n metrics: metricsQuery.data ?? [],\n workouts: workoutsQuery.data?.items ?? [],\n sessions: sessionsQuery.data?.items ?? [],\n isLoading:\n membersQuery.isLoading ||\n (Boolean(athleteId) &&\n (metricsQuery.isLoading || workoutsQuery.isLoading || sessionsQuery.isLoading)),\n isError:\n membersQuery.isError ||\n metricsQuery.isError ||\n workoutsQuery.isError ||\n sessionsQuery.isError,\n };\n};\n\n// ============================================================================\n// Coach analytics — honest aggregates over real sessions / workouts / fixtures\n// / standings. No fabricated wellness or injury data.\n// ============================================================================\n\nexport interface SessionStatusCount {\n status: AthSessionStatus;\n count: number;\n}\n\nexport interface RatingByCoach {\n coachId: string;\n avgRating: number;\n count: number;\n}\n\nexport interface AnalyticsTrendPoint {\n label: string;\n value: number;\n}\n\nexport interface CoachAnalytics {\n totalSessions: number;\n ratedCount: number;\n /** Mean of session.rating over rated sessions, or null when none rated. */\n avgRating: number | null;\n completedCount: number;\n completedPct: number;\n sessionsByStatus: SessionStatusCount[];\n ratingByCoach: RatingByCoach[];\n coachIds: string[];\n /** Acute (last 7d) vs chronic (avg weekly over 28d) workout load + ratio. */\n trainingLoad: { acute: number; chronic: number; ratio: number | null };\n /** Weekly total workout load, oldest → newest (from real totalLoad). */\n loadTrend: AnalyticsTrendPoint[];\n /** Weekly session count, oldest → newest. */\n sessionTrend: AnalyticsTrendPoint[];\n /** Standings snapshot for the resolved competition. */\n standings: AthStanding[];\n competition: string | null;\n isLoading: boolean;\n isError: boolean;\n}\n\nexport interface CoachAnalyticsOptions {\n /** Competition for the standings snapshot; else the most-used one in fixtures. */\n competition?: string;\n /** Rows to sample from each list query. Default 200. */\n sampleLimit?: number;\n}\n\n/** Bucket rows into `weeks` weekly totals ending at \"now\" (oldest → newest). */\nfunction weeklyTrend<T>(\n rows: readonly T[],\n getDate: (row: T) => string,\n getValue: (row: T) => number,\n weeks = 8\n): AnalyticsTrendPoint[] {\n const MS_WEEK = 7 * 86_400_000;\n const now = Date.now();\n const buckets = Array.from({ length: weeks }, () => 0);\n for (const row of rows) {\n const time = new Date(getDate(row)).getTime();\n if (Number.isNaN(time)) {\n continue;\n }\n const age = now - time;\n if (age < 0) {\n continue;\n }\n const weeksAgo = Math.floor(age / MS_WEEK);\n if (weeksAgo >= 0 && weeksAgo < weeks) {\n buckets[weeks - 1 - weeksAgo] += getValue(row);\n }\n }\n return buckets.map((value, i) => {\n const ago = weeks - 1 - i;\n return { label: ago === 0 ? 'Now' : `${ago}w`, value };\n });\n}\n\n/** Aggregate real coaching data for the analytics dashboard. */\nexport const useCoachAnalytics = (options: CoachAnalyticsOptions = {}): CoachAnalytics => {\n const { competition: competitionOpt, sampleLimit = 200 } = options;\n\n const sessionsQuery = useSessions({ pagination: { limit: sampleLimit } });\n const workoutsQuery = useWorkoutLogs({ pagination: { limit: sampleLimit } });\n const fixturesQuery = useFixtures({ pagination: { limit: sampleLimit } });\n\n const sessions = useMemo(() => sessionsQuery.data?.items ?? [], [sessionsQuery.data]);\n const workouts = useMemo(() => workoutsQuery.data?.items ?? [], [workoutsQuery.data]);\n const fixtures = useMemo(() => fixturesQuery.data?.items ?? [], [fixturesQuery.data]);\n\n // Resolve the competition for the standings snapshot.\n const competition = useMemo(() => {\n if (competitionOpt) {\n return competitionOpt;\n }\n const counts = new Map<string, number>();\n for (const fixture of fixtures) {\n if (fixture.competition) {\n counts.set(fixture.competition, (counts.get(fixture.competition) ?? 0) + 1);\n }\n }\n let top: string | null = null;\n let best = 0;\n for (const [comp, n] of counts) {\n if (n > best) {\n best = n;\n top = comp;\n }\n }\n return top;\n }, [competitionOpt, fixtures]);\n\n const standingsQuery = useStandings(competition ?? undefined);\n\n return useMemo<CoachAnalytics>(() => {\n const totalSessions = sessions.length;\n const rated = sessions.filter((s) => s.rating !== null);\n const ratedCount = rated.length;\n const avgRating =\n ratedCount > 0 ? rated.reduce((acc, s) => acc + (s.rating ?? 0), 0) / ratedCount : null;\n const completedCount = sessions.filter((s) => s.status === 'COMPLETED').length;\n const completedPct = totalSessions > 0 ? (completedCount / totalSessions) * 100 : 0;\n\n const statusOrder: AthSessionStatus[] = [\n 'REQUESTED',\n 'CONFIRMED',\n 'COMPLETED',\n 'CANCELLED',\n 'NO_SHOW',\n ];\n const statusCounts = new Map<AthSessionStatus, number>();\n for (const s of sessions) {\n statusCounts.set(s.status, (statusCounts.get(s.status) ?? 0) + 1);\n }\n const sessionsByStatus = statusOrder\n .map((status) => ({ status, count: statusCounts.get(status) ?? 0 }))\n .filter((entry) => entry.count > 0);\n\n const byCoach = new Map<string, { sum: number; count: number }>();\n for (const s of rated) {\n const entry = byCoach.get(s.coachId) ?? { sum: 0, count: 0 };\n entry.sum += s.rating ?? 0;\n entry.count += 1;\n byCoach.set(s.coachId, entry);\n }\n const ratingByCoach = Array.from(byCoach.entries())\n .map(([coachId, { sum, count }]) => ({ coachId, avgRating: sum / count, count }))\n .sort((a, b) => b.avgRating - a.avgRating);\n\n const coachIds = Array.from(new Set(sessions.map((s) => s.coachId)));\n\n // Training load (ACWR) from real workout totalLoad.\n const now = Date.now();\n const DAY = 86_400_000;\n let acute = 0;\n let chronic28 = 0;\n for (const w of workouts) {\n const load = w.totalLoad ?? 0;\n const age = now - new Date(w.performedAt).getTime();\n if (Number.isNaN(age)) {\n continue;\n }\n if (age <= 7 * DAY) {\n acute += load;\n }\n if (age <= 28 * DAY) {\n chronic28 += load;\n }\n }\n const chronic = chronic28 / 4;\n const ratio = chronic > 0 ? acute / chronic : null;\n\n const loadTrend = weeklyTrend(\n workouts,\n (w) => w.performedAt,\n (w) => w.totalLoad ?? 0\n );\n const sessionTrend = weeklyTrend(\n sessions,\n (s) => s.scheduledAt,\n () => 1\n );\n\n return {\n totalSessions,\n ratedCount,\n avgRating,\n completedCount,\n completedPct,\n sessionsByStatus,\n ratingByCoach,\n coachIds,\n trainingLoad: { acute, chronic, ratio },\n loadTrend,\n sessionTrend,\n standings: standingsQuery.data ?? [],\n competition,\n isLoading:\n sessionsQuery.isLoading ||\n workoutsQuery.isLoading ||\n fixturesQuery.isLoading ||\n (Boolean(competition) && standingsQuery.isLoading),\n isError:\n sessionsQuery.isError ||\n workoutsQuery.isError ||\n fixturesQuery.isError ||\n standingsQuery.isError,\n };\n }, [\n sessions,\n workouts,\n competition,\n standingsQuery.data,\n standingsQuery.isLoading,\n standingsQuery.isError,\n sessionsQuery.isLoading,\n sessionsQuery.isError,\n workoutsQuery.isLoading,\n workoutsQuery.isError,\n fixturesQuery.isLoading,\n fixturesQuery.isError,\n ]);\n};\n\n// ============================================================================\n// Health probe\n// ============================================================================\n\nexport const useHealth = () => {\n const { apiGatewayUrl } = useAthleteBridge();\n return useQuery({\n queryKey: ['atheletebridge', 'health'],\n queryFn: async () => {\n const result = await athQuery<{ athHealth: string }>(\n `query AthHealth { athHealth }`,\n {},\n apiGatewayUrl\n );\n return result.athHealth;\n },\n });\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/atheletebridge/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard / Overview\n * @module atheletebridge/pages\n *\n * The AthleteBridge landing page: a coach command-center that reads live counts\n * and recent items across teams, fixtures, programs and sessions from the real\n * `ath*` API. Elevated to the concept bar: glass teal-stat KPI tiles, a\n * crest-vs-crest upcoming-fixtures rail, and a real (derived) recent-activity\n * timeline — every item mapped to a real record, nothing fabricated.\n */\n\nimport { useMemo } from 'react';\nimport {\n Users,\n CalendarDays,\n Dumbbell,\n ClipboardCheck,\n ArrowRight,\n Plus,\n Trophy,\n TrendingUp,\n UserPlus,\n CheckCircle2,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { TeamCard } from '../components/TeamCard';\nimport { MatchupRow } from '../components/MatchupRow';\nimport { SessionCard } from '../components/SessionCard';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingState } from '../components/LoadingState';\nimport { Timeline, type TimelineItem, type TimelineTone } from '../components/viz';\nimport { useAthleteBridge } from '../context/AthleteBridgeContext';\nimport {\n useTeams,\n useFixtures,\n usePrograms,\n useSessions,\n useRecentActivity,\n type ActivityType,\n} from '../hooks/useAthleteBridgeApi';\nimport { timeAgo } from '../utils/timeAgo';\n\n/**\n * Brand gradient CTA — orange action-primary blended toward accent-purple, the\n * same brand gradient the hero title uses. Token-only (no raw colors).\n */\nconst GRADIENT_CTA =\n 'inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-activity-type node styling for the recent-activity timeline. */\nconst ACTIVITY_VISUAL: Record<ActivityType, { tone: TimelineTone; Icon: LucideIcon }> = {\n 'program-published': { tone: 'success', Icon: ClipboardCheck },\n 'member-added': { tone: 'accent', Icon: UserPlus },\n 'fixture-updated': { tone: 'warning', Icon: CalendarDays },\n 'session-completed': { tone: 'info', Icon: CheckCircle2 },\n 'team-created': { tone: 'accent', Icon: Users },\n};\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 50 } });\n const fixtures = useFixtures({ pagination: { limit: 50 } });\n const programs = usePrograms({ pagination: { limit: 50 } });\n const sessions = useSessions({ pagination: { limit: 50 } });\n\n const firstName = currentUser?.firstName ?? currentUser?.name ?? 'coach';\n\n const teamItems = useMemo(() => teams.data?.items ?? [], [teams.data]);\n const fixtureItems = useMemo(() => fixtures.data?.items ?? [], [fixtures.data]);\n const programItems = useMemo(() => programs.data?.items ?? [], [programs.data]);\n const sessionItems = useMemo(() => sessions.data?.items ?? [], [sessions.data]);\n\n const upcomingFixtures = useMemo(\n () =>\n [...fixtureItems]\n .filter((f) => f.status === 'SCHEDULED' || f.status === 'LIVE')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 5),\n [fixtureItems]\n );\n\n const upcomingSessions = useMemo(\n () =>\n [...sessionItems]\n .filter((s) => s.status === 'REQUESTED' || s.status === 'CONFIRMED')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 3),\n [sessionItems]\n );\n\n const recentTeams = useMemo(() => teamItems.slice(0, 6), [teamItems]);\n\n const publishedPrograms = programItems.filter((p) => p.status === 'PUBLISHED').length;\n\n const activity = useRecentActivity({\n teams: teamItems,\n fixtures: fixtureItems,\n sessions: sessionItems,\n programs: programItems,\n limit: 6,\n });\n\n const activityTimeline = useMemo<TimelineItem[]>(\n () =>\n activity.map((item) => {\n const visual = ACTIVITY_VISUAL[item.type];\n const Icon = visual.Icon;\n return {\n id: item.id,\n tone: visual.tone,\n icon: <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />,\n title: item.title,\n meta: item.subtitle,\n time: timeAgo(item.at),\n };\n }),\n [activity]\n );\n\n const isLoading =\n teams.isLoading || fixtures.isLoading || programs.isLoading || sessions.isLoading;\n\n if (isLoading) {\n return (\n <PageLayout title=\"AthleteBridge\" hero gradientTitle>\n <LoadingState label=\"Loading your sporting operation…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={`Welcome back, ${firstName}`}\n description=\"Your teams, fixtures, training and coaching — one connected record.\"\n hero\n gradientTitle\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button type=\"button\" onClick={() => navigateTo('/teams/new')} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> New team\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/performance')}\n className={GRADIENT_CTA}\n >\n <TrendingUp className=\"h-4 w-4\" /> Track performance\n </button>\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n AthleteBridge is the operating system for sports, coaching and training. From here you can\n assemble your roster, schedule fixtures, build training programs, run coaching sessions and\n track athlete performance — all reading and writing the same workspace-scoped source of\n truth.\n </PagePurpose>\n\n {/* KPIs — glass tiles with teal stat values (2-col mobile → 4-col lg) */}\n <div className=\"mb-8 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={teamItems.length}\n label=\"Teams\"\n tone=\"accent\"\n featured\n />\n <MetricsCard\n icon={<CalendarDays className=\"h-5 w-5\" />}\n value={fixtureItems.length}\n label=\"Fixtures\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={`${publishedPrograms}/${programItems.length}`}\n label=\"Programs published\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n value={sessionItems.length}\n label=\"Sessions\"\n tone=\"accent\"\n />\n </div>\n\n {/* Upcoming fixtures (matchup rail) + Recent activity (timeline) */}\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Fixtures — spans two columns on large screens */}\n <section className=\"lg:col-span-2\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Upcoming fixtures</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all fixtures <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingFixtures.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarDays className=\"h-10 w-10\" />}\n title=\"No upcoming fixtures\"\n description=\"Schedule a match to start tracking results and standings.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Schedule fixture\n </button>\n }\n />\n ) : (\n <GlassCard treatment=\"glass\" className=\"divide-y divide-border-seam px-4 py-1 sm:px-5\">\n {upcomingFixtures.map((fixture) => (\n <MatchupRow\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </GlassCard>\n )}\n </section>\n\n {/* Recent activity */}\n <section>\n <div className=\"mb-4 flex items-center min-h-[1.75rem]\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Recent activity</h2>\n </div>\n {activityTimeline.length === 0 ? (\n <GlassCard treatment=\"metric\" className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<TrendingUp className=\"h-10 w-10\" />}\n title=\"No recent activity\"\n description=\"Create teams, publish programs or record results — the latest moves land here.\"\n />\n </GlassCard>\n ) : (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <Timeline items={activityTimeline} />\n </GlassCard>\n )}\n </section>\n </div>\n\n {/* Upcoming sessions */}\n <section className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Upcoming sessions</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingSessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<ClipboardCheck className=\"h-10 w-10\" />}\n title=\"No upcoming sessions\"\n description=\"Book a coaching session with an athlete to get started.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Book session\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {upcomingSessions.map((session) => (\n <SessionCard\n key={session.id}\n session={session}\n onClick={() => navigateTo(`/sessions/${session.id}`)}\n />\n ))}\n </div>\n )}\n </section>\n\n {/* Teams */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Your teams</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {recentTeams.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Trophy className=\"h-10 w-10\" />}\n title=\"No teams yet\"\n description=\"Create your first team to build a roster and schedule fixtures.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Create team\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {recentTeams.map((team) => (\n <TeamCard key={team.id} team={team} onClick={() => navigateTo(`/teams/${team.id}`)} />\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/atheletebridge/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard / Overview\n * @module atheletebridge/pages\n *\n * The AthleteBridge landing page: a coach command-center that reads live counts\n * and recent items across teams, fixtures, programs and sessions from the real\n * `ath*` API. Elevated to the concept bar: glass teal-stat KPI tiles, a\n * crest-vs-crest upcoming-fixtures rail, and a real (derived) recent-activity\n * timeline — every item mapped to a real record, nothing fabricated.\n */\n\nimport { useMemo } from 'react';\nimport {\n Users,\n CalendarDays,\n Dumbbell,\n ClipboardCheck,\n ArrowRight,\n Plus,\n Trophy,\n TrendingUp,\n UserPlus,\n CheckCircle2,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { TeamCard } from '../components/TeamCard';\nimport { MatchupRow } from '../components/MatchupRow';\nimport { SessionCard } from '../components/SessionCard';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingState } from '../components/LoadingState';\nimport { Timeline, type TimelineItem, type TimelineTone } from '../components/viz';\nimport { useAthleteBridge } from '../context/AthleteBridgeContext';\nimport {\n useTeams,\n useFixtures,\n usePrograms,\n useSessions,\n useRecentActivity,\n type ActivityType,\n} from '../hooks/useAthleteBridgeApi';\nimport { timeAgo } from '../utils/timeAgo';\n\n/**\n * Brand gradient CTA — orange action-primary blended toward accent-purple, the\n * same brand gradient the hero title uses. Token-only (no raw colors).\n */\nconst GRADIENT_CTA =\n 'inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-activity-type node styling for the recent-activity timeline. */\nconst ACTIVITY_VISUAL: Record<ActivityType, { tone: TimelineTone; Icon: LucideIcon }> = {\n 'program-published': { tone: 'success', Icon: ClipboardCheck },\n 'member-added': { tone: 'accent', Icon: UserPlus },\n 'fixture-updated': { tone: 'warning', Icon: CalendarDays },\n 'session-completed': { tone: 'info', Icon: CheckCircle2 },\n 'team-created': { tone: 'accent', Icon: Users },\n};\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 50 } });\n const fixtures = useFixtures({ pagination: { limit: 50 } });\n const programs = usePrograms({ pagination: { limit: 50 } });\n const sessions = useSessions({ pagination: { limit: 50 } });\n\n const firstName = currentUser?.firstName ?? currentUser?.name ?? 'coach';\n\n const teamItems = useMemo(() => teams.data?.items ?? [], [teams.data]);\n const fixtureItems = useMemo(() => fixtures.data?.items ?? [], [fixtures.data]);\n const programItems = useMemo(() => programs.data?.items ?? [], [programs.data]);\n const sessionItems = useMemo(() => sessions.data?.items ?? [], [sessions.data]);\n\n const upcomingFixtures = useMemo(\n () =>\n [...fixtureItems]\n .filter((f) => f.status === 'SCHEDULED' || f.status === 'LIVE')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 5),\n [fixtureItems]\n );\n\n const upcomingSessions = useMemo(\n () =>\n [...sessionItems]\n .filter((s) => s.status === 'REQUESTED' || s.status === 'CONFIRMED')\n .sort((a, b) => new Date(a.scheduledAt).getTime() - new Date(b.scheduledAt).getTime())\n .slice(0, 3),\n [sessionItems]\n );\n\n const recentTeams = useMemo(() => teamItems.slice(0, 6), [teamItems]);\n\n const publishedPrograms = programItems.filter((p) => p.status === 'PUBLISHED').length;\n\n const activity = useRecentActivity({\n teams: teamItems,\n fixtures: fixtureItems,\n sessions: sessionItems,\n programs: programItems,\n limit: 6,\n });\n\n const activityTimeline = useMemo<TimelineItem[]>(\n () =>\n activity.map((item) => {\n const visual = ACTIVITY_VISUAL[item.type];\n const Icon = visual.Icon;\n return {\n id: item.id,\n tone: visual.tone,\n icon: <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />,\n title: item.title,\n meta: item.subtitle,\n time: timeAgo(item.at),\n };\n }),\n [activity]\n );\n\n const isLoading =\n teams.isLoading || fixtures.isLoading || programs.isLoading || sessions.isLoading;\n\n if (isLoading) {\n return (\n <PageLayout title=\"AthleteBridge\" hero gradientTitle>\n <LoadingState label=\"Loading your sporting operation…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={`Welcome back, ${firstName}`}\n description=\"Your teams, fixtures, training and coaching — one connected record.\"\n hero\n gradientTitle\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button type=\"button\" onClick={() => navigateTo('/teams/new')} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> New team\n </button>\n <button type=\"button\" onClick={() => navigateTo('/performance')} className={GRADIENT_CTA}>\n <TrendingUp className=\"h-4 w-4\" /> Track performance\n </button>\n </div>\n }\n >\n <PagePurpose className=\"mb-6\">\n AthleteBridge is the operating system for sports, coaching and training. From here you can\n assemble your roster, schedule fixtures, build training programs, run coaching sessions and\n track athlete performance — all reading and writing the same workspace-scoped source of\n truth.\n </PagePurpose>\n\n {/* KPIs — glass tiles with teal stat values (2-col mobile → 4-col lg) */}\n <div className=\"mb-8 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={teamItems.length}\n label=\"Teams\"\n tone=\"accent\"\n featured\n />\n <MetricsCard\n icon={<CalendarDays className=\"h-5 w-5\" />}\n value={fixtureItems.length}\n label=\"Fixtures\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={`${publishedPrograms}/${programItems.length}`}\n label=\"Programs published\"\n tone=\"accent\"\n />\n <MetricsCard\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n value={sessionItems.length}\n label=\"Sessions\"\n tone=\"accent\"\n />\n </div>\n\n {/* Upcoming fixtures (matchup rail) + Recent activity (timeline) */}\n <div className=\"mb-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Fixtures — spans two columns on large screens */}\n <section className=\"lg:col-span-2\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Upcoming fixtures</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all fixtures <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingFixtures.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<CalendarDays className=\"h-10 w-10\" />}\n title=\"No upcoming fixtures\"\n description=\"Schedule a match to start tracking results and standings.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Schedule fixture\n </button>\n }\n />\n ) : (\n <GlassCard treatment=\"glass\" className=\"divide-y divide-border-seam px-4 py-1 sm:px-5\">\n {upcomingFixtures.map((fixture) => (\n <MatchupRow\n key={fixture.id}\n fixture={fixture}\n onClick={() => navigateTo(`/fixtures/${fixture.id}`)}\n />\n ))}\n </GlassCard>\n )}\n </section>\n\n {/* Recent activity */}\n <section>\n <div className=\"mb-4 flex items-center min-h-[1.75rem]\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Recent activity</h2>\n </div>\n {activityTimeline.length === 0 ? (\n <GlassCard treatment=\"metric\" className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<TrendingUp className=\"h-10 w-10\" />}\n title=\"No recent activity\"\n description=\"Create teams, publish programs or record results — the latest moves land here.\"\n />\n </GlassCard>\n ) : (\n <GlassCard treatment=\"metric\" className=\"p-5\">\n <Timeline items={activityTimeline} />\n </GlassCard>\n )}\n </section>\n </div>\n\n {/* Upcoming sessions */}\n <section className=\"mb-8\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Upcoming sessions</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {upcomingSessions.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n icon={<ClipboardCheck className=\"h-10 w-10\" />}\n title=\"No upcoming sessions\"\n description=\"Book a coaching session with an athlete to get started.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Book session\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {upcomingSessions.map((session) => (\n <SessionCard\n key={session.id}\n session={session}\n onClick={() => navigateTo(`/sessions/${session.id}`)}\n />\n ))}\n </div>\n )}\n </section>\n\n {/* Teams */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Your teams</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams')}\n className=\"flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View all <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n {recentTeams.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Trophy className=\"h-10 w-10\" />}\n title=\"No teams yet\"\n description=\"Create your first team to build a roster and schedule fixtures.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/teams/new')}\n className={GRADIENT_CTA}\n >\n <Plus className=\"h-4 w-4\" /> Create team\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {recentTeams.map((team) => (\n <TeamCard key={team.id} team={team} onClick={() => navigateTo(`/teams/${team.id}`)} />\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FixtureFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureFormPage.tsx"],"sourcesContent":["/**\n * Fixture Schedule Form\n * @module atheletebridge/pages/fixtures\n *\n * Mirrors the concept mock: a GlassCard-framed two-column form with a segmented\n * fixture-type control (all five AthFixtureType values incl. SCRIMMAGE), team\n * crest previews, competition/venue comboboxes seeded from existing fixtures,\n * and iconized date/time inputs. Every field maps to AthScheduleFixtureInput.\n * Semantic tokens only, responsive.\n */\n\nimport { type KeyboardEvent, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Loader2,\n Save,\n Calendar,\n Clock,\n Handshake,\n Shield,\n Trophy,\n Network,\n Swords,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { LoadingState } from '../../components/LoadingState';\nimport { Crest } from '../../components/Crest';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { cn } from '../../utils/cn';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useScheduleFixture, useTeams, useFixtures } from '../../hooks/useAthleteBridgeApi';\nimport { toIsoOrUndefined } from '../../utils/datetime';\nimport type { AthFixtureType } from '../../types';\n\nconst TYPE_ICONS: Record<AthFixtureType, LucideIcon> = {\n FRIENDLY: Handshake,\n LEAGUE: Shield,\n CUP: Trophy,\n TOURNAMENT: Network,\n SCRIMMAGE: Swords,\n};\nconst TYPES = Object.keys(FIXTURE_TYPE_LABELS) as AthFixtureType[];\n\nexport function FixtureFormPage() {\n const [searchParams] = useSearchParams();\n const { navigateTo } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 100 } });\n const existingFixtures = useFixtures({ pagination: { limit: 100 } });\n const scheduleFixture = useScheduleFixture();\n\n const [homeTeamId, setHomeTeamId] = useState(searchParams.get('homeTeamId') ?? '');\n const [awayTeamId, setAwayTeamId] = useState('');\n const [type, setType] = useState<AthFixtureType>('FRIENDLY');\n const [competition, setCompetition] = useState('');\n const [round, setRound] = useState('');\n const [venue, setVenue] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [kickoffAt, setKickoffAt] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const teamOptions = teams.data?.items ?? [];\n const homeTeam = teamOptions.find((t) => t.id === homeTeamId);\n const awayTeam = teamOptions.find((t) => t.id === awayTeamId);\n\n // Seed comboboxes from distinct values on existing fixtures (honest, derived).\n const { competitions, venues } = useMemo(() => {\n const comp = new Set<string>();\n const ven = new Set<string>();\n for (const f of existingFixtures.data?.items ?? []) {\n if (f.competition) comp.add(f.competition);\n if (f.venue) ven.add(f.venue);\n }\n return {\n competitions: Array.from(comp).sort(),\n venues: Array.from(ven).sort(),\n };\n }, [existingFixtures.data]);\n\n const handleTypeKey = (e: KeyboardEvent<HTMLDivElement>) => {\n const idx = TYPES.indexOf(type);\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setType(TYPES[(idx + 1) % TYPES.length]);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setType(TYPES[(idx - 1 + TYPES.length) % TYPES.length]);\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!homeTeamId) {\n setError('A home team is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n if (awayTeamId && awayTeamId === homeTeamId) {\n setError('Home and away teams must be different.');\n return;\n }\n\n try {\n const created = await scheduleFixture.mutateAsync({\n homeTeamId,\n awayTeamId: awayTeamId || undefined,\n type,\n competition: competition.trim() || undefined,\n round: round.trim() || undefined,\n venue: venue.trim() || undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n kickoffAt: toIsoOrUndefined(kickoffAt),\n notes: notes.trim() || undefined,\n });\n navigateTo(`/fixtures/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to schedule fixture.');\n }\n };\n\n if (teams.isLoading) {\n return (\n <PageLayout title=\"Schedule fixture\">\n <LoadingState label=\"Loading teams…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Schedule fixture\"\n description=\"Set up a match between a home team and an optional away team.\"\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\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-3xl\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-7\">\n {/* Home / away teams with crest preview */}\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Home team\" htmlFor=\"homeTeam\" required>\n <select\n id=\"homeTeam\"\n className={fieldInputClass}\n value={homeTeamId}\n onChange={(e) => setHomeTeamId(e.target.value)}\n >\n <option value=\"\">Select a team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {homeTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={homeTeam.name} sport={homeTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {homeTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n\n <FormField label=\"Away team\" htmlFor=\"awayTeam\" hint=\"Optional for trials / open events.\">\n <select\n id=\"awayTeam\"\n className={fieldInputClass}\n value={awayTeamId}\n onChange={(e) => setAwayTeamId(e.target.value)}\n >\n <option value=\"\">No away team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {awayTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={awayTeam.name} sport={awayTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {awayTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n </div>\n\n {/* Type — segmented control (all five values incl. SCRIMMAGE) */}\n <div className=\"mt-5\">\n <span className=\"mb-1.5 block text-sm font-medium text-text-primary\">Type</span>\n <div\n role=\"radiogroup\"\n aria-label=\"Fixture type\"\n onKeyDown={handleTypeKey}\n className=\"grid grid-cols-2 gap-2 sm:grid-cols-5\"\n >\n {TYPES.map((tp) => {\n const Icon = TYPE_ICONS[tp];\n const active = type === tp;\n return (\n <button\n key={tp}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n onClick={() => setType(tp)}\n className={cn(\n 'flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)] text-text-primary ring-1 ring-inset ring-action-primary-bg'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n <Icon className=\"h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n {FIXTURE_TYPE_LABELS[tp]}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Competition / round */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Competition\" htmlFor=\"competition\" hint=\"Used to group standings.\">\n <input\n id=\"competition\"\n list=\"ath-fixture-competitions\"\n className={fieldInputClass}\n value={competition}\n onChange={(e) => setCompetition(e.target.value)}\n placeholder=\"e.g. Spring League 2026\"\n />\n <datalist id=\"ath-fixture-competitions\">\n {competitions.map((c) => (\n <option key={c} value={c} />\n ))}\n </datalist>\n </FormField>\n\n <FormField label=\"Round\" htmlFor=\"round\">\n <input\n id=\"round\"\n className={fieldInputClass}\n value={round}\n onChange={(e) => setRound(e.target.value)}\n placeholder=\"e.g. Round 4\"\n />\n </FormField>\n </div>\n\n {/* Venue */}\n <div className=\"mt-5\">\n <FormField label=\"Venue\" htmlFor=\"venue\">\n <input\n id=\"venue\"\n list=\"ath-fixture-venues\"\n className={fieldInputClass}\n value={venue}\n onChange={(e) => setVenue(e.target.value)}\n placeholder=\"e.g. Riverside Stadium\"\n />\n <datalist id=\"ath-fixture-venues\">\n {venues.map((v) => (\n <option key={v} value={v} />\n ))}\n </datalist>\n </FormField>\n </div>\n\n {/* Schedule / kickoff */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <div className=\"relative\">\n <Calendar\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </div>\n </FormField>\n\n <FormField label=\"Kick-off at\" htmlFor=\"kickoffAt\" hint=\"Optional refinement.\">\n <div className=\"relative\">\n <Clock\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"kickoffAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={kickoffAt}\n onChange={(e) => setKickoffAt(e.target.value)}\n />\n </div>\n </FormField>\n </div>\n\n {/* Notes */}\n <div className=\"mt-5\">\n <FormField label=\"Notes\" htmlFor=\"notes\">\n <textarea\n id=\"notes\"\n className={fieldInputClass}\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Any extra context for this fixture.\"\n />\n </FormField>\n </div>\n\n {error && <p className=\"mt-5 text-sm text-status-error-text\">{error}</p>}\n\n {/* Footer */}\n <div className=\"mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={scheduleFixture.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {scheduleFixture.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Schedule fixture\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"FixtureFormPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/fixtures/FixtureFormPage.tsx"],"sourcesContent":["/**\n * Fixture Schedule Form\n * @module atheletebridge/pages/fixtures\n *\n * Mirrors the concept mock: a GlassCard-framed two-column form with a segmented\n * fixture-type control (all five AthFixtureType values incl. SCRIMMAGE), team\n * crest previews, competition/venue comboboxes seeded from existing fixtures,\n * and iconized date/time inputs. Every field maps to AthScheduleFixtureInput.\n * Semantic tokens only, responsive.\n */\n\nimport { type KeyboardEvent, useMemo, useState } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Loader2,\n Save,\n Calendar,\n Clock,\n Handshake,\n Shield,\n Trophy,\n Network,\n Swords,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { LoadingState } from '../../components/LoadingState';\nimport { Crest } from '../../components/Crest';\nimport { FIXTURE_TYPE_LABELS } from '../../components/domain';\nimport { cn } from '../../utils/cn';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport { useScheduleFixture, useTeams, useFixtures } from '../../hooks/useAthleteBridgeApi';\nimport { toIsoOrUndefined } from '../../utils/datetime';\nimport type { AthFixtureType } from '../../types';\n\nconst TYPE_ICONS: Record<AthFixtureType, LucideIcon> = {\n FRIENDLY: Handshake,\n LEAGUE: Shield,\n CUP: Trophy,\n TOURNAMENT: Network,\n SCRIMMAGE: Swords,\n};\nconst TYPES = Object.keys(FIXTURE_TYPE_LABELS) as AthFixtureType[];\n\nexport function FixtureFormPage() {\n const [searchParams] = useSearchParams();\n const { navigateTo } = useAthleteBridge();\n\n const teams = useTeams({ pagination: { limit: 100 } });\n const existingFixtures = useFixtures({ pagination: { limit: 100 } });\n const scheduleFixture = useScheduleFixture();\n\n const [homeTeamId, setHomeTeamId] = useState(searchParams.get('homeTeamId') ?? '');\n const [awayTeamId, setAwayTeamId] = useState('');\n const [type, setType] = useState<AthFixtureType>('FRIENDLY');\n const [competition, setCompetition] = useState('');\n const [round, setRound] = useState('');\n const [venue, setVenue] = useState('');\n const [scheduledAt, setScheduledAt] = useState('');\n const [kickoffAt, setKickoffAt] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const teamOptions = teams.data?.items ?? [];\n const homeTeam = teamOptions.find((t) => t.id === homeTeamId);\n const awayTeam = teamOptions.find((t) => t.id === awayTeamId);\n\n // Seed comboboxes from distinct values on existing fixtures (honest, derived).\n const { competitions, venues } = useMemo(() => {\n const comp = new Set<string>();\n const ven = new Set<string>();\n for (const f of existingFixtures.data?.items ?? []) {\n if (f.competition) comp.add(f.competition);\n if (f.venue) ven.add(f.venue);\n }\n return {\n competitions: Array.from(comp).sort(),\n venues: Array.from(ven).sort(),\n };\n }, [existingFixtures.data]);\n\n const handleTypeKey = (e: KeyboardEvent<HTMLDivElement>) => {\n const idx = TYPES.indexOf(type);\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n setType(TYPES[(idx + 1) % TYPES.length]);\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n setType(TYPES[(idx - 1 + TYPES.length) % TYPES.length]);\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!homeTeamId) {\n setError('A home team is required.');\n return;\n }\n if (!scheduledAt) {\n setError('A scheduled date and time is required.');\n return;\n }\n if (awayTeamId && awayTeamId === homeTeamId) {\n setError('Home and away teams must be different.');\n return;\n }\n\n try {\n const created = await scheduleFixture.mutateAsync({\n homeTeamId,\n awayTeamId: awayTeamId || undefined,\n type,\n competition: competition.trim() || undefined,\n round: round.trim() || undefined,\n venue: venue.trim() || undefined,\n scheduledAt: new Date(scheduledAt).toISOString(),\n kickoffAt: toIsoOrUndefined(kickoffAt),\n notes: notes.trim() || undefined,\n });\n navigateTo(`/fixtures/${created.id}`);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to schedule fixture.');\n }\n };\n\n if (teams.isLoading) {\n return (\n <PageLayout title=\"Schedule fixture\">\n <LoadingState label=\"Loading teams…\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Schedule fixture\"\n description=\"Set up a match between a home team and an optional away team.\"\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\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-3xl\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-7\">\n {/* Home / away teams with crest preview */}\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Home team\" htmlFor=\"homeTeam\" required>\n <select\n id=\"homeTeam\"\n className={fieldInputClass}\n value={homeTeamId}\n onChange={(e) => setHomeTeamId(e.target.value)}\n >\n <option value=\"\">Select a team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {homeTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={homeTeam.name} sport={homeTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {homeTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n\n <FormField\n label=\"Away team\"\n htmlFor=\"awayTeam\"\n hint=\"Optional for trials / open events.\"\n >\n <select\n id=\"awayTeam\"\n className={fieldInputClass}\n value={awayTeamId}\n onChange={(e) => setAwayTeamId(e.target.value)}\n >\n <option value=\"\">No away team</option>\n {teamOptions.map((tm) => (\n <option key={tm.id} value={tm.id}>\n {tm.name}\n </option>\n ))}\n </select>\n {awayTeam ? (\n <div className=\"mt-2 flex items-center gap-2\">\n <Crest name={awayTeam.name} sport={awayTeam.sport} size={28} />\n <span className=\"truncate text-xs font-medium text-text-secondary\">\n {awayTeam.name}\n </span>\n </div>\n ) : null}\n </FormField>\n </div>\n\n {/* Type — segmented control (all five values incl. SCRIMMAGE) */}\n <div className=\"mt-5\">\n <span className=\"mb-1.5 block text-sm font-medium text-text-primary\">Type</span>\n <div\n role=\"radiogroup\"\n aria-label=\"Fixture type\"\n onKeyDown={handleTypeKey}\n className=\"grid grid-cols-2 gap-2 sm:grid-cols-5\"\n >\n {TYPES.map((tp) => {\n const Icon = TYPE_ICONS[tp];\n const active = type === tp;\n return (\n <button\n key={tp}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n onClick={() => setType(tp)}\n className={cn(\n 'flex items-center justify-center gap-1.5 rounded-lg border px-2.5 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? 'border-action-primary-bg bg-[var(--color-accent-soft)] text-text-primary ring-1 ring-inset ring-action-primary-bg'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n <Icon className=\"h-4 w-4 shrink-0\" aria-hidden=\"true\" />\n {FIXTURE_TYPE_LABELS[tp]}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Competition / round */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Competition\" htmlFor=\"competition\" hint=\"Used to group standings.\">\n <input\n id=\"competition\"\n list=\"ath-fixture-competitions\"\n className={fieldInputClass}\n value={competition}\n onChange={(e) => setCompetition(e.target.value)}\n placeholder=\"e.g. Spring League 2026\"\n />\n <datalist id=\"ath-fixture-competitions\">\n {competitions.map((c) => (\n <option key={c} value={c} />\n ))}\n </datalist>\n </FormField>\n\n <FormField label=\"Round\" htmlFor=\"round\">\n <input\n id=\"round\"\n className={fieldInputClass}\n value={round}\n onChange={(e) => setRound(e.target.value)}\n placeholder=\"e.g. Round 4\"\n />\n </FormField>\n </div>\n\n {/* Venue */}\n <div className=\"mt-5\">\n <FormField label=\"Venue\" htmlFor=\"venue\">\n <input\n id=\"venue\"\n list=\"ath-fixture-venues\"\n className={fieldInputClass}\n value={venue}\n onChange={(e) => setVenue(e.target.value)}\n placeholder=\"e.g. Riverside Stadium\"\n />\n <datalist id=\"ath-fixture-venues\">\n {venues.map((v) => (\n <option key={v} value={v} />\n ))}\n </datalist>\n </FormField>\n </div>\n\n {/* Schedule / kickoff */}\n <div className=\"mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n <FormField label=\"Scheduled at\" htmlFor=\"scheduledAt\" required>\n <div className=\"relative\">\n <Calendar\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"scheduledAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={scheduledAt}\n onChange={(e) => setScheduledAt(e.target.value)}\n />\n </div>\n </FormField>\n\n <FormField label=\"Kick-off at\" htmlFor=\"kickoffAt\" hint=\"Optional refinement.\">\n <div className=\"relative\">\n <Clock\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"kickoffAt\"\n type=\"datetime-local\"\n className={cn(fieldInputClass, 'pl-9')}\n value={kickoffAt}\n onChange={(e) => setKickoffAt(e.target.value)}\n />\n </div>\n </FormField>\n </div>\n\n {/* Notes */}\n <div className=\"mt-5\">\n <FormField label=\"Notes\" htmlFor=\"notes\">\n <textarea\n id=\"notes\"\n className={fieldInputClass}\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Any extra context for this fixture.\"\n />\n </FormField>\n </div>\n\n {error && <p className=\"mt-5 text-sm text-status-error-text\">{error}</p>}\n\n {/* Footer */}\n <div className=\"mt-6 flex items-center justify-end gap-3 border-t border-border-seam pt-5\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/fixtures')}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={scheduleFixture.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {scheduleFixture.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Schedule fixture\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) => (log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>),\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\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 <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({ metric }: { metric: MetricSeries }) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">vs previous</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\">\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\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 <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({ metric }: { metric: MetricSeries }) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">vs previous</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-atheletebridge",
3
- "version": "2026.706.1",
3
+ "version": "2026.706.2",
4
4
  "description": "Burdenoff AthleteBridge Microfrontend - Sports, coaching & training platform for teams, fixtures, programs, sessions and athlete performance",
5
5
  "type": "module",
6
6
  "files": [