@burdenoff/microfe-mybodyshield 2026.1014.1 → 2026.1026.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.
Files changed (32) hide show
  1. package/dist/mybodyshield/MyBodyShieldRoutes.d.ts.map +1 -1
  2. package/dist/mybodyshield/MyBodyShieldRoutes.js +42 -24
  3. package/dist/mybodyshield/MyBodyShieldRoutes.js.map +1 -1
  4. package/dist/mybodyshield/components/FilterTabs.js +1 -1
  5. package/dist/mybodyshield/components/FilterTabs.js.map +1 -1
  6. package/dist/mybodyshield/hooks/useMyBodyShieldApi.d.ts +6 -1
  7. package/dist/mybodyshield/hooks/useMyBodyShieldApi.d.ts.map +1 -1
  8. package/dist/mybodyshield/hooks/useMyBodyShieldApi.js +89 -63
  9. package/dist/mybodyshield/hooks/useMyBodyShieldApi.js.map +1 -1
  10. package/dist/mybodyshield/pages/coaching/CoachClientPage.d.ts +2 -0
  11. package/dist/mybodyshield/pages/coaching/CoachClientPage.d.ts.map +1 -0
  12. package/dist/mybodyshield/pages/coaching/CoachClientPage.js +264 -0
  13. package/dist/mybodyshield/pages/coaching/CoachClientPage.js.map +1 -0
  14. package/dist/mybodyshield/pages/coaching/CoachingPage.d.ts.map +1 -1
  15. package/dist/mybodyshield/pages/coaching/CoachingPage.js +74 -67
  16. package/dist/mybodyshield/pages/coaching/CoachingPage.js.map +1 -1
  17. package/dist/mybodyshield/pages/coaching/coachAnalytics.d.ts +6 -0
  18. package/dist/mybodyshield/pages/coaching/coachAnalytics.d.ts.map +1 -0
  19. package/dist/mybodyshield/pages/coaching/coachAnalytics.js +13 -0
  20. package/dist/mybodyshield/pages/coaching/coachAnalytics.js.map +1 -0
  21. package/dist/mybodyshield/pages/insights/InsightsPage.js +1 -1
  22. package/dist/mybodyshield/pages/insights/InsightsPage.js.map +1 -1
  23. package/dist/mybodyshield/pages/recovery/MindfulnessPage.d.ts +2 -0
  24. package/dist/mybodyshield/pages/recovery/MindfulnessPage.d.ts.map +1 -0
  25. package/dist/mybodyshield/pages/recovery/MindfulnessPage.js +181 -0
  26. package/dist/mybodyshield/pages/recovery/MindfulnessPage.js.map +1 -0
  27. package/dist/mybodyshield/pages/recovery/RecoveryPage.d.ts.map +1 -1
  28. package/dist/mybodyshield/pages/recovery/RecoveryPage.js +210 -201
  29. package/dist/mybodyshield/pages/recovery/RecoveryPage.js.map +1 -1
  30. package/dist/mybodyshield/types/index.d.ts +31 -0
  31. package/dist/mybodyshield/types/index.d.ts.map +1 -1
  32. package/package.json +2 -1
@@ -318,11 +318,11 @@ function $({ t: e, label: t, icon: n, on: r }) {
318
318
  ]
319
319
  });
320
320
  }
321
- function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
322
- let o = c(), s = se(o), l = !!t && e.coachId === t, u = !!t && e.memberId === t, d = s[e.status], f = e.status === "ACTIVE" || e.status === "PAUSED", [m, y] = b(!1), C = _();
321
+ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a, onOpenClient: o }) {
322
+ let s = c(), l = se(s), u = !!t && e.coachId === t, d = !!t && e.memberId === t, f = l[e.status], m = e.status === "ACTIVE" || e.status === "PAUSED", [y, C] = b(!1), D = _(), O = e.status === "ACTIVE";
323
323
  return /* @__PURE__ */ R(B, {
324
324
  treatment: "glass",
325
- glow: e.status === "ACTIVE",
325
+ glow: O,
326
326
  className: "flex h-full flex-col gap-4 p-5 transition-shadow hover:shadow-float" + (e.status === "PENDING" ? " ring-1 ring-inset ring-[color-mix(in_oklab,var(--color-status-warning-text)_35%,transparent)]" : ""),
327
327
  children: [
328
328
  /* @__PURE__ */ R("div", {
@@ -333,10 +333,10 @@ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
333
333
  className: "min-w-0",
334
334
  children: [/* @__PURE__ */ L("p", {
335
335
  className: "truncate text-sm font-semibold text-text-primary",
336
- children: e.specialty?.trim() || o("mybodyshield.pages.coaching.coachingRelationship", "Coaching relationship")
336
+ children: e.specialty?.trim() || s("mybodyshield.pages.coaching.coachingRelationship", "Coaching relationship")
337
337
  }), /* @__PURE__ */ L("p", {
338
338
  className: "text-xs text-text-tertiary",
339
- children: o("mybodyshield.pages.coaching.coachMember", "Coach ↔ member")
339
+ children: s("mybodyshield.pages.coaching.coachMember", "Coach ↔ member")
340
340
  })]
341
341
  })]
342
342
  }), /* @__PURE__ */ L(r, { status: e.status })]
@@ -347,34 +347,34 @@ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
347
347
  className: "flex items-center gap-2.5 rounded-lg bg-bg-sunken/60 p-2.5",
348
348
  children: [/* @__PURE__ */ L("span", {
349
349
  "aria-hidden": "true",
350
- className: `flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${l ? "bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]" : "bg-bg-surface text-text-tertiary ring-border-seam"}`,
350
+ className: `flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${u ? "bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]" : "bg-bg-surface text-text-tertiary ring-border-seam"}`,
351
351
  children: /* @__PURE__ */ L(I, { className: "h-4 w-4" })
352
352
  }), /* @__PURE__ */ R("div", {
353
353
  className: "min-w-0",
354
354
  children: [/* @__PURE__ */ L("p", {
355
355
  className: "text-[11px] font-medium uppercase tracking-wide text-text-tertiary",
356
- children: o("mybodyshield.pages.coaching.coachLabel", "Coach")
356
+ children: s("mybodyshield.pages.coaching.coachLabel", "Coach")
357
357
  }), /* @__PURE__ */ L("p", {
358
358
  className: "mt-0.5 truncate text-sm font-medium text-text-primary",
359
359
  title: e.coachId,
360
- children: l ? o("mybodyshield.pages.coaching.you", "You") : `#${X(e.coachId)}`
360
+ children: u ? s("mybodyshield.pages.coaching.you", "You") : `#${X(e.coachId)}`
361
361
  })]
362
362
  })]
363
363
  }), /* @__PURE__ */ R("div", {
364
364
  className: "flex items-center gap-2.5 rounded-lg bg-bg-sunken/60 p-2.5",
365
365
  children: [/* @__PURE__ */ L("span", {
366
366
  "aria-hidden": "true",
367
- className: `flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${u ? "bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]" : "bg-bg-surface text-text-tertiary ring-border-seam"}`,
367
+ className: `flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${d ? "bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]" : "bg-bg-surface text-text-tertiary ring-border-seam"}`,
368
368
  children: /* @__PURE__ */ L(I, { className: "h-4 w-4" })
369
369
  }), /* @__PURE__ */ R("div", {
370
370
  className: "min-w-0",
371
371
  children: [/* @__PURE__ */ L("p", {
372
372
  className: "text-[11px] font-medium uppercase tracking-wide text-text-tertiary",
373
- children: o("mybodyshield.pages.coaching.memberLabel", "Member")
373
+ children: s("mybodyshield.pages.coaching.memberLabel", "Member")
374
374
  }), /* @__PURE__ */ L("p", {
375
375
  className: "mt-0.5 truncate text-sm font-medium text-text-primary",
376
376
  title: e.memberId,
377
- children: u ? o("mybodyshield.pages.coaching.you", "You") : `#${X(e.memberId)}`
377
+ children: d ? s("mybodyshield.pages.coaching.you", "You") : `#${X(e.memberId)}`
378
378
  })]
379
379
  })]
380
380
  })]
