@burdenoff/microfe-mybodyshield 2026.705.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/mybodyshield/components/DataTable.js +8 -4
- package/dist/mybodyshield/components/DataTable.js.map +1 -1
- package/dist/mybodyshield/components/EvidenceUpload.js +92 -4
- package/dist/mybodyshield/components/EvidenceUpload.js.map +1 -0
- package/dist/mybodyshield/components/FileLink.js +35 -2
- package/dist/mybodyshield/components/FileLink.js.map +1 -0
- package/dist/mybodyshield/components/MybDiscussion.js +107 -6
- package/dist/mybodyshield/components/MybDiscussion.js.map +1 -0
- package/dist/mybodyshield/components/MybTags.js +148 -4
- package/dist/mybodyshield/components/MybTags.js.map +1 -0
- package/dist/mybodyshield/components/Pagination.js.map +1 -1
- package/dist/mybodyshield/components/UserSearchSelect.js +148 -0
- package/dist/mybodyshield/components/UserSearchSelect.js.map +1 -0
- package/dist/mybodyshield/components/index.js +1 -0
- package/dist/mybodyshield/hooks/index.js +1 -0
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.js +47 -27
- package/dist/mybodyshield/hooks/useMyBodyShieldApi.js.map +1 -1
- package/dist/mybodyshield/hooks/useMybComments.js +44 -2
- package/dist/mybodyshield/hooks/useMybComments.js.map +1 -0
- package/dist/mybodyshield/hooks/useMybFiles.js +80 -3
- package/dist/mybodyshield/hooks/useMybFiles.js.map +1 -0
- package/dist/mybodyshield/hooks/useMybTags.js +88 -2
- package/dist/mybodyshield/hooks/useMybTags.js.map +1 -0
- package/dist/mybodyshield/hooks/useMybUsers.js +36 -0
- package/dist/mybodyshield/hooks/useMybUsers.js.map +1 -0
- package/dist/mybodyshield/pages/DashboardPage.js +41 -36
- package/dist/mybodyshield/pages/DashboardPage.js.map +1 -1
- package/dist/mybodyshield/pages/_shared/visuals.js +15 -9
- package/dist/mybodyshield/pages/_shared/visuals.js.map +1 -1
- package/dist/mybodyshield/pages/coaching/CoachingPage.js +171 -147
- package/dist/mybodyshield/pages/coaching/CoachingPage.js.map +1 -1
- package/dist/mybodyshield/pages/goals/GoalsPage.js +0 -8
- package/dist/mybodyshield/pages/goals/GoalsPage.js.map +1 -1
- package/dist/mybodyshield/pages/nutrition/NutritionPage.js +352 -278
- package/dist/mybodyshield/pages/nutrition/NutritionPage.js.map +1 -1
- package/dist/mybodyshield/pages/programs/ProgramDetailPage.js +600 -296
- package/dist/mybodyshield/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/mybodyshield/pages/progress/ProgressPage.js +88 -51
- package/dist/mybodyshield/pages/progress/ProgressPage.js.map +1 -1
- package/dist/mybodyshield/pages/train/SessionDetailPage.js +146 -126
- package/dist/mybodyshield/pages/train/SessionDetailPage.js.map +1 -1
- package/dist/mybodyshield/pages/train/SessionsPage.js +1 -4
- package/dist/mybodyshield/pages/train/SessionsPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoachingPage.js","names":[],"sources":["../../../../src/mybodyshield/pages/coaching/CoachingPage.tsx"],"sourcesContent":["/**\n * Coaching — coach ↔ member relationships and shared data scope.\n * @module mybodyshield/pages/coaching\n *\n * Lists the member's `MybCoachingRelationship`s (a PLAIN ARRAY from\n * `mybListCoaching` — no pagination) with a status filter, per-relationship\n * sharing scope (workouts / nutrition / metrics), lifecycle transition actions\n * (accept / pause / resume / end) and a create flow. No mock data.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n HeartHandshake,\n Plus,\n Dumbbell,\n Salad,\n LineChart,\n CalendarClock,\n CalendarX,\n} from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Switch,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n FilterTabs,\n type FilterOption,\n} from '../../components';\nimport { IconChip } from '../_shared/visuals';\nimport { useMyBodyShield } from '../../context';\nimport { useMybCoaching, useMybCreateCoaching, useMybTransitionCoaching } from '../../hooks';\nimport type {\n MybCoachingRelationship,\n MybCoachingStatus,\n MybCreateCoachingInput,\n} from '../../types';\nimport { formatDate } from '../../utils/formatters';\n\ntype StatusFilter = MybCoachingStatus | 'ALL';\n\nconst FILTER_ORDER: MybCoachingStatus[] = ['PENDING', 'ACTIVE', 'PAUSED', 'ENDED'];\n\n/** Allowed lifecycle transitions per current status (contract-defined). */\nconst TRANSITIONS: Record<\n MybCoachingStatus,\n { to: MybCoachingStatus; label: string; primary: boolean }[]\n> = {\n PENDING: [{ to: 'ACTIVE', label: 'Accept', primary: true }],\n ACTIVE: [\n { to: 'PAUSED', label: 'Pause', primary: false },\n { to: 'ENDED', label: 'End', primary: false },\n ],\n PAUSED: [\n { to: 'ACTIVE', label: 'Resume', primary: true },\n { to: 'ENDED', label: 'End', primary: false },\n ],\n ENDED: [],\n};\n\n/** Shorten an opaque actor id for compact display (full id kept as a title). */\nfunction shortId(id: string): string {\n return id.length <= 10 ? id : `${id.slice(0, 8)}…`;\n}\n\n// ============================================================================\n// Create-coaching dialog\n// ============================================================================\n\ninterface CreateCoachingForm {\n memberId: string;\n coachId: string;\n specialty: string;\n shareWorkouts: boolean;\n shareNutrition: boolean;\n shareMetrics: boolean;\n}\n\nfunction buildEmptyForm(defaultMemberId: string): CreateCoachingForm {\n return {\n memberId: defaultMemberId,\n coachId: '',\n specialty: '',\n shareWorkouts: true,\n shareNutrition: true,\n shareMetrics: true,\n };\n}\n\nfunction ShareToggle({\n id,\n label,\n description,\n checked,\n onChange,\n}: {\n id: string;\n label: string;\n description: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <div className=\"flex items-center justify-between gap-4 rounded-xl bg-bg-sunken/60 p-3\">\n <div className=\"min-w-0\">\n <Label htmlFor={id} className=\"cursor-pointer\">\n {label}\n </Label>\n <p className=\"text-xs text-text-tertiary\">{description}</p>\n </div>\n <Switch id={id} checked={checked} onCheckedChange={onChange} />\n </div>\n );\n}\n\nfunction CreateCoachingDialog({\n open,\n onOpenChange,\n defaultMemberId,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n defaultMemberId: string;\n}) {\n const create = useMybCreateCoaching();\n const [form, setForm] = useState<CreateCoachingForm>(() => buildEmptyForm(defaultMemberId));\n const [error, setError] = useState<string | null>(null);\n\n const handleOpenChange = (next: boolean) => {\n if (!next) {\n setForm(buildEmptyForm(defaultMemberId));\n setError(null);\n }\n onOpenChange(next);\n };\n\n const submit = async () => {\n const coachId = form.coachId.trim();\n const memberId = form.memberId.trim();\n if (!coachId) {\n setError('Coach user id is required.');\n return;\n }\n if (!memberId) {\n setError('Member user id is required.');\n return;\n }\n setError(null);\n const input: MybCreateCoachingInput = {\n coachId,\n memberId,\n specialty: form.specialty.trim() || undefined,\n shareWorkouts: form.shareWorkouts,\n shareNutrition: form.shareNutrition,\n shareMetrics: form.shareMetrics,\n };\n try {\n await create.mutateAsync(input);\n setForm(buildEmptyForm(defaultMemberId));\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create coaching relationship.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={handleOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New coaching</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Link a coach and member, and choose exactly what training, nutrition and progress data\n is shared.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form\n className=\"space-y-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-coaching-member\">Member</Label>\n <Input\n id=\"myb-coaching-member\"\n value={form.memberId}\n onChange={(event) => setForm((f) => ({ ...f, memberId: event.target.value }))}\n placeholder=\"member user id\"\n />\n <p className=\"text-xs text-text-tertiary\">member user id</p>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-coaching-coach\">Coach</Label>\n <Input\n id=\"myb-coaching-coach\"\n value={form.coachId}\n onChange={(event) => setForm((f) => ({ ...f, coachId: event.target.value }))}\n placeholder=\"coach user id\"\n />\n <p className=\"text-xs text-text-tertiary\">coach user id</p>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-coaching-specialty\">Specialty</Label>\n <Input\n id=\"myb-coaching-specialty\"\n value={form.specialty}\n onChange={(event) => setForm((f) => ({ ...f, specialty: event.target.value }))}\n placeholder=\"e.g., Strength & conditioning\"\n />\n </div>\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">Shared with the coach</p>\n <ShareToggle\n id=\"myb-coaching-share-workouts\"\n label=\"Workouts\"\n description=\"Programs, sessions and logged sets.\"\n checked={form.shareWorkouts}\n onChange={(value) => setForm((f) => ({ ...f, shareWorkouts: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-nutrition\"\n label=\"Nutrition\"\n description=\"Plans, meals and daily macros.\"\n checked={form.shareNutrition}\n onChange={(value) => setForm((f) => ({ ...f, shareNutrition: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-metrics\"\n label=\"Metrics\"\n description=\"Body metrics and recovery logs.\"\n checked={form.shareMetrics}\n onChange={(value) => setForm((f) => ({ ...f, shareMetrics: value }))}\n />\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => handleOpenChange(false)}\n disabled={create.isPending}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={create.isPending}>\n {create.isPending ? 'Creating…' : 'Create coaching'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Sharing scope chips\n// ============================================================================\n\nfunction ShareChip({ label, icon, on }: { label: string; icon: typeof Dumbbell; on: boolean }) {\n const Icon = icon;\n return (\n <span\n className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${\n on\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-tertiary'\n }`}\n >\n <Icon className=\"h-3 w-3\" />\n {label}\n </span>\n );\n}\n\n// ============================================================================\n// Coaching card\n// ============================================================================\n\nfunction CoachingCard({\n relationship,\n currentUserId,\n busy,\n onTransition,\n}: {\n relationship: MybCoachingRelationship;\n currentUserId?: string;\n busy: boolean;\n onTransition: (id: string, status: MybCoachingStatus) => void;\n}) {\n const coachIsYou = Boolean(currentUserId) && relationship.coachId === currentUserId;\n const memberIsYou = Boolean(currentUserId) && relationship.memberId === currentUserId;\n const actions = TRANSITIONS[relationship.status];\n\n return (\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <IconChip icon={<HeartHandshake className=\"h-4 w-4\" />} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {relationship.specialty?.trim() || 'Coaching relationship'}\n </p>\n <p className=\"text-xs text-text-tertiary\">Coach ↔ member</p>\n </div>\n </div>\n <StatusBadge status={relationship.status} />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg bg-bg-sunken/60 p-2.5\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n Coach\n </p>\n <p\n className=\"mt-0.5 truncate text-sm font-medium text-text-primary\"\n title={relationship.coachId}\n >\n {coachIsYou ? 'You' : `#${shortId(relationship.coachId)}`}\n </p>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/60 p-2.5\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n Member\n </p>\n <p\n className=\"mt-0.5 truncate text-sm font-medium text-text-primary\"\n title={relationship.memberId}\n >\n {memberIsYou ? 'You' : `#${shortId(relationship.memberId)}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap gap-1.5\">\n <ShareChip label=\"Workouts\" icon={Dumbbell} on={relationship.shareWorkouts} />\n <ShareChip label=\"Nutrition\" icon={Salad} on={relationship.shareNutrition} />\n <ShareChip label=\"Metrics\" icon={LineChart} on={relationship.shareMetrics} />\n </div>\n\n {(relationship.startedAt || relationship.endedAt) && (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-tertiary\">\n {relationship.startedAt && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Started {formatDate(relationship.startedAt)}\n </span>\n )}\n {relationship.endedAt && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarX className=\"h-3.5 w-3.5\" />\n Ended {formatDate(relationship.endedAt)}\n </span>\n )}\n </div>\n )}\n\n {actions.length > 0 && (\n <div className=\"mt-auto flex flex-wrap gap-2 pt-1\">\n {actions.map((action) => (\n <Button\n key={action.to}\n size=\"sm\"\n variant={action.primary ? undefined : 'outline'}\n disabled={busy}\n onClick={() => onTransition(relationship.id, action.to)}\n >\n {action.label}\n </Button>\n ))}\n </div>\n )}\n </GlassCard>\n );\n}\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport function CoachingPage() {\n const { currentUser } = useMyBodyShield();\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n const [transitioningId, setTransitioningId] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n // `useMybCoaching` returns a PLAIN ARRAY — fetch every relationship once and\n // filter client-side so the tab counts stay accurate across filters.\n const coaching = useMybCoaching();\n const transition = useMybTransitionCoaching();\n\n const all = coaching.data ?? [];\n\n const counts = useMemo(() => {\n const map: Record<MybCoachingStatus, number> = {\n PENDING: 0,\n ACTIVE: 0,\n PAUSED: 0,\n ENDED: 0,\n };\n for (const rel of all) map[rel.status] += 1;\n return map;\n }, [all]);\n\n const statusFilters = useMemo<FilterOption<StatusFilter>[]>(\n () => [\n { value: 'ALL', label: 'All', count: all.length },\n ...FILTER_ORDER.map((status) => ({\n value: status as StatusFilter,\n label: status.charAt(0) + status.slice(1).toLowerCase(),\n count: counts[status],\n })),\n ],\n [all.length, counts]\n );\n\n const rows = useMemo(\n () => (statusFilter === 'ALL' ? all : all.filter((rel) => rel.status === statusFilter)),\n [all, statusFilter]\n );\n\n const handleTransition = async (id: string, status: MybCoachingStatus) => {\n setTransitioningId(id);\n setActionError(null);\n try {\n await transition.mutateAsync({ id, status });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to update the relationship.');\n } finally {\n setTransitioningId(null);\n }\n };\n\n const showLoading = coaching.isLoading && !coaching.data;\n\n return (\n <PageLayout\n title=\"Coaching\"\n description=\"Coach ↔ member relationships and what's shared.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New coaching\n </Button>\n }\n >\n <div className=\"mb-6\">\n <FilterTabs options={statusFilters} value={statusFilter} onChange={setStatusFilter} />\n </div>\n\n {actionError && (\n <p className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {showLoading ? (\n <LoadingView label=\"Loading coaching relationships…\" />\n ) : coaching.isError ? (\n <ErrorView\n message=\"We couldn't load your coaching relationships.\"\n onRetry={() => void coaching.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<HeartHandshake className=\"h-10 w-10\" />}\n title={\n all.length === 0 ? 'No coaching relationships' : 'No relationships match this filter'\n }\n description={\n all.length === 0\n ? 'Connect a coach and a member to start sharing training, nutrition and progress data.'\n : 'Try a different status filter, or create a new coaching relationship.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New coaching\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {rows.map((relationship) => (\n <CoachingCard\n key={relationship.id}\n relationship={relationship}\n currentUserId={currentUser?.id}\n busy={transitioningId === relationship.id}\n onTransition={(id, status) => void handleTransition(id, status)}\n />\n ))}\n </div>\n )}\n\n <CreateCoachingDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n defaultMemberId={currentUser?.id ?? ''}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoachingPage.js","names":[],"sources":["../../../../src/mybodyshield/pages/coaching/CoachingPage.tsx"],"sourcesContent":["/**\n * Coaching — coach ↔ member relationships and shared data scope.\n * @module mybodyshield/pages/coaching\n *\n * Lists the member's `MybCoachingRelationship`s (a PLAIN ARRAY from\n * `mybListCoaching` — no pagination) with a status filter, per-relationship\n * sharing scope (workouts / nutrition / metrics), lifecycle transition actions\n * (accept / pause / resume / end) and a create flow. No mock data.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n HeartHandshake,\n Plus,\n Dumbbell,\n Salad,\n LineChart,\n CalendarClock,\n CalendarX,\n Check,\n Minus,\n MessageSquare,\n ChevronDown,\n ChevronUp,\n} from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Switch,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n FilterTabs,\n MybDiscussion,\n UserSearchSelect,\n type FilterOption,\n type UserSelectValue,\n} from '../../components';\nimport { IconChip } from '../_shared/visuals';\nimport { useMyBodyShield } from '../../context';\nimport { useMybCoaching, useMybCreateCoaching, useMybTransitionCoaching } from '../../hooks';\nimport type {\n MybCoachingRelationship,\n MybCoachingStatus,\n MybCreateCoachingInput,\n} from '../../types';\nimport { formatDate } from '../../utils/formatters';\n\ntype StatusFilter = MybCoachingStatus | 'ALL';\n\nconst FILTER_ORDER: MybCoachingStatus[] = ['PENDING', 'ACTIVE', 'PAUSED', 'ENDED'];\n\n/** Allowed lifecycle transitions per current status (contract-defined). */\nconst TRANSITIONS: Record<\n MybCoachingStatus,\n { to: MybCoachingStatus; label: string; primary: boolean }[]\n> = {\n PENDING: [{ to: 'ACTIVE', label: 'Accept', primary: true }],\n ACTIVE: [\n { to: 'PAUSED', label: 'Pause', primary: false },\n { to: 'ENDED', label: 'End', primary: false },\n ],\n PAUSED: [\n { to: 'ACTIVE', label: 'Resume', primary: true },\n { to: 'ENDED', label: 'End', primary: false },\n ],\n ENDED: [],\n};\n\n/** Shorten an opaque actor id for compact display (full id kept as a title). */\nfunction shortId(id: string): string {\n return id.length <= 10 ? id : `${id.slice(0, 8)}…`;\n}\n\n// ============================================================================\n// Create-coaching dialog\n// ============================================================================\n\ninterface CreateCoachingForm {\n memberId: string;\n memberName: string;\n coachId: string;\n coachName: string;\n specialty: string;\n shareWorkouts: boolean;\n shareNutrition: boolean;\n shareMetrics: boolean;\n}\n\nfunction buildEmptyForm(defaultMember: UserSelectValue): CreateCoachingForm {\n return {\n memberId: defaultMember.userId,\n memberName: defaultMember.displayName,\n coachId: '',\n coachName: '',\n specialty: '',\n shareWorkouts: true,\n shareNutrition: true,\n shareMetrics: true,\n };\n}\n\nfunction ShareToggle({\n id,\n label,\n description,\n checked,\n onChange,\n}: {\n id: string;\n label: string;\n description: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <div className=\"flex items-center justify-between gap-4 rounded-xl bg-bg-sunken/60 p-3\">\n <div className=\"min-w-0\">\n <Label htmlFor={id} className=\"cursor-pointer\">\n {label}\n </Label>\n <p className=\"text-xs text-text-tertiary\">{description}</p>\n </div>\n <Switch id={id} checked={checked} onCheckedChange={onChange} />\n </div>\n );\n}\n\nfunction CreateCoachingDialog({\n open,\n onOpenChange,\n defaultMember,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n defaultMember: UserSelectValue;\n}) {\n const create = useMybCreateCoaching();\n const [form, setForm] = useState<CreateCoachingForm>(() => buildEmptyForm(defaultMember));\n const [error, setError] = useState<string | null>(null);\n\n const handleOpenChange = (next: boolean) => {\n if (!next) {\n setForm(buildEmptyForm(defaultMember));\n setError(null);\n }\n onOpenChange(next);\n };\n\n const submit = async () => {\n const coachId = form.coachId.trim();\n const memberId = form.memberId.trim();\n if (!coachId) {\n setError('Coach user id is required.');\n return;\n }\n if (!memberId) {\n setError('Member user id is required.');\n return;\n }\n setError(null);\n const input: MybCreateCoachingInput = {\n coachId,\n memberId,\n specialty: form.specialty.trim() || undefined,\n shareWorkouts: form.shareWorkouts,\n shareNutrition: form.shareNutrition,\n shareMetrics: form.shareMetrics,\n };\n try {\n await create.mutateAsync(input);\n setForm(buildEmptyForm(defaultMember));\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create coaching relationship.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={handleOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New coaching</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Link a coach and member, and choose exactly what training, nutrition and progress data\n is shared.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form\n className=\"space-y-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <UserSearchSelect\n label=\"Coach\"\n required\n htmlId=\"myb-coaching-coach\"\n userId={form.coachId}\n displayName={form.coachName}\n onSelect={({ userId, displayName }) =>\n setForm((f) => ({ ...f, coachId: userId, coachName: displayName }))\n }\n />\n <UserSearchSelect\n label=\"Member\"\n htmlId=\"myb-coaching-member\"\n userId={form.memberId}\n displayName={form.memberName}\n onSelect={({ userId, displayName }) =>\n setForm((f) => ({ ...f, memberId: userId, memberName: displayName }))\n }\n />\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-coaching-specialty\">Specialty</Label>\n <Input\n id=\"myb-coaching-specialty\"\n value={form.specialty}\n onChange={(event) => setForm((f) => ({ ...f, specialty: event.target.value }))}\n placeholder=\"e.g., Strength & conditioning\"\n />\n </div>\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">Shared with the coach</p>\n <ShareToggle\n id=\"myb-coaching-share-workouts\"\n label=\"Workouts\"\n description=\"Programs, sessions and logged sets.\"\n checked={form.shareWorkouts}\n onChange={(value) => setForm((f) => ({ ...f, shareWorkouts: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-nutrition\"\n label=\"Nutrition\"\n description=\"Plans, meals and daily macros.\"\n checked={form.shareNutrition}\n onChange={(value) => setForm((f) => ({ ...f, shareNutrition: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-metrics\"\n label=\"Metrics\"\n description=\"Body metrics and recovery logs.\"\n checked={form.shareMetrics}\n onChange={(value) => setForm((f) => ({ ...f, shareMetrics: value }))}\n />\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => handleOpenChange(false)}\n disabled={create.isPending}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={create.isPending}>\n {create.isPending ? 'Creating…' : 'Create coaching'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Sharing scope chips\n// ============================================================================\n\nfunction ShareChip({ label, icon, on }: { label: string; icon: typeof Dumbbell; on: boolean }) {\n const Icon = icon;\n const StatusIcon = on ? Check : Minus;\n return (\n <span\n aria-label={`${label} ${on ? 'shared' : 'not shared'}`}\n className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${\n on\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-bg-sunken text-text-tertiary'\n }`}\n >\n <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />\n {label}\n <StatusIcon className=\"h-3 w-3\" aria-hidden=\"true\" />\n <span aria-hidden=\"true\">{on ? 'On' : 'Off'}</span>\n </span>\n );\n}\n\n// ============================================================================\n// Coaching card\n// ============================================================================\n\nfunction CoachingCard({\n relationship,\n currentUserId,\n busy,\n onTransition,\n}: {\n relationship: MybCoachingRelationship;\n currentUserId?: string;\n busy: boolean;\n onTransition: (id: string, status: MybCoachingStatus) => void;\n}) {\n const coachIsYou = Boolean(currentUserId) && relationship.coachId === currentUserId;\n const memberIsYou = Boolean(currentUserId) && relationship.memberId === currentUserId;\n const actions = TRANSITIONS[relationship.status];\n const canDiscuss = relationship.status === 'ACTIVE' || relationship.status === 'PAUSED';\n const [showDiscussion, setShowDiscussion] = useState(false);\n\n return (\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-4 p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <IconChip icon={<HeartHandshake className=\"h-4 w-4\" />} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {relationship.specialty?.trim() || 'Coaching relationship'}\n </p>\n <p className=\"text-xs text-text-tertiary\">Coach ↔ member</p>\n </div>\n </div>\n <StatusBadge status={relationship.status} />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg bg-bg-sunken/60 p-2.5\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n Coach\n </p>\n <p\n className=\"mt-0.5 truncate text-sm font-medium text-text-primary\"\n title={relationship.coachId}\n >\n {coachIsYou ? 'You' : `#${shortId(relationship.coachId)}`}\n </p>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/60 p-2.5\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n Member\n </p>\n <p\n className=\"mt-0.5 truncate text-sm font-medium text-text-primary\"\n title={relationship.memberId}\n >\n {memberIsYou ? 'You' : `#${shortId(relationship.memberId)}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap gap-1.5\">\n <ShareChip label=\"Workouts\" icon={Dumbbell} on={relationship.shareWorkouts} />\n <ShareChip label=\"Nutrition\" icon={Salad} on={relationship.shareNutrition} />\n <ShareChip label=\"Metrics\" icon={LineChart} on={relationship.shareMetrics} />\n </div>\n\n {(relationship.startedAt || relationship.endedAt) && (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-tertiary\">\n {relationship.startedAt && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Started {formatDate(relationship.startedAt)}\n </span>\n )}\n {relationship.endedAt && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarX className=\"h-3.5 w-3.5\" />\n Ended {formatDate(relationship.endedAt)}\n </span>\n )}\n </div>\n )}\n\n {actions.length > 0 && (\n <div className=\"mt-auto flex flex-wrap gap-2 pt-1\">\n {actions.map((action) => (\n <Button\n key={action.to}\n size=\"sm\"\n variant={action.primary ? undefined : 'outline'}\n disabled={busy}\n onClick={() => onTransition(relationship.id, action.to)}\n >\n {action.label}\n </Button>\n ))}\n </div>\n )}\n\n {canDiscuss && (\n <div\n className={`border-t border-border-seam pt-3 ${actions.length === 0 ? 'mt-auto' : ''}`}\n >\n <button\n type=\"button\"\n onClick={() => setShowDiscussion((prev) => !prev)}\n aria-expanded={showDiscussion}\n className=\"inline-flex items-center gap-1.5 text-sm font-medium text-accent hover:underline\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n {showDiscussion ? 'Hide discussion' : 'Discussion'}\n {showDiscussion ? (\n <ChevronUp className=\"h-4 w-4\" />\n ) : (\n <ChevronDown className=\"h-4 w-4\" />\n )}\n </button>\n {showDiscussion && (\n <div className=\"mt-3\">\n <MybDiscussion\n entityType=\"mybodyshield-coaching\"\n entityId={relationship.id}\n title=\"Coaching discussion\"\n description=\"A private thread between the coach and member.\"\n />\n </div>\n )}\n </div>\n )}\n </GlassCard>\n );\n}\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport function CoachingPage() {\n const { currentUser } = useMyBodyShield();\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n const [transitioningId, setTransitioningId] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n // `useMybCoaching` returns a PLAIN ARRAY — fetch every relationship once and\n // filter client-side so the tab counts stay accurate across filters.\n const coaching = useMybCoaching();\n const transition = useMybTransitionCoaching();\n\n const all = coaching.data ?? [];\n\n const counts = useMemo(() => {\n const map: Record<MybCoachingStatus, number> = {\n PENDING: 0,\n ACTIVE: 0,\n PAUSED: 0,\n ENDED: 0,\n };\n for (const rel of all) map[rel.status] += 1;\n return map;\n }, [all]);\n\n const statusFilters = useMemo<FilterOption<StatusFilter>[]>(\n () => [\n { value: 'ALL', label: 'All', count: all.length },\n ...FILTER_ORDER.map((status) => ({\n value: status as StatusFilter,\n label: status.charAt(0) + status.slice(1).toLowerCase(),\n count: counts[status],\n })),\n ],\n [all.length, counts]\n );\n\n const rows = useMemo(\n () => (statusFilter === 'ALL' ? all : all.filter((rel) => rel.status === statusFilter)),\n [all, statusFilter]\n );\n\n const handleTransition = async (id: string, status: MybCoachingStatus) => {\n setTransitioningId(id);\n setActionError(null);\n try {\n await transition.mutateAsync({ id, status });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to update the relationship.');\n } finally {\n setTransitioningId(null);\n }\n };\n\n const showLoading = coaching.isLoading && !coaching.data;\n\n return (\n <PageLayout\n title=\"Coaching\"\n description=\"Coach ↔ member relationships and what's shared.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New coaching\n </Button>\n }\n >\n <div className=\"mb-6\">\n <FilterTabs options={statusFilters} value={statusFilter} onChange={setStatusFilter} />\n </div>\n\n {actionError && (\n <p className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {showLoading ? (\n <LoadingView label=\"Loading coaching relationships…\" />\n ) : coaching.isError ? (\n <ErrorView\n message=\"We couldn't load your coaching relationships.\"\n onRetry={() => void coaching.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<HeartHandshake className=\"h-10 w-10\" />}\n title={\n all.length === 0 ? 'No coaching relationships' : 'No relationships match this filter'\n }\n description={\n all.length === 0\n ? 'Connect a coach and a member to start sharing training, nutrition and progress data.'\n : 'Try a different status filter, or create a new coaching relationship.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New coaching\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {rows.map((relationship) => (\n <CoachingCard\n key={relationship.id}\n relationship={relationship}\n currentUserId={currentUser?.id}\n busy={transitioningId === relationship.id}\n onTransition={(id, status) => void handleTransition(id, status)}\n />\n ))}\n </div>\n )}\n\n <CreateCoachingDialog\n open={createOpen}\n onOpenChange={setCreateOpen}\n defaultMember={{\n userId: currentUser?.id ?? '',\n displayName: currentUser?.name || currentUser?.email || '',\n }}\n />\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA8DA,IAAM,IAAoC;CAAC;CAAW;CAAU;CAAU;CAAQ,EAG5E,IAGF;CACF,SAAS,CAAC;EAAE,IAAI;EAAU,OAAO;EAAU,SAAS;EAAM,CAAC;CAC3D,QAAQ,CACN;EAAE,IAAI;EAAU,OAAO;EAAS,SAAS;EAAO,EAChD;EAAE,IAAI;EAAS,OAAO;EAAO,SAAS;EAAO,CAC9C;CACD,QAAQ,CACN;EAAE,IAAI;EAAU,OAAO;EAAU,SAAS;EAAM,EAChD;EAAE,IAAI;EAAS,OAAO;EAAO,SAAS;EAAO,CAC9C;CACD,OAAO,EAAE;CACV;AAGD,SAAS,EAAQ,GAAoB;AACnC,QAAO,EAAG,UAAU,KAAK,IAAK,GAAG,EAAG,MAAM,GAAG,EAAE,CAAC;;AAkBlD,SAAS,EAAe,GAAoD;AAC1E,QAAO;EACL,UAAU,EAAc;EACxB,YAAY,EAAc;EAC1B,SAAS;EACT,WAAW;EACX,WAAW;EACX,eAAe;EACf,gBAAgB;EAChB,cAAc;EACf;;AAGH,SAAS,EAAY,EACnB,OACA,UACA,gBACA,YACA,eAOC;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAO,SAAS;IAAI,WAAU;cAC3B;IACK,CAAA,EACR,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAgB,CAAA,CACvD;MACN,kBAAC,GAAD;GAAY;GAAa;GAAS,iBAAiB;GAAY,CAAA,CAC3D;;;AAIV,SAAS,EAAqB,EAC5B,SACA,iBACA,oBAKC;CACD,IAAM,IAAS,GAAsB,EAC/B,CAAC,GAAM,KAAW,QAAmC,EAAe,EAAc,CAAC,EACnF,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,KAAoB,MAAkB;AAK1C,EAJK,MACH,EAAQ,EAAe,EAAc,CAAC,EACtC,EAAS,KAAK,GAEhB,EAAa,EAAK;IAGd,IAAS,YAAY;EACzB,IAAM,IAAU,EAAK,QAAQ,MAAM,EAC7B,IAAW,EAAK,SAAS,MAAM;AACrC,MAAI,CAAC,GAAS;AACZ,KAAS,6BAA6B;AACtC;;AAEF,MAAI,CAAC,GAAU;AACb,KAAS,8BAA8B;AACvC;;AAEF,IAAS,KAAK;EACd,IAAM,IAAgC;GACpC;GACA;GACA,WAAW,EAAK,UAAU,MAAM,IAAI,KAAA;GACpC,eAAe,EAAK;GACpB,gBAAgB,EAAK;GACrB,cAAc,EAAK;GACpB;AACD,MAAI;AAGF,GAFA,MAAM,EAAO,YAAY,EAAM,EAC/B,EAAQ,EAAe,EAAc,CAAC,EACtC,EAAa,GAAM;WACZ,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,0CAA0C;;;AAI5F,QACE,kBAAC,GAAD;EAAwB;EAAM,cAAc;YAC1C,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAuB,gBAAoC,CAAA,EAC3D,kBAAC,GAAD,EAAA,UAA6B,qGAGC,CAAA,CACP,EAAA,CAAA,EACzB,kBAAC,QAAD;GACE,WAAU;GACV,WAAW,MAAU;AAEd,IADL,EAAM,gBAAgB,EACjB,GAAQ;;aAJjB;IAOG,KACC,kBAAC,KAAD;KAAG,WAAU;eACV;KACC,CAAA;IAEN,kBAAC,GAAD;KACE,OAAM;KACN,UAAA;KACA,QAAO;KACP,QAAQ,EAAK;KACb,aAAa,EAAK;KAClB,WAAW,EAAE,WAAQ,qBACnB,GAAS,OAAO;MAAE,GAAG;MAAG,SAAS;MAAQ,WAAW;MAAa,EAAE;KAErE,CAAA;IACF,kBAAC,GAAD;KACE,OAAM;KACN,QAAO;KACP,QAAQ,EAAK;KACb,aAAa,EAAK;KAClB,WAAW,EAAE,WAAQ,qBACnB,GAAS,OAAO;MAAE,GAAG;MAAG,UAAU;MAAQ,YAAY;MAAa,EAAE;KAEvE,CAAA;IACF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,SAAQ;gBAAyB;MAAiB,CAAA,EACzD,kBAAC,GAAD;MACE,IAAG;MACH,OAAO,EAAK;MACZ,WAAW,MAAU,GAAS,OAAO;OAAE,GAAG;OAAG,WAAW,EAAM,OAAO;OAAO,EAAE;MAC9E,aAAY;MACZ,CAAA,CACE;;IACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,KAAD;OAAG,WAAU;iBAAwC;OAAyB,CAAA;MAC9E,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,aAAY;OACZ,SAAS,EAAK;OACd,WAAW,MAAU,GAAS,OAAO;QAAE,GAAG;QAAG,eAAe;QAAO,EAAE;OACrE,CAAA;MACF,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,aAAY;OACZ,SAAS,EAAK;OACd,WAAW,MAAU,GAAS,OAAO;QAAE,GAAG;QAAG,gBAAgB;QAAO,EAAE;OACtE,CAAA;MACF,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,aAAY;OACZ,SAAS,EAAK;OACd,WAAW,MAAU,GAAS,OAAO;QAAE,GAAG;QAAG,cAAc;QAAO,EAAE;OACpE,CAAA;MACE;;IACN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,eAAe,EAAiB,GAAM;KACtC,UAAU,EAAO;eAClB;KAEQ,CAAA,EACT,kBAAC,GAAD;KAAQ,MAAK;KAAS,UAAU,EAAO;eACpC,EAAO,YAAY,cAAc;KAC3B,CAAA,CACc,EAAA,CAAA;IACpB;KACiB,EAAA,CAAA;EACT,CAAA;;AAQvB,SAAS,EAAU,EAAE,UAAO,SAAM,SAA6D;CAC7F,IAAM,IAAO,GACP,IAAa,IAAK,IAAQ;AAChC,QACE,kBAAC,QAAD;EACE,cAAY,GAAG,EAAM,GAAG,IAAK,WAAW;EACxC,WAAW,+EACT,IACI,yDACA;YALR;GAQE,kBAAC,GAAD;IAAM,WAAU;IAAU,eAAY;IAAS,CAAA;GAC9C;GACD,kBAAC,GAAD;IAAY,WAAU;IAAU,eAAY;IAAS,CAAA;GACrD,kBAAC,QAAD;IAAM,eAAY;cAAQ,IAAK,OAAO;IAAa,CAAA;GAC9C;;;AAQX,SAAS,EAAa,EACpB,iBACA,kBACA,SACA,mBAMC;CACD,IAAM,IAAa,EAAQ,KAAkB,EAAa,YAAY,GAChE,IAAc,EAAQ,KAAkB,EAAa,aAAa,GAClE,IAAU,EAAY,EAAa,SACnC,IAAa,EAAa,WAAW,YAAY,EAAa,WAAW,UACzE,CAAC,GAAgB,KAAqB,EAAS,GAAM;AAE3D,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAU,MAAM,kBAAC,GAAD,EAAgB,WAAU,WAAY,CAAA,EAAI,CAAA,EAC1D,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAa,WAAW,MAAM,IAAI;OACjC,CAAA,EACJ,kBAAC,KAAD;OAAG,WAAU;iBAA6B;OAAkB,CAAA,CACxD;QACF;QACN,kBAAC,GAAD,EAAa,QAAQ,EAAa,QAAU,CAAA,CACxC;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAqE;MAE9E,CAAA,EACJ,kBAAC,KAAD;MACE,WAAU;MACV,OAAO,EAAa;gBAEnB,IAAa,QAAQ,IAAI,EAAQ,EAAa,QAAQ;MACrD,CAAA,CACA;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAqE;MAE9E,CAAA,EACJ,kBAAC,KAAD;MACE,WAAU;MACV,OAAO,EAAa;gBAEnB,IAAc,QAAQ,IAAI,EAAQ,EAAa,SAAS;MACvD,CAAA,CACA;OACF;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAW,OAAM;MAAW,MAAM;MAAU,IAAI,EAAa;MAAiB,CAAA;KAC9E,kBAAC,GAAD;MAAW,OAAM;MAAY,MAAM;MAAO,IAAI,EAAa;MAAkB,CAAA;KAC7E,kBAAC,GAAD;MAAW,OAAM;MAAU,MAAM;MAAW,IAAI,EAAa;MAAgB,CAAA;KACzE;;IAEJ,EAAa,aAAa,EAAa,YACvC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAa,aACZ,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACE,kBAAC,GAAD,EAAe,WAAU,eAAgB,CAAA;;MAChC,EAAW,EAAa,UAAU;MACtC;QAER,EAAa,WACZ,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA;;MAC9B,EAAW,EAAa,QAAQ;MAClC;OAEL;;GAGP,EAAQ,SAAS,KAChB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MACZ,kBAAC,GAAD;KAEE,MAAK;KACL,SAAS,EAAO,UAAU,KAAA,IAAY;KACtC,UAAU;KACV,eAAe,EAAa,EAAa,IAAI,EAAO,GAAG;eAEtD,EAAO;KACD,EAPF,EAAO,GAOL,CACT;IACE,CAAA;GAGP,KACC,kBAAC,OAAD;IACE,WAAW,oCAAoC,EAAQ,WAAW,IAAI,YAAY;cADpF,CAGE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,GAAmB,MAAS,CAAC,EAAK;KACjD,iBAAe;KACf,WAAU;eAJZ;MAME,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;MACpC,IAAiB,oBAAoB;MAEpC,EADD,IACE,IAEA,GAFD,EAAW,WAAU,WAAY,CAEE;MAE9B;QACR,KACC,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,YAAW;MACX,UAAU,EAAa;MACvB,OAAM;MACN,aAAY;MACZ,CAAA;KACE,CAAA,CAEJ;;GAEE;;;AAQhB,SAAgB,IAAe;CAC7B,IAAM,EAAE,mBAAgB,GAAiB,EACnC,CAAC,GAAc,KAAmB,EAAuB,MAAM,EAC/D,CAAC,GAAY,KAAiB,EAAS,GAAM,EAC7C,CAAC,GAAiB,KAAsB,EAAwB,KAAK,EACrE,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAI7D,IAAW,GAAgB,EAC3B,IAAa,GAA0B,EAEvC,IAAM,EAAS,QAAQ,EAAE,EAEzB,IAAS,QAAc;EAC3B,IAAM,IAAyC;GAC7C,SAAS;GACT,QAAQ;GACR,QAAQ;GACR,OAAO;GACR;AACD,OAAK,IAAM,KAAO,EAAK,GAAI,EAAI,WAAW;AAC1C,SAAO;IACN,CAAC,EAAI,CAAC,EAEH,IAAgB,QACd,CACJ;EAAE,OAAO;EAAO,OAAO;EAAO,OAAO,EAAI;EAAQ,EACjD,GAAG,EAAa,KAAK,OAAY;EAC/B,OAAO;EACP,OAAO,EAAO,OAAO,EAAE,GAAG,EAAO,MAAM,EAAE,CAAC,aAAa;EACvD,OAAO,EAAO;EACf,EAAE,CACJ,EACD,CAAC,EAAI,QAAQ,EAAO,CACrB,EAEK,IAAO,QACJ,MAAiB,QAAQ,IAAM,EAAI,QAAQ,MAAQ,EAAI,WAAW,EAAa,EACtF,CAAC,GAAK,EAAa,CACpB,EAEK,IAAmB,OAAO,GAAY,MAA8B;AAExE,EADA,EAAmB,EAAG,EACtB,EAAe,KAAK;AACpB,MAAI;AACF,SAAM,EAAW,YAAY;IAAE;IAAI;IAAQ,CAAC;WACrC,GAAK;AACZ,KAAe,aAAe,QAAQ,EAAI,UAAU,qCAAqC;YACjF;AACR,KAAmB,KAAK;;IAItB,IAAc,EAAS,aAAa,CAAC,EAAS;AAEpD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD;GAAQ,eAAe,EAAc,GAAK;aAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,eAE1B;;YAPb;GAUE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAY,SAAS;KAAe,OAAO;KAAc,UAAU;KAAmB,CAAA;IAClF,CAAA;GAEL,KACC,kBAAC,KAAD;IAAG,WAAU;cACV;IACC,CAAA;GAGL,IACC,kBAAC,GAAD,EAAa,OAAM,mCAAoC,CAAA,GACrD,EAAS,UACX,kBAAC,GAAD;IACE,SAAQ;IACR,eAAe,KAAK,EAAS,SAAS;IACtC,CAAA,GACA,EAAK,WAAW,IAClB,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAgB,WAAU,aAAc,CAAA;IAC9C,OACE,EAAI,WAAW,IAAI,8BAA8B;IAEnD,aACE,EAAI,WAAW,IACX,yFACA;IAEN,QACE,kBAAC,GAAD;KAAQ,eAAe,EAAc,GAAK;eAA1C,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,eAE1B;;IAEX,CAAA,GAEF,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAK,KAAK,MACT,kBAAC,GAAD;KAEgB;KACd,eAAe,GAAa;KAC5B,MAAM,MAAoB,EAAa;KACvC,eAAe,GAAI,MAAW,KAAK,EAAiB,GAAI,EAAO;KAC/D,EALK,EAAa,GAKlB,CACF;IACE,CAAA;GAGR,kBAAC,GAAD;IACE,MAAM;IACN,cAAc;IACd,eAAe;KACb,QAAQ,GAAa,MAAM;KAC3B,aAAa,GAAa,QAAQ,GAAa,SAAS;KACzD;IACD,CAAA;GACS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoalsPage.js","names":[],"sources":["../../../../src/mybodyshield/pages/goals/GoalsPage.tsx"],"sourcesContent":["/**\n * Goals — targets across training, nutrition and body composition.\n * @module mybodyshield/pages/goals\n *\n * Lists the member's `MybGoal`s with a status filter, per-goal progress meters\n * and inline \"update progress\" / \"mark achieved\" actions, plus a create flow.\n * KPIs (active count, achieved count, average active progress) are aggregated\n * client-side from the real `mybListGoals` endpoint. No mock data.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Target, Plus, CheckCircle2, TrendingUp, Trophy, CalendarClock } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n FilterTabs,\n Pagination,\n MiniBar,\n type FilterOption,\n} from '../../components';\nimport { Meter, IconChip, GOAL_TYPE_META, computeGoalProgress } from '../_shared/visuals';\nimport {\n useMybGoals,\n useMybCreateGoal,\n useMybUpdateGoal,\n useMybAchieveGoal,\n useOffsetPager,\n} from '../../hooks';\nimport type { MybGoal, MybGoalStatus, MybGoalType, MybCreateGoalInput } from '../../types';\nimport {\n formatDate,\n formatRelativeTime,\n formatNumber,\n formatPercent,\n formatEnumLabel,\n} from '../../utils/formatters';\n\nconst PAGE_SIZE = 12;\nconst KPI_LIMIT = 100;\n\ntype StatusFilter = MybGoalStatus | 'ALL';\n\nconst STATUS_FILTERS: FilterOption<StatusFilter>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'ACHIEVED', label: 'Achieved' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'ABANDONED', label: 'Abandoned' },\n];\n\nconst GOAL_TYPE_OPTIONS: MybGoalType[] = [\n 'WEIGHT_LOSS',\n 'MUSCLE_GAIN',\n 'STRENGTH',\n 'ENDURANCE',\n 'MOBILITY',\n 'GENERAL_FITNESS',\n];\n\n/** Parse a free-text numeric input → a finite number, or undefined when blank/invalid. */\nfunction numOrUndef(raw: string): number | undefined {\n const trimmed = raw.trim();\n if (trimmed === '') return undefined;\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n// ============================================================================\n// Create-goal dialog\n// ============================================================================\n\ninterface CreateGoalForm {\n title: string;\n type: MybGoalType;\n baselineValue: string;\n targetValue: string;\n unit: string;\n targetDate: string;\n description: string;\n}\n\nconst EMPTY_CREATE_FORM: CreateGoalForm = {\n title: '',\n type: 'GENERAL_FITNESS',\n baselineValue: '',\n targetValue: '',\n unit: '',\n targetDate: '',\n description: '',\n};\n\nfunction CreateGoalDialog({\n open,\n onOpenChange,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n const create = useMybCreateGoal();\n const [form, setForm] = useState<CreateGoalForm>(EMPTY_CREATE_FORM);\n const [error, setError] = useState<string | null>(null);\n\n const handleOpenChange = (next: boolean) => {\n if (!next) {\n setForm(EMPTY_CREATE_FORM);\n setError(null);\n }\n onOpenChange(next);\n };\n\n const submit = async () => {\n const title = form.title.trim();\n if (!title) {\n setError('Goal title is required.');\n return;\n }\n setError(null);\n const input: MybCreateGoalInput = {\n title,\n type: form.type,\n description: form.description.trim() || undefined,\n baselineValue: numOrUndef(form.baselineValue),\n targetValue: numOrUndef(form.targetValue),\n unit: form.unit.trim() || undefined,\n targetDate: form.targetDate ? new Date(form.targetDate).toISOString() : undefined,\n };\n try {\n await create.mutateAsync(input);\n setForm(EMPTY_CREATE_FORM);\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create goal.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={handleOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New goal</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Set a target across training, nutrition or body composition and track it to completion.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form\n className=\"space-y-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-title\">Title</Label>\n <Input\n id=\"myb-goal-title\"\n value={form.title}\n onChange={(event) => setForm((f) => ({ ...f, title: event.target.value }))}\n placeholder=\"e.g., Squat 140 kg\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-type\">Type</Label>\n <Select\n value={form.type}\n // SelectItem values are exactly the MybGoalType members below, so the\n // string emitted by Radix is always a valid MybGoalType.\n onValueChange={(value) => setForm((f) => ({ ...f, type: value as MybGoalType }))}\n >\n <SelectTrigger id=\"myb-goal-type\">\n <SelectValue placeholder=\"Select a goal type\" />\n </SelectTrigger>\n <SelectContent>\n {GOAL_TYPE_OPTIONS.map((type) => (\n <SelectItem key={type} value={type}>\n {formatEnumLabel(type)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-baseline\">Baseline</Label>\n <Input\n id=\"myb-goal-baseline\"\n type=\"number\"\n inputMode=\"decimal\"\n value={form.baselineValue}\n onChange={(event) => setForm((f) => ({ ...f, baselineValue: event.target.value }))}\n placeholder=\"0\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-target\">Target</Label>\n <Input\n id=\"myb-goal-target\"\n type=\"number\"\n inputMode=\"decimal\"\n value={form.targetValue}\n onChange={(event) => setForm((f) => ({ ...f, targetValue: event.target.value }))}\n placeholder=\"100\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-unit\">Unit</Label>\n <Input\n id=\"myb-goal-unit\"\n value={form.unit}\n onChange={(event) => setForm((f) => ({ ...f, unit: event.target.value }))}\n placeholder=\"kg / reps / min\"\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-date\">Target date</Label>\n <Input\n id=\"myb-goal-date\"\n type=\"date\"\n value={form.targetDate}\n onChange={(event) => setForm((f) => ({ ...f, targetDate: event.target.value }))}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-desc\">Description</Label>\n <Textarea\n id=\"myb-goal-desc\"\n rows={3}\n value={form.description}\n onChange={(event) => setForm((f) => ({ ...f, description: event.target.value }))}\n placeholder=\"Why this goal matters and how you'll get there.\"\n />\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => handleOpenChange(false)}\n disabled={create.isPending}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={create.isPending}>\n {create.isPending ? 'Creating…' : 'Create goal'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Update-progress dialog\n// ============================================================================\n\nfunction UpdateProgressDialog({\n goal,\n onOpenChange,\n}: {\n goal: MybGoal | null;\n onOpenChange: (open: boolean) => void;\n}) {\n const update = useMybUpdateGoal();\n const [value, setValue] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [seededId, setSeededId] = useState<string | null>(null);\n\n // Seed the input with the goal's current value whenever the dialog target\n // changes — React's render-phase \"reset state when a prop changes\" pattern\n // (no effect, so no synchronous-setState-in-effect cascade).\n if (goal && goal.id !== seededId) {\n setSeededId(goal.id);\n setValue(String(goal.currentValue));\n setError(null);\n }\n\n const submit = async () => {\n if (!goal) return;\n const parsed = numOrUndef(value);\n if (parsed === undefined) {\n setError('Enter a valid number.');\n return;\n }\n setError(null);\n try {\n await update.mutateAsync({ id: goal.id, input: { currentValue: parsed } });\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update progress.');\n }\n };\n\n return (\n <ResponsiveDialog open={goal != null} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Update progress</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {goal ? goal.title : 'Log your latest value for this goal.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form\n className=\"space-y-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-current\">\n Current value{goal?.unit ? ` (${goal.unit})` : ''}\n </Label>\n <Input\n id=\"myb-goal-current\"\n type=\"number\"\n inputMode=\"decimal\"\n autoFocus\n value={value}\n onChange={(event) => setValue(event.target.value)}\n placeholder=\"0\"\n />\n {goal && (\n <p className=\"text-xs text-text-tertiary\">\n Target: {formatNumber(goal.targetValue)}\n {goal.unit ? ` ${goal.unit}` : ''}\n </p>\n )}\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={update.isPending}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={update.isPending}>\n {update.isPending ? 'Saving…' : 'Save progress'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Goal card\n// ============================================================================\n\nfunction GoalCard({\n goal,\n onUpdateProgress,\n onAchieve,\n isAchieving,\n}: {\n goal: MybGoal;\n onUpdateProgress: (goal: MybGoal) => void;\n onAchieve: (goal: MybGoal) => void;\n isAchieving: boolean;\n}) {\n const meta = GOAL_TYPE_META[goal.type];\n const Icon = meta.icon;\n const pct = computeGoalProgress(goal);\n const unitSuffix = goal.unit ? ` ${goal.unit}` : '';\n\n return (\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} tint={meta.tint} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{goal.title}</p>\n <span className=\"mt-1 inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(goal.type)}\n </span>\n </div>\n </div>\n <StatusBadge status={goal.status} />\n </div>\n\n {goal.description && (\n <p className=\"line-clamp-2 text-xs text-text-secondary\">{goal.description}</p>\n )}\n\n <div className=\"space-y-1.5\">\n <Meter value={pct} color={meta.tint} />\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(goal.currentValue)}\n {unitSuffix} / {formatNumber(goal.targetValue)}\n {unitSuffix}\n </span>\n <span className=\"tabular-nums text-text-tertiary\">{formatPercent(pct)}</span>\n </div>\n </div>\n\n {(goal.targetDate || (goal.status === 'ACHIEVED' && goal.achievedAt)) && (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-tertiary\">\n {goal.targetDate && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Target {formatDate(goal.targetDate)}\n <span className=\"text-text-secondary\">· {formatRelativeTime(goal.targetDate)}</span>\n </span>\n )}\n {goal.status === 'ACHIEVED' && goal.achievedAt && (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" />\n Achieved {formatDate(goal.achievedAt)}\n </span>\n )}\n </div>\n )}\n\n <div className=\"mt-auto flex flex-wrap gap-2 pt-1\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => onUpdateProgress(goal)}>\n Update progress\n </Button>\n {goal.status === 'ACTIVE' && (\n <Button size=\"sm\" onClick={() => onAchieve(goal)} disabled={isAchieving}>\n {isAchieving ? 'Marking…' : 'Mark achieved'}\n </Button>\n )}\n </div>\n </GlassCard>\n );\n}\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport function GoalsPage() {\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n const [progressGoal, setProgressGoal] = useState<MybGoal | null>(null);\n const [achievingId, setAchievingId] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n const pager = useOffsetPager(PAGE_SIZE);\n\n const goalsQuery = useMybGoals({\n status: statusFilter === 'ALL' ? undefined : statusFilter,\n pagination: { limit: PAGE_SIZE, offset: pager.offset },\n });\n\n // Separate bounded queries power the KPI row so the counts stay accurate\n // regardless of the active filter / page (connections expose no total count).\n const activeGoals = useMybGoals({ status: 'ACTIVE', pagination: { limit: KPI_LIMIT } });\n const achievedGoals = useMybGoals({ status: 'ACHIEVED', pagination: { limit: KPI_LIMIT } });\n\n const achieve = useMybAchieveGoal();\n\n const rows = goalsQuery.data?.items ?? [];\n const activeItems = activeGoals.data?.items ?? [];\n const achievedItems = achievedGoals.data?.items ?? [];\n\n const avgActiveProgress = useMemo(() => {\n if (activeItems.length === 0) return 0;\n const sum = activeItems.reduce((acc, goal) => acc + computeGoalProgress(goal), 0);\n return sum / activeItems.length;\n }, [activeItems]);\n\n const handleFilterChange = (next: StatusFilter) => {\n setStatusFilter(next);\n pager.reset();\n };\n\n const handleAchieve = async (goal: MybGoal) => {\n setAchievingId(goal.id);\n setActionError(null);\n try {\n await achieve.mutateAsync(goal.id);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to mark goal as achieved.');\n } finally {\n setAchievingId(null);\n }\n };\n\n const showLoading = goalsQuery.isLoading && !goalsQuery.data;\n\n return (\n <PageLayout\n title=\"Goals\"\n description=\"Targets across training, nutrition and body composition.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New goal\n </Button>\n }\n >\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<Target className=\"h-5 w-5\" />}\n value={formatNumber(activeGoals.data ? activeItems.length : undefined)}\n label=\"Active goals\"\n />\n <MetricsCard\n icon={<Trophy className=\"h-5 w-5\" />}\n value={formatNumber(achievedGoals.data ? achievedItems.length : undefined)}\n label=\"Achieved\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<TrendingUp className=\"h-5 w-5\" />}\n value={activeGoals.data ? formatPercent(avgActiveProgress) : '—'}\n label=\"Avg progress (active)\"\n footer={<MiniBar value={avgActiveProgress} height={4} barClassName=\"bg-accent-teal\" />}\n />\n </div>\n\n {/* Filter */}\n <div className=\"mt-6\">\n <FilterTabs options={STATUS_FILTERS} value={statusFilter} onChange={handleFilterChange} />\n </div>\n\n {actionError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {/* Grid */}\n <div className=\"mt-4\">\n {showLoading ? (\n <LoadingView label=\"Loading your goals…\" />\n ) : goalsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your goals.\"\n onRetry={() => void goalsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Target className=\"h-10 w-10\" />}\n title={statusFilter === 'ALL' ? 'No goals yet' : 'No goals match this filter'}\n description={\n statusFilter === 'ALL'\n ? 'Set a goal to give your training and nutrition a clear target to work toward.'\n : 'Try a different status filter, or create a new goal.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New goal\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {rows.map((goal) => (\n <GoalCard\n key={goal.id}\n goal={goal}\n onUpdateProgress={setProgressGoal}\n onAchieve={(g) => void handleAchieve(g)}\n isAchieving={achievingId === goal.id}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={goalsQuery.data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target)}\n />\n </>\n )}\n </div>\n\n <CreateGoalDialog open={createOpen} onOpenChange={setCreateOpen} />\n <UpdateProgressDialog\n goal={progressGoal}\n onOpenChange={(open) => {\n if (!open) setProgressGoal(null);\n }}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GoalsPage.js","names":[],"sources":["../../../../src/mybodyshield/pages/goals/GoalsPage.tsx"],"sourcesContent":["/**\n * Goals — targets across training, nutrition and body composition.\n * @module mybodyshield/pages/goals\n *\n * Lists the member's `MybGoal`s with a status filter, per-goal progress meters\n * and inline \"update progress\" / \"mark achieved\" actions, plus a create flow.\n * KPIs (active count, achieved count, average active progress) are aggregated\n * client-side from the real `mybListGoals` endpoint. No mock data.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Target, Plus, CheckCircle2, TrendingUp, Trophy, CalendarClock } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n FilterTabs,\n Pagination,\n MiniBar,\n type FilterOption,\n} from '../../components';\nimport { Meter, IconChip, GOAL_TYPE_META, computeGoalProgress } from '../_shared/visuals';\nimport {\n useMybGoals,\n useMybCreateGoal,\n useMybUpdateGoal,\n useMybAchieveGoal,\n useOffsetPager,\n} from '../../hooks';\nimport type { MybGoal, MybGoalStatus, MybGoalType, MybCreateGoalInput } from '../../types';\nimport {\n formatDate,\n formatRelativeTime,\n formatNumber,\n formatPercent,\n formatEnumLabel,\n} from '../../utils/formatters';\n\nconst PAGE_SIZE = 12;\nconst KPI_LIMIT = 100;\n\ntype StatusFilter = MybGoalStatus | 'ALL';\n\n// Only statuses the backend can actually produce are offered as filters:\n// create → ACTIVE, achieve → ACHIEVED. There is no path to PAUSED/ABANDONED,\n// so those tabs would always be empty and mislead. StatusBadge still renders\n// every enum value for display safety.\nconst STATUS_FILTERS: FilterOption<StatusFilter>[] = [\n { value: 'ALL', label: 'All' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'ACHIEVED', label: 'Achieved' },\n];\n\nconst GOAL_TYPE_OPTIONS: MybGoalType[] = [\n 'WEIGHT_LOSS',\n 'MUSCLE_GAIN',\n 'STRENGTH',\n 'ENDURANCE',\n 'MOBILITY',\n 'GENERAL_FITNESS',\n];\n\n/** Parse a free-text numeric input → a finite number, or undefined when blank/invalid. */\nfunction numOrUndef(raw: string): number | undefined {\n const trimmed = raw.trim();\n if (trimmed === '') return undefined;\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n// ============================================================================\n// Create-goal dialog\n// ============================================================================\n\ninterface CreateGoalForm {\n title: string;\n type: MybGoalType;\n baselineValue: string;\n targetValue: string;\n unit: string;\n targetDate: string;\n description: string;\n}\n\nconst EMPTY_CREATE_FORM: CreateGoalForm = {\n title: '',\n type: 'GENERAL_FITNESS',\n baselineValue: '',\n targetValue: '',\n unit: '',\n targetDate: '',\n description: '',\n};\n\nfunction CreateGoalDialog({\n open,\n onOpenChange,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}) {\n const create = useMybCreateGoal();\n const [form, setForm] = useState<CreateGoalForm>(EMPTY_CREATE_FORM);\n const [error, setError] = useState<string | null>(null);\n\n const handleOpenChange = (next: boolean) => {\n if (!next) {\n setForm(EMPTY_CREATE_FORM);\n setError(null);\n }\n onOpenChange(next);\n };\n\n const submit = async () => {\n const title = form.title.trim();\n if (!title) {\n setError('Goal title is required.');\n return;\n }\n setError(null);\n const input: MybCreateGoalInput = {\n title,\n type: form.type,\n description: form.description.trim() || undefined,\n baselineValue: numOrUndef(form.baselineValue),\n targetValue: numOrUndef(form.targetValue),\n unit: form.unit.trim() || undefined,\n targetDate: form.targetDate ? new Date(form.targetDate).toISOString() : undefined,\n };\n try {\n await create.mutateAsync(input);\n setForm(EMPTY_CREATE_FORM);\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create goal.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={handleOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New goal</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Set a target across training, nutrition or body composition and track it to completion.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form\n className=\"space-y-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-title\">Title</Label>\n <Input\n id=\"myb-goal-title\"\n value={form.title}\n onChange={(event) => setForm((f) => ({ ...f, title: event.target.value }))}\n placeholder=\"e.g., Squat 140 kg\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-type\">Type</Label>\n <Select\n value={form.type}\n // SelectItem values are exactly the MybGoalType members below, so the\n // string emitted by Radix is always a valid MybGoalType.\n onValueChange={(value) => setForm((f) => ({ ...f, type: value as MybGoalType }))}\n >\n <SelectTrigger id=\"myb-goal-type\">\n <SelectValue placeholder=\"Select a goal type\" />\n </SelectTrigger>\n <SelectContent>\n {GOAL_TYPE_OPTIONS.map((type) => (\n <SelectItem key={type} value={type}>\n {formatEnumLabel(type)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-baseline\">Baseline</Label>\n <Input\n id=\"myb-goal-baseline\"\n type=\"number\"\n inputMode=\"decimal\"\n value={form.baselineValue}\n onChange={(event) => setForm((f) => ({ ...f, baselineValue: event.target.value }))}\n placeholder=\"0\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-target\">Target</Label>\n <Input\n id=\"myb-goal-target\"\n type=\"number\"\n inputMode=\"decimal\"\n value={form.targetValue}\n onChange={(event) => setForm((f) => ({ ...f, targetValue: event.target.value }))}\n placeholder=\"100\"\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-unit\">Unit</Label>\n <Input\n id=\"myb-goal-unit\"\n value={form.unit}\n onChange={(event) => setForm((f) => ({ ...f, unit: event.target.value }))}\n placeholder=\"kg / reps / min\"\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-date\">Target date</Label>\n <Input\n id=\"myb-goal-date\"\n type=\"date\"\n value={form.targetDate}\n onChange={(event) => setForm((f) => ({ ...f, targetDate: event.target.value }))}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-desc\">Description</Label>\n <Textarea\n id=\"myb-goal-desc\"\n rows={3}\n value={form.description}\n onChange={(event) => setForm((f) => ({ ...f, description: event.target.value }))}\n placeholder=\"Why this goal matters and how you'll get there.\"\n />\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => handleOpenChange(false)}\n disabled={create.isPending}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={create.isPending}>\n {create.isPending ? 'Creating…' : 'Create goal'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Update-progress dialog\n// ============================================================================\n\nfunction UpdateProgressDialog({\n goal,\n onOpenChange,\n}: {\n goal: MybGoal | null;\n onOpenChange: (open: boolean) => void;\n}) {\n const update = useMybUpdateGoal();\n const [value, setValue] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [seededId, setSeededId] = useState<string | null>(null);\n\n // Seed the input with the goal's current value whenever the dialog target\n // changes — React's render-phase \"reset state when a prop changes\" pattern\n // (no effect, so no synchronous-setState-in-effect cascade).\n if (goal && goal.id !== seededId) {\n setSeededId(goal.id);\n setValue(String(goal.currentValue));\n setError(null);\n }\n\n const submit = async () => {\n if (!goal) return;\n const parsed = numOrUndef(value);\n if (parsed === undefined) {\n setError('Enter a valid number.');\n return;\n }\n setError(null);\n try {\n await update.mutateAsync({ id: goal.id, input: { currentValue: parsed } });\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update progress.');\n }\n };\n\n return (\n <ResponsiveDialog open={goal != null} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Update progress</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {goal ? goal.title : 'Log your latest value for this goal.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <form\n className=\"space-y-4\"\n onSubmit={(event) => {\n event.preventDefault();\n void submit();\n }}\n >\n {error && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {error}\n </p>\n )}\n <div className=\"space-y-2\">\n <Label htmlFor=\"myb-goal-current\">\n Current value{goal?.unit ? ` (${goal.unit})` : ''}\n </Label>\n <Input\n id=\"myb-goal-current\"\n type=\"number\"\n inputMode=\"decimal\"\n autoFocus\n value={value}\n onChange={(event) => setValue(event.target.value)}\n placeholder=\"0\"\n />\n {goal && (\n <p className=\"text-xs text-text-tertiary\">\n Target: {formatNumber(goal.targetValue)}\n {goal.unit ? ` ${goal.unit}` : ''}\n </p>\n )}\n </div>\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={update.isPending}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={update.isPending}>\n {update.isPending ? 'Saving…' : 'Save progress'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Goal card\n// ============================================================================\n\nfunction GoalCard({\n goal,\n onUpdateProgress,\n onAchieve,\n isAchieving,\n}: {\n goal: MybGoal;\n onUpdateProgress: (goal: MybGoal) => void;\n onAchieve: (goal: MybGoal) => void;\n isAchieving: boolean;\n}) {\n const meta = GOAL_TYPE_META[goal.type];\n const Icon = meta.icon;\n const pct = computeGoalProgress(goal);\n const unitSuffix = goal.unit ? ` ${goal.unit}` : '';\n\n return (\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} tint={meta.tint} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{goal.title}</p>\n <span className=\"mt-1 inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(goal.type)}\n </span>\n </div>\n </div>\n <StatusBadge status={goal.status} />\n </div>\n\n {goal.description && (\n <p className=\"line-clamp-2 text-xs text-text-secondary\">{goal.description}</p>\n )}\n\n <div className=\"space-y-1.5\">\n <Meter value={pct} color={meta.tint} />\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(goal.currentValue)}\n {unitSuffix} / {formatNumber(goal.targetValue)}\n {unitSuffix}\n </span>\n <span className=\"tabular-nums text-text-tertiary\">{formatPercent(pct)}</span>\n </div>\n </div>\n\n {(goal.targetDate || (goal.status === 'ACHIEVED' && goal.achievedAt)) && (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-tertiary\">\n {goal.targetDate && (\n <span className=\"inline-flex items-center gap-1\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Target {formatDate(goal.targetDate)}\n <span className=\"text-text-secondary\">· {formatRelativeTime(goal.targetDate)}</span>\n </span>\n )}\n {goal.status === 'ACHIEVED' && goal.achievedAt && (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" />\n Achieved {formatDate(goal.achievedAt)}\n </span>\n )}\n </div>\n )}\n\n <div className=\"mt-auto flex flex-wrap gap-2 pt-1\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => onUpdateProgress(goal)}>\n Update progress\n </Button>\n {goal.status === 'ACTIVE' && (\n <Button size=\"sm\" onClick={() => onAchieve(goal)} disabled={isAchieving}>\n {isAchieving ? 'Marking…' : 'Mark achieved'}\n </Button>\n )}\n </div>\n </GlassCard>\n );\n}\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport function GoalsPage() {\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [createOpen, setCreateOpen] = useState(false);\n const [progressGoal, setProgressGoal] = useState<MybGoal | null>(null);\n const [achievingId, setAchievingId] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n\n const pager = useOffsetPager(PAGE_SIZE);\n\n const goalsQuery = useMybGoals({\n status: statusFilter === 'ALL' ? undefined : statusFilter,\n pagination: { limit: PAGE_SIZE, offset: pager.offset },\n });\n\n // Separate bounded queries power the KPI row so the counts stay accurate\n // regardless of the active filter / page (connections expose no total count).\n const activeGoals = useMybGoals({ status: 'ACTIVE', pagination: { limit: KPI_LIMIT } });\n const achievedGoals = useMybGoals({ status: 'ACHIEVED', pagination: { limit: KPI_LIMIT } });\n\n const achieve = useMybAchieveGoal();\n\n const rows = goalsQuery.data?.items ?? [];\n const activeItems = activeGoals.data?.items ?? [];\n const achievedItems = achievedGoals.data?.items ?? [];\n\n const avgActiveProgress = useMemo(() => {\n if (activeItems.length === 0) return 0;\n const sum = activeItems.reduce((acc, goal) => acc + computeGoalProgress(goal), 0);\n return sum / activeItems.length;\n }, [activeItems]);\n\n const handleFilterChange = (next: StatusFilter) => {\n setStatusFilter(next);\n pager.reset();\n };\n\n const handleAchieve = async (goal: MybGoal) => {\n setAchievingId(goal.id);\n setActionError(null);\n try {\n await achieve.mutateAsync(goal.id);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to mark goal as achieved.');\n } finally {\n setAchievingId(null);\n }\n };\n\n const showLoading = goalsQuery.isLoading && !goalsQuery.data;\n\n return (\n <PageLayout\n title=\"Goals\"\n description=\"Targets across training, nutrition and body composition.\"\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New goal\n </Button>\n }\n >\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<Target className=\"h-5 w-5\" />}\n value={formatNumber(activeGoals.data ? activeItems.length : undefined)}\n label=\"Active goals\"\n />\n <MetricsCard\n icon={<Trophy className=\"h-5 w-5\" />}\n value={formatNumber(achievedGoals.data ? achievedItems.length : undefined)}\n label=\"Achieved\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<TrendingUp className=\"h-5 w-5\" />}\n value={activeGoals.data ? formatPercent(avgActiveProgress) : '—'}\n label=\"Avg progress (active)\"\n footer={<MiniBar value={avgActiveProgress} height={4} barClassName=\"bg-accent-teal\" />}\n />\n </div>\n\n {/* Filter */}\n <div className=\"mt-6\">\n <FilterTabs options={STATUS_FILTERS} value={statusFilter} onChange={handleFilterChange} />\n </div>\n\n {actionError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {/* Grid */}\n <div className=\"mt-4\">\n {showLoading ? (\n <LoadingView label=\"Loading your goals…\" />\n ) : goalsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your goals.\"\n onRetry={() => void goalsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Target className=\"h-10 w-10\" />}\n title={statusFilter === 'ALL' ? 'No goals yet' : 'No goals match this filter'}\n description={\n statusFilter === 'ALL'\n ? 'Set a goal to give your training and nutrition a clear target to work toward.'\n : 'Try a different status filter, or create a new goal.'\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New goal\n </Button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {rows.map((goal) => (\n <GoalCard\n key={goal.id}\n goal={goal}\n onUpdateProgress={setProgressGoal}\n onAchieve={(g) => void handleAchieve(g)}\n isAchieving={achievingId === goal.id}\n />\n ))}\n </div>\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={goalsQuery.data?.pageInfo?.hasMore ?? false}\n onPageChange={(target) => pager.onPageChange(target)}\n />\n </>\n )}\n </div>\n\n <CreateGoalDialog open={createOpen} onOpenChange={setCreateOpen} />\n <UpdateProgressDialog\n goal={progressGoal}\n onOpenChange={(open) => {\n if (!open) setProgressGoal(null);\n }}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|