@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,3 +1,45 @@
|
|
|
1
|
+
import { useMyBodyShield as e } from "../context/MyBodyShieldContext.js";
|
|
1
2
|
import "../context/index.js";
|
|
2
|
-
import "./useMybFiles.js";
|
|
3
|
-
import "@tanstack/react-query";
|
|
3
|
+
import { wspaceFetch as t } from "./useMybFiles.js";
|
|
4
|
+
import { useMutation as n, useQuery as r, useQueryClient as i } from "@tanstack/react-query";
|
|
5
|
+
//#region src/mybodyshield/hooks/useMybComments.ts
|
|
6
|
+
var a = (e, t) => [
|
|
7
|
+
"mybodyshield",
|
|
8
|
+
"comments",
|
|
9
|
+
e,
|
|
10
|
+
t
|
|
11
|
+
], o = (n, i) => {
|
|
12
|
+
let { apiGatewayUrl: o } = e();
|
|
13
|
+
return r({
|
|
14
|
+
queryKey: a(n, i ?? ""),
|
|
15
|
+
enabled: !!i,
|
|
16
|
+
queryFn: async () => {
|
|
17
|
+
let e = await t("query MybGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }", { input: {
|
|
18
|
+
entityType: n,
|
|
19
|
+
entityId: i,
|
|
20
|
+
take: 100,
|
|
21
|
+
parentCommentId: null
|
|
22
|
+
} }, o), r = e.getEntityComments.items.filter((e) => !e.parentCommentId);
|
|
23
|
+
return r.sort((e, t) => new Date(e.createdAt).getTime() - new Date(t.createdAt).getTime()), {
|
|
24
|
+
items: r,
|
|
25
|
+
total: e.getEntityComments.total
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}, s = (r, o) => {
|
|
30
|
+
let { apiGatewayUrl: s } = e(), c = i();
|
|
31
|
+
return n({
|
|
32
|
+
mutationFn: async (e) => (await t("mutation MybCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }", { input: {
|
|
33
|
+
entityType: r,
|
|
34
|
+
entityId: o,
|
|
35
|
+
content: { text: e }
|
|
36
|
+
} }, s)).createEntityComment,
|
|
37
|
+
onSuccess: () => {
|
|
38
|
+
c.invalidateQueries({ queryKey: a(r, o) });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { o as useMybComments, s as useMybCreateComment };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=useMybComments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMybComments.js","names":[],"sources":["../../../src/mybodyshield/hooks/useMybComments.ts"],"sourcesContent":["/**\n * MyBodyShield — wspace-conversations-svc EntityComment REUSE hooks (Integration B).\n * @module mybodyshield/hooks/useMybComments\n *\n * Adds a discussion surface to mybodyshield entities WITHOUT any mybodyshield-svc change by\n * reusing the generic EntityComment API composed into the WORKSPACE supergraph.\n * Comments attach to any entity by `(entityType, entityId)`:\n * - `getEntityComments(input): EntityCommentConnection!`\n * - `createEntityComment(input): EntityComment!`\n *\n * Transport reuses `wspaceFetch` from `useMybFiles` (workspace gateway +\n * workspace token). Lives in a separate file; `useMyBodyShieldApi.ts` is\n * untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useMyBodyShield } from '../context';\nimport { wspaceFetch } from './useMybFiles';\n\nexport interface MybComment {\n id: string;\n authorId: string;\n authorType: string;\n content: {\n text: string | null;\n markdown: string | null;\n };\n createdAt: string;\n edited: boolean;\n parentCommentId: string | null;\n replyCount: number;\n}\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['mybodyshield', 'comments', entityType, entityId] as const;\n\n/** Top-level (root) comments for an mybodyshield entity, oldest first. */\nexport const useMybComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{\n getEntityComments: { items: MybComment[]; total: number };\n }>(\n `query MybGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }`,\n {\n input: {\n entityType,\n entityId,\n take: 100,\n parentCommentId: null,\n },\n },\n apiGatewayUrl\n );\n // Render root comments oldest-first (server order is not guaranteed).\n const roots = data.getEntityComments.items.filter((c) => !c.parentCommentId);\n roots.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());\n return { items: roots, total: data.getEntityComments.total };\n },\n });\n};\n\nexport const useMybCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const data = await wspaceFetch<{ createEntityComment: { id: string } }>(\n `mutation MybCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n {\n input: {\n entityType,\n entityId,\n content: { text },\n },\n },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) });\n },\n });\n};\n"],"mappings":";;;;;AAkCA,IAAM,KAAe,GAAoB,MACvC;CAAC;CAAgB;CAAY;CAAY;CAAS,EAGvC,KAAkB,GAAoB,MAAiC;CAClF,IAAM,EAAE,qBAAkB,GAAiB;AAC3C,QAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,GAAG;EACjD,SAAS,EAAQ;EACjB,SAAS,YAAY;GACnB,IAAM,IAAO,MAAM,EAGjB,6YAeA,EACE,OAAO;IACL;IACA;IACA,MAAM;IACN,iBAAiB;IAClB,EACF,EACD,EACD,EAEK,IAAQ,EAAK,kBAAkB,MAAM,QAAQ,MAAM,CAAC,EAAE,gBAAgB;AAE5E,UADA,EAAM,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,EAChF;IAAE,OAAO;IAAO,OAAO,EAAK,kBAAkB;IAAO;;EAE/D,CAAC;GAGS,KAAuB,GAAoB,MAAqB;CAC3E,IAAM,EAAE,qBAAkB,GAAiB,EACrC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,OACJ,MAAM,EACjB,wIAGA,EACE,OAAO;GACL;GACA;GACA,SAAS,EAAE,SAAM;GAClB,EACF,EACD,EACD,EACW;EAEd,iBAAiB;AACf,KAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,EAAS,EAAE,CAAC;;EAExE,CAAC"}
|
|
@@ -1,4 +1,81 @@
|
|
|
1
|
+
import { useMyBodyShield as e } from "../context/MyBodyShieldContext.js";
|
|
1
2
|
import "../context/index.js";
|
|
2
|
-
import "react";
|
|
3
|
-
import "@tanstack/react-query";
|
|
4
|
-
import "@burdenoff/fe-libs/shared/graphql";
|
|
3
|
+
import { useCallback as t, useState as n } from "react";
|
|
4
|
+
import { useQuery as r } from "@tanstack/react-query";
|
|
5
|
+
import { graphqlFetch as i } from "@burdenoff/fe-libs/shared/graphql";
|
|
6
|
+
//#region src/mybodyshield/hooks/useMybFiles.ts
|
|
7
|
+
var a = () => {
|
|
8
|
+
let e = globalThis;
|
|
9
|
+
e.__graphqlFetchResponseCache?.clear(), e.__graphqlFetchPendingResponses?.clear(), e.__boffGraphQLResponseCache?.clear();
|
|
10
|
+
}, o = async (e, t, n) => {
|
|
11
|
+
let r = await i({
|
|
12
|
+
gateway: "workspace",
|
|
13
|
+
baseUrl: n,
|
|
14
|
+
query: e,
|
|
15
|
+
variables: t,
|
|
16
|
+
workspaceToken: !0
|
|
17
|
+
});
|
|
18
|
+
if (r.errors?.length) throw Error(r.errors[0]?.message ?? "GraphQL request failed");
|
|
19
|
+
if (!r.data) throw Error("GraphQL response did not include data");
|
|
20
|
+
return /^\s*mutation\b/.test(e) && a(), r.data;
|
|
21
|
+
};
|
|
22
|
+
function s(e, t, n, r) {
|
|
23
|
+
return new Promise((i, a) => {
|
|
24
|
+
let o = new XMLHttpRequest();
|
|
25
|
+
o.open("PUT", e), o.setRequestHeader("Content-Type", n), o.upload.onprogress = (e) => {
|
|
26
|
+
e.lengthComputable && r(Math.round(e.loaded / e.total * 100));
|
|
27
|
+
}, o.onload = () => {
|
|
28
|
+
o.status >= 200 && o.status < 300 ? i() : a(/* @__PURE__ */ Error(`File storage rejected the upload (HTTP ${o.status}).`));
|
|
29
|
+
}, o.onerror = () => a(/* @__PURE__ */ Error("Network error while uploading the file.")), o.onabort = () => a(/* @__PURE__ */ Error("The upload was cancelled.")), o.send(t);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
var c = () => {
|
|
33
|
+
let { apiGatewayUrl: r } = e(), [i, a] = n(!1), [c, l] = n(0), [u, d] = n(null), f = t(() => {
|
|
34
|
+
a(!1), l(0), d(null);
|
|
35
|
+
}, []);
|
|
36
|
+
return {
|
|
37
|
+
upload: t(async (e) => {
|
|
38
|
+
a(!0), l(0), d(null);
|
|
39
|
+
try {
|
|
40
|
+
let t = e.name.lastIndexOf("."), n = t > -1 ? e.name.slice(t + 1) : "", i = e.type || "application/octet-stream", a = await o("mutation MybInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }", { input: {
|
|
41
|
+
fileName: e.name,
|
|
42
|
+
extension: n,
|
|
43
|
+
mimeType: i,
|
|
44
|
+
fileSize: String(e.size),
|
|
45
|
+
visibility: "WORKSPACE"
|
|
46
|
+
} }, r), { uploadUrl: c } = a.initiateUpload, u = a.initiateUpload.session.id;
|
|
47
|
+
if (!c) throw Error("The file service did not return an upload URL.");
|
|
48
|
+
await s(c, e, i, l);
|
|
49
|
+
let d = await o("mutation MybCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }", { input: { sessionId: u } }, r);
|
|
50
|
+
return l(100), {
|
|
51
|
+
fileId: d.completeUpload.fileId,
|
|
52
|
+
fileName: d.completeUpload.file?.name ?? e.name
|
|
53
|
+
};
|
|
54
|
+
} catch (e) {
|
|
55
|
+
let t = e instanceof Error ? e.message : "The file could not be uploaded.";
|
|
56
|
+
throw d(t), e instanceof Error ? e : Error(t);
|
|
57
|
+
} finally {
|
|
58
|
+
a(!1);
|
|
59
|
+
}
|
|
60
|
+
}, [r]),
|
|
61
|
+
isUploading: i,
|
|
62
|
+
progress: c,
|
|
63
|
+
error: u,
|
|
64
|
+
reset: f
|
|
65
|
+
};
|
|
66
|
+
}, l = (t) => {
|
|
67
|
+
let { apiGatewayUrl: n } = e();
|
|
68
|
+
return r({
|
|
69
|
+
queryKey: [
|
|
70
|
+
"mybodyshield",
|
|
71
|
+
"file",
|
|
72
|
+
t ?? ""
|
|
73
|
+
],
|
|
74
|
+
enabled: !!t,
|
|
75
|
+
queryFn: async () => (await o("query MybGetFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }", { id: t }, n)).getFile
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
export { l as useMybFile, c as useMybFileUpload, o as wspaceFetch };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=useMybFiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMybFiles.js","names":[],"sources":["../../../src/mybodyshield/hooks/useMybFiles.ts"],"sourcesContent":["/**\n * MyBodyShield — wspace-files-svc REUSE hooks (Integration A).\n * @module mybodyshield/hooks/useMybFiles\n *\n * Reuses the platform file store (`wspace-files-svc`) WITHOUT any mybodyshield-svc\n * change: the eco schema already exposes `MybObservation.evidenceFileId` /\n * `MybReport.fileId`; this module lets the UI actually produce a `fileId` via\n * the canonical 3-step upload flow composed into the WORKSPACE supergraph:\n * 1. `initiateUpload(input)` → `{ uploadUrl, session { id } }`\n * 2. HTTP `PUT` the bytes to the returned signed `uploadUrl`\n * 3. `completeUpload(input: { sessionId })` → `{ fileId }`\n * plus `getFile(id)` to resolve an existing evidence/report file's name + URL.\n *\n * Transport mirrors `useMyBodyShieldApi` exactly — `graphqlFetch({ gateway:\n * 'workspace', workspaceToken: true })` — so no ApolloProvider is required.\n * These hooks live in a SEPARATE file by design; `useMyBodyShieldApi.ts` is\n * left untouched.\n */\n\nimport { useCallback, useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useMyBodyShield } from '../context';\n\n// ============================================================================\n// Shared workspace-gateway transport (reused by the comments + tags hooks too)\n// ============================================================================\n\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in a short-lived in-memory\n * cache. After a write we drop those caches so the invalidation refetch that\n * follows reads fresh data (same remedy `useMyBodyShieldApi` applies).\n */\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\n/** Execute a workspace-gateway GraphQL operation with the workspace token. */\nexport const wspaceFetch = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// ============================================================================\n// File upload (initiate → PUT → complete)\n// ============================================================================\n\nexport interface UploadedMybFile {\n fileId: string;\n fileName: string;\n}\n\n/** `PUT` the blob to the signed URL, reporting byte-progress via `onProgress`. */\nfunction putWithProgress(\n url: string,\n file: File,\n contentType: string,\n onProgress: (percent: number) => void\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('PUT', url);\n xhr.setRequestHeader('Content-Type', contentType);\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable) {\n onProgress(Math.round((event.loaded / event.total) * 100));\n }\n };\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`File storage rejected the upload (HTTP ${xhr.status}).`));\n }\n };\n xhr.onerror = () => reject(new Error('Network error while uploading the file.'));\n xhr.onabort = () => reject(new Error('The upload was cancelled.'));\n xhr.send(file);\n });\n}\n\nexport interface UseMybFileUploadResult {\n upload: (file: File) => Promise<UploadedMybFile>;\n isUploading: boolean;\n /** 0–100 byte-progress of the signed-URL PUT. */\n progress: number;\n error: string | null;\n reset: () => void;\n}\n\n/**\n * Imperative upload hook: `await upload(file)` runs the platform 3-step flow and\n * resolves to `{ fileId, fileName }`. `fileId` is what the eco mutations accept\n * (`MybRecordMetricInput.photoFileId`, `MybCreateExerciseInput.demoFileId`).\n */\nexport const useMybFileUpload = (): UseMybFileUploadResult => {\n const { apiGatewayUrl } = useMyBodyShield();\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const reset = useCallback(() => {\n setIsUploading(false);\n setProgress(0);\n setError(null);\n }, []);\n\n const upload = useCallback(\n async (file: File): Promise<UploadedMybFile> => {\n setIsUploading(true);\n setProgress(0);\n setError(null);\n try {\n const dotIndex = file.name.lastIndexOf('.');\n const extension = dotIndex > -1 ? file.name.slice(dotIndex + 1) : '';\n const mimeType = file.type || 'application/octet-stream';\n\n // 1. initiate\n const initiated = await wspaceFetch<{\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n }>(\n `mutation MybInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n extension,\n mimeType,\n fileSize: String(file.size),\n visibility: 'WORKSPACE',\n },\n },\n apiGatewayUrl\n );\n\n const { uploadUrl } = initiated.initiateUpload;\n const sessionId = initiated.initiateUpload.session.id;\n if (!uploadUrl) {\n throw new Error('The file service did not return an upload URL.');\n }\n\n // 2. PUT the bytes\n await putWithProgress(uploadUrl, file, mimeType, setProgress);\n\n // 3. complete\n const completed = await wspaceFetch<{\n completeUpload: { fileId: string; file: { id: string; name: string } | null };\n }>(\n `mutation MybCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file { id name }\n }\n }`,\n { input: { sessionId } },\n apiGatewayUrl\n );\n\n setProgress(100);\n return {\n fileId: completed.completeUpload.fileId,\n fileName: completed.completeUpload.file?.name ?? file.name,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : 'The file could not be uploaded.';\n setError(message);\n throw err instanceof Error ? err : new Error(message);\n } finally {\n setIsUploading(false);\n }\n },\n [apiGatewayUrl]\n );\n\n return { upload, isUploading, progress, error, reset };\n};\n\n// ============================================================================\n// Resolve an existing file (name + download URL) for rendering evidence links\n// ============================================================================\n\nexport interface MybFileMeta {\n id: string;\n name: string;\n downloadUrl: string | null;\n mimeType: string;\n size: string;\n}\n\nexport const useMybFile = (id: string | null | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: ['mybodyshield', 'file', id ?? ''],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await wspaceFetch<{ getFile: MybFileMeta | null }>(\n `query MybGetFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.getFile;\n },\n });\n};\n"],"mappings":"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"}
|
|
@@ -1,3 +1,89 @@
|
|
|
1
|
+
import { useMyBodyShield as e } from "../context/MyBodyShieldContext.js";
|
|
1
2
|
import "../context/index.js";
|
|
2
|
-
import "./useMybFiles.js";
|
|
3
|
-
import "@tanstack/react-query";
|
|
3
|
+
import { wspaceFetch as t } from "./useMybFiles.js";
|
|
4
|
+
import { useMutation as n, useQuery as r, useQueryClient as i } from "@tanstack/react-query";
|
|
5
|
+
//#region src/mybodyshield/hooks/useMybTags.ts
|
|
6
|
+
var a = "mybodyshield", o = (e, t) => [
|
|
7
|
+
"mybodyshield",
|
|
8
|
+
"entity-tags",
|
|
9
|
+
e,
|
|
10
|
+
t
|
|
11
|
+
], s = "id label color key", c = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "tag", l = (n, i) => {
|
|
12
|
+
let { apiGatewayUrl: a } = e();
|
|
13
|
+
return r({
|
|
14
|
+
queryKey: o(n, i ?? ""),
|
|
15
|
+
enabled: !!i,
|
|
16
|
+
queryFn: async () => (await t(`query MybGetEntityTags($entityType: String!, $entityId: ID!) {
|
|
17
|
+
getEntityTags(entityType: $entityType, entityId: $entityId) {
|
|
18
|
+
id
|
|
19
|
+
tag { ${s} }
|
|
20
|
+
}
|
|
21
|
+
}`, {
|
|
22
|
+
entityType: n,
|
|
23
|
+
entityId: i
|
|
24
|
+
}, a)).getEntityTags
|
|
25
|
+
});
|
|
26
|
+
}, u = (n) => {
|
|
27
|
+
let { apiGatewayUrl: i } = e(), a = n.trim();
|
|
28
|
+
return r({
|
|
29
|
+
queryKey: [
|
|
30
|
+
"mybodyshield",
|
|
31
|
+
"tag-search",
|
|
32
|
+
a
|
|
33
|
+
],
|
|
34
|
+
enabled: a.length > 0,
|
|
35
|
+
queryFn: async () => (await t(`query MybSearchTags($query: String!, $limit: Int) {
|
|
36
|
+
searchTags(query: $query, limit: $limit) { ${s} }
|
|
37
|
+
}`, {
|
|
38
|
+
query: a,
|
|
39
|
+
limit: 8
|
|
40
|
+
}, i)).searchTags
|
|
41
|
+
});
|
|
42
|
+
}, d = (r, a) => {
|
|
43
|
+
let { apiGatewayUrl: s } = e(), c = i();
|
|
44
|
+
return n({
|
|
45
|
+
mutationFn: async (e) => (await t("mutation MybAssignTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }", { input: {
|
|
46
|
+
entityType: r,
|
|
47
|
+
entityId: a,
|
|
48
|
+
tagId: e
|
|
49
|
+
} }, s)).assignTag,
|
|
50
|
+
onSuccess: () => c.invalidateQueries({ queryKey: o(r, a) })
|
|
51
|
+
});
|
|
52
|
+
}, f = (r, a) => {
|
|
53
|
+
let { apiGatewayUrl: s } = e(), c = i();
|
|
54
|
+
return n({
|
|
55
|
+
mutationFn: async (e) => (await t("mutation MybRemoveTagAssignment($id: ID!) {\n removeTagAssignment(id: $id)\n }", { id: e }, s)).removeTagAssignment,
|
|
56
|
+
onSuccess: () => c.invalidateQueries({ queryKey: o(r, a) })
|
|
57
|
+
});
|
|
58
|
+
}, p = async (e) => {
|
|
59
|
+
let n = await t("query MybGetVocabulary($key: String!) {\n getVocabularyByKey(key: $key) { id }\n }", { key: a }, e);
|
|
60
|
+
return n.getVocabularyByKey?.id ? n.getVocabularyByKey.id : (await t("mutation MybCreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) { id }\n }", { input: {
|
|
61
|
+
key: a,
|
|
62
|
+
name: "MyBodyShield",
|
|
63
|
+
description: "Tags for MyBodyShield programs, coaching and other records.",
|
|
64
|
+
allowCustomTags: !0
|
|
65
|
+
} }, e)).createVocabulary.id;
|
|
66
|
+
}, m = (r, a) => {
|
|
67
|
+
let { apiGatewayUrl: s } = e(), l = i();
|
|
68
|
+
return n({
|
|
69
|
+
mutationFn: async (e) => {
|
|
70
|
+
let n = e.trim();
|
|
71
|
+
if (!n) throw Error("A tag label is required.");
|
|
72
|
+
let i = await t("mutation MybCreateTag($input: CreateTagInput!) {\n createTag(input: $input) { id }\n }", { input: {
|
|
73
|
+
vocabularyId: await p(s),
|
|
74
|
+
key: `${c(n)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
75
|
+
label: n
|
|
76
|
+
} }, s);
|
|
77
|
+
return await t("mutation MybAssignCreatedTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }", { input: {
|
|
78
|
+
entityType: r,
|
|
79
|
+
entityId: a,
|
|
80
|
+
tagId: i.createTag.id
|
|
81
|
+
} }, s), i.createTag;
|
|
82
|
+
},
|
|
83
|
+
onSuccess: () => l.invalidateQueries({ queryKey: o(r, a) })
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
export { d as useMybAssignTag, m as useMybCreateAndAssignTag, l as useMybEntityTags, f as useMybRemoveTagAssignment, u as useMybTagSearch };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=useMybTags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMybTags.js","names":[],"sources":["../../../src/mybodyshield/hooks/useMybTags.ts"],"sourcesContent":["/**\n * MyBodyShield — wspace-tags-svc UniversalTag REUSE hooks (Integration C).\n * @module mybodyshield/hooks/useMybTags\n *\n * The mybodyshield svc does NOT expose a tags field, so we drive the platform tag\n * assignment store directly (approach (b) in the reuse audit) — no mybodyshield-svc\n * change. All operations are composed into the WORKSPACE supergraph:\n * - `getEntityTags(entityType, entityId)` → current assignments (display)\n * - `searchTags(query)` → pick an existing tag\n * - `createTag(input)` → create a new tag (needs a vocabulary)\n * - `assignTag(input)` → link tag ↔ entity\n * - `removeTagAssignment(id)` → unlink\n *\n * `createTag` requires a `vocabularyId`; we resolve a stable, workspace-scoped\n * \"mybodyshield\" vocabulary (`getVocabularyByKey`, creating it on first use).\n *\n * Transport reuses `wspaceFetch` from `useMybFiles`. Lives in a separate file;\n * `useMyBodyShieldApi.ts` is untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useMyBodyShield } from '../context';\nimport { wspaceFetch } from './useMybFiles';\n\nexport interface MybTag {\n id: string;\n label: string;\n color: string | null;\n key: string;\n}\n\nexport interface MybTagAssignment {\n id: string;\n tag: MybTag;\n}\n\nconst MYB_VOCABULARY_KEY = 'mybodyshield';\n\nconst entityTagsKey = (entityType: string, entityId: string) =>\n ['mybodyshield', 'entity-tags', entityType, entityId] as const;\n\nconst TAG_FIELDS = 'id label color key';\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48) || 'tag';\n\n/** Current tag assignments for an mybodyshield entity. */\nexport const useMybEntityTags = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useMyBodyShield();\n return useQuery({\n queryKey: entityTagsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getEntityTags: MybTagAssignment[] }>(\n `query MybGetEntityTags($entityType: String!, $entityId: ID!) {\n getEntityTags(entityType: $entityType, entityId: $entityId) {\n id\n tag { ${TAG_FIELDS} }\n }\n }`,\n { entityType, entityId },\n apiGatewayUrl\n );\n return data.getEntityTags;\n },\n });\n};\n\n/** Search existing tags (enabled only for a non-empty query). */\nexport const useMybTagSearch = (query: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const trimmed = query.trim();\n return useQuery({\n queryKey: ['mybodyshield', 'tag-search', trimmed],\n enabled: trimmed.length > 0,\n queryFn: async () => {\n const data = await wspaceFetch<{ searchTags: MybTag[] }>(\n `query MybSearchTags($query: String!, $limit: Int) {\n searchTags(query: $query, limit: $limit) { ${TAG_FIELDS} }\n }`,\n { query: trimmed, limit: 8 },\n apiGatewayUrl\n );\n return data.searchTags;\n },\n });\n};\n\n/** Assign an existing tag to the entity. */\nexport const useMybAssignTag = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (tagId: string) => {\n const data = await wspaceFetch<{ assignTag: { id: string } }>(\n `mutation MybAssignTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }`,\n { input: { entityType, entityId, tagId } },\n apiGatewayUrl\n );\n return data.assignTag;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: entityTagsKey(entityType, entityId) }),\n });\n};\n\n/** Remove a tag assignment by its assignment id. */\nexport const useMybRemoveTagAssignment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assignmentId: string) => {\n const data = await wspaceFetch<{ removeTagAssignment: boolean }>(\n `mutation MybRemoveTagAssignment($id: ID!) {\n removeTagAssignment(id: $id)\n }`,\n { id: assignmentId },\n apiGatewayUrl\n );\n return data.removeTagAssignment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: entityTagsKey(entityType, entityId) }),\n });\n};\n\n/** Resolve (creating on first use) the workspace-scoped \"mybodyshield\" vocabulary. */\nconst resolveMybVocabularyId = async (baseUrl?: string): Promise<string> => {\n const existing = await wspaceFetch<{ getVocabularyByKey: { id: string } | null }>(\n `query MybGetVocabulary($key: String!) {\n getVocabularyByKey(key: $key) { id }\n }`,\n { key: MYB_VOCABULARY_KEY },\n baseUrl\n );\n if (existing.getVocabularyByKey?.id) {\n return existing.getVocabularyByKey.id;\n }\n const created = await wspaceFetch<{ createVocabulary: { id: string } }>(\n `mutation MybCreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) { id }\n }`,\n {\n input: {\n key: MYB_VOCABULARY_KEY,\n name: 'MyBodyShield',\n description: 'Tags for MyBodyShield programs, coaching and other records.',\n allowCustomTags: true,\n },\n },\n baseUrl\n );\n return created.createVocabulary.id;\n};\n\n/** Create a new tag from a free-text label and assign it to the entity. */\nexport const useMybCreateAndAssignTag = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useMyBodyShield();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (label: string) => {\n const trimmed = label.trim();\n if (!trimmed) {\n throw new Error('A tag label is required.');\n }\n const vocabularyId = await resolveMybVocabularyId(apiGatewayUrl);\n // A unique key avoids collisions with existing tags in the vocabulary;\n // the (non-unique) label is what users see.\n const key = `${slugify(trimmed)}-${Math.random().toString(36).slice(2, 6)}`;\n const created = await wspaceFetch<{ createTag: { id: string } }>(\n `mutation MybCreateTag($input: CreateTagInput!) {\n createTag(input: $input) { id }\n }`,\n { input: { vocabularyId, key, label: trimmed } },\n apiGatewayUrl\n );\n await wspaceFetch<{ assignTag: { id: string } }>(\n `mutation MybAssignCreatedTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }`,\n { input: { entityType, entityId, tagId: created.createTag.id } },\n apiGatewayUrl\n );\n return created.createTag;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: entityTagsKey(entityType, entityId) }),\n });\n};\n"],"mappings":";;;;;AAqCA,IAAM,IAAqB,gBAErB,KAAiB,GAAoB,MACzC;CAAC;CAAgB;CAAe;CAAY;CAAS,EAEjD,IAAa,sBAEb,KAAW,MACf,EACG,aAAa,CACb,MAAM,CACN,QAAQ,eAAe,IAAI,CAC3B,QAAQ,YAAY,GAAG,CACvB,MAAM,GAAG,GAAG,IAAI,OAGR,KAAoB,GAAoB,MAAiC;CACpF,IAAM,EAAE,qBAAkB,GAAiB;AAC3C,QAAO,EAAS;EACd,UAAU,EAAc,GAAY,KAAY,GAAG;EACnD,SAAS,EAAQ;EACjB,SAAS,aACM,MAAM,EACjB;;;oBAGY,EAAW;;YAGvB;GAAE;GAAY;GAAU,EACxB,EACD,EACW;EAEf,CAAC;GAIS,KAAmB,MAAkB;CAChD,IAAM,EAAE,qBAAkB,GAAiB,EACrC,IAAU,EAAM,MAAM;AAC5B,QAAO,EAAS;EACd,UAAU;GAAC;GAAgB;GAAc;GAAQ;EACjD,SAAS,EAAQ,SAAS;EAC1B,SAAS,aACM,MAAM,EACjB;uDAC+C,EAAW;YAE1D;GAAE,OAAO;GAAS,OAAO;GAAG,EAC5B,EACD,EACW;EAEf,CAAC;GAIS,KAAmB,GAAoB,MAAqB;CACvE,IAAM,EAAE,qBAAkB,GAAiB,EACrC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,OACJ,MAAM,EACjB,0GAGA,EAAE,OAAO;GAAE;GAAY;GAAU;GAAO,EAAE,EAC1C,EACD,EACW;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAc,GAAY,EAAS,EAAE,CAAC;EACzF,CAAC;GAIS,KAA6B,GAAoB,MAAqB;CACjF,IAAM,EAAE,qBAAkB,GAAiB,EACrC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,OACJ,MAAM,EACjB,kGAGA,EAAE,IAAI,GAAc,EACpB,EACD,EACW;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAc,GAAY,EAAS,EAAE,CAAC;EACzF,CAAC;GAIE,IAAyB,OAAO,MAAsC;CAC1E,IAAM,IAAW,MAAM,EACrB,8FAGA,EAAE,KAAK,GAAoB,EAC3B,EACD;AAkBD,QAjBI,EAAS,oBAAoB,KACxB,EAAS,mBAAmB,MAErB,MAAM,EACpB,uHAGA,EACE,OAAO;EACL,KAAK;EACL,MAAM;EACN,aAAa;EACb,iBAAiB;EAClB,EACF,EACD,EACD,EACc,iBAAiB;GAIrB,KAA4B,GAAoB,MAAqB;CAChF,IAAM,EAAE,qBAAkB,GAAiB,EACrC,IAAK,GAAgB;AAC3B,QAAO,EAAY;EACjB,YAAY,OAAO,MAAkB;GACnC,IAAM,IAAU,EAAM,MAAM;AAC5B,OAAI,CAAC,EACH,OAAU,MAAM,2BAA2B;GAM7C,IAAM,IAAU,MAAM,EACpB,0GAGA,EAAE,OAAO;IAAE,cARQ,MAAM,EAAuB,EAAc;IAQrC,KALf,GAAG,EAAQ,EAAQ,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE;IAKzC,OAAO;IAAS,EAAE,EAChD,EACD;AAQD,UAPA,MAAM,EACJ,iHAGA,EAAE,OAAO;IAAE;IAAY;IAAU,OAAO,EAAQ,UAAU;IAAI,EAAE,EAChE,EACD,EACM,EAAQ;;EAEjB,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAc,GAAY,EAAS,EAAE,CAAC;EACzF,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useQuery as e } from "@tanstack/react-query";
|
|
2
|
+
import { graphqlFetch as t } from "@burdenoff/fe-libs/shared/graphql";
|
|
3
|
+
//#region src/mybodyshield/hooks/useMybUsers.ts
|
|
4
|
+
var n = (e) => e.name?.trim() ? e.name.trim() : [e.firstName, e.lastName].filter(Boolean).join(" ").trim() || (e.username?.trim() ? e.username.trim() : e.email), r = async (e, n) => {
|
|
5
|
+
let r = await t({
|
|
6
|
+
gateway: "global",
|
|
7
|
+
query: e,
|
|
8
|
+
variables: n
|
|
9
|
+
});
|
|
10
|
+
if (r.errors?.length) throw Error(r.errors[0]?.message ?? "GraphQL request failed");
|
|
11
|
+
if (!r.data) throw Error("GraphQL response did not include data");
|
|
12
|
+
return r.data;
|
|
13
|
+
}, i = (t) => {
|
|
14
|
+
let n = t.trim();
|
|
15
|
+
return e({
|
|
16
|
+
queryKey: [
|
|
17
|
+
"mybodyshield",
|
|
18
|
+
"user-search",
|
|
19
|
+
n
|
|
20
|
+
],
|
|
21
|
+
enabled: n.length >= 2,
|
|
22
|
+
staleTime: 3e4,
|
|
23
|
+
retry: !1,
|
|
24
|
+
queryFn: async () => (await r("query MybUserSearch($filter: UserFilterInput, $pagination: PaginationInput) {\n users(filter: $filter, pagination: $pagination) {\n users { id name firstName lastName email username }\n hasMore\n }\n }", {
|
|
25
|
+
filter: { search: n },
|
|
26
|
+
pagination: {
|
|
27
|
+
limit: 8,
|
|
28
|
+
offset: 0
|
|
29
|
+
}
|
|
30
|
+
})).users.users
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { n as directoryUserName, i as useMybUserSearch };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=useMybUsers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMybUsers.js","names":[],"sources":["../../../src/mybodyshield/hooks/useMybUsers.ts"],"sourcesContent":["/**\n * MyBodyShield — global user directory search (identity reuse).\n * @module mybodyshield/hooks/useMybUsers\n *\n * Resolves real users (coach / member) from the global auth directory so the\n * coaching form can pick a person by name/email instead of pasting a raw UUID.\n * Reuses the composed GLOBAL supergraph `users(filter, pagination)` op — no\n * bespoke identity storage in the mybodyshield svc. Transport mirrors the other\n * hooks (`graphqlFetch`) but targets the GLOBAL gateway.\n */\n\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useMyBodyShield } from '../context';\n\nexport interface DirectoryUser {\n id: string;\n name: string | null;\n firstName: string | null;\n lastName: string | null;\n email: string;\n username: string;\n}\n\n/** Best display name for a directory user (name → first+last → username → email). */\nexport const directoryUserName = (user: DirectoryUser): string => {\n if (user.name?.trim()) return user.name.trim();\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n if (full) return full;\n if (user.username?.trim()) return user.username.trim();\n return user.email;\n};\n\nconst globalQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({ gateway: 'global', query, variables });\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n return response.data;\n};\n\n/**\n * Search the tenant user directory by name/email. Enabled once the term is at\n * least two characters; failures are surfaced to the caller (which offers a\n * manual-id fallback) rather than as a global error toast.\n */\nexport const useMybUserSearch = (search: string) => {\n const term = search.trim();\n return useQuery({\n queryKey: ['mybodyshield', 'user-search', term],\n enabled: term.length >= 2,\n staleTime: 30_000,\n retry: false,\n queryFn: async () => {\n const result = await globalQuery<{\n users: { users: DirectoryUser[]; hasMore: boolean };\n }>(\n `query MybUserSearch($filter: UserFilterInput, $pagination: PaginationInput) {\n users(filter: $filter, pagination: $pagination) {\n users { id name firstName lastName email username }\n hasMore\n }\n }`,\n { filter: { search: term }, pagination: { limit: 8, offset: 0 } }\n );\n return result.users.users;\n },\n });\n};\n"],"mappings":";;;AA0BA,IAAa,KAAqB,MAC5B,EAAK,MAAM,MAAM,GAAS,EAAK,KAAK,MAAM,GACjC,CAAC,EAAK,WAAW,EAAK,SAAS,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,CAAC,MAAM,KAEzE,EAAK,UAAU,MAAM,GAAS,EAAK,SAAS,MAAM,GAC/C,EAAK,QAGR,IAAc,OAClB,GACA,MACmB;CACnB,IAAM,IAAW,MAAM,EAAoB;EAAE,SAAS;EAAU;EAAO;EAAW,CAAC;AACnF,KAAI,EAAS,QAAQ,OACnB,OAAU,MAAM,EAAS,OAAO,IAAI,WAAW,yBAAyB;AAE1E,KAAI,CAAC,EAAS,KACZ,OAAU,MAAM,wCAAwC;AAE1D,QAAO,EAAS;GAQL,KAAoB,MAAmB;CAClD,IAAM,IAAO,EAAO,MAAM;AAC1B,QAAO,EAAS;EACd,UAAU;GAAC;GAAgB;GAAe;GAAK;EAC/C,SAAS,EAAK,UAAU;EACxB,WAAW;EACX,OAAO;EACP,SAAS,aACQ,MAAM,EAGnB,4PAMA;GAAE,QAAQ,EAAE,QAAQ,GAAM;GAAE,YAAY;IAAE,OAAO;IAAG,QAAQ;IAAG;GAAE,CAClE,EACa,MAAM;EAEvB,CAAC"}
|
|
@@ -327,46 +327,51 @@ function j() {
|
|
|
327
327
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
328
328
|
children: V.slice(0, 6).map((e) => {
|
|
329
329
|
let t = g(e), n = ne[e.type], o = n.icon;
|
|
330
|
-
return /* @__PURE__ */
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
330
|
+
return /* @__PURE__ */ O("button", {
|
|
331
|
+
type: "button",
|
|
332
|
+
onClick: () => j("goals"),
|
|
333
|
+
className: "text-left",
|
|
334
|
+
children: /* @__PURE__ */ k(A, {
|
|
335
|
+
treatment: "plain",
|
|
336
|
+
className: "h-full p-4 transition-shadow hover:shadow-float",
|
|
337
|
+
children: [/* @__PURE__ */ k("div", {
|
|
338
|
+
className: "flex items-start gap-3",
|
|
339
|
+
children: [/* @__PURE__ */ O(d, {
|
|
340
|
+
icon: /* @__PURE__ */ O(o, { className: "h-4 w-4" }),
|
|
341
|
+
tint: n.tint
|
|
342
|
+
}), /* @__PURE__ */ k("div", {
|
|
343
|
+
className: "min-w-0 flex-1",
|
|
344
|
+
children: [/* @__PURE__ */ O("p", {
|
|
345
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
346
|
+
children: e.title
|
|
347
|
+
}), /* @__PURE__ */ O("p", {
|
|
348
|
+
className: "text-xs text-text-tertiary",
|
|
349
|
+
children: r(e.type)
|
|
350
|
+
})]
|
|
346
351
|
})]
|
|
347
|
-
})]
|
|
348
|
-
}), /* @__PURE__ */ k("div", {
|
|
349
|
-
className: "mt-3 space-y-1.5",
|
|
350
|
-
children: [/* @__PURE__ */ O(m, {
|
|
351
|
-
value: t,
|
|
352
|
-
color: n.tint
|
|
353
352
|
}), /* @__PURE__ */ k("div", {
|
|
354
|
-
className: "
|
|
355
|
-
children: [/* @__PURE__ */
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
353
|
+
className: "mt-3 space-y-1.5",
|
|
354
|
+
children: [/* @__PURE__ */ O(m, {
|
|
355
|
+
value: t,
|
|
356
|
+
color: n.tint
|
|
357
|
+
}), /* @__PURE__ */ k("div", {
|
|
358
|
+
className: "flex items-center justify-between text-xs",
|
|
359
|
+
children: [/* @__PURE__ */ k("span", {
|
|
360
|
+
className: "tabular-nums text-text-secondary",
|
|
361
|
+
children: [
|
|
362
|
+
i(e.currentValue),
|
|
363
|
+
e.unit ? ` ${e.unit}` : "",
|
|
364
|
+
" / ",
|
|
365
|
+
i(e.targetValue),
|
|
366
|
+
e.unit ? ` ${e.unit}` : ""
|
|
367
|
+
]
|
|
368
|
+
}), /* @__PURE__ */ O("span", {
|
|
369
|
+
className: "tabular-nums text-text-tertiary",
|
|
370
|
+
children: a(t)
|
|
371
|
+
})]
|
|
367
372
|
})]
|
|
368
373
|
})]
|
|
369
|
-
})
|
|
374
|
+
})
|
|
370
375
|
}, e.id);
|
|
371
376
|
})
|
|
372
377
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/mybodyshield/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard — MyBodyShield health & fitness overview.\n * @module mybodyshield/pages\n *\n * KPIs are aggregated client-side from the real `myb*` list endpoints:\n * active programs, this-week's sessions, goal progress, today's macros vs\n * target, latest body weight and recovery readiness. No mock data.\n */\n\nimport { useMemo } from 'react';\nimport {\n Dumbbell,\n Flame,\n Target,\n Salad,\n Moon,\n ArrowRight,\n CheckCircle2,\n Weight,\n CalendarCheck,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { MiniBar } from '../components/MiniBar';\nimport { LoadingView, Section } from '../components/StateViews';\nimport { EmptyState } from '../components/EmptyState';\nimport {\n Meter,\n ProgressRing,\n MacroBar,\n MacroLegend,\n IconChip,\n GOAL_TYPE_META,\n RECOVERY_META,\n WORKOUT_TYPE_META,\n computeGoalProgress,\n} from './_shared/visuals';\nimport { useMyBodyShield } from '../context';\nimport {\n useMybPrograms,\n useMybSessions,\n useMybGoals,\n useMybDailyMacros,\n useMybBodyMetrics,\n useMybRecoveryLogs,\n} from '../hooks';\nimport {\n formatNumber,\n formatRelativeTime,\n formatKcal,\n formatWeight,\n formatPercent,\n formatEnumLabel,\n} from '../utils/formatters';\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useMyBodyShield();\n\n const programs = useMybPrograms({ status: 'ACTIVE', pagination: { limit: 50 } });\n const sessions = useMybSessions({ pagination: { limit: 20 } });\n const goals = useMybGoals({ status: 'ACTIVE', pagination: { limit: 50 } });\n const macros = useMybDailyMacros();\n const weightMetrics = useMybBodyMetrics({ type: 'WEIGHT', pagination: { limit: 30 } });\n const readiness = useMybRecoveryLogs('READINESS');\n\n const programRows = programs.data?.items ?? [];\n const sessionRows = sessions.data?.items ?? [];\n const goalRows = goals.data?.items ?? [];\n const weightRows = weightMetrics.data?.items ?? [];\n const readinessRows = readiness.data ?? [];\n\n const now = new Date();\n const weekAgo = now.getTime() - 7 * 24 * 60 * 60 * 1000;\n const sessionsThisWeek = sessionRows.filter((s) => {\n const ref = s.completedAt ?? s.startedAt ?? s.scheduledFor ?? s.createdAt;\n return ref ? new Date(ref).getTime() >= weekAgo : false;\n });\n const completedThisWeek = sessionsThisWeek.filter((s) => s.status === 'COMPLETED').length;\n const activeSession = sessionRows.find((s) => s.status === 'IN_PROGRESS');\n const upcomingSession = sessionRows\n .filter((s) => s.status === 'SCHEDULED' && s.scheduledFor)\n .sort((a, b) => new Date(a.scheduledFor!).getTime() - new Date(b.scheduledFor!).getTime())[0];\n\n const latestWeight = weightRows[0];\n const priorWeight = weightRows[1];\n const weightDelta =\n latestWeight && priorWeight ? latestWeight.value - priorWeight.value : undefined;\n\n const latestReadiness = readinessRows\n .slice()\n .sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime())[0];\n\n const goalProgressAvg = useMemo(() => {\n if (goalRows.length === 0) return 0;\n const sum = goalRows.reduce((acc, g) => acc + computeGoalProgress(g), 0);\n return sum / goalRows.length;\n }, [goalRows]);\n\n const macroData = macros.data;\n const caloriePct = macroData?.calorieTarget\n ? (macroData.calories / macroData.calorieTarget) * 100\n : 0;\n\n const isInitialLoading =\n programs.isLoading && sessions.isLoading && goals.isLoading && macros.isLoading;\n\n if (isInitialLoading) {\n return (\n <PageLayout title=\"MyBodyShield\" titleGradient aurora>\n <LoadingView label=\"Loading your fitness overview…\" />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name || 'there';\n\n return (\n <PageLayout\n title=\"MyBodyShield\"\n titleGradient\n aurora\n description={`Welcome back, ${greetingName} — your training, nutrition, progress & recovery control room.`}\n >\n {/* KPI grid */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5\">\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={formatNumber(programRows.length)}\n label=\"Active programs\"\n footer={\n <p className=\"text-xs text-text-tertiary\">\n <button\n type=\"button\"\n onClick={() => navigateTo('programs')}\n className=\"font-medium text-accent hover:underline\"\n >\n Manage programs\n </button>\n </p>\n }\n />\n <MetricsCard\n icon={<Flame className=\"h-5 w-5\" />}\n value={formatNumber(completedThisWeek)}\n label=\"Sessions this week\"\n accent={activeSession ? 'warning' : undefined}\n footer={\n activeSession ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`train/${activeSession.id}`)}\n className=\"text-xs font-medium text-status-warning-text hover:underline\"\n >\n Resume live session →\n </button>\n ) : (\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{sessionsThisWeek.length}</span>{' '}\n logged\n </p>\n )\n }\n />\n <MetricsCard\n icon={<Target className=\"h-5 w-5\" />}\n value={formatNumber(goalRows.length)}\n label=\"Active goals\"\n footer={\n <div className=\"space-y-2\">\n <MiniBar value={goalProgressAvg} height={4} barClassName=\"bg-accent-teal\" />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatPercent(goalProgressAvg)}\n </span>{' '}\n avg progress\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Weight className=\"h-5 w-5\" />}\n value={latestWeight ? formatWeight(latestWeight.value) : '—'}\n label=\"Latest weight\"\n footer={\n weightDelta != null ? (\n <p className=\"text-xs text-text-tertiary\">\n <span\n className={\n weightDelta <= 0\n ? 'font-medium text-status-success-text'\n : 'font-medium text-text-secondary'\n }\n >\n {weightDelta > 0 ? '+' : ''}\n {weightDelta.toFixed(1)} kg\n </span>{' '}\n vs last\n </p>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo('progress')}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n Log a measurement →\n </button>\n )\n }\n />\n <MetricsCard\n icon={<Moon className=\"h-5 w-5\" />}\n value={latestReadiness ? `${latestReadiness.score}` : '—'}\n label=\"Recovery readiness\"\n accent={\n latestReadiness\n ? latestReadiness.score >= 70\n ? 'success'\n : latestReadiness.score >= 40\n ? 'warning'\n : 'error'\n : undefined\n }\n footer={\n latestReadiness ? (\n <p className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(latestReadiness.loggedAt)}\n </p>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo('recovery')}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n Log recovery →\n </button>\n )\n }\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Today's nutrition */}\n <GlassCard treatment=\"glass\" className=\"p-6 lg:col-span-2\">\n <div className=\"mb-4 flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Today's nutrition</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n Calories & macros logged against your active plan.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('nutrition')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Nutrition <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {!macroData || (macroData.calorieTarget === 0 && macroData.calories === 0) ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Salad className=\"h-10 w-10\" />}\n title=\"No nutrition data yet\"\n description=\"Activate a nutrition plan and log meals to track your daily macros here.\"\n />\n ) : (\n <div className=\"flex flex-col gap-6 sm:flex-row sm:items-center\">\n <ProgressRing value={caloriePct} size={128} stroke={12}>\n <span className=\"text-xl font-bold tabular-nums text-text-primary\">\n {formatNumber(macroData.calories)}\n </span>\n <span className=\"text-xs text-text-tertiary\">\n / {formatNumber(macroData.calorieTarget)} kcal\n </span>\n </ProgressRing>\n <div className=\"flex-1 space-y-4\">\n <div className=\"space-y-2\">\n <MacroBar\n proteinG={macroData.proteinG}\n carbsG={macroData.carbsG}\n fatG={macroData.fatG}\n height={10}\n />\n <MacroLegend />\n </div>\n <div className=\"grid grid-cols-3 gap-3\">\n {(\n [\n ['Protein', macroData.proteinG, macroData.proteinTarget],\n ['Carbs', macroData.carbsG, macroData.carbsTarget],\n ['Fat', macroData.fatG, macroData.fatTarget],\n ] as const\n ).map(([label, val, target]) => (\n <div key={label} className=\"rounded-xl bg-bg-sunken/60 p-3\">\n <p className=\"text-xs text-text-tertiary\">{label}</p>\n <p className=\"mt-0.5 text-sm font-semibold tabular-nums text-text-primary\">\n {formatNumber(val)}\n <span className=\"text-xs font-normal text-text-tertiary\">\n {' '}\n / {formatNumber(target)} g\n </span>\n </p>\n <Meter\n value={target ? (val / target) * 100 : 0}\n height={4}\n className=\"mt-2\"\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Next up */}\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-base font-semibold text-text-primary\">Next up</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('train')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Train <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {activeSession ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`train/${activeSession.id}`)}\n className=\"w-full rounded-xl border border-status-warning-text/40 bg-status-warning-bg-subtle p-4 text-left\"\n >\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={activeSession.status} />\n </div>\n <p className=\"mt-2 text-sm font-medium text-text-primary\">Live workout in progress</p>\n <p className=\"mt-0.5 text-xs text-text-tertiary\">\n Started {formatRelativeTime(activeSession.startedAt)}\n </p>\n </button>\n ) : upcomingSession ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`train/${upcomingSession.id}`)}\n className=\"w-full rounded-xl bg-bg-sunken/60 p-4 text-left transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-center gap-2\">\n <IconChip icon={<CalendarCheck className=\"h-4 w-4\" />} />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">Scheduled session</p>\n <p className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(upcomingSession.scheduledFor)}\n </p>\n </div>\n </div>\n </button>\n ) : (\n <div className=\"flex flex-col items-center justify-center rounded-xl border border-dashed border-border-seam py-8 text-center\">\n <span className=\"flex size-11 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Flame className=\"h-5 w-5 text-accent\" />\n </span>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">No session scheduled</p>\n <button\n type=\"button\"\n onClick={() => navigateTo('train')}\n className=\"mt-1 text-xs font-medium text-accent hover:underline\"\n >\n Start a workout\n </button>\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Active goals */}\n <Section\n title=\"Active goals\"\n description=\"Your current targets across training, nutrition and body composition.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('goals')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All goals <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {goalRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Target className=\"h-10 w-10\" />}\n title=\"No active goals\"\n description=\"Set a goal to give your training and nutrition a clear target to work toward.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {goalRows.slice(0, 6).map((g) => {\n const pct = computeGoalProgress(g);\n const meta = GOAL_TYPE_META[g.type];\n const Icon = meta.icon;\n return (\n <GlassCard\n key={g.id}\n treatment=\"plain\"\n className=\"p-4 transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-start gap-3\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} tint={meta.tint} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{g.title}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(g.type)}</p>\n </div>\n </div>\n <div className=\"mt-3 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(g.currentValue)}\n {g.unit ? ` ${g.unit}` : ''} / {formatNumber(g.targetValue)}\n {g.unit ? ` ${g.unit}` : ''}\n </span>\n <span className=\"tabular-nums text-text-tertiary\">{formatPercent(pct)}</span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </Section>\n\n {/* Recent sessions */}\n <Section\n title=\"Recent sessions\"\n description=\"Your latest logged and scheduled workouts.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('train')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All sessions <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {sessionRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Flame className=\"h-10 w-10\" />}\n title=\"No sessions yet\"\n description=\"Start a workout session to begin logging sets and tracking your training.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {sessionRows.slice(0, 6).map((s) => {\n const meta = WORKOUT_TYPE_META.STRENGTH;\n const Icon = s.status === 'COMPLETED' ? CheckCircle2 : meta.icon;\n return (\n <button\n key={s.id}\n type=\"button\"\n onClick={() => navigateTo(`train/${s.id}`)}\n className=\"text-left\"\n >\n <GlassCard treatment=\"plain\" className=\"p-4 transition-shadow hover:shadow-float\">\n <div className=\"flex items-center justify-between gap-2\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} tint={meta.tint} />\n <StatusBadge status={s.status} />\n </div>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">\n {s.caloriesBurned > 0\n ? `${formatNumber(s.caloriesBurned)} kcal burned`\n : 'Workout session'}\n </p>\n <p className=\"mt-0.5 text-xs text-text-tertiary\">\n {formatRelativeTime(s.completedAt ?? s.startedAt ?? s.createdAt)}\n </p>\n </GlassCard>\n </button>\n );\n })}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/mybodyshield/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard — MyBodyShield health & fitness overview.\n * @module mybodyshield/pages\n *\n * KPIs are aggregated client-side from the real `myb*` list endpoints:\n * active programs, this-week's sessions, goal progress, today's macros vs\n * target, latest body weight and recovery readiness. No mock data.\n */\n\nimport { useMemo } from 'react';\nimport {\n Dumbbell,\n Flame,\n Target,\n Salad,\n Moon,\n ArrowRight,\n CheckCircle2,\n Weight,\n CalendarCheck,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { MiniBar } from '../components/MiniBar';\nimport { LoadingView, Section } from '../components/StateViews';\nimport { EmptyState } from '../components/EmptyState';\nimport {\n Meter,\n ProgressRing,\n MacroBar,\n MacroLegend,\n IconChip,\n GOAL_TYPE_META,\n RECOVERY_META,\n WORKOUT_TYPE_META,\n computeGoalProgress,\n} from './_shared/visuals';\nimport { useMyBodyShield } from '../context';\nimport {\n useMybPrograms,\n useMybSessions,\n useMybGoals,\n useMybDailyMacros,\n useMybBodyMetrics,\n useMybRecoveryLogs,\n} from '../hooks';\nimport {\n formatNumber,\n formatRelativeTime,\n formatKcal,\n formatWeight,\n formatPercent,\n formatEnumLabel,\n} from '../utils/formatters';\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useMyBodyShield();\n\n const programs = useMybPrograms({ status: 'ACTIVE', pagination: { limit: 50 } });\n const sessions = useMybSessions({ pagination: { limit: 20 } });\n const goals = useMybGoals({ status: 'ACTIVE', pagination: { limit: 50 } });\n const macros = useMybDailyMacros();\n const weightMetrics = useMybBodyMetrics({ type: 'WEIGHT', pagination: { limit: 30 } });\n const readiness = useMybRecoveryLogs('READINESS');\n\n const programRows = programs.data?.items ?? [];\n const sessionRows = sessions.data?.items ?? [];\n const goalRows = goals.data?.items ?? [];\n const weightRows = weightMetrics.data?.items ?? [];\n const readinessRows = readiness.data ?? [];\n\n const now = new Date();\n const weekAgo = now.getTime() - 7 * 24 * 60 * 60 * 1000;\n const sessionsThisWeek = sessionRows.filter((s) => {\n const ref = s.completedAt ?? s.startedAt ?? s.scheduledFor ?? s.createdAt;\n return ref ? new Date(ref).getTime() >= weekAgo : false;\n });\n const completedThisWeek = sessionsThisWeek.filter((s) => s.status === 'COMPLETED').length;\n const activeSession = sessionRows.find((s) => s.status === 'IN_PROGRESS');\n const upcomingSession = sessionRows\n .filter((s) => s.status === 'SCHEDULED' && s.scheduledFor)\n .sort((a, b) => new Date(a.scheduledFor!).getTime() - new Date(b.scheduledFor!).getTime())[0];\n\n const latestWeight = weightRows[0];\n const priorWeight = weightRows[1];\n const weightDelta =\n latestWeight && priorWeight ? latestWeight.value - priorWeight.value : undefined;\n\n const latestReadiness = readinessRows\n .slice()\n .sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime())[0];\n\n const goalProgressAvg = useMemo(() => {\n if (goalRows.length === 0) return 0;\n const sum = goalRows.reduce((acc, g) => acc + computeGoalProgress(g), 0);\n return sum / goalRows.length;\n }, [goalRows]);\n\n const macroData = macros.data;\n const caloriePct = macroData?.calorieTarget\n ? (macroData.calories / macroData.calorieTarget) * 100\n : 0;\n\n const isInitialLoading =\n programs.isLoading && sessions.isLoading && goals.isLoading && macros.isLoading;\n\n if (isInitialLoading) {\n return (\n <PageLayout title=\"MyBodyShield\" titleGradient aurora>\n <LoadingView label=\"Loading your fitness overview…\" />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name || 'there';\n\n return (\n <PageLayout\n title=\"MyBodyShield\"\n titleGradient\n aurora\n description={`Welcome back, ${greetingName} — your training, nutrition, progress & recovery control room.`}\n >\n {/* KPI grid */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5\">\n <MetricsCard\n icon={<Dumbbell className=\"h-5 w-5\" />}\n value={formatNumber(programRows.length)}\n label=\"Active programs\"\n footer={\n <p className=\"text-xs text-text-tertiary\">\n <button\n type=\"button\"\n onClick={() => navigateTo('programs')}\n className=\"font-medium text-accent hover:underline\"\n >\n Manage programs\n </button>\n </p>\n }\n />\n <MetricsCard\n icon={<Flame className=\"h-5 w-5\" />}\n value={formatNumber(completedThisWeek)}\n label=\"Sessions this week\"\n accent={activeSession ? 'warning' : undefined}\n footer={\n activeSession ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`train/${activeSession.id}`)}\n className=\"text-xs font-medium text-status-warning-text hover:underline\"\n >\n Resume live session →\n </button>\n ) : (\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{sessionsThisWeek.length}</span>{' '}\n logged\n </p>\n )\n }\n />\n <MetricsCard\n icon={<Target className=\"h-5 w-5\" />}\n value={formatNumber(goalRows.length)}\n label=\"Active goals\"\n footer={\n <div className=\"space-y-2\">\n <MiniBar value={goalProgressAvg} height={4} barClassName=\"bg-accent-teal\" />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatPercent(goalProgressAvg)}\n </span>{' '}\n avg progress\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Weight className=\"h-5 w-5\" />}\n value={latestWeight ? formatWeight(latestWeight.value) : '—'}\n label=\"Latest weight\"\n footer={\n weightDelta != null ? (\n <p className=\"text-xs text-text-tertiary\">\n <span\n className={\n weightDelta <= 0\n ? 'font-medium text-status-success-text'\n : 'font-medium text-text-secondary'\n }\n >\n {weightDelta > 0 ? '+' : ''}\n {weightDelta.toFixed(1)} kg\n </span>{' '}\n vs last\n </p>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo('progress')}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n Log a measurement →\n </button>\n )\n }\n />\n <MetricsCard\n icon={<Moon className=\"h-5 w-5\" />}\n value={latestReadiness ? `${latestReadiness.score}` : '—'}\n label=\"Recovery readiness\"\n accent={\n latestReadiness\n ? latestReadiness.score >= 70\n ? 'success'\n : latestReadiness.score >= 40\n ? 'warning'\n : 'error'\n : undefined\n }\n footer={\n latestReadiness ? (\n <p className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(latestReadiness.loggedAt)}\n </p>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo('recovery')}\n className=\"text-xs font-medium text-accent hover:underline\"\n >\n Log recovery →\n </button>\n )\n }\n />\n </div>\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Today's nutrition */}\n <GlassCard treatment=\"glass\" className=\"p-6 lg:col-span-2\">\n <div className=\"mb-4 flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Today's nutrition</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n Calories & macros logged against your active plan.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('nutrition')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Nutrition <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {!macroData || (macroData.calorieTarget === 0 && macroData.calories === 0) ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Salad className=\"h-10 w-10\" />}\n title=\"No nutrition data yet\"\n description=\"Activate a nutrition plan and log meals to track your daily macros here.\"\n />\n ) : (\n <div className=\"flex flex-col gap-6 sm:flex-row sm:items-center\">\n <ProgressRing value={caloriePct} size={128} stroke={12}>\n <span className=\"text-xl font-bold tabular-nums text-text-primary\">\n {formatNumber(macroData.calories)}\n </span>\n <span className=\"text-xs text-text-tertiary\">\n / {formatNumber(macroData.calorieTarget)} kcal\n </span>\n </ProgressRing>\n <div className=\"flex-1 space-y-4\">\n <div className=\"space-y-2\">\n <MacroBar\n proteinG={macroData.proteinG}\n carbsG={macroData.carbsG}\n fatG={macroData.fatG}\n height={10}\n />\n <MacroLegend />\n </div>\n <div className=\"grid grid-cols-3 gap-3\">\n {(\n [\n ['Protein', macroData.proteinG, macroData.proteinTarget],\n ['Carbs', macroData.carbsG, macroData.carbsTarget],\n ['Fat', macroData.fatG, macroData.fatTarget],\n ] as const\n ).map(([label, val, target]) => (\n <div key={label} className=\"rounded-xl bg-bg-sunken/60 p-3\">\n <p className=\"text-xs text-text-tertiary\">{label}</p>\n <p className=\"mt-0.5 text-sm font-semibold tabular-nums text-text-primary\">\n {formatNumber(val)}\n <span className=\"text-xs font-normal text-text-tertiary\">\n {' '}\n / {formatNumber(target)} g\n </span>\n </p>\n <Meter\n value={target ? (val / target) * 100 : 0}\n height={4}\n className=\"mt-2\"\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n </GlassCard>\n\n {/* Next up */}\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-base font-semibold text-text-primary\">Next up</h2>\n <button\n type=\"button\"\n onClick={() => navigateTo('train')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Train <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {activeSession ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`train/${activeSession.id}`)}\n className=\"w-full rounded-xl border border-status-warning-text/40 bg-status-warning-bg-subtle p-4 text-left\"\n >\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={activeSession.status} />\n </div>\n <p className=\"mt-2 text-sm font-medium text-text-primary\">Live workout in progress</p>\n <p className=\"mt-0.5 text-xs text-text-tertiary\">\n Started {formatRelativeTime(activeSession.startedAt)}\n </p>\n </button>\n ) : upcomingSession ? (\n <button\n type=\"button\"\n onClick={() => navigateTo(`train/${upcomingSession.id}`)}\n className=\"w-full rounded-xl bg-bg-sunken/60 p-4 text-left transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-center gap-2\">\n <IconChip icon={<CalendarCheck className=\"h-4 w-4\" />} />\n <div>\n <p className=\"text-sm font-medium text-text-primary\">Scheduled session</p>\n <p className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(upcomingSession.scheduledFor)}\n </p>\n </div>\n </div>\n </button>\n ) : (\n <div className=\"flex flex-col items-center justify-center rounded-xl border border-dashed border-border-seam py-8 text-center\">\n <span className=\"flex size-11 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Flame className=\"h-5 w-5 text-accent\" />\n </span>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">No session scheduled</p>\n <button\n type=\"button\"\n onClick={() => navigateTo('train')}\n className=\"mt-1 text-xs font-medium text-accent hover:underline\"\n >\n Start a workout\n </button>\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Active goals */}\n <Section\n title=\"Active goals\"\n description=\"Your current targets across training, nutrition and body composition.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('goals')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All goals <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {goalRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Target className=\"h-10 w-10\" />}\n title=\"No active goals\"\n description=\"Set a goal to give your training and nutrition a clear target to work toward.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {goalRows.slice(0, 6).map((g) => {\n const pct = computeGoalProgress(g);\n const meta = GOAL_TYPE_META[g.type];\n const Icon = meta.icon;\n return (\n <button\n key={g.id}\n type=\"button\"\n onClick={() => navigateTo('goals')}\n className=\"text-left\"\n >\n <GlassCard\n treatment=\"plain\"\n className=\"h-full p-4 transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-start gap-3\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} tint={meta.tint} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {g.title}\n </p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(g.type)}</p>\n </div>\n </div>\n <div className=\"mt-3 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(g.currentValue)}\n {g.unit ? ` ${g.unit}` : ''} / {formatNumber(g.targetValue)}\n {g.unit ? ` ${g.unit}` : ''}\n </span>\n <span className=\"tabular-nums text-text-tertiary\">\n {formatPercent(pct)}\n </span>\n </div>\n </div>\n </GlassCard>\n </button>\n );\n })}\n </div>\n )}\n </Section>\n\n {/* Recent sessions */}\n <Section\n title=\"Recent sessions\"\n description=\"Your latest logged and scheduled workouts.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('train')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All sessions <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {sessionRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Flame className=\"h-10 w-10\" />}\n title=\"No sessions yet\"\n description=\"Start a workout session to begin logging sets and tracking your training.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {sessionRows.slice(0, 6).map((s) => {\n const meta = WORKOUT_TYPE_META.STRENGTH;\n const Icon = s.status === 'COMPLETED' ? CheckCircle2 : meta.icon;\n return (\n <button\n key={s.id}\n type=\"button\"\n onClick={() => navigateTo(`train/${s.id}`)}\n className=\"text-left\"\n >\n <GlassCard treatment=\"plain\" className=\"p-4 transition-shadow hover:shadow-float\">\n <div className=\"flex items-center justify-between gap-2\">\n <IconChip icon={<Icon className=\"h-4 w-4\" />} tint={meta.tint} />\n <StatusBadge status={s.status} />\n </div>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">\n {s.caloriesBurned > 0\n ? `${formatNumber(s.caloriesBurned)} kcal burned`\n : 'Workout session'}\n </p>\n <p className=\"mt-0.5 text-xs text-text-tertiary\">\n {formatRelativeTime(s.completedAt ?? s.startedAt ?? s.createdAt)}\n </p>\n </GlassCard>\n </button>\n );\n })}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|