@@ -385,42 +385,42 @@ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
385
385
  className: "flex flex-wrap gap-1.5",
386
386
  children: [
387
387
  /* @__PURE__ */ L($, {
388
- t: o,
389
- label: o("mybodyshield.pages.coaching.workouts", "Workouts"),
388
+ t: s,
389
+ label: s("mybodyshield.pages.coaching.workouts", "Workouts"),
390
390
  icon: E,
391
391
  on: e.shareWorkouts
392
392
  }),
393
393
  /* @__PURE__ */ L($, {
394
- t: o,
395
- label: o("mybodyshield.pages.coaching.nutrition", "Nutrition"),
394
+ t: s,
395
+ label: s("mybodyshield.pages.coaching.nutrition", "Nutrition"),
396
396
  icon: F,
397
397
  on: e.shareNutrition
398
398
  }),
399
399
  /* @__PURE__ */ L($, {
400
- t: o,
401
- label: o("mybodyshield.pages.coaching.metrics", "Metrics"),
400
+ t: s,
401
+ label: s("mybodyshield.pages.coaching.metrics", "Metrics"),
402
402
  icon: A,
403
403
  on: e.shareMetrics
404
404
  })
405
405
  ]
406
406
  }), /* @__PURE__ */ L("p", {
407
407
  className: "text-xs leading-relaxed text-text-tertiary",
408
- children: ce(o, e.status)
408
+ children: ce(s, e.status)
409
409
  })]
410
410
  }),
411
411
  (e.startedAt || e.endedAt) && /* @__PURE__ */ R("div", {
412
412
  className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-tertiary",
413
413
  children: [e.startedAt && /* @__PURE__ */ R("span", {
414
414
  className: "inline-flex items-center gap-1",
415
- children: [/* @__PURE__ */ L(x, { className: "h-3.5 w-3.5" }), o("mybodyshield.pages.coaching.started", "Started {{date}}", { date: n(e.startedAt) })]
415
+ children: [/* @__PURE__ */ L(x, { className: "h-3.5 w-3.5" }), s("mybodyshield.pages.coaching.started", "Started {{date}}", { date: n(e.startedAt) })]
416
416
  }), e.endedAt && /* @__PURE__ */ R("span", {
417
417
  className: "inline-flex items-center gap-1",
418
- children: [/* @__PURE__ */ L(S, { className: "h-3.5 w-3.5" }), o("mybodyshield.pages.coaching.ended", "Ended {{date}}", { date: n(e.endedAt) })]
418
+ children: [/* @__PURE__ */ L(S, { className: "h-3.5 w-3.5" }), s("mybodyshield.pages.coaching.ended", "Ended {{date}}", { date: n(e.endedAt) })]
419
419
  })]
420
420
  }),
