@burdenoff/microfe-atheletebridge 2026.629.1 → 2026.705.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.
- package/dist/atheletebridge/AthleteBridgeRoot.js +45 -37
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js +10 -6
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/components/AthleteSelect.js +66 -0
- package/dist/atheletebridge/components/AthleteSelect.js.map +1 -0
- package/dist/atheletebridge/components/DrillVideoLink.js +28 -0
- package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -0
- package/dist/atheletebridge/components/MetricsCard.js +20 -46
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/UserSearchSelect.js +129 -0
- package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -0
- package/dist/atheletebridge/components/domain.js +10 -1
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +313 -180
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js +77 -69
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +4 -4
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js +496 -0
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -0
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +176 -142
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +86 -85
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js +92 -98
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js +132 -137
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/utils/formatters.js +2 -25
- package/dist/atheletebridge/utils/formatters.js.map +1 -1
- package/dist/atheletebridge/utils/url.js +17 -0
- package/dist/atheletebridge/utils/url.js.map +1 -0
- package/dist/index.js +4 -4
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgramDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"sourcesContent":["/**\n * Program Detail\n * @module atheletebridge/pages/programs\n *\n * Reads a single program with its drills (athProgram), supports the publish\n * lifecycle (athTransitionProgram), editing/archiving, and adding/removing\n * drills (athAddDrill / athRemoveDrill).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Edit, Archive, Plus, Trash2, Loader2, Send, Undo2, Copy } from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useProgram,\n useTransitionProgram,\n useArchiveProgram,\n useAddDrill,\n useRemoveDrill,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthDrill, AthProgramStatus } from '../../types';\n\ninterface DrillForm {\n name: string;\n description: string;\n weekNumber: string;\n sets: string;\n reps: string;\n durationSec: string;\n restSec: string;\n}\n\nconst EMPTY_DRILL: DrillForm = {\n name: '',\n description: '',\n weekNumber: '1',\n sets: '',\n reps: '',\n durationSec: '',\n restSec: '',\n};\n\nconst numOrUndefined = (v: string): number | undefined => {\n const t = v.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nexport function ProgramDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const program = useProgram(id);\n const transition = useTransitionProgram();\n const archive = useArchiveProgram();\n const addDrill = useAddDrill();\n const removeDrill = useRemoveDrill(id);\n\n const [drillOpen, setDrillOpen] = useState(false);\n const [drillForm, setDrillForm] = useState<DrillForm>(EMPTY_DRILL);\n const [drillError, setDrillError] = useState<string | null>(null);\n\n const drillsByWeek = useMemo(() => {\n const drills = program.data?.drills ?? [];\n const groups = new Map<number, AthDrill[]>();\n for (const d of drills) {\n const list = groups.get(d.weekNumber) ?? [];\n list.push(d);\n groups.set(d.weekNumber, list);\n }\n return Array.from(groups.entries())\n .sort(([a], [b]) => a - b)\n .map(([week, list]) => ({\n week,\n drills: [...list].sort((a, b) => a.orderIndex - b.orderIndex),\n }));\n }, [program.data]);\n\n if (program.isLoading) {\n return (\n <PageLayout title=\"Program\">\n <LoadingState label=\"Loading program…\" />\n </PageLayout>\n );\n }\n\n if (program.isError || !program.data) {\n return (\n <PageLayout title=\"Program\">\n <ErrorState\n title=\"Program not found\"\n message=\"This program does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/programs')}\n />\n </PageLayout>\n );\n }\n\n const p = program.data;\n const drillCount = p.drills?.length ?? 0;\n\n const handleTransition = (status: AthProgramStatus) => {\n if (!id) return;\n void transition.mutate({ id, status });\n };\n\n const handleArchive = async () => {\n if (!id) return;\n await archive.mutateAsync(id);\n navigateTo('/programs');\n };\n\n const handleAddDrill = async (e: React.FormEvent) => {\n e.preventDefault();\n setDrillError(null);\n if (!id) return;\n if (!drillForm.name.trim()) {\n setDrillError('Drill name is required.');\n return;\n }\n const week = numOrUndefined(drillForm.weekNumber) ?? 1;\n try {\n await addDrill.mutateAsync({\n programId: id,\n name: drillForm.name.trim(),\n description: drillForm.description.trim() || undefined,\n weekNumber: week,\n orderIndex: program.data?.drills?.filter((d) => d.weekNumber === week).length ?? 0,\n sets: numOrUndefined(drillForm.sets),\n reps: numOrUndefined(drillForm.reps),\n durationSec: numOrUndefined(drillForm.durationSec),\n restSec: numOrUndefined(drillForm.restSec),\n });\n setDrillForm(EMPTY_DRILL);\n setDrillOpen(false);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to add drill.');\n }\n };\n\n return (\n <PageLayout\n title={p.name}\n description={`${SPORT_LABELS[p.sport]} · ${PROGRAM_LEVEL_LABELS[p.level]} · ${p.weeks} ${\n p.weeks === 1 ? 'week' : 'weeks'\n }`}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/programs')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/programs/${p.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Edit className=\"h-4 w-4\" /> Edit\n </button>\n {p.status !== 'ARCHIVED' && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {archive.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"h-4 w-4\" />\n )}{' '}\n Archive\n </button>\n )}\n </div>\n }\n >\n {/* Summary + lifecycle */}\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={p.status} />\n {p.isTemplate && (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Copy className=\"h-3.5 w-3.5\" /> Template\n </span>\n )}\n </div>\n {p.summary && <p className=\"mt-4 text-sm text-text-secondary\">{p.summary}</p>}\n {p.tags.length > 0 && (\n <div className=\"mt-4 flex flex-wrap gap-1.5\">\n {p.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Lifecycle transitions */}\n <div className=\"mt-5 flex flex-wrap items-center gap-2 border-t border-border-seam pt-5\">\n <span className=\"text-sm font-medium text-text-primary\">Publish lifecycle:</span>\n {p.status === 'DRAFT' && (\n <button\n type=\"button\"\n onClick={() => handleTransition('PUBLISHED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n <Send className=\"h-4 w-4\" /> Publish\n </button>\n )}\n {p.status === 'PUBLISHED' && (\n <>\n <button\n type=\"button\"\n onClick={() => handleTransition('DRAFT')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Undo2 className=\"h-4 w-4\" /> Pull back to draft\n </button>\n <button\n type=\"button\"\n onClick={() => handleTransition('ARCHIVED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Archive className=\"h-4 w-4\" /> Archive\n </button>\n </>\n )}\n {p.status === 'ARCHIVED' && (\n <span className=\"text-sm text-text-secondary\">This program is archived.</span>\n )}\n {transition.isPending && <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />}\n </div>\n </GlassCard>\n\n {/* Drills */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Drills ({drillCount})</h2>\n <button\n type=\"button\"\n onClick={() => {\n setDrillError(null);\n setDrillOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> Add drill\n </button>\n </div>\n\n {drillCount === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Plus className=\"h-10 w-10\" />}\n title=\"No drills yet\"\n description=\"Add drills with sets, reps and durations, organized by week.\"\n />\n ) : (\n <div className=\"space-y-6\">\n {drillsByWeek.map(({ week, drills }) => (\n <div key={week}>\n <h3 className=\"mb-3 text-sm font-semibold uppercase tracking-wide text-text-secondary\">\n Week {week}\n </h3>\n <EmphasisPanel>\n <div className=\"divide-y divide-border-seam\">\n {drills.map((d) => (\n <div key={d.id} className=\"flex items-start gap-4 py-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{d.name}</p>\n {d.description && (\n <p className=\"mt-0.5 text-xs text-text-secondary\">{d.description}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-secondary\">\n {d.sets !== null && <span>{d.sets} sets</span>}\n {d.reps !== null && <span>{d.reps} reps</span>}\n {d.durationSec !== null && <span>{d.durationSec}s work</span>}\n {d.restSec !== null && <span>{d.restSec}s rest</span>}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void removeDrill.mutate(d.id)}\n disabled={removeDrill.isPending}\n aria-label={`Remove ${d.name}`}\n className=\"rounded-lg p-2 text-text-secondary transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-60\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n </div>\n </EmphasisPanel>\n </div>\n ))}\n </div>\n )}\n </section>\n\n {/* Add drill dialog */}\n <ResponsiveDialog open={drillOpen} onOpenChange={setDrillOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add drill</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleAddDrill} className=\"space-y-4 px-1\">\n <FormField label=\"Name\" htmlFor=\"drillName\" required>\n <input\n id=\"drillName\"\n className={fieldInputClass}\n value={drillForm.name}\n onChange={(e) => setDrillForm((p) => ({ ...p, name: e.target.value }))}\n placeholder=\"e.g. Box jumps\"\n />\n </FormField>\n <FormField label=\"Description\" htmlFor=\"drillDesc\">\n <textarea\n id=\"drillDesc\"\n className={fieldInputClass}\n rows={2}\n value={drillForm.description}\n onChange={(e) => setDrillForm((p) => ({ ...p, description: e.target.value }))}\n placeholder=\"How to perform the drill.\"\n />\n </FormField>\n <div className=\"grid grid-cols-3 gap-4\">\n <FormField label=\"Week\" htmlFor=\"drillWeek\">\n <input\n id=\"drillWeek\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.weekNumber}\n onChange={(e) => setDrillForm((p) => ({ ...p, weekNumber: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Sets\" htmlFor=\"drillSets\">\n <input\n id=\"drillSets\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.sets}\n onChange={(e) => setDrillForm((p) => ({ ...p, sets: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Reps\" htmlFor=\"drillReps\">\n <input\n id=\"drillReps\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.reps}\n onChange={(e) => setDrillForm((p) => ({ ...p, reps: e.target.value }))}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Work (sec)\" htmlFor=\"drillDuration\">\n <input\n id=\"drillDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.durationSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, durationSec: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Rest (sec)\" htmlFor=\"drillRest\">\n <input\n id=\"drillRest\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.restSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, restSec: e.target.value }))}\n />\n </FormField>\n </div>\n {drillError && <p className=\"text-sm text-status-error-text\">{drillError}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setDrillOpen(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={addDrill.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {addDrill.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Add drill\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProgramDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"sourcesContent":["/**\n * Program Detail\n * @module atheletebridge/pages/programs\n *\n * Reads a single program with its drills (athProgram), supports the publish\n * lifecycle (athTransitionProgram), editing/archiving, and adding/removing\n * drills (athAddDrill / athRemoveDrill).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Edit,\n Archive,\n Plus,\n Trash2,\n Loader2,\n Send,\n Undo2,\n Copy,\n FileVideo,\n} from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { DrillVideoLink } from '../../components/DrillVideoLink';\nimport { SPORT_LABELS, PROGRAM_LEVEL_LABELS } from '../../components/domain';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useProgram,\n useTransitionProgram,\n useArchiveProgram,\n useAddDrill,\n useRemoveDrill,\n useUploadFile,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthDrill, AthProgramStatus } from '../../types';\n\ninterface DrillForm {\n name: string;\n description: string;\n weekNumber: string;\n sets: string;\n reps: string;\n durationSec: string;\n restSec: string;\n}\n\nconst EMPTY_DRILL: DrillForm = {\n name: '',\n description: '',\n weekNumber: '1',\n sets: '',\n reps: '',\n durationSec: '',\n restSec: '',\n};\n\nconst numOrUndefined = (v: string): number | undefined => {\n const t = v.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nexport function ProgramDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const program = useProgram(id);\n const transition = useTransitionProgram();\n const archive = useArchiveProgram();\n const addDrill = useAddDrill();\n const removeDrill = useRemoveDrill(id);\n const uploadFile = useUploadFile();\n\n const [drillOpen, setDrillOpen] = useState(false);\n const [drillForm, setDrillForm] = useState<DrillForm>(EMPTY_DRILL);\n const [videoFile, setVideoFile] = useState<File | null>(null);\n const [uploadingVideo, setUploadingVideo] = useState(false);\n const [drillError, setDrillError] = useState<string | null>(null);\n\n const drillsByWeek = useMemo(() => {\n const drills = program.data?.drills ?? [];\n const groups = new Map<number, AthDrill[]>();\n for (const d of drills) {\n const list = groups.get(d.weekNumber) ?? [];\n list.push(d);\n groups.set(d.weekNumber, list);\n }\n return Array.from(groups.entries())\n .sort(([a], [b]) => a - b)\n .map(([week, list]) => ({\n week,\n drills: [...list].sort((a, b) => a.orderIndex - b.orderIndex),\n }));\n }, [program.data]);\n\n if (program.isLoading) {\n return (\n <PageLayout title=\"Program\">\n <LoadingState label=\"Loading program…\" />\n </PageLayout>\n );\n }\n\n if (program.isError || !program.data) {\n return (\n <PageLayout title=\"Program\">\n <ErrorState\n title=\"Program not found\"\n message=\"This program does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/programs')}\n />\n </PageLayout>\n );\n }\n\n const p = program.data;\n const drillCount = p.drills?.length ?? 0;\n\n const handleTransition = (status: AthProgramStatus) => {\n if (!id) return;\n void transition.mutate({ id, status });\n };\n\n const handleArchive = async () => {\n if (!id) return;\n await archive.mutateAsync(id);\n navigateTo('/programs');\n };\n\n const handleAddDrill = async (e: React.FormEvent) => {\n e.preventDefault();\n setDrillError(null);\n if (!id) return;\n if (!drillForm.name.trim()) {\n setDrillError('Drill name is required.');\n return;\n }\n const week = numOrUndefined(drillForm.weekNumber) ?? 1;\n\n let videoFileId: string | undefined;\n if (videoFile) {\n setUploadingVideo(true);\n try {\n videoFileId = await uploadFile(videoFile);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to upload the demo video.');\n setUploadingVideo(false);\n return;\n }\n setUploadingVideo(false);\n }\n\n try {\n await addDrill.mutateAsync({\n programId: id,\n name: drillForm.name.trim(),\n description: drillForm.description.trim() || undefined,\n weekNumber: week,\n orderIndex: program.data?.drills?.filter((d) => d.weekNumber === week).length ?? 0,\n sets: numOrUndefined(drillForm.sets),\n reps: numOrUndefined(drillForm.reps),\n durationSec: numOrUndefined(drillForm.durationSec),\n restSec: numOrUndefined(drillForm.restSec),\n videoFileId,\n });\n setDrillForm(EMPTY_DRILL);\n setVideoFile(null);\n setDrillOpen(false);\n } catch (err) {\n setDrillError(err instanceof Error ? err.message : 'Failed to add drill.');\n }\n };\n\n return (\n <PageLayout\n title={p.name}\n description={`${SPORT_LABELS[p.sport]} · ${PROGRAM_LEVEL_LABELS[p.level]} · ${p.weeks} ${\n p.weeks === 1 ? 'week' : 'weeks'\n }`}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/programs')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/programs/${p.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Edit className=\"h-4 w-4\" /> Edit\n </button>\n {p.status !== 'ARCHIVED' && (\n <button\n type=\"button\"\n onClick={handleArchive}\n disabled={archive.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {archive.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"h-4 w-4\" />\n )}{' '}\n Archive\n </button>\n )}\n </div>\n }\n >\n {/* Summary + lifecycle */}\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={p.status} />\n {p.isTemplate && (\n <span className=\"inline-flex items-center gap-1 text-sm text-text-secondary\">\n <Copy className=\"h-3.5 w-3.5\" /> Template\n </span>\n )}\n </div>\n {p.summary && <p className=\"mt-4 text-sm text-text-secondary\">{p.summary}</p>}\n {p.tags.length > 0 && (\n <div className=\"mt-4 flex flex-wrap gap-1.5\">\n {p.tags.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Lifecycle transitions */}\n <div className=\"mt-5 flex flex-wrap items-center gap-2 border-t border-border-seam pt-5\">\n <span className=\"text-sm font-medium text-text-primary\">Publish lifecycle:</span>\n {p.status === 'DRAFT' && (\n <button\n type=\"button\"\n onClick={() => handleTransition('PUBLISHED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n <Send className=\"h-4 w-4\" /> Publish\n </button>\n )}\n {p.status === 'PUBLISHED' && (\n <>\n <button\n type=\"button\"\n onClick={() => handleTransition('DRAFT')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Undo2 className=\"h-4 w-4\" /> Pull back to draft\n </button>\n <button\n type=\"button\"\n onClick={() => handleTransition('ARCHIVED')}\n disabled={transition.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Archive className=\"h-4 w-4\" /> Archive\n </button>\n </>\n )}\n {p.status === 'ARCHIVED' && (\n <span className=\"text-sm text-text-secondary\">This program is archived.</span>\n )}\n {transition.isPending && <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />}\n </div>\n </GlassCard>\n\n {/* Drills */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Drills ({drillCount})</h2>\n <button\n type=\"button\"\n onClick={() => {\n setDrillError(null);\n setDrillOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> Add drill\n </button>\n </div>\n\n {drillCount === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Plus className=\"h-10 w-10\" />}\n title=\"No drills yet\"\n description=\"Add drills with sets, reps and durations, organized by week.\"\n />\n ) : (\n <div className=\"space-y-6\">\n {drillsByWeek.map(({ week, drills }) => (\n <div key={week}>\n <h3 className=\"mb-3 text-sm font-semibold uppercase tracking-wide text-text-secondary\">\n Week {week}\n </h3>\n <EmphasisPanel>\n <div className=\"divide-y divide-border-seam\">\n {drills.map((d) => (\n <div key={d.id} className=\"flex items-start gap-4 py-3\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">{d.name}</p>\n {d.description && (\n <p className=\"mt-0.5 text-xs text-text-secondary\">{d.description}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-secondary\">\n {d.sets !== null && <span>{d.sets} sets</span>}\n {d.reps !== null && <span>{d.reps} reps</span>}\n {d.durationSec !== null && <span>{d.durationSec}s work</span>}\n {d.restSec !== null && <span>{d.restSec}s rest</span>}\n </div>\n {d.video?.id && (\n <div>\n <DrillVideoLink fileId={d.video.id} />\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => void removeDrill.mutate(d.id)}\n disabled={removeDrill.isPending}\n aria-label={`Remove ${d.name}`}\n className=\"rounded-lg p-2 text-text-secondary transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-60\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n </div>\n </EmphasisPanel>\n </div>\n ))}\n </div>\n )}\n </section>\n\n {/* Add drill dialog */}\n <ResponsiveDialog open={drillOpen} onOpenChange={setDrillOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add drill</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleAddDrill} className=\"space-y-4 px-1\">\n <FormField label=\"Name\" htmlFor=\"drillName\" required>\n <input\n id=\"drillName\"\n className={fieldInputClass}\n value={drillForm.name}\n onChange={(e) => setDrillForm((p) => ({ ...p, name: e.target.value }))}\n placeholder=\"e.g. Box jumps\"\n />\n </FormField>\n <FormField label=\"Description\" htmlFor=\"drillDesc\">\n <textarea\n id=\"drillDesc\"\n className={fieldInputClass}\n rows={2}\n value={drillForm.description}\n onChange={(e) => setDrillForm((p) => ({ ...p, description: e.target.value }))}\n placeholder=\"How to perform the drill.\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Week\" htmlFor=\"drillWeek\">\n <input\n id=\"drillWeek\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.weekNumber}\n onChange={(e) => setDrillForm((p) => ({ ...p, weekNumber: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Sets\" htmlFor=\"drillSets\">\n <input\n id=\"drillSets\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.sets}\n onChange={(e) => setDrillForm((p) => ({ ...p, sets: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Reps\" htmlFor=\"drillReps\">\n <input\n id=\"drillReps\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.reps}\n onChange={(e) => setDrillForm((p) => ({ ...p, reps: e.target.value }))}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Work (sec)\" htmlFor=\"drillDuration\">\n <input\n id=\"drillDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.durationSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, durationSec: e.target.value }))}\n />\n </FormField>\n <FormField label=\"Rest (sec)\" htmlFor=\"drillRest\">\n <input\n id=\"drillRest\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={drillForm.restSec}\n onChange={(e) => setDrillForm((p) => ({ ...p, restSec: e.target.value }))}\n />\n </FormField>\n </div>\n <FormField\n label=\"Demo video\"\n htmlFor=\"drillVideo\"\n hint=\"Optional — attach a short demo clip; it's stored in workspace files.\"\n >\n <input\n id=\"drillVideo\"\n type=\"file\"\n accept=\"video/*\"\n className={fieldInputClass}\n onChange={(e) => setVideoFile(e.target.files?.[0] ?? null)}\n />\n {videoFile && (\n <p className=\"mt-1 inline-flex items-center gap-1 text-xs text-text-secondary\">\n <FileVideo className=\"h-3.5 w-3.5\" /> {videoFile.name}\n </p>\n )}\n </FormField>\n {drillError && <p className=\"text-sm text-status-error-text\">{drillError}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => {\n setDrillOpen(false);\n setVideoFile(null);\n }}\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={addDrill.isPending || uploadingVideo}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {(addDrill.isPending || uploadingVideo) && (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n )}\n {uploadingVideo ? 'Uploading video…' : 'Add drill'}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -6,109 +6,110 @@ import { formatDate as i, formatDuration as a } from "../../utils/formatters.js"
|
|
|
6
6
|
import { ErrorState as o, LoadingState as s } from "../../components/LoadingState.js";
|
|
7
7
|
import { useCancelSession as c, useCompleteSession as l, useConfirmSession as u, useSession as d } from "../../hooks/useAthleteBridgeApi.js";
|
|
8
8
|
import { FormField as f, fieldInputClass as p } from "../../components/form.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
9
|
+
import { safeExternalHref as m } from "../../utils/url.js";
|
|
10
|
+
import { useState as h } from "react";
|
|
11
|
+
import { ArrowLeft as g, CalendarClock as _, CheckCircle2 as v, Clock as y, Loader2 as b, MapPin as x, Star as S, Video as C, XCircle as w } from "lucide-react";
|
|
12
|
+
import { useParams as T } from "react-router-dom";
|
|
13
|
+
import { jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
14
|
+
import { EmphasisPanel as O, GlassCard as k, ResponsiveDialog as A, ResponsiveDialogContent as j, ResponsiveDialogFooter as M, ResponsiveDialogHeader as N, ResponsiveDialogTitle as P } from "@burdenoff/fe-libs/ui";
|
|
14
15
|
//#region src/atheletebridge/pages/sessions/SessionDetailPage.tsx
|
|
15
|
-
function
|
|
16
|
-
let { id:
|
|
17
|
-
if (
|
|
16
|
+
function F() {
|
|
17
|
+
let { id: F } = T(), { navigateTo: I } = e(), L = d(F), R = u(), z = l(), B = c(), [V, H] = h(!1), [U, W] = h(""), [G, K] = h(""), [q, J] = h(null);
|
|
18
|
+
if (L.isLoading) return /* @__PURE__ */ E(t, {
|
|
18
19
|
title: "Session",
|
|
19
|
-
children: /* @__PURE__ */
|
|
20
|
+
children: /* @__PURE__ */ E(s, { label: "Loading session…" })
|
|
20
21
|
});
|
|
21
|
-
if (
|
|
22
|
+
if (L.isError || !L.data) return /* @__PURE__ */ E(t, {
|
|
22
23
|
title: "Session",
|
|
23
|
-
children: /* @__PURE__ */
|
|
24
|
+
children: /* @__PURE__ */ E(o, {
|
|
24
25
|
title: "Session not found",
|
|
25
26
|
message: "This session does not exist or you do not have access to it.",
|
|
26
|
-
onRetry: () =>
|
|
27
|
+
onRetry: () => I("/sessions")
|
|
27
28
|
})
|
|
28
29
|
});
|
|
29
|
-
let
|
|
30
|
-
return /* @__PURE__ */
|
|
31
|
-
title:
|
|
32
|
-
description: r[
|
|
33
|
-
actions: /* @__PURE__ */
|
|
30
|
+
let Y = L.data, X = Y.status === "REQUESTED", Z = Y.status === "CONFIRMED", Q = Y.status === "REQUESTED" || Y.status === "CONFIRMED", $ = m(Y.meetingUrl);
|
|
31
|
+
return /* @__PURE__ */ D(t, {
|
|
32
|
+
title: Y.title,
|
|
33
|
+
description: r[Y.type],
|
|
34
|
+
actions: /* @__PURE__ */ D("button", {
|
|
34
35
|
type: "button",
|
|
35
|
-
onClick: () =>
|
|
36
|
+
onClick: () => I("/sessions"),
|
|
36
37
|
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
37
|
-
children: [/* @__PURE__ */
|
|
38
|
+
children: [/* @__PURE__ */ E(g, { className: "h-4 w-4" }), " Back"]
|
|
38
39
|
}),
|
|
39
40
|
children: [
|
|
40
|
-
/* @__PURE__ */
|
|
41
|
+
/* @__PURE__ */ D(k, {
|
|
41
42
|
treatment: "insight",
|
|
42
43
|
className: "mb-8 p-6",
|
|
43
44
|
children: [
|
|
44
|
-
/* @__PURE__ */
|
|
45
|
+
/* @__PURE__ */ E("div", {
|
|
45
46
|
className: "flex flex-wrap items-center gap-3",
|
|
46
|
-
children: /* @__PURE__ */
|
|
47
|
+
children: /* @__PURE__ */ E(n, { status: Y.status })
|
|
47
48
|
}),
|
|
48
|
-
/* @__PURE__ */
|
|
49
|
+
/* @__PURE__ */ D("div", {
|
|
49
50
|
className: "mt-4 grid grid-cols-1 gap-3 text-sm text-text-secondary sm:grid-cols-2",
|
|
50
51
|
children: [
|
|
51
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ D("span", {
|
|
52
53
|
className: "flex items-center gap-2",
|
|
53
54
|
children: [
|
|
54
|
-
/* @__PURE__ */
|
|
55
|
+
/* @__PURE__ */ E(_, { className: "h-4 w-4" }),
|
|
55
56
|
" ",
|
|
56
|
-
i(
|
|
57
|
+
i(Y.scheduledAt)
|
|
57
58
|
]
|
|
58
59
|
}),
|
|
59
|
-
/* @__PURE__ */
|
|
60
|
+
/* @__PURE__ */ D("span", {
|
|
60
61
|
className: "flex items-center gap-2",
|
|
61
62
|
children: [
|
|
62
|
-
/* @__PURE__ */
|
|
63
|
+
/* @__PURE__ */ E(y, { className: "h-4 w-4" }),
|
|
63
64
|
" ",
|
|
64
|
-
a(
|
|
65
|
+
a(Y.durationMin)
|
|
65
66
|
]
|
|
66
67
|
}),
|
|
67
|
-
|
|
68
|
+
Y.location && /* @__PURE__ */ D("span", {
|
|
68
69
|
className: "flex items-center gap-2",
|
|
69
70
|
children: [
|
|
70
|
-
/* @__PURE__ */
|
|
71
|
+
/* @__PURE__ */ E(x, { className: "h-4 w-4" }),
|
|
71
72
|
" ",
|
|
72
|
-
|
|
73
|
+
Y.location
|
|
73
74
|
]
|
|
74
75
|
}),
|
|
75
|
-
|
|
76
|
-
href:
|
|
76
|
+
$ && /* @__PURE__ */ D("a", {
|
|
77
|
+
href: $,
|
|
77
78
|
target: "_blank",
|
|
78
79
|
rel: "noopener noreferrer",
|
|
79
80
|
className: "flex items-center gap-2 text-primary hover:underline",
|
|
80
|
-
children: [/* @__PURE__ */
|
|
81
|
+
children: [/* @__PURE__ */ E(C, { className: "h-4 w-4" }), " Join meeting"]
|
|
81
82
|
})
|
|
82
83
|
]
|
|
83
84
|
}),
|
|
84
|
-
|
|
85
|
+
Y.completedAt && /* @__PURE__ */ D("div", {
|
|
85
86
|
className: "mt-5 border-t border-border-seam pt-5",
|
|
86
87
|
children: [
|
|
87
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ E("h2", {
|
|
88
89
|
className: "mb-2 text-sm font-semibold text-text-primary",
|
|
89
90
|
children: "Outcome"
|
|
90
91
|
}),
|
|
91
|
-
/* @__PURE__ */
|
|
92
|
+
/* @__PURE__ */ D("p", {
|
|
92
93
|
className: "text-sm text-text-secondary",
|
|
93
94
|
children: [
|
|
94
95
|
"Completed ",
|
|
95
|
-
i(
|
|
96
|
-
|
|
96
|
+
i(Y.completedAt),
|
|
97
|
+
Y.rating !== null && /* @__PURE__ */ D("span", {
|
|
97
98
|
className: "ml-2 inline-flex items-center gap-1",
|
|
98
99
|
children: [
|
|
99
|
-
/* @__PURE__ */
|
|
100
|
+
/* @__PURE__ */ E(S, { className: "h-3.5 w-3.5" }),
|
|
100
101
|
" ",
|
|
101
|
-
|
|
102
|
+
Y.rating,
|
|
102
103
|
"/5"
|
|
103
104
|
]
|
|
104
105
|
})
|
|
105
106
|
]
|
|
106
107
|
}),
|
|
107
|
-
|
|
108
|
+
Y.feedback && /* @__PURE__ */ D("p", {
|
|
108
109
|
className: "mt-2 text-sm text-text-secondary",
|
|
109
110
|
children: [
|
|
110
111
|
"“",
|
|
111
|
-
|
|
112
|
+
Y.feedback,
|
|
112
113
|
"”"
|
|
113
114
|
]
|
|
114
115
|
})
|
|
@@ -116,101 +117,101 @@ function P() {
|
|
|
116
117
|
})
|
|
117
118
|
]
|
|
118
119
|
}),
|
|
119
|
-
(
|
|
120
|
+
(X || Z || Q) && /* @__PURE__ */ D(O, { children: [/* @__PURE__ */ E("h2", {
|
|
120
121
|
className: "mb-4 text-sm font-semibold text-text-primary",
|
|
121
122
|
children: "Actions"
|
|
122
|
-
}), /* @__PURE__ */
|
|
123
|
+
}), /* @__PURE__ */ D("div", {
|
|
123
124
|
className: "flex flex-wrap items-center gap-2",
|
|
124
125
|
children: [
|
|
125
|
-
|
|
126
|
+
X && /* @__PURE__ */ D("button", {
|
|
126
127
|
type: "button",
|
|
127
|
-
onClick: () =>
|
|
128
|
-
disabled:
|
|
128
|
+
onClick: () => F && R.mutate(F),
|
|
129
|
+
disabled: R.isPending,
|
|
129
130
|
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60",
|
|
130
|
-
children: [
|
|
131
|
+
children: [R.isPending ? /* @__PURE__ */ E(b, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ E(v, { className: "h-4 w-4" }), "Confirm"]
|
|
131
132
|
}),
|
|
132
|
-
|
|
133
|
+
Z && /* @__PURE__ */ D("button", {
|
|
133
134
|
type: "button",
|
|
134
135
|
onClick: () => {
|
|
135
|
-
|
|
136
|
+
J(null), H(!0);
|
|
136
137
|
},
|
|
137
138
|
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90",
|
|
138
|
-
children: [/* @__PURE__ */
|
|
139
|
+
children: [/* @__PURE__ */ E(v, { className: "h-4 w-4" }), " Complete"]
|
|
139
140
|
}),
|
|
140
|
-
|
|
141
|
+
Q && /* @__PURE__ */ D("button", {
|
|
141
142
|
type: "button",
|
|
142
|
-
onClick: () =>
|
|
143
|
-
disabled:
|
|
143
|
+
onClick: () => F && B.mutate(F),
|
|
144
|
+
disabled: B.isPending,
|
|
144
145
|
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60",
|
|
145
|
-
children: [
|
|
146
|
+
children: [B.isPending ? /* @__PURE__ */ E(b, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ E(w, { className: "h-4 w-4" }), "Cancel"]
|
|
146
147
|
})
|
|
147
148
|
]
|
|
148
149
|
})] }),
|
|
149
|
-
/* @__PURE__ */
|
|
150
|
-
open:
|
|
151
|
-
onOpenChange:
|
|
152
|
-
children: /* @__PURE__ */
|
|
150
|
+
/* @__PURE__ */ E(A, {
|
|
151
|
+
open: V,
|
|
152
|
+
onOpenChange: H,
|
|
153
|
+
children: /* @__PURE__ */ D(j, { children: [/* @__PURE__ */ E(N, { children: /* @__PURE__ */ E(P, { children: "Complete session" }) }), /* @__PURE__ */ D("form", {
|
|
153
154
|
onSubmit: async (e) => {
|
|
154
|
-
if (e.preventDefault(),
|
|
155
|
+
if (e.preventDefault(), J(null), !F) return;
|
|
155
156
|
let t;
|
|
156
|
-
if (
|
|
157
|
-
|
|
157
|
+
if (G.trim() && (t = Number.parseInt(G, 10), Number.isNaN(t) || t < 1 || t > 5)) {
|
|
158
|
+
J("Rating must be between 1 and 5.");
|
|
158
159
|
return;
|
|
159
160
|
}
|
|
160
161
|
try {
|
|
161
|
-
await
|
|
162
|
-
id:
|
|
162
|
+
await z.mutateAsync({
|
|
163
|
+
id: F,
|
|
163
164
|
input: {
|
|
164
|
-
feedback:
|
|
165
|
+
feedback: U.trim() || void 0,
|
|
165
166
|
rating: t
|
|
166
167
|
}
|
|
167
|
-
}),
|
|
168
|
+
}), H(!1);
|
|
168
169
|
} catch (e) {
|
|
169
|
-
|
|
170
|
+
J(e instanceof Error ? e.message : "Failed to complete session.");
|
|
170
171
|
}
|
|
171
172
|
},
|
|
172
173
|
className: "space-y-4 px-1",
|
|
173
174
|
children: [
|
|
174
|
-
/* @__PURE__ */
|
|
175
|
+
/* @__PURE__ */ E(f, {
|
|
175
176
|
label: "Feedback",
|
|
176
177
|
htmlFor: "feedback",
|
|
177
178
|
hint: "Optional post-session notes.",
|
|
178
|
-
children: /* @__PURE__ */
|
|
179
|
+
children: /* @__PURE__ */ E("textarea", {
|
|
179
180
|
id: "feedback",
|
|
180
181
|
className: p,
|
|
181
182
|
rows: 3,
|
|
182
|
-
value:
|
|
183
|
-
onChange: (e) =>
|
|
183
|
+
value: U,
|
|
184
|
+
onChange: (e) => W(e.target.value),
|
|
184
185
|
placeholder: "How did the session go?"
|
|
185
186
|
})
|
|
186
187
|
}),
|
|
187
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ E(f, {
|
|
188
189
|
label: "Rating (1–5)",
|
|
189
190
|
htmlFor: "rating",
|
|
190
191
|
hint: "Optional.",
|
|
191
|
-
children: /* @__PURE__ */
|
|
192
|
+
children: /* @__PURE__ */ E("input", {
|
|
192
193
|
id: "rating",
|
|
193
194
|
inputMode: "numeric",
|
|
194
195
|
className: p,
|
|
195
|
-
value:
|
|
196
|
-
onChange: (e) =>
|
|
196
|
+
value: G,
|
|
197
|
+
onChange: (e) => K(e.target.value),
|
|
197
198
|
placeholder: "5"
|
|
198
199
|
})
|
|
199
200
|
}),
|
|
200
|
-
|
|
201
|
+
q && /* @__PURE__ */ E("p", {
|
|
201
202
|
className: "text-sm text-status-error-text",
|
|
202
|
-
children:
|
|
203
|
+
children: q
|
|
203
204
|
}),
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
+
/* @__PURE__ */ D(M, { children: [/* @__PURE__ */ E("button", {
|
|
205
206
|
type: "button",
|
|
206
|
-
onClick: () =>
|
|
207
|
+
onClick: () => H(!1),
|
|
207
208
|
className: "rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
208
209
|
children: "Cancel"
|
|
209
|
-
}), /* @__PURE__ */
|
|
210
|
+
}), /* @__PURE__ */ D("button", {
|
|
210
211
|
type: "submit",
|
|
211
|
-
disabled:
|
|
212
|
+
disabled: z.isPending,
|
|
212
213
|
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60",
|
|
213
|
-
children: [
|
|
214
|
+
children: [z.isPending && /* @__PURE__ */ E(b, { className: "h-4 w-4 animate-spin" }), "Complete session"]
|
|
214
215
|
})] })
|
|
215
216
|
]
|
|
216
217
|
})] })
|
|
@@ -219,6 +220,6 @@ function P() {
|
|
|
219
220
|
});
|
|
220
221
|
}
|
|
221
222
|
//#endregion
|
|
222
|
-
export {
|
|
223
|
+
export { F as SessionDetailPage };
|
|
223
224
|
|
|
224
225
|
//# sourceMappingURL=SessionDetailPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionDetailPage.tsx"],"sourcesContent":["/**\n * Session Detail\n * @module atheletebridge/pages/sessions\n *\n * Reads a single session (athSession) and drives its lifecycle:\n * confirm (athConfirmSession), complete with feedback + rating\n * (athCompleteSession) and cancel (athCancelSession).\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Clock,\n MapPin,\n Video,\n Star,\n CheckCircle2,\n XCircle,\n Loader2,\n} from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useSession,\n useConfirmSession,\n useCompleteSession,\n useCancelSession,\n} from '../../hooks/useAthleteBridgeApi';\n\nexport function SessionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const session = useSession(id);\n const confirmSession = useConfirmSession();\n const completeSession = useCompleteSession();\n const cancelSession = useCancelSession();\n\n const [completeOpen, setCompleteOpen] = useState(false);\n const [feedback, setFeedback] = useState('');\n const [rating, setRating] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (session.isLoading) {\n return (\n <PageLayout title=\"Session\">\n <LoadingState label=\"Loading session…\" />\n </PageLayout>\n );\n }\n\n if (session.isError || !session.data) {\n return (\n <PageLayout title=\"Session\">\n <ErrorState\n title=\"Session not found\"\n message=\"This session does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/sessions')}\n />\n </PageLayout>\n );\n }\n\n const s = session.data;\n const canConfirm = s.status === 'REQUESTED';\n const canComplete = s.status === 'CONFIRMED';\n const canCancel = s.status === 'REQUESTED' || s.status === 'CONFIRMED';\n\n const handleComplete = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n let ratingNum: number | undefined;\n if (rating.trim()) {\n ratingNum = Number.parseInt(rating, 10);\n if (Number.isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {\n setError('Rating must be between 1 and 5.');\n return;\n }\n }\n try {\n await completeSession.mutateAsync({\n id,\n input: {\n feedback: feedback.trim() || undefined,\n rating: ratingNum,\n },\n });\n setCompleteOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to complete session.');\n }\n };\n\n return (\n <PageLayout\n title={s.title}\n description={SESSION_TYPE_LABELS[s.type]}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={s.status} />\n </div>\n <div className=\"mt-4 grid grid-cols-1 gap-3 text-sm text-text-secondary sm:grid-cols-2\">\n <span className=\"flex items-center gap-2\">\n <CalendarClock className=\"h-4 w-4\" /> {formatDate(s.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" /> {formatDuration(s.durationMin)}\n </span>\n {s.location && (\n <span className=\"flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" /> {s.location}\n </span>\n )}\n {s.meetingUrl && (\n <a\n href={s.meetingUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 text-primary hover:underline\"\n >\n <Video className=\"h-4 w-4\" /> Join meeting\n </a>\n )}\n </div>\n\n {s.completedAt && (\n <div className=\"mt-5 border-t border-border-seam pt-5\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">Outcome</h2>\n <p className=\"text-sm text-text-secondary\">\n Completed {formatDate(s.completedAt)}\n {s.rating !== null && (\n <span className=\"ml-2 inline-flex items-center gap-1\">\n <Star className=\"h-3.5 w-3.5\" /> {s.rating}/5\n </span>\n )}\n </p>\n {s.feedback && <p className=\"mt-2 text-sm text-text-secondary\">“{s.feedback}”</p>}\n </div>\n )}\n </GlassCard>\n\n {/* Lifecycle actions */}\n {(canConfirm || canComplete || canCancel) && (\n <EmphasisPanel>\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">Actions</h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n {canConfirm && (\n <button\n type=\"button\"\n onClick={() => id && confirmSession.mutate(id)}\n disabled={confirmSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {confirmSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n Confirm\n </button>\n )}\n {canComplete && (\n <button\n type=\"button\"\n onClick={() => {\n setError(null);\n setCompleteOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <CheckCircle2 className=\"h-4 w-4\" /> Complete\n </button>\n )}\n {canCancel && (\n <button\n type=\"button\"\n onClick={() => id && cancelSession.mutate(id)}\n disabled={cancelSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {cancelSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n Cancel\n </button>\n )}\n </div>\n </EmphasisPanel>\n )}\n\n {/* Complete dialog */}\n <ResponsiveDialog open={completeOpen} onOpenChange={setCompleteOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Complete session</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleComplete} className=\"space-y-4 px-1\">\n <FormField label=\"Feedback\" htmlFor=\"feedback\" hint=\"Optional post-session notes.\">\n <textarea\n id=\"feedback\"\n className={fieldInputClass}\n rows={3}\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n <FormField label=\"Rating (1–5)\" htmlFor=\"rating\" hint=\"Optional.\">\n <input\n id=\"rating\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rating}\n onChange={(e) => setRating(e.target.value)}\n placeholder=\"5\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setCompleteOpen(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={completeSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {completeSession.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Complete session\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA6CA,SAAgB,IAAoB;CAClC,IAAM,EAAE,UAAO,GAA2B,EACpC,EAAE,kBAAe,GAAkB,EAEnC,IAAU,EAAW,EAAG,EACxB,IAAiB,GAAmB,EACpC,IAAkB,GAAoB,EACtC,IAAgB,GAAkB,EAElC,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAU,KAAe,EAAS,GAAG,EACtC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAO,KAAY,EAAwB,KAAK;AAEvD,KAAI,EAAQ,UACV,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAc,OAAM,oBAAqB,CAAA;EAC9B,CAAA;AAIjB,KAAI,EAAQ,WAAW,CAAC,EAAQ,KAC9B,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,EAAW,YAAY;GACtC,CAAA;EACS,CAAA;CAIjB,IAAM,IAAI,EAAQ,MACZ,IAAa,EAAE,WAAW,aAC1B,IAAc,EAAE,WAAW,aAC3B,IAAY,EAAE,WAAW,eAAe,EAAE,WAAW;AA4B3D,QACE,kBAAC,GAAD;EACE,OAAO,EAAE;EACT,aAAa,EAAoB,EAAE;EACnC,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,YAAY;GACtC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,QAC1B;;YAVb;GAaE,kBAAC,GAAD;IAAW,WAAU;IAAU,WAAU;cAAzC;KACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA;MAC7B,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;SAAE,EAAW,EAAE,YAAY;SAC3D;;OACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;;SAAE,EAAe,EAAE,YAAY;SACvD;;OACN,EAAE,YACD,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;;SAAE,EAAE;SAC7B;;OAER,EAAE,cACD,kBAAC,KAAD;QACE,MAAM,EAAE;QACR,QAAO;QACP,KAAI;QACJ,WAAU;kBAJZ,CAME,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAAA,gBAC3B;;OAEF;;KAEL,EAAE,eACD,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,MAAD;QAAI,WAAU;kBAA+C;QAAY,CAAA;OACzE,kBAAC,KAAD;QAAG,WAAU;kBAAb;SAA2C;SAC9B,EAAW,EAAE,YAAY;SACnC,EAAE,WAAW,QACZ,kBAAC,QAAD;UAAM,WAAU;oBAAhB;WACE,kBAAC,GAAD,EAAM,WAAU,eAAgB,CAAA;;WAAE,EAAE;WAAO;WACtC;;SAEP;;OACH,EAAE,YAAY,kBAAC,KAAD;QAAG,WAAU;kBAAb;SAAgD;SAAE,EAAE;SAAS;SAAK;;OAC7E;;KAEE;;IAGV,KAAc,KAAe,MAC7B,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,MAAD;IAAI,WAAU;cAA+C;IAAY,CAAA,EACzE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAM,EAAe,OAAO,EAAG;MAC9C,UAAU,EAAe;MACzB,WAAU;gBAJZ,CAMG,EAAe,YACd,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA,EACpC,UAEK;;KAEV,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe;AAEb,OADA,EAAS,KAAK,EACd,EAAgB,GAAK;;MAEvB,WAAU;gBANZ,CAQE,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA,EAAA,YAC7B;;KAEV,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAM,EAAc,OAAO,EAAG;MAC7C,UAAU,EAAc;MACxB,WAAU;gBAJZ,CAMG,EAAc,YACb,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA,EAC/B,SAEK;;KAEP;MACQ,EAAA,CAAA;GAIlB,kBAAC,GAAD;IAAkB,MAAM;IAAc,cAAc;cAClD,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAuB,oBAAwC,CAAA,EACxC,CAAA,EACzB,kBAAC,QAAD;KAAM,UA7IS,OAAO,MAAuB;AAGnD,UAFA,EAAE,gBAAgB,EAClB,EAAS,KAAK,EACV,CAAC,EAAI;MACT,IAAI;AACJ,UAAI,EAAO,MAAM,KACf,IAAY,OAAO,SAAS,GAAQ,GAAG,EACnC,OAAO,MAAM,EAAU,IAAI,IAAY,KAAK,IAAY,IAAG;AAC7D,SAAS,kCAAkC;AAC3C;;AAGJ,UAAI;AAQF,OAPA,MAAM,EAAgB,YAAY;QAChC;QACA,OAAO;SACL,UAAU,EAAS,MAAM,IAAI,KAAA;SAC7B,QAAQ;SACT;QACF,CAAC,EACF,EAAgB,GAAM;eACf,GAAK;AACZ,SAAS,aAAe,QAAQ,EAAI,UAAU,8BAA8B;;;KAuHxC,WAAU;eAA1C;MACE,kBAAC,GAAD;OAAW,OAAM;OAAW,SAAQ;OAAW,MAAK;iBAClD,kBAAC,YAAD;QACE,IAAG;QACH,WAAW;QACX,MAAM;QACN,OAAO;QACP,WAAW,MAAM,EAAY,EAAE,OAAO,MAAM;QAC5C,aAAY;QACZ,CAAA;OACQ,CAAA;MACZ,kBAAC,GAAD;OAAW,OAAM;OAAe,SAAQ;OAAS,MAAK;iBACpD,kBAAC,SAAD;QACE,IAAG;QACH,WAAU;QACV,WAAW;QACX,OAAO;QACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;QAC1C,aAAY;QACZ,CAAA;OACQ,CAAA;MACX,KAAS,kBAAC,KAAD;OAAG,WAAU;iBAAkC;OAAU,CAAA;MACnE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAgB,GAAM;OACrC,WAAU;iBACX;OAEQ,CAAA,EACT,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,EAAgB;OAC1B,WAAU;iBAHZ,CAKG,EAAgB,aAAa,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAAC,mBAEpE;SACc,EAAA,CAAA;MACpB;OACiB,EAAA,CAAA;IACT,CAAA;GACR"}
|
|
1
|
+
{"version":3,"file":"SessionDetailPage.js","names":[],"sources":["../../../../src/atheletebridge/pages/sessions/SessionDetailPage.tsx"],"sourcesContent":["/**\n * Session Detail\n * @module atheletebridge/pages/sessions\n *\n * Reads a single session (athSession) and drives its lifecycle:\n * confirm (athConfirmSession), complete with feedback + rating\n * (athCompleteSession) and cancel (athCancelSession).\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CalendarClock,\n Clock,\n MapPin,\n Video,\n Star,\n CheckCircle2,\n XCircle,\n Loader2,\n} from 'lucide-react';\nimport {\n GlassCard,\n EmphasisPanel,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { SESSION_TYPE_LABELS } from '../../components/domain';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { safeExternalHref } from '../../utils/url';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useSession,\n useConfirmSession,\n useCompleteSession,\n useCancelSession,\n} from '../../hooks/useAthleteBridgeApi';\n\nexport function SessionDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useAthleteBridge();\n\n const session = useSession(id);\n const confirmSession = useConfirmSession();\n const completeSession = useCompleteSession();\n const cancelSession = useCancelSession();\n\n const [completeOpen, setCompleteOpen] = useState(false);\n const [feedback, setFeedback] = useState('');\n const [rating, setRating] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n if (session.isLoading) {\n return (\n <PageLayout title=\"Session\">\n <LoadingState label=\"Loading session…\" />\n </PageLayout>\n );\n }\n\n if (session.isError || !session.data) {\n return (\n <PageLayout title=\"Session\">\n <ErrorState\n title=\"Session not found\"\n message=\"This session does not exist or you do not have access to it.\"\n onRetry={() => navigateTo('/sessions')}\n />\n </PageLayout>\n );\n }\n\n const s = session.data;\n const canConfirm = s.status === 'REQUESTED';\n const canComplete = s.status === 'CONFIRMED';\n const canCancel = s.status === 'REQUESTED' || s.status === 'CONFIRMED';\n // Sanitize the user-controlled meeting URL before it becomes a clickable href.\n const meetingHref = safeExternalHref(s.meetingUrl);\n\n const handleComplete = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!id) return;\n let ratingNum: number | undefined;\n if (rating.trim()) {\n ratingNum = Number.parseInt(rating, 10);\n if (Number.isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {\n setError('Rating must be between 1 and 5.');\n return;\n }\n }\n try {\n await completeSession.mutateAsync({\n id,\n input: {\n feedback: feedback.trim() || undefined,\n rating: ratingNum,\n },\n });\n setCompleteOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to complete session.');\n }\n };\n\n return (\n <PageLayout\n title={s.title}\n description={SESSION_TYPE_LABELS[s.type]}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/sessions')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <GlassCard treatment=\"insight\" className=\"mb-8 p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={s.status} />\n </div>\n <div className=\"mt-4 grid grid-cols-1 gap-3 text-sm text-text-secondary sm:grid-cols-2\">\n <span className=\"flex items-center gap-2\">\n <CalendarClock className=\"h-4 w-4\" /> {formatDate(s.scheduledAt)}\n </span>\n <span className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" /> {formatDuration(s.durationMin)}\n </span>\n {s.location && (\n <span className=\"flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" /> {s.location}\n </span>\n )}\n {meetingHref && (\n <a\n href={meetingHref}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-2 text-primary hover:underline\"\n >\n <Video className=\"h-4 w-4\" /> Join meeting\n </a>\n )}\n </div>\n\n {s.completedAt && (\n <div className=\"mt-5 border-t border-border-seam pt-5\">\n <h2 className=\"mb-2 text-sm font-semibold text-text-primary\">Outcome</h2>\n <p className=\"text-sm text-text-secondary\">\n Completed {formatDate(s.completedAt)}\n {s.rating !== null && (\n <span className=\"ml-2 inline-flex items-center gap-1\">\n <Star className=\"h-3.5 w-3.5\" /> {s.rating}/5\n </span>\n )}\n </p>\n {s.feedback && <p className=\"mt-2 text-sm text-text-secondary\">“{s.feedback}”</p>}\n </div>\n )}\n </GlassCard>\n\n {/* Lifecycle actions */}\n {(canConfirm || canComplete || canCancel) && (\n <EmphasisPanel>\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">Actions</h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n {canConfirm && (\n <button\n type=\"button\"\n onClick={() => id && confirmSession.mutate(id)}\n disabled={confirmSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {confirmSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n Confirm\n </button>\n )}\n {canComplete && (\n <button\n type=\"button\"\n onClick={() => {\n setError(null);\n setCompleteOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <CheckCircle2 className=\"h-4 w-4\" /> Complete\n </button>\n )}\n {canCancel && (\n <button\n type=\"button\"\n onClick={() => id && cancelSession.mutate(id)}\n disabled={cancelSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-60\"\n >\n {cancelSession.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n Cancel\n </button>\n )}\n </div>\n </EmphasisPanel>\n )}\n\n {/* Complete dialog */}\n <ResponsiveDialog open={completeOpen} onOpenChange={setCompleteOpen}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Complete session</ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleComplete} className=\"space-y-4 px-1\">\n <FormField label=\"Feedback\" htmlFor=\"feedback\" hint=\"Optional post-session notes.\">\n <textarea\n id=\"feedback\"\n className={fieldInputClass}\n rows={3}\n value={feedback}\n onChange={(e) => setFeedback(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n <FormField label=\"Rating (1–5)\" htmlFor=\"rating\" hint=\"Optional.\">\n <input\n id=\"rating\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rating}\n onChange={(e) => setRating(e.target.value)}\n placeholder=\"5\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => setCompleteOpen(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={completeSession.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-60\"\n >\n {completeSession.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Complete session\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|