421
- d.length > 0 && /* @__PURE__ */ L("div", {
421
+ f.length > 0 && /* @__PURE__ */ L("div", {
422
422
  className: "mt-auto flex flex-wrap gap-2 pt-1",
423
- children: d.map((t) => /* @__PURE__ */ L(z, {
423
+ children: f.map((t) => /* @__PURE__ */ L(z, {
424
424
  size: "sm",
425
425
  variant: t.primary ? void 0 : "outline",
426
426
  disabled: i,
@@ -428,22 +428,22 @@ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
428
428
  children: t.label
429
429
  }, t.to))
430
430
  }),
431
- f && /* @__PURE__ */ R("div", {
432
- className: `border-t border-border-seam pt-3 ${d.length === 0 ? "mt-auto" : ""}`,
431
+ m && /* @__PURE__ */ R("div", {
432
+ className: `border-t border-border-seam pt-3 ${f.length === 0 ? "mt-auto" : ""}`,
433
433
  children: [/* @__PURE__ */ R("button", {
434
434
  type: "button",
435
- onClick: () => y((e) => !e),
436
- "aria-expanded": m,
435
+ onClick: () => C((e) => !e),
436
+ "aria-expanded": y,
437
437
  className: "inline-flex items-center gap-1.5 text-sm font-medium text-accent hover:underline",
438
438
  children: [
439
439
  /* @__PURE__ */ L(M, { className: "h-4 w-4" }),
440
- m ? o("mybodyshield.pages.coaching.hideDiscussion", "Hide discussion") : o("mybodyshield.pages.coaching.discussion", "Discussion"),
441
- L(m ? T : w, { className: "h-4 w-4" })
440
+ y ? s("mybodyshield.pages.coaching.hideDiscussion", "Hide discussion") : s("mybodyshield.pages.coaching.discussion", "Discussion"),
441
+ L(y ? T : w, { className: "h-4 w-4" })
442
442
  ]
443
443
  }), /* @__PURE__ */ L(h, {
444
444
  initial: !1,
445
- children: m && /* @__PURE__ */ L(g.div, {
446
- initial: C ? !1 : {
445
+ children: y && /* @__PURE__ */ L(g.div, {
446
+ initial: D ? !1 : {
447
447
  opacity: 0,
448
448
  height: 0
449
449
  },
@@ -451,11 +451,11 @@ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
451
451
  opacity: 1,
452
452
  height: "auto"
453
453
  },
454
- exit: C ? { opacity: 0 } : {
454
+ exit: D ? { opacity: 0 } : {
455
455
  opacity: 0,
456
456
  height: 0
457
457
  },
458
- transition: C ? { duration: 0 } : {
458
+ transition: D ? { duration: 0 } : {
459
459
  duration: .25,
460
460
  ease: Y
461
461
  },
@@ -465,12 +465,18 @@ function de({ relationship: e, currentUserId: t, busy: i, onTransition: a }) {
465
465
  children: /* @__PURE__ */ L(p, {
466
466
  entityType: "mybodyshield-coaching",
467
467
  entityId: e.id,
468
- title: o("mybodyshield.pages.coaching.coachingDiscussion", "Coaching discussion"),
469
- description: o("mybodyshield.pages.coaching.coachingDiscussionDescription", "A private thread between the coach and member.")
468
+ title: s("mybodyshield.pages.coaching.coachingDiscussion", "Coaching discussion"),
469
+ description: s("mybodyshield.pages.coaching.coachingDiscussionDescription", "A private thread between the coach and member.")
470
470
  })
471
471
  })
472
472
  }, "discussion")
473
473
  })]
474
+ }),
475
+ O && u && /* @__PURE__ */ L(z, {
476
+ variant: "outline",
477
+ size: "sm",
478
+ onClick: () => o(e.memberId),
479
+ children: s("mybodyshield.pages.coaching.openClient", "Open client workspace")
474
480
  })
475
481
  ]
476
482
  });
@@ -506,92 +512,93 @@ function fe() {
506
512
  });
507
513
  }
508
514
  function pe() {
509
- let n = c(), { currentUser: r } = e(), o = _(), [d, p] = b("ALL"), [m, h] = b(!1), [v, x] = b(null), [S, C] = b(null), w = a(), T = s(), E = w.data?.items ?? [], D = y(() => {
515
+ let n = c(), { currentUser: r } = e(), { navigateTo: o } = e(), d = _(), [p, m] = b("ALL"), [h, v] = b(!1), [x, S] = b(null), [C, w] = b(null), T = a(), E = s(), D = T.data?.items ?? [], O = y(() => {
510
516
  let e = {
511
517
  PENDING: 0,
512
518
  ACTIVE: 0,
513
519
  PAUSED: 0,
514
520
  ENDED: 0
515
521
  };
516
- for (let t of E) e[t.status] += 1;
522
+ for (let t of D) e[t.status] += 1;
517
523
  return e;
518
- }, [E]), O = y(() => [{
524
+ }, [D]), A = y(() => [{
519
525
  value: "ALL",
520
526
  label: n("mybodyshield.common.all", "All"),
521
- count: E.length
527
+ count: D.length
522
528
  }, ...J.map((e) => ({
523
529
  value: e,
524
530
  label: oe(n, e),
525
- count: D[e]
531
+ count: O[e]
526
532
  }))], [
527
- E.length,
528
- D,
533
+ D.length,
534
+ O,
529
535
  n
530
- ]), A = y(() => d === "ALL" ? E : E.filter((e) => e.status === d), [E, d]), j = async (e, t) => {
531
- x(e), C(null);
536
+ ]), j = y(() => p === "ALL" ? D : D.filter((e) => e.status === p), [D, p]), M = async (e, t) => {
537
+ S(e), w(null);
532
538
  try {
533
- await T.mutateAsync({
539
+ await E.mutateAsync({
534
540
  id: e,
535
541
  status: t
536
542
  });
537
543
  } catch (e) {
538
- C(e instanceof Error ? e.message : n("mybodyshield.pages.coaching.failedToUpdate", "Failed to update the relationship."));
544
+ w(e instanceof Error ? e.message : n("mybodyshield.pages.coaching.failedToUpdate", "Failed to update the relationship."));
539
545
  } finally {
540
- x(null);
546
+ S(null);
541
547
  }
542
- }, M = w.isLoading && !w.data;
548
+ }, N = T.isLoading && !T.data;
543
549
  return /* @__PURE__ */ R(t, {
544
550
  title: n("mybodyshield.pages.coaching.title", "Coaching"),
545
551
  description: n("mybodyshield.pages.coaching.description", "Coach ↔ member relationships and what's shared."),
546
552
  actions: /* @__PURE__ */ R(z, {
547
- onClick: () => h(!0),
553
+ onClick: () => v(!0),
548
554
  children: [/* @__PURE__ */ L(P, { className: "mr-2 h-4 w-4" }), n("mybodyshield.pages.coaching.newCoaching", "New coaching")]
549
555
  }),
550
556
  children: [
551
557
  /* @__PURE__ */ L("div", {
552
558
  className: "mb-6",
553
559
  children: /* @__PURE__ */ L(f, {
554
- options: O,
555
- value: d,
556
- onChange: p
560
+ options: A,
561
+ value: p,
562
+ onChange: m
557
563
  })
558
564
  }),
559
- S && /* @__PURE__ */ L("p", {
565
+ C && /* @__PURE__ */ L("p", {
560
566
  className: "mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
561
- children: S
567
+ children: C
562
568
  }),
563
- M ? /* @__PURE__ */ L(u, { label: n("mybodyshield.pages.coaching.loading", "Loading coaching relationships…") }) : w.isError ? /* @__PURE__ */ L(l, {
569
+ N ? /* @__PURE__ */ L(u, { label: n("mybodyshield.pages.coaching.loading", "Loading coaching relationships…") }) : T.isError ? /* @__PURE__ */ L(l, {
564
570
  message: n("mybodyshield.pages.coaching.error", "We couldn't load your coaching relationships."),
565
- onRetry: () => void w.refetch()
566
- }) : A.length === 0 ? /* @__PURE__ */ L(i, {
571
+ onRetry: () => void T.refetch()
572
+ }) : j.length === 0 ? /* @__PURE__ */ L(i, {
567
573
  illustration: "empty-data",
568
574
  icon: /* @__PURE__ */ L(k, { className: "h-10 w-10" }),
569
- title: E.length === 0 ? n("mybodyshield.pages.coaching.emptyTitle", "No coaching relationships") : n("mybodyshield.pages.coaching.emptyFilteredTitle", "No relationships match this filter"),
570
- description: E.length === 0 ? n("mybodyshield.pages.coaching.emptyDescription", "Connect a coach and a member to start sharing training, nutrition and progress data.") : n("mybodyshield.pages.coaching.emptyFilteredDescription", "Try a different status filter, or create a new coaching relationship."),
575
+ title: D.length === 0 ? n("mybodyshield.pages.coaching.emptyTitle", "No coaching relationships") : n("mybodyshield.pages.coaching.emptyFilteredTitle", "No relationships match this filter"),
576
+ description: D.length === 0 ? n("mybodyshield.pages.coaching.emptyDescription", "Connect a coach and a member to start sharing training, nutrition and progress data.") : n("mybodyshield.pages.coaching.emptyFilteredDescription", "Try a different status filter, or create a new coaching relationship."),
571
577
  action: /* @__PURE__ */ R(z, {
572
- onClick: () => h(!0),
578
+ onClick: () => v(!0),
573
579
  children: [/* @__PURE__ */ L(P, { className: "mr-2 h-4 w-4" }), n("mybodyshield.pages.coaching.newCoaching", "New coaching")]
574
580
  })
575
581
  }) : /* @__PURE__ */ L(g.div, {
576
582
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
577
- variants: o ? void 0 : ie,
578
- initial: o ? !1 : "hidden",
583
+ variants: d ? void 0 : ie,
584
+ initial: d ? !1 : "hidden",
579
585
  animate: "show",
580
- children: A.map((e) => /* @__PURE__ */ L(g.div, {
586
+ children: j.map((e) => /* @__PURE__ */ L(g.div, {
581
587
  className: "h-full",
582
- variants: o ? void 0 : ae,
588
+ variants: d ? void 0 : ae,
583
589
  children: /* @__PURE__ */ L(de, {
584
590
  relationship: e,
585
591
  currentUserId: r?.id,
586
- busy: v === e.id,
587
- onTransition: (e, t) => void j(e, t)
592
+ busy: x === e.id,
593
+ onTransition: (e, t) => void M(e, t),
594
+ onOpenClient: (e) => o(`coaching/${e}`)
588
595
  })
589
596
  }, e.id))
590
597
  }),
591
598
  /* @__PURE__ */ L(fe, {}),
592
599
  /* @__PURE__ */ L(ue, {
593
- open: m,
594
- onOpenChange: h,
600
+ open: h,
601
+ onOpenChange: v,
595
602
  defaultMember: {
596
603
  userId: r?.id ?? "",
597
604
  displayName: r?.name || r?.email || ""
@@ -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 { motion, AnimatePresence, useReducedMotion } from 'framer-motion';\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 User,\n ShieldCheck,\n ScanEye,\n FileLock2,\n EyeOff,\n Lock,\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 Section,\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, useTr } 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/** Shared entrance easing — a soft, decelerating cubic-bezier. */\nconst MOTION_EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];\n\nconst STAGGER_CONTAINER = {\n hidden: {},\n show: { transition: { staggerChildren: 0.06 } },\n};\nconst RISE_ITEM = {\n hidden: { opacity: 0, y: 12 },\n show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: MOTION_EASE } },\n};\n\n/** Localised status name for filter labels. */\nfunction coachingStatusLabel(t: ReturnType<typeof useTr>, status: MybCoachingStatus): string {\n const keyRoot = 'mybodyshield.pages.coaching.status';\n switch (status) {\n case 'PENDING':\n return t(`${keyRoot}.pending`, 'Pending');\n case 'ACTIVE':\n return t(`${keyRoot}.active`, 'Active');\n case 'PAUSED':\n return t(`${keyRoot}.paused`, 'Paused');\n case 'ENDED':\n return t(`${keyRoot}.ended`, 'Ended');\n }\n}\n\n/**\n * Allowed lifecycle transitions per current status. Mirrors the service's\n * `COACHING_TRANSITIONS` map exactly — PENDING may be accepted (-> ACTIVE) or\n * declined (-> ENDED), ACTIVE may be paused or ended, PAUSED may be resumed or\n * ended, and ENDED is terminal.\n */\nfunction useTransitions(t: ReturnType<typeof useTr>) {\n return useMemo<\n Record<MybCoachingStatus, { to: MybCoachingStatus; label: string; primary: boolean }[]>\n >(\n () => ({\n PENDING: [\n { to: 'ACTIVE', label: t('mybodyshield.pages.coaching.accept', 'Accept'), primary: true },\n // Neutral label: PENDING -> ENDED is open to BOTH parties, so this is a\n // decline for the recipient and a withdrawal for the sender, and the\n // payload carries no `createdBy` to tell the two apart.\n {\n to: 'ENDED',\n label: t('mybodyshield.pages.coaching.endRequest', 'End request'),\n primary: false,\n },\n ],\n ACTIVE: [\n {\n to: 'PAUSED',\n label: t('mybodyshield.pages.coaching.pause', 'Pause'),\n primary: false,\n },\n { to: 'ENDED', label: t('mybodyshield.pages.coaching.end', 'End'), primary: false },\n ],\n PAUSED: [\n {\n to: 'ACTIVE',\n label: t('mybodyshield.pages.coaching.resume', 'Resume'),\n primary: true,\n },\n { to: 'ENDED', label: t('mybodyshield.pages.coaching.end', 'End'), primary: false },\n ],\n ENDED: [],\n }),\n [t]\n );\n}\n\n/**\n * What the sharing chips actually mean in each lifecycle state.\n *\n * The service gates every member-scoped read through `canAccessMember`, whose\n * lookup requires `status: 'ACTIVE'` **and** the matching `share*` flag. So a\n * PENDING, PAUSED or ENDED relationship grants nothing regardless of the chips\n * above, and accepting/pausing/ending changes access on the very next read.\n * A PENDING request can only be accepted by the counterparty — the service\n * refuses `PENDING -> ACTIVE` when the caller is the one who created it.\n */\nfunction accessNote(t: ReturnType<typeof useTr>, status: MybCoachingStatus): string {\n switch (status) {\n case 'PENDING':\n return t(\n 'mybodyshield.pages.coaching.pendingNote',\n 'Nothing is shared yet. The other party has to accept this request — the person who sent it cannot accept it themselves.'\n );\n case 'ACTIVE':\n return t(\n 'mybodyshield.pages.coaching.activeNote',\n 'The coach can read the categories marked On. Pausing or ending this revokes that access on the next request.'\n );\n case 'PAUSED':\n return t(\n 'mybodyshield.pages.coaching.pausedNote',\n 'Sharing is suspended while paused. Resuming restores exactly the scope above.'\n );\n case 'ENDED':\n return t(\n 'mybodyshield.pages.coaching.endedNote',\n 'Access was revoked when this relationship ended.'\n );\n }\n}\n\n/**\n * How the sharing scope is enforced, stated only where the behaviour is\n * implemented in `wspace-mybodyshield-svc` (see `src/utils/memberAccess.ts`,\n * `src/utils/field-encryption.ts` and `src/plugins/mutation-audit-plugin.ts`).\n */\nfunction useEnforcementFacts(t: ReturnType<typeof useTr>) {\n return useMemo<{ icon: typeof ShieldCheck; title: string; detail: string }[]>(\n () => [\n {\n icon: ScanEye,\n title: t('mybodyshield.pages.coaching.checkedOnEveryRead', 'Checked on every read'),\n detail: t(\n 'mybodyshield.pages.coaching.checkedOnEveryReadDetail',\n 'Each request re-checks the relationship and the matching switch. Access is not granted once and cached for the session.'\n ),\n },\n {\n icon: EyeOff,\n title: t(\n 'mybodyshield.pages.coaching.refusalsLookLikeNotFound',\n 'Refusals look like \"not found\"'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.refusalsLookLikeNotFoundDetail',\n 'Asking for data you have not shared returns not-found, never \"forbidden\" — so nobody can use the error to confirm a record exists.'\n ),\n },\n {\n icon: Lock,\n title: t(\n 'mybodyshield.pages.coaching.encryptedAtRest',\n 'Health values are encrypted at rest'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.encryptedAtRestDetail',\n 'Body metrics, recovery scores, sleep hours and free-text notes are stored encrypted and decrypted only when returned to someone allowed to read them.'\n ),\n },\n {\n icon: FileLock2,\n title: t(\n 'mybodyshield.pages.coaching.redactedFromAudit',\n 'Health fields are redacted from the audit log'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.redactedFromAuditDetail',\n 'Every mutation is audited, but weights, scores, macros and notes are replaced with a redaction marker before the entry leaves this service.'\n ),\n },\n {\n icon: ShieldCheck,\n title: t(\n 'mybodyshield.pages.coaching.scopeFixed',\n 'Scope is fixed for the life of the relationship'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.scopeFixedDetail',\n 'The three switches are chosen when the relationship is created. To change them, end this one and create a new relationship with the scope you want.'\n ),\n },\n ],\n [t]\n );\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 t = useTr();\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(t('mybodyshield.pages.coaching.coachRequired', 'Coach user id is required.'));\n return;\n }\n if (!memberId) {\n setError(t('mybodyshield.pages.coaching.memberRequired', '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(\n err instanceof Error\n ? err.message\n : t(\n 'mybodyshield.pages.coaching.failedToCreate',\n 'Failed to create coaching relationship.'\n )\n );\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={handleOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {t('mybodyshield.pages.coaching.dialogTitle', 'New coaching')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {t(\n 'mybodyshield.pages.coaching.dialogDescription',\n 'Link a coach and member, and choose exactly what training, nutrition and progress data is shared.'\n )}\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={t('mybodyshield.pages.coaching.coach', '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={t('mybodyshield.pages.coaching.member', '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\">\n {t('mybodyshield.pages.coaching.specialty', 'Specialty')}\n </Label>\n <Input\n id=\"myb-coaching-specialty\"\n value={form.specialty}\n onChange={(event) => setForm((f) => ({ ...f, specialty: event.target.value }))}\n placeholder={t(\n 'mybodyshield.pages.coaching.specialtyPlaceholder',\n 'e.g., Strength & conditioning'\n )}\n />\n </div>\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {t('mybodyshield.pages.coaching.sharedWithCoach', 'Shared with the coach')}\n </p>\n <ShareToggle\n id=\"myb-coaching-share-workouts\"\n label={t('mybodyshield.pages.coaching.workouts', 'Workouts')}\n description={t(\n 'mybodyshield.pages.coaching.workoutsDescription',\n 'Programs, sessions and logged sets.'\n )}\n checked={form.shareWorkouts}\n onChange={(value) => setForm((f) => ({ ...f, shareWorkouts: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-nutrition\"\n label={t('mybodyshield.pages.coaching.nutrition', 'Nutrition')}\n description={t(\n 'mybodyshield.pages.coaching.nutritionDescription',\n 'Plans, meals and daily macros.'\n )}\n checked={form.shareNutrition}\n onChange={(value) => setForm((f) => ({ ...f, shareNutrition: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-metrics\"\n label={t('mybodyshield.pages.coaching.metrics', 'Metrics')}\n description={t(\n 'mybodyshield.pages.coaching.metricsDescription',\n 'Body metrics and recovery logs.'\n )}\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 {t('mybodyshield.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={create.isPending}>\n {create.isPending\n ? t('mybodyshield.common.creating', 'Creating…')\n : t('mybodyshield.pages.coaching.createCoachingButton', 'Create coaching')}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Sharing scope chips\n// ============================================================================\n\nfunction ShareChip({\n t,\n label,\n icon,\n on,\n}: {\n t: ReturnType<typeof useTr>;\n label: string;\n icon: typeof Dumbbell;\n on: boolean;\n}) {\n const Icon = icon;\n const StatusIcon = on ? Check : Minus;\n return (\n <span\n aria-label={`${label} ${on ? t('mybodyshield.pages.coaching.shared', 'shared') : t('mybodyshield.pages.coaching.notShared', 'not shared')}`}\n className={`inline-flex items-center gap-1.5 rounded-full py-0.5 pl-1 pr-2.5 text-xs font-medium ${\n on\n ? 'bg-status-success-bg-subtle text-status-success-text ring-1 ring-inset ring-[color-mix(in_oklab,var(--color-status-success-text)_28%,transparent)]'\n : 'bg-bg-sunken text-text-tertiary ring-1 ring-inset ring-border-seam'\n }`}\n >\n <span\n aria-hidden=\"true\"\n className={`inline-flex size-4 items-center justify-center rounded-full ${\n on\n ? 'bg-[color-mix(in_oklab,var(--color-status-success-text)_22%,transparent)]'\n : 'bg-[color-mix(in_oklab,var(--color-text-tertiary)_18%,transparent)]'\n }`}\n >\n <StatusIcon className=\"h-2.5 w-2.5\" />\n </span>\n <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />\n <span>\n {label} ·{' '}\n {on\n ? t('mybodyshield.pages.coaching.on', 'On')\n : t('mybodyshield.pages.coaching.off', 'Off')}\n </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 t = useTr();\n const transitions = useTransitions(t);\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 const reduce = useReducedMotion();\n\n const isActive = relationship.status === 'ACTIVE';\n const isPending = relationship.status === 'PENDING';\n\n return (\n <GlassCard\n treatment=\"glass\"\n glow={isActive}\n className={\n 'flex h-full flex-col gap-4 p-5 transition-shadow hover:shadow-float' +\n (isPending\n ? ' ring-1 ring-inset ring-[color-mix(in_oklab,var(--color-status-warning-text)_35%,transparent)]'\n : '')\n }\n >\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() ||\n t('mybodyshield.pages.coaching.coachingRelationship', 'Coaching relationship')}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {t('mybodyshield.pages.coaching.coachMember', 'Coach ↔ member')}\n </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=\"flex items-center gap-2.5 rounded-lg bg-bg-sunken/60 p-2.5\">\n <span\n aria-hidden=\"true\"\n className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${\n coachIsYou\n ? 'bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]'\n : 'bg-bg-surface text-text-tertiary ring-border-seam'\n }`}\n >\n <User className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n {t('mybodyshield.pages.coaching.coachLabel', '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\n ? t('mybodyshield.pages.coaching.you', 'You')\n : `#${shortId(relationship.coachId)}`}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2.5 rounded-lg bg-bg-sunken/60 p-2.5\">\n <span\n aria-hidden=\"true\"\n className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${\n memberIsYou\n ? 'bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]'\n : 'bg-bg-surface text-text-tertiary ring-border-seam'\n }`}\n >\n <User className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n {t('mybodyshield.pages.coaching.memberLabel', '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\n ? t('mybodyshield.pages.coaching.you', 'You')\n : `#${shortId(relationship.memberId)}`}\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap gap-1.5\">\n <ShareChip\n t={t}\n label={t('mybodyshield.pages.coaching.workouts', 'Workouts')}\n icon={Dumbbell}\n on={relationship.shareWorkouts}\n />\n <ShareChip\n t={t}\n label={t('mybodyshield.pages.coaching.nutrition', 'Nutrition')}\n icon={Salad}\n on={relationship.shareNutrition}\n />\n <ShareChip\n t={t}\n label={t('mybodyshield.pages.coaching.metrics', 'Metrics')}\n icon={LineChart}\n on={relationship.shareMetrics}\n />\n </div>\n <p className=\"text-xs leading-relaxed text-text-tertiary\">\n {accessNote(t, relationship.status)}\n </p>\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 {t('mybodyshield.pages.coaching.started', 'Started {{date}}', {\n date: formatDate(relationship.startedAt),\n })}\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 {t('mybodyshield.pages.coaching.ended', 'Ended {{date}}', {\n date: formatDate(relationship.endedAt),\n })}\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\n ? t('mybodyshield.pages.coaching.hideDiscussion', 'Hide discussion')\n : t('mybodyshield.pages.coaching.discussion', 'Discussion')}\n {showDiscussion ? (\n <ChevronUp className=\"h-4 w-4\" />\n ) : (\n <ChevronDown className=\"h-4 w-4\" />\n )}\n </button>\n <AnimatePresence initial={false}>\n {showDiscussion && (\n <motion.div\n key=\"discussion\"\n initial={reduce ? false : { opacity: 0, height: 0 }}\n animate={{ opacity: 1, height: 'auto' }}\n exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n transition={reduce ? { duration: 0 } : { duration: 0.25, ease: MOTION_EASE }}\n className=\"overflow-hidden\"\n >\n <div className=\"mt-3\">\n <MybDiscussion\n entityType=\"mybodyshield-coaching\"\n entityId={relationship.id}\n title={t(\n 'mybodyshield.pages.coaching.coachingDiscussion',\n 'Coaching discussion'\n )}\n description={t(\n 'mybodyshield.pages.coaching.coachingDiscussionDescription',\n 'A private thread between the coach and member.'\n )}\n />\n </div>\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n )}\n </GlassCard>\n );\n}\n\n// ============================================================================\n// Sharing-enforcement explainer\n// ============================================================================\n\n/**\n * \"Who can see what\" panel. Every statement here mirrors behaviour that is\n * implemented in the subgraph today — see `ENFORCEMENT_FACTS` for the source\n * files. Nothing here is aspirational.\n */\nfunction SharingEnforcement() {\n const t = useTr();\n const facts = useEnforcementFacts(t);\n return (\n <Section\n title={t('mybodyshield.pages.coaching.sharingEnforcementTitle', 'How sharing is enforced')}\n description={t(\n 'mybodyshield.pages.coaching.sharingEnforcementDescription',\n 'Private by default — a coach only ever sees the categories you switched on, for as long as the relationship is active.'\n )}\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <ul className=\"grid gap-4 sm:grid-cols-2\">\n {facts.map((fact) => {\n const Icon = fact.icon;\n return (\n <li key={fact.title} className=\"flex items-start gap-3\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">{fact.title}</p>\n <p className=\"mt-0.5 text-xs leading-relaxed text-text-secondary\">\n {fact.detail}\n </p>\n </div>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n </Section>\n );\n}\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport function CoachingPage() {\n const t = useTr();\n const { currentUser } = useMyBodyShield();\n const reduce = useReducedMotion();\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 paginated connection — fetch every relationship\n // once and 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?.items ?? [];\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: t('mybodyshield.common.all', 'All'), count: all.length },\n ...FILTER_ORDER.map((status) => ({\n value: status as StatusFilter,\n label: coachingStatusLabel(t, status),\n count: counts[status],\n })),\n ],\n [all.length, counts, t]\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(\n err instanceof Error\n ? err.message\n : t('mybodyshield.pages.coaching.failedToUpdate', 'Failed to update the relationship.')\n );\n } finally {\n setTransitioningId(null);\n }\n };\n\n const showLoading = coaching.isLoading && !coaching.data;\n\n return (\n <PageLayout\n title={t('mybodyshield.pages.coaching.title', 'Coaching')}\n description={t(\n 'mybodyshield.pages.coaching.description',\n \"Coach ↔ member relationships and what's shared.\"\n )}\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('mybodyshield.pages.coaching.newCoaching', '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\n label={t('mybodyshield.pages.coaching.loading', 'Loading coaching relationships…')}\n />\n ) : coaching.isError ? (\n <ErrorView\n message={t(\n 'mybodyshield.pages.coaching.error',\n \"We couldn't load your coaching relationships.\"\n )}\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\n ? t('mybodyshield.pages.coaching.emptyTitle', 'No coaching relationships')\n : t(\n 'mybodyshield.pages.coaching.emptyFilteredTitle',\n 'No relationships match this filter'\n )\n }\n description={\n all.length === 0\n ? t(\n 'mybodyshield.pages.coaching.emptyDescription',\n 'Connect a coach and a member to start sharing training, nutrition and progress data.'\n )\n : t(\n 'mybodyshield.pages.coaching.emptyFilteredDescription',\n 'Try a different status filter, or create a new coaching relationship.'\n )\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('mybodyshield.pages.coaching.newCoaching', 'New coaching')}\n </Button>\n }\n />\n ) : (\n <motion.div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\"\n variants={reduce ? undefined : STAGGER_CONTAINER}\n initial={reduce ? false : 'hidden'}\n animate=\"show\"\n >\n {rows.map((relationship) => (\n <motion.div\n key={relationship.id}\n className=\"h-full\"\n variants={reduce ? undefined : RISE_ITEM}\n >\n <CoachingCard\n relationship={relationship}\n currentUserId={currentUser?.id}\n busy={transitioningId === relationship.id}\n onTransition={(id, status) => void handleTransition(id, status)}\n />\n </motion.div>\n ))}\n </motion.div>\n )}\n\n <SharingEnforcement />\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":"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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 { motion, AnimatePresence, useReducedMotion } from 'framer-motion';\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 User,\n ShieldCheck,\n ScanEye,\n FileLock2,\n EyeOff,\n Lock,\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 Section,\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, useTr } 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/** Shared entrance easing — a soft, decelerating cubic-bezier. */\nconst MOTION_EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];\n\nconst STAGGER_CONTAINER = {\n hidden: {},\n show: { transition: { staggerChildren: 0.06 } },\n};\nconst RISE_ITEM = {\n hidden: { opacity: 0, y: 12 },\n show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: MOTION_EASE } },\n};\n\n/** Localised status name for filter labels. */\nfunction coachingStatusLabel(t: ReturnType<typeof useTr>, status: MybCoachingStatus): string {\n const keyRoot = 'mybodyshield.pages.coaching.status';\n switch (status) {\n case 'PENDING':\n return t(`${keyRoot}.pending`, 'Pending');\n case 'ACTIVE':\n return t(`${keyRoot}.active`, 'Active');\n case 'PAUSED':\n return t(`${keyRoot}.paused`, 'Paused');\n case 'ENDED':\n return t(`${keyRoot}.ended`, 'Ended');\n }\n}\n\n/**\n * Allowed lifecycle transitions per current status. Mirrors the service's\n * `COACHING_TRANSITIONS` map exactly — PENDING may be accepted (-> ACTIVE) or\n * declined (-> ENDED), ACTIVE may be paused or ended, PAUSED may be resumed or\n * ended, and ENDED is terminal.\n */\nfunction useTransitions(t: ReturnType<typeof useTr>) {\n return useMemo<\n Record<MybCoachingStatus, { to: MybCoachingStatus; label: string; primary: boolean }[]>\n >(\n () => ({\n PENDING: [\n { to: 'ACTIVE', label: t('mybodyshield.pages.coaching.accept', 'Accept'), primary: true },\n // Neutral label: PENDING -> ENDED is open to BOTH parties, so this is a\n // decline for the recipient and a withdrawal for the sender, and the\n // payload carries no `createdBy` to tell the two apart.\n {\n to: 'ENDED',\n label: t('mybodyshield.pages.coaching.endRequest', 'End request'),\n primary: false,\n },\n ],\n ACTIVE: [\n {\n to: 'PAUSED',\n label: t('mybodyshield.pages.coaching.pause', 'Pause'),\n primary: false,\n },\n { to: 'ENDED', label: t('mybodyshield.pages.coaching.end', 'End'), primary: false },\n ],\n PAUSED: [\n {\n to: 'ACTIVE',\n label: t('mybodyshield.pages.coaching.resume', 'Resume'),\n primary: true,\n },\n { to: 'ENDED', label: t('mybodyshield.pages.coaching.end', 'End'), primary: false },\n ],\n ENDED: [],\n }),\n [t]\n );\n}\n\n/**\n * What the sharing chips actually mean in each lifecycle state.\n *\n * The service gates every member-scoped read through `canAccessMember`, whose\n * lookup requires `status: 'ACTIVE'` **and** the matching `share*` flag. So a\n * PENDING, PAUSED or ENDED relationship grants nothing regardless of the chips\n * above, and accepting/pausing/ending changes access on the very next read.\n * A PENDING request can only be accepted by the counterparty — the service\n * refuses `PENDING -> ACTIVE` when the caller is the one who created it.\n */\nfunction accessNote(t: ReturnType<typeof useTr>, status: MybCoachingStatus): string {\n switch (status) {\n case 'PENDING':\n return t(\n 'mybodyshield.pages.coaching.pendingNote',\n 'Nothing is shared yet. The other party has to accept this request — the person who sent it cannot accept it themselves.'\n );\n case 'ACTIVE':\n return t(\n 'mybodyshield.pages.coaching.activeNote',\n 'The coach can read the categories marked On. Pausing or ending this revokes that access on the next request.'\n );\n case 'PAUSED':\n return t(\n 'mybodyshield.pages.coaching.pausedNote',\n 'Sharing is suspended while paused. Resuming restores exactly the scope above.'\n );\n case 'ENDED':\n return t(\n 'mybodyshield.pages.coaching.endedNote',\n 'Access was revoked when this relationship ended.'\n );\n }\n}\n\n/**\n * How the sharing scope is enforced, stated only where the behaviour is\n * implemented in `wspace-mybodyshield-svc` (see `src/utils/memberAccess.ts`,\n * `src/utils/field-encryption.ts` and `src/plugins/mutation-audit-plugin.ts`).\n */\nfunction useEnforcementFacts(t: ReturnType<typeof useTr>) {\n return useMemo<{ icon: typeof ShieldCheck; title: string; detail: string }[]>(\n () => [\n {\n icon: ScanEye,\n title: t('mybodyshield.pages.coaching.checkedOnEveryRead', 'Checked on every read'),\n detail: t(\n 'mybodyshield.pages.coaching.checkedOnEveryReadDetail',\n 'Each request re-checks the relationship and the matching switch. Access is not granted once and cached for the session.'\n ),\n },\n {\n icon: EyeOff,\n title: t(\n 'mybodyshield.pages.coaching.refusalsLookLikeNotFound',\n 'Refusals look like \"not found\"'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.refusalsLookLikeNotFoundDetail',\n 'Asking for data you have not shared returns not-found, never \"forbidden\" — so nobody can use the error to confirm a record exists.'\n ),\n },\n {\n icon: Lock,\n title: t(\n 'mybodyshield.pages.coaching.encryptedAtRest',\n 'Health values are encrypted at rest'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.encryptedAtRestDetail',\n 'Body metrics, recovery scores, sleep hours and free-text notes are stored encrypted and decrypted only when returned to someone allowed to read them.'\n ),\n },\n {\n icon: FileLock2,\n title: t(\n 'mybodyshield.pages.coaching.redactedFromAudit',\n 'Health fields are redacted from the audit log'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.redactedFromAuditDetail',\n 'Every mutation is audited, but weights, scores, macros and notes are replaced with a redaction marker before the entry leaves this service.'\n ),\n },\n {\n icon: ShieldCheck,\n title: t(\n 'mybodyshield.pages.coaching.scopeFixed',\n 'Scope is fixed for the life of the relationship'\n ),\n detail: t(\n 'mybodyshield.pages.coaching.scopeFixedDetail',\n 'The three switches are chosen when the relationship is created. To change them, end this one and create a new relationship with the scope you want.'\n ),\n },\n ],\n [t]\n );\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 t = useTr();\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(t('mybodyshield.pages.coaching.coachRequired', 'Coach user id is required.'));\n return;\n }\n if (!memberId) {\n setError(t('mybodyshield.pages.coaching.memberRequired', '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(\n err instanceof Error\n ? err.message\n : t(\n 'mybodyshield.pages.coaching.failedToCreate',\n 'Failed to create coaching relationship.'\n )\n );\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={handleOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {t('mybodyshield.pages.coaching.dialogTitle', 'New coaching')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {t(\n 'mybodyshield.pages.coaching.dialogDescription',\n 'Link a coach and member, and choose exactly what training, nutrition and progress data is shared.'\n )}\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={t('mybodyshield.pages.coaching.coach', '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={t('mybodyshield.pages.coaching.member', '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\">\n {t('mybodyshield.pages.coaching.specialty', 'Specialty')}\n </Label>\n <Input\n id=\"myb-coaching-specialty\"\n value={form.specialty}\n onChange={(event) => setForm((f) => ({ ...f, specialty: event.target.value }))}\n placeholder={t(\n 'mybodyshield.pages.coaching.specialtyPlaceholder',\n 'e.g., Strength & conditioning'\n )}\n />\n </div>\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {t('mybodyshield.pages.coaching.sharedWithCoach', 'Shared with the coach')}\n </p>\n <ShareToggle\n id=\"myb-coaching-share-workouts\"\n label={t('mybodyshield.pages.coaching.workouts', 'Workouts')}\n description={t(\n 'mybodyshield.pages.coaching.workoutsDescription',\n 'Programs, sessions and logged sets.'\n )}\n checked={form.shareWorkouts}\n onChange={(value) => setForm((f) => ({ ...f, shareWorkouts: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-nutrition\"\n label={t('mybodyshield.pages.coaching.nutrition', 'Nutrition')}\n description={t(\n 'mybodyshield.pages.coaching.nutritionDescription',\n 'Plans, meals and daily macros.'\n )}\n checked={form.shareNutrition}\n onChange={(value) => setForm((f) => ({ ...f, shareNutrition: value }))}\n />\n <ShareToggle\n id=\"myb-coaching-share-metrics\"\n label={t('mybodyshield.pages.coaching.metrics', 'Metrics')}\n description={t(\n 'mybodyshield.pages.coaching.metricsDescription',\n 'Body metrics and recovery logs.'\n )}\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 {t('mybodyshield.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"submit\" disabled={create.isPending}>\n {create.isPending\n ? t('mybodyshield.common.creating', 'Creating…')\n : t('mybodyshield.pages.coaching.createCoachingButton', 'Create coaching')}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ============================================================================\n// Sharing scope chips\n// ============================================================================\n\nfunction ShareChip({\n t,\n label,\n icon,\n on,\n}: {\n t: ReturnType<typeof useTr>;\n label: string;\n icon: typeof Dumbbell;\n on: boolean;\n}) {\n const Icon = icon;\n const StatusIcon = on ? Check : Minus;\n return (\n <span\n aria-label={`${label} ${on ? t('mybodyshield.pages.coaching.shared', 'shared') : t('mybodyshield.pages.coaching.notShared', 'not shared')}`}\n className={`inline-flex items-center gap-1.5 rounded-full py-0.5 pl-1 pr-2.5 text-xs font-medium ${\n on\n ? 'bg-status-success-bg-subtle text-status-success-text ring-1 ring-inset ring-[color-mix(in_oklab,var(--color-status-success-text)_28%,transparent)]'\n : 'bg-bg-sunken text-text-tertiary ring-1 ring-inset ring-border-seam'\n }`}\n >\n <span\n aria-hidden=\"true\"\n className={`inline-flex size-4 items-center justify-center rounded-full ${\n on\n ? 'bg-[color-mix(in_oklab,var(--color-status-success-text)_22%,transparent)]'\n : 'bg-[color-mix(in_oklab,var(--color-text-tertiary)_18%,transparent)]'\n }`}\n >\n <StatusIcon className=\"h-2.5 w-2.5\" />\n </span>\n <Icon className=\"h-3 w-3\" aria-hidden=\"true\" />\n <span>\n {label} ·{' '}\n {on\n ? t('mybodyshield.pages.coaching.on', 'On')\n : t('mybodyshield.pages.coaching.off', 'Off')}\n </span>\n </span>\n );\n}\n\n// ============================================================================\n// Coaching card\n// ============================================================================\n\nfunction CoachingCard({\n relationship,\n currentUserId,\n busy,\n onTransition,\n onOpenClient,\n}: {\n relationship: MybCoachingRelationship;\n currentUserId?: string;\n busy: boolean;\n onTransition: (id: string, status: MybCoachingStatus) => void;\n onOpenClient: (memberId: string) => void;\n}) {\n const t = useTr();\n const transitions = useTransitions(t);\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 const reduce = useReducedMotion();\n\n const isActive = relationship.status === 'ACTIVE';\n const isPending = relationship.status === 'PENDING';\n\n return (\n <GlassCard\n treatment=\"glass\"\n glow={isActive}\n className={\n 'flex h-full flex-col gap-4 p-5 transition-shadow hover:shadow-float' +\n (isPending\n ? ' ring-1 ring-inset ring-[color-mix(in_oklab,var(--color-status-warning-text)_35%,transparent)]'\n : '')\n }\n >\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() ||\n t('mybodyshield.pages.coaching.coachingRelationship', 'Coaching relationship')}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {t('mybodyshield.pages.coaching.coachMember', 'Coach ↔ member')}\n </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=\"flex items-center gap-2.5 rounded-lg bg-bg-sunken/60 p-2.5\">\n <span\n aria-hidden=\"true\"\n className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${\n coachIsYou\n ? 'bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]'\n : 'bg-bg-surface text-text-tertiary ring-border-seam'\n }`}\n >\n <User className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n {t('mybodyshield.pages.coaching.coachLabel', '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\n ? t('mybodyshield.pages.coaching.you', 'You')\n : `#${shortId(relationship.coachId)}`}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2.5 rounded-lg bg-bg-sunken/60 p-2.5\">\n <span\n aria-hidden=\"true\"\n className={`flex size-8 shrink-0 items-center justify-center rounded-full ring-1 ring-inset ${\n memberIsYou\n ? 'bg-[color-mix(in_oklab,var(--color-action-primary-bg)_16%,transparent)] text-action-primary-bg ring-[color-mix(in_oklab,var(--color-action-primary-bg)_40%,transparent)]'\n : 'bg-bg-surface text-text-tertiary ring-border-seam'\n }`}\n >\n <User className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-tertiary\">\n {t('mybodyshield.pages.coaching.memberLabel', '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\n ? t('mybodyshield.pages.coaching.you', 'You')\n : `#${shortId(relationship.memberId)}`}\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap gap-1.5\">\n <ShareChip\n t={t}\n label={t('mybodyshield.pages.coaching.workouts', 'Workouts')}\n icon={Dumbbell}\n on={relationship.shareWorkouts}\n />\n <ShareChip\n t={t}\n label={t('mybodyshield.pages.coaching.nutrition', 'Nutrition')}\n icon={Salad}\n on={relationship.shareNutrition}\n />\n <ShareChip\n t={t}\n label={t('mybodyshield.pages.coaching.metrics', 'Metrics')}\n icon={LineChart}\n on={relationship.shareMetrics}\n />\n </div>\n <p className=\"text-xs leading-relaxed text-text-tertiary\">\n {accessNote(t, relationship.status)}\n </p>\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 {t('mybodyshield.pages.coaching.started', 'Started {{date}}', {\n date: formatDate(relationship.startedAt),\n })}\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 {t('mybodyshield.pages.coaching.ended', 'Ended {{date}}', {\n date: formatDate(relationship.endedAt),\n })}\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\n ? t('mybodyshield.pages.coaching.hideDiscussion', 'Hide discussion')\n : t('mybodyshield.pages.coaching.discussion', 'Discussion')}\n {showDiscussion ? (\n <ChevronUp className=\"h-4 w-4\" />\n ) : (\n <ChevronDown className=\"h-4 w-4\" />\n )}\n </button>\n <AnimatePresence initial={false}>\n {showDiscussion && (\n <motion.div\n key=\"discussion\"\n initial={reduce ? false : { opacity: 0, height: 0 }}\n animate={{ opacity: 1, height: 'auto' }}\n exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}\n transition={reduce ? { duration: 0 } : { duration: 0.25, ease: MOTION_EASE }}\n className=\"overflow-hidden\"\n >\n <div className=\"mt-3\">\n <MybDiscussion\n entityType=\"mybodyshield-coaching\"\n entityId={relationship.id}\n title={t(\n 'mybodyshield.pages.coaching.coachingDiscussion',\n 'Coaching discussion'\n )}\n description={t(\n 'mybodyshield.pages.coaching.coachingDiscussionDescription',\n 'A private thread between the coach and member.'\n )}\n />\n </div>\n </motion.div>\n )}\n </AnimatePresence>\n </div>\n )}\n {isActive && coachIsYou && (\n <Button variant=\"outline\" size=\"sm\" onClick={() => onOpenClient(relationship.memberId)}>\n {t('mybodyshield.pages.coaching.openClient', 'Open client workspace')}\n </Button>\n )}\n </GlassCard>\n );\n}\n\n// ============================================================================\n// Sharing-enforcement explainer\n// ============================================================================\n\n/**\n * \"Who can see what\" panel. Every statement here mirrors behaviour that is\n * implemented in the subgraph today — see `ENFORCEMENT_FACTS` for the source\n * files. Nothing here is aspirational.\n */\nfunction SharingEnforcement() {\n const t = useTr();\n const facts = useEnforcementFacts(t);\n return (\n <Section\n title={t('mybodyshield.pages.coaching.sharingEnforcementTitle', 'How sharing is enforced')}\n description={t(\n 'mybodyshield.pages.coaching.sharingEnforcementDescription',\n 'Private by default — a coach only ever sees the categories you switched on, for as long as the relationship is active.'\n )}\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <ul className=\"grid gap-4 sm:grid-cols-2\">\n {facts.map((fact) => {\n const Icon = fact.icon;\n return (\n <li key={fact.title} className=\"flex items-start gap-3\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">{fact.title}</p>\n <p className=\"mt-0.5 text-xs leading-relaxed text-text-secondary\">\n {fact.detail}\n </p>\n </div>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n </Section>\n );\n}\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport function CoachingPage() {\n const t = useTr();\n const { currentUser } = useMyBodyShield();\n const { navigateTo } = useMyBodyShield();\n const reduce = useReducedMotion();\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 paginated connection — fetch every relationship\n // once and 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?.items ?? [];\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: t('mybodyshield.common.all', 'All'), count: all.length },\n ...FILTER_ORDER.map((status) => ({\n value: status as StatusFilter,\n label: coachingStatusLabel(t, status),\n count: counts[status],\n })),\n ],\n [all.length, counts, t]\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(\n err instanceof Error\n ? err.message\n : t('mybodyshield.pages.coaching.failedToUpdate', 'Failed to update the relationship.')\n );\n } finally {\n setTransitioningId(null);\n }\n };\n\n const showLoading = coaching.isLoading && !coaching.data;\n\n return (\n <PageLayout\n title={t('mybodyshield.pages.coaching.title', 'Coaching')}\n description={t(\n 'mybodyshield.pages.coaching.description',\n \"Coach ↔ member relationships and what's shared.\"\n )}\n actions={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('mybodyshield.pages.coaching.newCoaching', '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\n label={t('mybodyshield.pages.coaching.loading', 'Loading coaching relationships…')}\n />\n ) : coaching.isError ? (\n <ErrorView\n message={t(\n 'mybodyshield.pages.coaching.error',\n \"We couldn't load your coaching relationships.\"\n )}\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\n ? t('mybodyshield.pages.coaching.emptyTitle', 'No coaching relationships')\n : t(\n 'mybodyshield.pages.coaching.emptyFilteredTitle',\n 'No relationships match this filter'\n )\n }\n description={\n all.length === 0\n ? t(\n 'mybodyshield.pages.coaching.emptyDescription',\n 'Connect a coach and a member to start sharing training, nutrition and progress data.'\n )\n : t(\n 'mybodyshield.pages.coaching.emptyFilteredDescription',\n 'Try a different status filter, or create a new coaching relationship.'\n )\n }\n action={\n <Button onClick={() => setCreateOpen(true)}>\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('mybodyshield.pages.coaching.newCoaching', 'New coaching')}\n </Button>\n }\n />\n ) : (\n <motion.div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\"\n variants={reduce ? undefined : STAGGER_CONTAINER}\n initial={reduce ? false : 'hidden'}\n animate=\"show\"\n >\n {rows.map((relationship) => (\n <motion.div\n key={relationship.id}\n className=\"h-full\"\n variants={reduce ? undefined : RISE_ITEM}\n >\n <CoachingCard\n relationship={relationship}\n currentUserId={currentUser?.id}\n busy={transitioningId === relationship.id}\n onTransition={(id, status) => void handleTransition(id, status)}\n onOpenClient={(memberId) => navigateTo(`coaching/${memberId}`)}\n />\n </motion.div>\n ))}\n </motion.div>\n )}\n\n <SharingEnforcement />\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":"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"}
@@ -0,0 +1,6 @@
1
+ import { MybInsights } from '../../types';
2
+
3
+ /** True only when at least one real source row contributes to the roll-up. */
4
+ export declare function hasCoachAnalyticsData(insights: MybInsights | null | undefined): boolean;
5
+ export declare function formatCoachPercent(value: number): string;
6
+ //# sourceMappingURL=coachAnalytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coachAnalytics.d.ts","sourceRoot":"","sources":["../../../../src/mybodyshield/pages/coaching/coachAnalytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAUvF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD"}
@@ -0,0 +1,13 @@
1
+ //#region src/mybodyshield/pages/coaching/coachAnalytics.ts
2
+ function e(e) {
3
+ if (!e) return !1;
4
+ let { summary: t, daily: n } = e;
5
+ return t.totalSessions > 0 || t.totalVolumeKg > 0 || t.averageReadiness > 0 || t.macroAdherencePercent > 0 || n.some((e) => e.weight != null || e.readiness != null || e.sleepHours != null);
6
+ }
7
+ function t(e) {
8
+ return `${Math.round(e)}%`;
9
+ }
10
+ //#endregion
11
+ export { t as formatCoachPercent, e as hasCoachAnalyticsData };
12
+
13
+ //# sourceMappingURL=coachAnalytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coachAnalytics.js","names":[],"sources":["../../../../src/mybodyshield/pages/coaching/coachAnalytics.ts"],"sourcesContent":["import type { MybInsights } from '../../types';\n\n/** True only when at least one real source row contributes to the roll-up. */\nexport function hasCoachAnalyticsData(insights: MybInsights | null | undefined): boolean {\n if (!insights) return false;\n const { summary, daily } = insights;\n return (\n summary.totalSessions > 0 ||\n summary.totalVolumeKg > 0 ||\n summary.averageReadiness > 0 ||\n summary.macroAdherencePercent > 0 ||\n daily.some((day) => day.weight != null || day.readiness != null || day.sleepHours != null)\n );\n}\n\nexport function formatCoachPercent(value: number): string {\n return `${Math.round(value)}%`;\n}\n"],"mappings":";AAGA,SAAgB,EAAsB,GAAmD;AACvF,KAAI,CAAC,EAAU,QAAO;CACtB,IAAM,EAAE,YAAS,aAAU;AAC3B,QACE,EAAQ,gBAAgB,KACxB,EAAQ,gBAAgB,KACxB,EAAQ,mBAAmB,KAC3B,EAAQ,wBAAwB,KAChC,EAAM,MAAM,MAAQ,EAAI,UAAU,QAAQ,EAAI,aAAa,QAAQ,EAAI,cAAc,KAAK;;AAI9F,SAAgB,EAAmB,GAAuB;AACxD,QAAO,GAAG,KAAK,MAAM,EAAM,CAAC"}
@@ -277,7 +277,7 @@ function K() {
277
277
  children: z.map((e) => /* @__PURE__ */ F("button", {
278
278
  type: "button",
279
279
  onClick: () => H(e.days),
280
- className: `rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${V === e.days ? "bg-accent text-white" : "bg-bg-sunken text-text-secondary hover:bg-bg-elevated"}`,
280
+ className: `rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${V === e.days ? "bg-accent text-action-primary-text" : "bg-bg-sunken text-text-secondary hover:bg-bg-elevated"}`,
281
281
  children: C(`mybodyshield.pages.insights.${e.labelKey}`, e.fallback)
282
282
  }, e.days))
283
283
  }),