diff --git a/.ai/specs/2026-06-16-share-links.md b/.ai/specs/2026-06-16-share-links.md index 986dfe0..c9734ef 100644 --- a/.ai/specs/2026-06-16-share-links.md +++ b/.ai/specs/2026-06-16-share-links.md @@ -46,16 +46,17 @@ src/ │ └── share-links/ │ └── index.ts # new collection ├── app/ -│ ├── (payload)/ -│ │ └── admin/modules/share-link-url/ -│ │ ├── share-link-url.tsx # custom UI component (admin) -│ │ └── index.ts │ └── [locale]/(frontend)/ │ └── share/ │ └── [token]/ │ └── page.tsx # public share page -└── loaders/ - └── share-link-loader.ts # token validation + data fetching +└── modules/ + └── sharing/ + ├── admin/share-link-url/ + │ ├── share-link-url.tsx # custom UI component (admin) + │ └── index.ts + └── server/ + └── load-share-link.ts # token validation + data fetching ``` ### Modified files @@ -89,7 +90,7 @@ src/ ## Token Validation (loader) -`loadShareLink(token: string)` in `src/loaders/share-link-loader.ts`: +`loadShareLink(token: string)` in `src/modules/sharing/server/load-share-link.ts`: 1. Query `share-links` with `where: { token: { equals: token } }`, `overrideAccess: true`, `limit: 1` 2. If no document found → return `null` @@ -166,8 +167,8 @@ Build the loader, the share page, and any read-only variants of existing workout ### Phase 2 -- [ ] Create `src/app/(payload)/admin/modules/share-link-url/share-link-url.tsx` — client component using `useFormFields` to read `token`, compose URL from `NEXT_PUBLIC_SERVER_URL`, render copy button -- [ ] Create `src/app/(payload)/admin/modules/share-link-url/index.ts` — re-export +- [ ] Create `src/modules/sharing/admin/share-link-url/share-link-url.tsx` — client component using `useFormFields` to read `token`, compose URL from `NEXT_PUBLIC_SERVER_URL`, render copy button +- [ ] Create `src/modules/sharing/admin/share-link-url/index.ts` — re-export - [ ] Wire component into `shareUrl` ui field in the collection - [ ] Run `yarn generate:importmap` - [ ] Run `yarn build` — verify no type errors @@ -175,7 +176,7 @@ Build the loader, the share page, and any read-only variants of existing workout ### Phase 3 - [ ] Add `share` namespace keys to `messages/pl.json` and `messages/en.json` -- [ ] Create `src/loaders/share-link-loader.ts` +- [ ] Create `src/modules/sharing/server/load-share-link.ts` - [ ] Create `src/app/[locale]/(frontend)/share/[token]/page.tsx` - [ ] Adapt plan components to support `readOnly` prop (hide set forms, logging buttons) — or create a read-only wrapper - [ ] Run `yarn build` — verify no type errors diff --git a/.ai/specs/2026-06-20-exercise-client-note.md b/.ai/specs/2026-06-20-exercise-client-note.md index 4d29fd1..3b16bed 100644 --- a/.ai/specs/2026-06-20-exercise-client-note.md +++ b/.ai/specs/2026-06-20-exercise-client-note.md @@ -24,7 +24,7 @@ Today a client can only add a note **per set** (`set-logs.note`). We add a new l ## Problem Statement -In the workout tracker a client logs exercise execution as sets (`set-logs`). Each set can have its own note (`set-logs.note`, the field in [SeriesForm](../../src/components/workout/series-form/series-form.tsx)). What is missing is a note **for the whole exercise** — an annotation like "shoulders felt weak today, lower the weight next time" that applies to the exercise as a whole, not to one specific set. +In the workout tracker a client logs exercise execution as sets (`set-logs`). Each set can have its own note (`set-logs.note`, the field in [SeriesForm](../../src/modules/training/components/series-form/series-form.tsx)). What is missing is a note **for the whole exercise** — an annotation like "shoulders felt weak today, lower the weight next time" that applies to the exercise as a whole, not to one specific set. The note cannot live in the plan: `workout-exercise-rows` has `update: isAdmin` — clients do not write to the template. It must therefore be created in the log layer, tied to the **(session, exercise)** pair. @@ -111,7 +111,7 @@ src/ │ │ └── index.ts # NEW collection │ └── index.ts # + re-export ExerciseLogs ├── payload.config.ts # + register in collections[] -├── components/workout/ +├── modules/training/components/ │ ├── workout-tracker/ │ │ ├── workout-tracker.tsx # pass note + onSaveNote to the card │ │ └── hooks/ @@ -192,7 +192,7 @@ useWorkoutSession (hook) └── update local state ``` -UI: [workout-tracker.tsx](../../src/components/workout/workout-tracker/workout-tracker.tsx) passes `note` + `onSaveNote` to [exercise-card.tsx](../../src/components/workout/exercise-card/exercise-card.tsx), which renders the note in the exercise header (distinct from the plan note) and — when `!readOnly` — exposes an editable field. +UI: [workout-tracker.tsx](../../src/modules/training/components/workout-tracker/workout-tracker.tsx) passes `note` + `onSaveNote` to [exercise-card.tsx](../../src/modules/training/components/exercise-card/exercise-card.tsx), which renders the note in the exercise header (distinct from the plan note) and — when `!readOnly` — exposes an editable field. --- @@ -220,13 +220,13 @@ Render and edit the note in `ExerciseCard`, strings in `pl.json`/`en.json`, visi - [x] `yarn payload migrate` — run by the user (DB-mutating step). ### Phase 2 — Runtime -- [x] [use-workout-session.ts](../../src/components/workout/workout-tracker/hooks/use-workout-session.ts): load `exercise-logs` by `session` (parallel with set-logs), add `exerciseNotes` state. +- [x] [use-workout-session.ts](../../src/modules/training/components/workout-tracker/hooks/use-workout-session.ts): load `exercise-logs` by `session` (parallel with set-logs), add `exerciseNotes` state. - [x] Add the `noteForRow(rowId)` selector and the `saveExerciseNote(ex, note)` mutation (upsert). - [x] Expose both in the hook's returned API. ### Phase 3 — UI + i18n -- [x] [workout-tracker.tsx](../../src/components/workout/workout-tracker/workout-tracker.tsx): pass `clientNote` + `onSaveNote` to `ExerciseCard`. -- [x] [exercise-card.tsx](../../src/components/workout/exercise-card/exercise-card.tsx): render the client note in the header (new `exercise-note` subcomponent) + allow editing when `!readOnly`. +- [x] [workout-tracker.tsx](../../src/modules/training/components/workout-tracker/workout-tracker.tsx): pass `clientNote` + `onSaveNote` to `ExerciseCard`. +- [x] [exercise-card.tsx](../../src/modules/training/components/exercise-card/exercise-card.tsx): render the client note in the header (new `exercise-note` subcomponent) + allow editing when `!readOnly`. - [x] Strings (`addNote`, `notePlaceholder`, `saveNote`, `cancelNote`) in `messages/pl.json` + `messages/en.json`. - [x] [export-seed.ts](../../src/scripts/export-seed.ts): comment updated (logs already excluded via the allow-list). - [x] Typecheck (`tsc --noEmit`) + lint pass. diff --git a/.changeset/admin-training-navigation.md b/.changeset/admin-training-navigation.md new file mode 100644 index 0000000..87c7a8b --- /dev/null +++ b/.changeset/admin-training-navigation.md @@ -0,0 +1,5 @@ +--- +'training-app': minor +--- + +Add plan and workout structure navigation in the admin panel and fix exercise catalog selection in the structure editor. diff --git a/.changeset/exercise-target-type.md b/.changeset/exercise-target-type.md new file mode 100644 index 0000000..89228bb --- /dev/null +++ b/.changeset/exercise-target-type.md @@ -0,0 +1,5 @@ +--- +'training-app': minor +--- + +Allow workout exercises to use either repetition or duration targets. diff --git a/.changeset/green-sides-log.md b/.changeset/green-sides-log.md new file mode 100644 index 0000000..b0a793e --- /dev/null +++ b/.changeset/green-sides-log.md @@ -0,0 +1,5 @@ +--- +'training-app': minor +--- + +Record left and right weights and repetitions separately for strength sets and exercise defaults. diff --git a/.changeset/group-protocol-meta.md b/.changeset/group-protocol-meta.md new file mode 100644 index 0000000..d3043d6 --- /dev/null +++ b/.changeset/group-protocol-meta.md @@ -0,0 +1,5 @@ +--- +'training-app': minor +--- + +Show group protocol details above exercises in workout tracking. diff --git a/.changeset/modular-training-architecture.md b/.changeset/modular-training-architecture.md new file mode 100644 index 0000000..12769b6 --- /dev/null +++ b/.changeset/modular-training-architecture.md @@ -0,0 +1,5 @@ +--- +'training-app': patch +--- + +Reorganize training and sharing code into domain-oriented business modules. Move frontend components, server queries, and Payload Admin extensions behind explicit module boundaries without changing user-facing behavior. diff --git a/.changeset/standardize-rest-duration-labels.md b/.changeset/standardize-rest-duration-labels.md new file mode 100644 index 0000000..b4665ef --- /dev/null +++ b/.changeset/standardize-rest-duration-labels.md @@ -0,0 +1,5 @@ +--- +'training-app': patch +--- + +Standardize rest duration labels as `Rest(s)` in workout group configuration. diff --git a/AGENTS.md b/AGENTS.md index 769eee8..1ab1743 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,10 +7,11 @@ Operational guide for AI agents working in this repository. ## Before Writing Code - Write everything in English: code, comments, variable names, documentation. +- Do not implement code, schema, migration, script, configuration, or documentation changes unless the user explicitly authorizes implementation with "wdrażamy" or an equivalent clear instruction. Analysis, investigation, and recommendations do not authorize changes. - Check `.ai/specs/` before coding any non-trivial feature. - Skills are installed in `.claude/skills/` (Claude Code) and `.agents/skills/` (Codex). `.agents/skills/` is the local source of truth — edit a skill **only** there, then run `ags push-skill` to propagate it to the source repo (it also syncs the `.claude/skills/` copy). See Installing skills. - Prefer minimal, focused changes. Do not refactor code outside the task scope. -- Run `yarn build` after every implementation to catch type errors. +- Do not run `yarn build` automatically after implementation. Run it only when the user explicitly asks. - Comment and naming conventions are defined in the `code-style` skill. Load it before writing or reviewing TypeScript. --- @@ -23,7 +24,7 @@ Match the task to the table before starting. A single task often maps to multipl |---|---| | Creating a new collection or extending the schema | Load skill `payload-build-collections` | | Adding a custom admin view, tab, or field UI | Load skill `payload-build-modules` | -| Building a front-end component or page (`src/components`, `(frontend)`) | Load skill `payload-frontend-build-components` | +| Building a front-end component or page (`src/components`, `src/modules/*/components`, `(frontend)`) | Load skill `payload-frontend-build-components` | | Debugging hooks, queries, access control, transactions | Load skill `payload` | | Security review, or adding/modifying auth, access control, uploads, CORS/CSRF, headers | Load skill `payload-security`; keep `.ai/audits/security-audit.md` current | | Writing questions for a client or stakeholder | Load skill `writing-questions` | @@ -97,16 +98,19 @@ src/ │ └── (payload)/ # Payload admin routes and API ├── collections/ # One folder per collection (index.ts + optional hooks.ts, types.ts) ├── components/ -│ ├── common/ -│ ├── ui/ -│ └── workout/ +│ ├── common/ # App-wide, non-domain components +│ └── ui/ # Domain-agnostic UI primitives ├── data/ # Static/seed data ├── i18n/ # next-intl config -├── lib/ # Utilities and SDK client -├── loaders/ # Server-side data fetching +├── lib/ # Business-agnostic utilities and SDK client ├── migrations/ # Payload DB migrations (auto-generated) +├── modules/ # Vertical business modules +│ └── / +│ ├── / # Domain area (types, constants, rules) +│ ├── components/ # Module-owned frontend components +│ ├── server/ # Server-only queries and use cases +│ └── admin/ # Module-owned Payload Admin UI ├── scripts/ # One-off CLI scripts -├── types/ ├── payload-types.ts # Auto-generated — do not edit └── payload.config.ts .claude/skills/ # Skills for Claude Code @@ -118,12 +122,62 @@ src/ --- +## Module Architecture + +- `src/modules/` is organized by business capability, not by Payload collection. A module + is a vertical slice that may own domain rules, frontend components, server queries, and + Payload Admin UI. +- Closely related concepts belong to areas inside one module. For example, `training` + contains `exercises`, `plans`, and `logs`; do not create a separate top-level module for + every table or entity. +- Keep `src/app/` thin: route files compose module entry points but do not own feature + implementations. Modules must never import from `src/app/`. +- Keep Payload `CollectionConfig`, fields, hooks, access control, relationships, and + indexes in `src/collections/`. Collections may import only client-safe domain APIs from + modules, never module components, admin UI, or server entry points. +- Put domain-specific React components under `src/modules//components/`. Keep only + domain-agnostic primitives in `src/components/ui/` and cross-module application UI in + `src/components/common/`. +- Put server-side queries and use cases under an explicit `server/` entry point and add + `import 'server-only'`. Do not re-export server code from a client-safe module barrel. +- Put Payload Admin implementations under `src/modules//admin//`. + Collection configuration must reference the exact component file and the import map + must be regenerated after a path changes. +- Use `types.ts` only for types and interfaces, and `constants.ts` only for constants and + declarative configuration. Name operation files after their responsibility; do not add + generic `utils.ts`, `helpers.ts`, or `models.ts` files. +- Code outside a domain area must use its public `index.ts`. Environment-specific APIs use + explicit subpaths such as `@/modules/training/plans/server` or + `@/modules/training/components/workout-plans`. +- Cross-module dependencies must use public entry points, remain one-directional, and + stay cycle-free. For example, `sharing` may use the public `training/plans/server` API; + `training` must not depend back on `sharing`. +- Keep only business-agnostic technical functions and SDK clients in `src/lib/`. + +--- + +## Payload-Generated Types + +- `src/payload-types.ts` is generated by Payload from collection, field, global, and + `payload.config.ts` definitions. Never edit it manually; regenerate it with + `yarn generate:types`. +- A type declared in a module may use the same name as a generated Payload type. Types + exported from different files do not conflict merely because their names match. +- A naming conflict occurs only when two types with the same local name are imported + into the same scope. Alias the imports when both representations are needed, for + example `Workout as PayloadWorkout` and `Workout as PlanWorkout`. +- Name module-owned types according to their domain role. Do not add prefixes or + suffixes solely to avoid a same-name type exported by another module. + +--- + ## Key Commands ```bash yarn dev # start dev server -yarn build # production build — run after every implementation -yarn payload migrate # run pending DB migrations +yarn build # production build - run only when explicitly requested +yarn payload migrate:create # generate a migration after a schema change - run manually +yarn payload migrate # run pending DB migrations - run manually yarn generate:types # regenerate payload-types.ts yarn generate:importmap # regenerate admin import map (after adding custom views) yarn seed # seed demo data @@ -131,6 +185,19 @@ yarn lint # ESLint npx skills add -a claude-code -a codex --copy # install skills ``` +## Database Migrations + +- Generate migrations with `yarn payload migrate:create` after every schema change. +- Maintainers run `yarn payload migrate:create` and `yarn payload migrate` manually. Agents must not run either command. +- Agents must never create migration files manually or edit Payload-generated migration files. +- Keep Payload-generated schema migrations in their generated form. Do not add manual SQL or data updates to them. + +## Data Backfills + +- Implement every data backfill as a separate, idempotent script through the Payload Local API. +- Do not add data backfill SQL to Payload-generated schema migrations. +- Maintainers run backfill scripts manually. Agents must not run them. + --- ## Release diff --git a/README.md b/README.md index c321c7d..c718329 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A coach-facing admin and client-facing training tracker built with Payload CMS a **Client (web app)** — logs in, sees their active plan, works through workouts session by session, and logs each set (reps, weight, RIR, time, etc.). ## Navigation + - [Data model & flow](#data-model--flow) - [Tech stack](#tech-stack) - [Getting started](#getting-started) @@ -159,7 +160,7 @@ For every log collection: a client may only create/read/update/delete **their ow | Layer | Technology | |---|---| -| Framework | Next.js 15 (App Router) | +| Framework | Next.js 16 (App Router) | | CMS / Auth | Payload CMS 3 | | Database | PostgreSQL (`@payloadcms/db-postgres`) | | Styling | Tailwind CSS | @@ -189,14 +190,24 @@ Edit `.env`: ```env DATABASE_URL=postgresql://user:password@localhost:5432/training_app PAYLOAD_SECRET=your-long-random-secret-here +NEXT_PUBLIC_BASE_URL=http://localhost:3000 ``` -Run migrations: +Apply existing migrations: ```bash yarn payload migrate ``` +Create a new migration only after changing the Payload schema: + +```bash +yarn payload migrate:create +yarn payload migrate +``` + +Maintainers run migration commands manually. Agents do not generate or apply database migrations. + ### Run ```bash @@ -239,16 +250,23 @@ src/ │ └── users/ ├── components/ │ ├── common/ # App-wide UI (logout button…) -│ ├── ui/ # Primitive components (button, input, surface…) -│ └── workout/ # Workout tracker components +│ └── ui/ # Domain-agnostic primitives (button, input, surface…) ├── data/ # Static/seed data ├── i18n/ # next-intl routing and request config -├── lib/ # Shared utilities and SDK client -├── loaders/ # Server-side data fetching functions +├── lib/ # Domain-agnostic technical functions and SDK client ├── migrations/ # Payload database migrations -├── scripts/ # One-off CLI scripts (seed, import-plan…) -├── types/ # Shared TypeScript types -├── middleware.ts +├── modules/ # Vertical business modules +│ ├── training/ +│ │ ├── exercises/ # Exercise types, constants, tracking rules, formatters +│ │ ├── plans/ # Payload plan documents, tree building, formatters, server loaders +│ │ ├── logs/ # Training-log types, constants, metric transformations +│ │ ├── components/ # Training-specific frontend components and hooks +│ │ └── admin/ # Training-specific Payload Admin UI +│ └── sharing/ +│ ├── server/ # Share-link validation and data loading +│ └── admin/ # Sharing-specific Payload Admin UI +├── scripts/ # Seed and seed-export CLI scripts +├── proxy.ts # Locale routing and share-token cookie handling ├── payload-types.ts # Auto-generated — do not edit manually └── payload.config.ts .claude/skills/ # AI skills for Claude Code @@ -256,26 +274,47 @@ src/ .ai/specs/ # Feature specifications ``` +### Module architecture + +- A module represents a business capability, not a Payload collection. +- Closely related areas stay inside one module. `training` owns `exercises`, `plans`, and + `logs`. +- `src/app` contains routing and page composition. Feature implementations live outside it. +- Domain-specific frontend components live in `src/modules//components`. +- Server queries and use cases use an explicit `server/` entry point with `server-only`. +- Payload Admin implementations live in `src/modules//admin`. +- Payload collection configuration, access control, hooks, and relationships remain in + `src/collections`. +- Cross-module imports use public entry points and must remain one-directional. +- `src/lib` is reserved for technical, domain-agnostic functions and SDK clients. + ## Key scripts | Script | Description | |---|---| | `yarn dev` | Start dev server | +| `yarn devsafe` | Clear the Next.js build cache and start the dev server | | `yarn build` | Production build | | `yarn start` | Start production server | +| `yarn payload migrate:create` | Generate a database migration after a schema change. Run manually. | | `yarn payload migrate` | Run pending database migrations | | `yarn generate:types` | Regenerate `payload-types.ts` from collection configs | -| `yarn generate:importmap` | Regenerate Payload admin import map (run after adding custom views) | +| `yarn generate:importmap` | Regenerate the Payload admin import map after adding or moving a custom admin component | | `yarn seed` | Seed database with demo data | | `yarn seed:export` | Export current database state to seed file | | `yarn lint` | Run ESLint | +| `yarn format` | Format TypeScript and TSX files with Prettier | +| `yarn format:check` | Check TypeScript and TSX formatting | +| `yarn install-skills` | Install the repository's AI skills | +| `yarn changeset` | Create a release changeset | | `npx skills add ` | Install AI skills into `.claude/skills/` and `.agents/skills/` | ## Development ### Adding a collection -Follow `.ai/skills/payload-build-collections` — each collection lives in `src/collections/{kebab-case}/index.ts` and is registered in `src/collections/index.ts`. +Follow `.agents/skills/payload-build-collections` — each collection lives in +`src/collections/{kebab-case}/index.ts` and is registered in `src/collections/index.ts`. After changing collection configs, regenerate types: @@ -285,12 +324,23 @@ yarn generate:types ### Adding an admin view or custom field UI -Follow `.ai/skills/payload-build-modules`. After registering a new component path, run: +Follow `.agents/skills/payload-build-modules`. Place the implementation in +`src/modules//admin//`. After adding or changing a registered component +path, run: ```bash yarn generate:importmap ``` +### Adding a frontend component + +Follow `.agents/skills/payload-frontend-build-components`. + +- Generic primitives belong in `src/components/ui`. +- Cross-module application components belong in `src/components/common`. +- Domain-specific components belong in `src/modules//components/`. +- Routed pages remain in `src/app/[locale]/(frontend)`. + ### AI skills This project uses skill files for AI-assisted development. Skills are managed with [npx skills](https://github.com/vercel-labs/skills) and installed into `.claude/skills/` (Claude Code) and `.agents/skills/` (Codex). @@ -301,6 +351,14 @@ To install skills from the source repository: npx skills add -a claude-code -a codex --copy ``` +`.agents/skills/` is the local source of truth. After editing a skill, synchronize it with: + +```bash +ags push-skill +``` + +Do not edit `.claude/skills/` manually; it is a generated copy. + Skills cover: Payload patterns, collection scaffolding, admin module structure, UI copy, and spec writing. ## Screenshots diff --git a/messages/en.json b/messages/en.json index 98d7f1b..dd58adc 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6,7 +6,7 @@ "seriesPrefix": "Sets", "repsPrefix": "Reps", "durationPrefix": "Time", - "restPrefix": "Rest" + "restPrefix": "Rest(s)" }, "exercise": { "video": "▶ video", @@ -68,6 +68,6 @@ "seriesPrefix": "Sets", "repsPrefix": "Reps", "durationPrefix": "Time", - "restPrefix": "Rest" + "restPrefix": "Rest(s)" } } diff --git a/messages/pl.json b/messages/pl.json index d01b2ca..cd6d242 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -6,7 +6,7 @@ "seriesPrefix": "Serie", "repsPrefix": "Powt.", "durationPrefix": "Czas", - "restPrefix": "Przerwa" + "restPrefix": "Przerwa (s)" }, "exercise": { "video": "▶ wideo", @@ -68,6 +68,6 @@ "seriesPrefix": "Serie", "repsPrefix": "Powt.", "durationPrefix": "Czas", - "restPrefix": "Przerwa" + "restPrefix": "Przerwa (s)" } } diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 0b66e61..199db91 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,12 +1,16 @@ -import { WorkoutLogsNotice as WorkoutLogsNotice_39bf38d00355d79c704c09f118dbdf2d } from '@/app/(payload)/admin/modules/workout-logs-notice/workout-logs-notice' -import { WorkoutStructureView as WorkoutStructureView_30ad39f13ceb101227cf63fc7340b241 } from '@/app/(payload)/admin/modules/workout-structure/workout-structure' -import { ShareLinkUrl as ShareLinkUrl_650cf55127df39a4288b83046d4e6bf2 } from '@/app/(payload)/admin/modules/share-link-url/share-link-url' +import { PlanMicrocycles as PlanMicrocycles_aa9e64122204e91344087361d305f543 } from '@/modules/training/admin/training-navigation/training-navigation' +import { MicrocycleWorkouts as MicrocycleWorkouts_aa9e64122204e91344087361d305f543 } from '@/modules/training/admin/training-navigation/training-navigation' +import { WorkoutLogsNotice as WorkoutLogsNotice_328f06a3a687f2f021e5e330fd11e578 } from '@/modules/training/admin/workout-logs-notice/workout-logs-notice' +import { WorkoutStructureView as WorkoutStructureView_b5f29e6f6bba1c7e450b005df58cd1c9 } from '@/modules/training/admin/workout-structure/workout-structure' +import { ShareLinkUrl as ShareLinkUrl_5b4ded69930ab17298c3ec9588e5f2c2 } from '@/modules/sharing/admin/share-link-url/share-link-url' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' /** @type import('payload').ImportMap */ export const importMap = { - "@/app/(payload)/admin/modules/workout-logs-notice/workout-logs-notice#WorkoutLogsNotice": WorkoutLogsNotice_39bf38d00355d79c704c09f118dbdf2d, - "@/app/(payload)/admin/modules/workout-structure/workout-structure#WorkoutStructureView": WorkoutStructureView_30ad39f13ceb101227cf63fc7340b241, - "@/app/(payload)/admin/modules/share-link-url/share-link-url#ShareLinkUrl": ShareLinkUrl_650cf55127df39a4288b83046d4e6bf2, + "@/modules/training/admin/training-navigation/training-navigation#PlanMicrocycles": PlanMicrocycles_aa9e64122204e91344087361d305f543, + "@/modules/training/admin/training-navigation/training-navigation#MicrocycleWorkouts": MicrocycleWorkouts_aa9e64122204e91344087361d305f543, + "@/modules/training/admin/workout-logs-notice/workout-logs-notice#WorkoutLogsNotice": WorkoutLogsNotice_328f06a3a687f2f021e5e330fd11e578, + "@/modules/training/admin/workout-structure/workout-structure#WorkoutStructureView": WorkoutStructureView_b5f29e6f6bba1c7e450b005df58cd1c9, + "@/modules/sharing/admin/share-link-url/share-link-url#ShareLinkUrl": ShareLinkUrl_5b4ded69930ab17298c3ec9588e5f2c2, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/src/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx b/src/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx deleted file mode 100644 index cfe1367..0000000 --- a/src/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client' - -import { Button, Form, RelationshipField, TextField, toast, useFormProcessing } from '@payloadcms/ui' -import type { FormState, SingleRelationshipFieldClient, ValueWithRelation } from 'payload' -import type { ExerciseRow } from '../../types' -import { s } from '../../styles' -import { sdk } from '@/lib/sdk' -import { textField } from '@/app/(payload)/admin/utils/fields' -import { validateKgOrReps, validateRepsOrKg, validateRounds } from '../../utils' - -type Props = { - groupId: number - nextOrder: number - initial?: ExerciseRow - onSaved: (row: ExerciseRow) => void - onCancel: () => void -} - -const exerciseRelField: SingleRelationshipFieldClient = { - name: 'exercise', - type: 'relationship', - relationTo: 'exercises', - hasMany: false, - label: 'Exercise (catalog)', -} as SingleRelationshipFieldClient - -function FormFields({ isEdit, onCancel }: { isEdit: boolean; onCancel: () => void }) { - const processing = useFormProcessing() - - return ( - <> -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- - -
- - ) -} - -export function ExerciseForm({ groupId, nextOrder, initial, onSaved, onCancel }: Props) { - const isEdit = !!initial - - const initialState: FormState = { - numer: { value: initial?.numer ?? '' }, - rounds: { value: initial?.rounds ?? '' }, - exercise: { value: initial?.exercise ? { value: initial.exercise.id, relationTo: 'exercises' } : null }, - note: { value: initial?.note ?? '' }, - reps: { value: initial?.reps ?? '' }, - kg: { value: initial?.kg ?? '' }, - rir: { value: initial?.rir ?? '' }, - tut: { value: initial?.tut ?? '' }, - rest: { value: initial?.rest ?? '' }, - } - - const handleSubmit = async (_: FormState, data: Record) => { - const exerciseRaw = data.exercise as ValueWithRelation | null - const body = { - numer: (data.numer as string) || null, - rounds: (data.rounds as string) || null, - exercise: exerciseRaw ? Number(exerciseRaw.value) : null, - note: (data.note as string) || null, - reps: (data.reps as string) || null, - kg: (data.kg as string) || null, - rir: (data.rir as string) || null, - tut: (data.tut as string) || null, - rest: (data.rest as string) || null, - ...(!isEdit && { group: groupId, order: nextOrder }), - } - - try { - const doc = isEdit - ? await sdk.update({ collection: 'workout-exercise-rows', id: initial!.id, data: body as never, depth: 1 }) - : await sdk.create({ collection: 'workout-exercise-rows', data: body as never, depth: 1 }) - - const exerciseDoc = doc.exercise - const exerciseObj = exerciseDoc && typeof exerciseDoc === 'object' - ? { id: (exerciseDoc as { id: number }).id, name: (exerciseDoc as { name?: string | null }).name ?? null } - : null - const normalizedGroup = - typeof doc.group === 'object' && doc.group !== null ? (doc.group as { id: number }).id : doc.group - - toast.success(isEdit ? 'Exercise updated' : 'Exercise added') - onSaved({ ...doc, group: normalizedGroup, exercise: exerciseObj } as ExerciseRow) - } catch (e) { - toast.error(e instanceof Error ? e.message : 'Save failed') - } - } - - return ( -
-
- {isEdit ? 'Edit exercise' : 'New exercise'} -
- -
- - -
- ) -} diff --git a/src/app/(payload)/admin/modules/workout-structure/constants.ts b/src/app/(payload)/admin/modules/workout-structure/constants.ts deleted file mode 100644 index 361c17f..0000000 --- a/src/app/(payload)/admin/modules/workout-structure/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const PROTOCOLS = [ - { value: 'standard', label: 'Standard' }, - { value: 'emom', label: 'EMOM' }, - { value: 'amrap', label: 'AMRAP' }, - { value: 'for_time', label: 'For Time' }, - { value: 'tabata', label: 'Tabata' }, -] - -export const PROTOCOL_LABEL: Record = { - standard: 'Standard', - emom: 'EMOM', - amrap: 'AMRAP', - for_time: 'For Time', - tabata: 'Tabata', -} diff --git a/src/app/(payload)/admin/modules/workout-structure/loader.ts b/src/app/(payload)/admin/modules/workout-structure/loader.ts deleted file mode 100644 index d56d577..0000000 --- a/src/app/(payload)/admin/modules/workout-structure/loader.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { ExerciseRow, Group, RawExerciseRow, Section, WorkoutStructureData } from './types' - -export async function loadWorkoutStructure( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payload: any, - docId: number | string, -): Promise { - const workout = await payload.findByID({ collection: 'workouts', id: docId, depth: 0 }) - - const groupsResult = await payload.find({ - collection: 'workout-groups', - where: { workout: { equals: docId } }, - sort: 'order', - limit: 500, - depth: 0, - }) - - const groupIds = groupsResult.docs.map((g: Group) => g.id) - - const exerciseRowsResult = groupIds.length - ? await payload.find({ - collection: 'workout-exercise-rows', - where: { group: { in: groupIds } }, - sort: 'order', - limit: 5000, - depth: 1, - }) - : { docs: [] } - - const exerciseRowIds = exerciseRowsResult.docs.map((r: RawExerciseRow) => r.id) - - const [roundLogsResult, setLogsResult] = await Promise.all([ - groupIds.length - ? payload.find({ collection: 'round-logs', where: { group: { in: groupIds } }, limit: 5000, depth: 0 }) - : { docs: [] }, - exerciseRowIds.length - ? payload.find({ collection: 'set-logs', where: { exerciseRow: { in: exerciseRowIds } }, limit: 5000, depth: 0 }) - : { docs: [] }, - ]) - - const groupIdsWithLogs: number[] = [ - ...new Set( - roundLogsResult.docs.map((r: { group: number | { id: number } }) => - typeof r.group === 'object' ? r.group.id : r.group - ) as number[], - ), - ] - - const exerciseRowIdsWithLogs: number[] = [ - ...new Set( - setLogsResult.docs.map((r: { exerciseRow: number | { id: number } }) => - typeof r.exerciseRow === 'object' ? r.exerciseRow.id : r.exerciseRow - ) as number[], - ), - ] - - const sections: Section[] = (workout.sections ?? []) as Section[] - - const initialGroups: Group[] = groupsResult.docs.map((g: Group) => ({ - id: g.id, - sectionRowId: g.sectionRowId ?? null, - order: g.order ?? 0, - label: g.label ?? null, - bundleWithPrevious: g.bundleWithPrevious ?? false, - protocol: g.protocol ?? 'standard', - rounds: g.rounds ?? null, - durationMinutes: g.durationMinutes ?? null, - intervalSeconds: g.intervalSeconds ?? null, - workSeconds: g.workSeconds ?? null, - restSeconds: g.restSeconds ?? null, - restBetweenRounds: g.restBetweenRounds ?? null, - })) - - const initialExerciseRows: ExerciseRow[] = exerciseRowsResult.docs.map((r: RawExerciseRow) => ({ - id: r.id, - group: typeof r.group === 'object' && r.group !== null ? r.group.id : (r.group ?? null), - order: r.order ?? 0, - numer: r.numer ?? null, - rounds: r.rounds ?? null, - exercise: - r.exercise && typeof r.exercise === 'object' - ? { id: (r.exercise as { id: number }).id, name: (r.exercise as { name?: string | null }).name ?? null } - : null, - note: r.note ?? null, - reps: r.reps ?? null, - kg: r.kg ?? null, - tut: r.tut ?? null, - rir: r.rir ?? null, - rest: r.rest ?? null, - durationMin: r.durationMin ?? null, - durationSec: r.durationSec ?? null, - })) - - return { sections, initialGroups, initialExerciseRows, groupIdsWithLogs, exerciseRowIdsWithLogs } -} diff --git a/src/app/(payload)/admin/modules/workout-structure/types.ts b/src/app/(payload)/admin/modules/workout-structure/types.ts deleted file mode 100644 index d4b7c5d..0000000 --- a/src/app/(payload)/admin/modules/workout-structure/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -export type Section = { id?: string; title?: string | null; subtitle?: string | null } - -export type Group = { - id: number - sectionRowId?: string | null - order?: number | null - label?: string | null - bundleWithPrevious?: boolean | null - protocol?: string | null - rounds?: string | null - durationMinutes?: number | null - intervalSeconds?: number | null - workSeconds?: number | null - restSeconds?: number | null - restBetweenRounds?: string | null -} - -export type ExerciseRow = { - id: number - group?: number | null - order?: number | null - numer?: string | null - exercise?: { id: number; name?: string | null } | null - note?: string | null - rounds?: string | null - reps?: string | null - kg?: string | null - tut?: string | null - rir?: string | null - rest?: string | null - durationMin?: number | null - durationSec?: number | null -} - -export type RawExerciseRow = Omit & { - group?: number | { id: number } | null - exercise?: { id: number; name?: string | null } | number | null -} - -export type WorkoutStructureData = { - sections: Section[] - initialGroups: Group[] - initialExerciseRows: ExerciseRow[] - groupIdsWithLogs: number[] - exerciseRowIdsWithLogs: number[] -} diff --git a/src/app/(payload)/admin/modules/workout-structure/utils/format.ts b/src/app/(payload)/admin/modules/workout-structure/utils/format.ts deleted file mode 100644 index 00fe314..0000000 --- a/src/app/(payload)/admin/modules/workout-structure/utils/format.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ExerciseRow, Group } from '../types' - -export const groupLabel = (g: Group): string => { - const p = g.protocol ?? 'standard' - const r = g.rounds - const d = g.durationMinutes - if (p === 'emom') return r ? `EMOM · ${r} min` : 'EMOM' - if (p === 'amrap') return d ? `AMRAP · ${d} min` : 'AMRAP' - if (p === 'for_time') return r ? `For Time · ${r} rounds` : 'For Time' - if (p === 'tabata') return 'Tabata' - return r ? `${r} sets` : 'Standard' -} - -export const exerciseLabel = (row: ExerciseRow): string => - row.exercise?.name ?? row.note ?? '—' - -export const exerciseMeta = (row: ExerciseRow): string => { - const parts: string[] = [] - if (row.rounds) parts.push(`${row.rounds} sets`) - if (row.reps) parts.push(`${row.reps} reps`) - if (row.kg) parts.push(`${row.kg} kg`) - if (row.rir) parts.push(`RIR ${row.rir}`) - if (row.tut) parts.push(`TUT ${row.tut}`) - if (row.rest) parts.push(`rest ${row.rest}`) - return parts.join(' · ') -} diff --git a/src/app/[locale]/(frontend)/page.tsx b/src/app/[locale]/(frontend)/page.tsx index ce132e5..778097f 100644 --- a/src/app/[locale]/(frontend)/page.tsx +++ b/src/app/[locale]/(frontend)/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation' import { getTranslations } from 'next-intl/server' -import { loadTrainingPlans } from '@/loaders/training-plan-loader' -import { WorkoutPlans } from '@/components/workout/workout-plans' +import { loadTrainingPlans } from '@/modules/training/plans/server' +import { WorkoutPlans } from '@/modules/training/components/workout-plans' import { LogoutButton } from '@/components/common/logout-button' import { PageContainer } from '@/components/ui/page-container' import { PageHeader } from '@/components/ui/page-header' diff --git a/src/app/[locale]/(frontend)/share/[token]/page.tsx b/src/app/[locale]/(frontend)/share/[token]/page.tsx index a7e570c..ecef3a7 100644 --- a/src/app/[locale]/(frontend)/share/[token]/page.tsx +++ b/src/app/[locale]/(frontend)/share/[token]/page.tsx @@ -2,8 +2,8 @@ import { notFound } from 'next/navigation' import { getFormatter, getTranslations } from 'next-intl/server' import React from 'react' -import { loadShareLink } from '@/loaders/share-link-loader' -import { WorkoutPlans } from '@/components/workout/workout-plans' +import { loadShareLink } from '@/modules/sharing/server' +import { WorkoutPlans } from '@/modules/training/components/workout-plans' import { PageContainer } from '@/components/ui/page-container' import { PageHeader } from '@/components/ui/page-header' diff --git a/src/collections/exercises/index.ts b/src/collections/exercises/index.ts index 54090ff..3f1d2e5 100644 --- a/src/collections/exercises/index.ts +++ b/src/collections/exercises/index.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from 'payload' import { isAdmin, isAuthenticated } from '../../access' -import { trackingOptions, DEFAULT_TRACKING } from './types' +import { DEFAULT_TRACKING, TRACKING_OPTIONS } from '@/modules/training/exercises' export const Exercises: CollectionConfig = { slug: 'exercises', @@ -27,7 +27,7 @@ export const Exercises: CollectionConfig = { type: 'select', label: 'Tracking type', defaultValue: DEFAULT_TRACKING, - options: trackingOptions, + options: TRACKING_OPTIONS, admin: { description: 'Determines which fields are shown in the set logging form' }, }, { diff --git a/src/collections/exercises/types.ts b/src/collections/exercises/types.ts deleted file mode 100644 index dd4d420..0000000 --- a/src/collections/exercises/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Single source of truth: exercise tracking type → which metrics we collect. - * Used by: the logging form (which inputs to show) and the SetLog hook (clearing fields outside the type). - */ - -export type MetricField = 'weight' | 'reps' | 'rir' | 'distanceM' | 'durationSec' -export type TrackingType = 'strength' | 'cardio' - -export type UnitOption = { label: string; value: string; factor: number } -export type MetricMeta = { - label: string - placeholder: string - numeric: boolean - // Composite field: input in min + sec, stored in DB as total seconds - composite?: 'duration' - // Front-end input units; stored in DB as base value (factor = how many base units per 1 selected unit) - units?: { default: string; options: UnitOption[] } -} - -export const METRIC_FIELDS: Record = { - weight: { label: 'Weight (kg)', placeholder: 'weight', numeric: true }, - reps: { label: 'Reps', placeholder: 'reps', numeric: false }, - rir: { label: 'RIR', placeholder: 'RIR (reserve)', numeric: false }, - distanceM: { label: 'Distance (m)', placeholder: 'distance', numeric: true }, - durationSec: { label: 'Duration', placeholder: 'minutes / seconds', numeric: true, composite: 'duration' }, -} - -export const ALL_METRIC_FIELDS = Object.keys(METRIC_FIELDS) as MetricField[] - -export const TRACKING: Record = { - strength: { label: 'Strength', fields: ['weight', 'reps', 'rir'] }, - cardio: { label: 'Cardio', fields: ['distanceM', 'durationSec'] }, -} - -export const DEFAULT_TRACKING: TrackingType = 'strength' - -/** List of fields for the given type (with fallback to the default). */ -export const trackingFields = (t?: string | null): MetricField[] => - t && t in TRACKING ? TRACKING[t as TrackingType].fields : TRACKING[DEFAULT_TRACKING].fields - -/** Select options for Payload. */ -export const trackingOptions = (Object.keys(TRACKING) as TrackingType[]).map((value) => ({ - label: TRACKING[value].label, - value, -})) diff --git a/src/collections/microcycles/index.ts b/src/collections/microcycles/index.ts index 77eea2a..cb48224 100644 --- a/src/collections/microcycles/index.ts +++ b/src/collections/microcycles/index.ts @@ -41,5 +41,18 @@ export const Microcycles: CollectionConfig = { label: 'Order', defaultValue: 0, }, + { + name: 'workoutsNavigation', + type: 'ui', + label: '', + admin: { + components: { + Field: { + path: '@/modules/training/admin/training-navigation/training-navigation', + exportName: 'MicrocycleWorkouts', + }, + }, + }, + }, ], } diff --git a/src/collections/plans/index.ts b/src/collections/plans/index.ts index 05490e0..bbd1cba 100644 --- a/src/collections/plans/index.ts +++ b/src/collections/plans/index.ts @@ -70,5 +70,18 @@ export const Plans: CollectionConfig = { label: 'Source (file)', admin: { description: 'Where the plan was imported from' }, }, + { + name: 'microcyclesNavigation', + type: 'ui', + label: '', + admin: { + components: { + Field: { + path: '@/modules/training/admin/training-navigation/training-navigation', + exportName: 'PlanMicrocycles', + }, + }, + }, + }, ], } diff --git a/src/collections/set-logs/index.ts b/src/collections/set-logs/index.ts index afa7b90..b13796e 100644 --- a/src/collections/set-logs/index.ts +++ b/src/collections/set-logs/index.ts @@ -1,12 +1,13 @@ import type { CollectionConfig } from 'payload' import { adminOrOwnByClient, canReadViaShareToken } from '../../access' -import { trackingFields, ALL_METRIC_FIELDS } from '../exercises/types' +import { ALL_METRIC_FIELDS, getTrackingFields } from '@/modules/training/exercises' +import { LEGACY_SET_LOG_FIELDS } from '@/modules/training/logs' export const SetLogs: CollectionConfig = { slug: 'set-logs', admin: { useAsTitle: 'id', - defaultColumns: ['exerciseName', 'setNumber', 'weight', 'reps', 'client'], + defaultColumns: ['exerciseName', 'setNumber', 'weightLeft', 'weightRight', 'repsLeft', 'repsRight', 'client'], group: 'Training log', }, access: { @@ -21,7 +22,7 @@ export const SetLogs: CollectionConfig = { }, hooks: { beforeValidate: [ - async ({ data, req }) => { + async ({ data, originalDoc, req }) => { if (!data) return data if (req.user?.collection === 'clients' && data.session) { const session = await req.payload.findByID({ @@ -34,15 +35,30 @@ export const SetLogs: CollectionConfig = { throw new Error('You cannot log sets to someone else\'s session.') } } - if (data.exercise) { + const exerciseRowId = data.exerciseRow ?? originalDoc?.exerciseRow + const exerciseRow = exerciseRowId + ? await req.payload.findByID({ + collection: 'workout-exercise-rows', + id: exerciseRowId, + depth: 0, + }) + : null + + if (exerciseRow?.targetType === 'duration') { + const allowedFields = new Set(['weightLeft', 'weightRight', 'durationSec', ...LEGACY_SET_LOG_FIELDS]) + for (const field of [...ALL_METRIC_FIELDS, ...LEGACY_SET_LOG_FIELDS]) { + if (!allowedFields.has(field)) data[field] = null + } + } else if (data.exercise) { const ex = await req.payload.findByID({ collection: 'exercises', id: data.exercise, depth: 0, }) - const allowed = trackingFields(ex?.trackingType) - for (const f of ALL_METRIC_FIELDS) { - if (!allowed.includes(f) && data[f] != null) data[f] = null + const allowed = getTrackingFields(ex?.trackingType) + const allowedFields = new Set([...allowed, ...LEGACY_SET_LOG_FIELDS]) + for (const field of [...ALL_METRIC_FIELDS, ...LEGACY_SET_LOG_FIELDS]) { + if (!allowedFields.has(field)) data[field] = null } } return data @@ -106,6 +122,17 @@ export const SetLogs: CollectionConfig = { name: 'weight', type: 'number', label: 'Weight (kg)', + admin: { hidden: true }, + }, + { + name: 'weightLeft', + type: 'number', + label: 'Weight left (kg)', + }, + { + name: 'weightRight', + type: 'number', + label: 'Weight right (kg)', }, { name: 'isBodyweight', @@ -127,11 +154,23 @@ export const SetLogs: CollectionConfig = { name: 'reps', type: 'text', label: 'Reps', + admin: { hidden: true }, + }, + { + name: 'repsLeft', + type: 'text', + label: 'Reps left', + }, + { + name: 'repsRight', + type: 'text', + label: 'Reps right', }, { name: 'rir', type: 'text', label: 'RIR', + admin: { hidden: true }, }, { name: 'note', diff --git a/src/collections/share-links/index.ts b/src/collections/share-links/index.ts index 633dbcf..342bd2a 100644 --- a/src/collections/share-links/index.ts +++ b/src/collections/share-links/index.ts @@ -90,7 +90,7 @@ export const ShareLinks: CollectionConfig = { position: 'sidebar', components: { Field: { - path: '@/app/(payload)/admin/modules/share-link-url/share-link-url', + path: '@/modules/sharing/admin/share-link-url/share-link-url', exportName: 'ShareLinkUrl', }, }, diff --git a/src/collections/workout-exercise-rows/index.ts b/src/collections/workout-exercise-rows/index.ts index c6eab78..c6b16d6 100644 --- a/src/collections/workout-exercise-rows/index.ts +++ b/src/collections/workout-exercise-rows/index.ts @@ -1,4 +1,5 @@ import { APIError, type CollectionConfig } from 'payload' +import { EXERCISE_TARGET_TYPE_OPTIONS } from '@/modules/training/exercises' import { isAdmin, isAuthenticated } from '../../access' const PROTOCOL_OPTIONS = [ @@ -14,7 +15,7 @@ export const WorkoutExerciseRows: CollectionConfig = { slug: 'workout-exercise-rows', admin: { useAsTitle: 'numer', - defaultColumns: ['numer', 'exercise', 'group', 'reps', 'kg'], + defaultColumns: ['numer', 'exercise', 'group', 'repsLeft', 'repsRight', 'kg'], group: 'Training plan', }, access: { @@ -71,12 +72,37 @@ export const WorkoutExerciseRows: CollectionConfig = { type: 'text', label: 'Note / variant', }, + { + name: 'targetType', + type: 'select', + label: 'Target type', + defaultValue: 'repetitions', + options: EXERCISE_TARGET_TYPE_OPTIONS, + }, { type: 'row', fields: [ { name: 'rounds', type: 'text', label: 'Sets', admin: { width: '25%', description: 'e.g. 4, 3-4' } }, - { name: 'reps', type: 'text', label: 'Reps', admin: { width: '25%' } }, - { name: 'kg', type: 'text', label: 'KG', admin: { width: '50%' } }, + { name: 'reps', type: 'text', label: 'Reps', admin: { hidden: true } }, + { + name: 'repsLeft', + type: 'text', + label: 'Reps left', + admin: { + condition: (_, siblingData) => (siblingData.targetType ?? 'repetitions') === 'repetitions', + width: '25%', + }, + }, + { + name: 'repsRight', + type: 'text', + label: 'Reps right', + admin: { + condition: (_, siblingData) => (siblingData.targetType ?? 'repetitions') === 'repetitions', + width: '25%', + }, + }, + { name: 'kg', type: 'text', label: 'KG', admin: { width: '25%' } }, ], }, { @@ -84,7 +110,7 @@ export const WorkoutExerciseRows: CollectionConfig = { fields: [ { name: 'tut', type: 'text', label: 'TUT', admin: { width: '33%' } }, { name: 'rir', type: 'text', label: 'RIR', admin: { width: '33%' } }, - { name: 'rest', type: 'text', label: 'Rest', admin: { width: '34%' } }, + { name: 'rest', type: 'text', label: 'Rest(s)', admin: { width: '34%' } }, ], }, { @@ -95,7 +121,10 @@ export const WorkoutExerciseRows: CollectionConfig = { type: 'number', label: 'Duration — minutes', min: 0, - admin: { width: '50%' }, + admin: { + condition: (_, siblingData) => siblingData.targetType === 'duration', + width: '50%', + }, }, { name: 'durationSec', @@ -103,7 +132,10 @@ export const WorkoutExerciseRows: CollectionConfig = { label: 'Duration — seconds', min: 0, max: 59, - admin: { width: '50%' }, + admin: { + condition: (_, siblingData) => siblingData.targetType === 'duration', + width: '50%', + }, }, ], }, @@ -138,7 +170,7 @@ export const WorkoutExerciseRows: CollectionConfig = { { name: 'durationMinutes', type: 'number', label: 'Duration (minutes)', min: 0 }, { name: 'intervalSeconds', type: 'number', label: 'Interval (s)', min: 1 }, { name: 'workSeconds', type: 'number', label: 'Work time (s)', min: 1 }, - { name: 'restSeconds', type: 'number', label: 'Rest (s)', min: 0 }, + { name: 'restSeconds', type: 'number', label: 'Rest(s)', min: 0 }, ], }, ], diff --git a/src/collections/workout-groups/index.ts b/src/collections/workout-groups/index.ts index 4419d74..f1923d6 100644 --- a/src/collections/workout-groups/index.ts +++ b/src/collections/workout-groups/index.ts @@ -119,7 +119,7 @@ export const WorkoutGroups: CollectionConfig = { { name: 'restSeconds', type: 'number', - label: 'Rest (s)', + label: 'Rest(s)', min: 0, admin: { description: 'Used for Tabata — default 10' }, defaultValue: 10, diff --git a/src/collections/workouts/index.ts b/src/collections/workouts/index.ts index ce71e0e..f29c7aa 100644 --- a/src/collections/workouts/index.ts +++ b/src/collections/workouts/index.ts @@ -40,7 +40,7 @@ export const Workouts: CollectionConfig = { edit: { structure: { Component: { - path: '@/app/(payload)/admin/modules/workout-structure/workout-structure', + path: '@/modules/training/admin/workout-structure/workout-structure', exportName: 'WorkoutStructureView', }, path: '/structure', @@ -72,7 +72,7 @@ export const Workouts: CollectionConfig = { admin: { components: { Field: { - path: '@/app/(payload)/admin/modules/workout-logs-notice/workout-logs-notice', + path: '@/modules/training/admin/workout-logs-notice/workout-logs-notice', exportName: 'WorkoutLogsNotice', }, }, diff --git a/src/components/workout/series-form/components/bodyweight-field/bodyweight-field.tsx b/src/components/workout/series-form/components/bodyweight-field/bodyweight-field.tsx deleted file mode 100644 index 8d839d4..0000000 --- a/src/components/workout/series-form/components/bodyweight-field/bodyweight-field.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client' - -import { useTranslations } from 'next-intl' -import React from 'react' -import type { RegisterOptions, UseFormRegister } from 'react-hook-form' -import { mutedTextClass } from '@/lib/class-names' -import { BODYWEIGHT_KEY } from '@/lib/metric-keys' -import { Field } from '@/components/ui/field' -import { Input } from '@/components/ui/input' -import type { Values } from '@/types/workout' - -export function BodyweightField({ - label, - placeholder, - autoFocus, - register, - registerOptions, - isBodyweight, - onToggleBodyweight, -}: { - label: string - placeholder?: string - autoFocus: boolean - register: UseFormRegister - registerOptions: RegisterOptions - isBodyweight: boolean - onToggleBodyweight: () => void -}) { - const t = useTranslations('seriesForm') - - return ( - - {isBodyweight ? ( - - ) : ( - - - - - )} - - - ) -} diff --git a/src/components/workout/series-form/components/bodyweight-field/index.ts b/src/components/workout/series-form/components/bodyweight-field/index.ts deleted file mode 100644 index 0f110dd..0000000 --- a/src/components/workout/series-form/components/bodyweight-field/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BodyweightField } from './bodyweight-field' diff --git a/src/lib/metric-keys.ts b/src/lib/metric-keys.ts deleted file mode 100644 index ab281ec..0000000 --- a/src/lib/metric-keys.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Composite form-field key helpers. The form flattens compound metrics into - * separate keys (e.g. duration -> `field__min` + `field__sec`), so the same - * naming must be used when writing form values and reading them back. Keeping - * the convention here prevents the write and read sides from drifting apart. - */ - -export const BODYWEIGHT_KEY = 'weight__bodyweight' - -export const minKey = (field: string): string => `${field}__min` -export const secKey = (field: string): string => `${field}__sec` -export const unitKey = (field: string): string => `${field}__unit` diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts deleted file mode 100644 index 3ba3c3d..0000000 --- a/src/lib/metrics.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { METRIC_FIELDS, type MetricField } from '@/collections/exercises/types' -import { PROTOCOL_LABEL } from '@/types/constants' -import type { SetLog, Values } from '@/types/workout' -import { formatMinSec, formatSec } from './date' -import { BODYWEIGHT_KEY, minKey, secKey, unitKey } from './metric-keys' - -type ExerciseMetaLabels = { - seriesPrefix: string - repsPrefix: string - durationPrefix: string - restPrefix: string -} - -type UnitMeta = { options: { value: string; factor: number }[]; default: string } - -/** Returns the conversion factor for a unit, defaulting to 1 when not found. */ -const unitFactor = (units: UnitMeta, unit: string): number => - units.options.find((option) => option.value === unit)?.factor ?? 1 - -/** A value counts as filled when it is non-empty and not the 'x' placeholder used for "no value". */ -export const isValidValue = (value?: string | null): boolean => { - const trimmed = value?.trim() ?? '' - return trimmed !== '' && trimmed.toLowerCase() !== 'x' -} - -export const buildExerciseMeta = ( - ex: { - rounds?: string | null - reps?: string | null - durationMin?: number | null - durationSec?: number | null - rest?: string | null - tut?: string | null - rir?: string | null - kg?: string | null - }, - labels: ExerciseMetaLabels, -): string[] => { - const parts: string[] = [] - if (isValidValue(ex.rounds)) parts.push(`${labels.seriesPrefix}: ${ex.rounds}`) - if (isValidValue(ex.reps)) parts.push(`${labels.repsPrefix}: ${ex.reps}`) - const dur = formatMinSec(ex.durationMin, ex.durationSec) - if (dur) parts.push(`${labels.durationPrefix}: ${dur}`) - if (isValidValue(ex.rest)) parts.push(`${labels.restPrefix}: ${ex.rest}`) - if (isValidValue(ex.tut)) parts.push(`TUT: ${ex.tut}`) - if (isValidValue(ex.rir)) parts.push(`RIR: ${ex.rir}`) - if (isValidValue(ex.kg)) parts.push(`${ex.kg} kg`) - return parts -} - -export const metricBody = (fields: MetricField[], values: Values): Record => { - const body: Record = {} - const isBodyweight = values[BODYWEIGHT_KEY] === 'true' - - for (const field of fields) { - const meta = METRIC_FIELDS[field] - if (meta.composite === 'duration') { - const mins = (values[minKey(field)] ?? '').trim() - const secs = (values[secKey(field)] ?? '').trim() - body[field] = mins === '' && secs === '' ? null : (Number(mins) || 0) * 60 + (Number(secs) || 0) - continue - } - - if (field === 'weight' && isBodyweight) { - body.weight = null - continue - } - - const raw = (values[field] ?? '').trim() - if (raw === '') { - body[field] = null - } else if (meta.units) { - const unit = values[unitKey(field)] || meta.units.default - body[field] = Number(raw) * unitFactor(meta.units, unit) - } else { - body[field] = meta.numeric ? Number(raw) : raw - } - } - - body.isBodyweight = isBodyweight - body.note = values.note?.trim() || null - return body -} - -export const toDefaultUnit = (field: MetricField, base: number): { value: string; unit: string } => { - const meta = METRIC_FIELDS[field] - if (!meta.units) return { value: String(base), unit: '' } - const unit = meta.units.default - return { value: String(base / unitFactor(meta.units, unit)), unit } -} - -export const setSummary = (set: SetLog): string => { - const parts: string[] = [] - if (set.isBodyweight) parts.push('MC') - else if (set.weight != null) parts.push(`${set.weight} kg`) - if (set.distanceM != null) parts.push(`${set.distanceM} m`) - if (set.durationSec != null) parts.push(formatSec(set.durationSec)) - // reps is free text - empty string means "not filled", so only render a truthy value - if (set.reps) parts.push(`× ${set.reps}`) - if (set.rir) parts.push(`RIR ${set.rir}`) - if (set.note) parts.push(set.note) - return parts.length ? parts.join(' · ') : '—' -} - -export const workoutGroupLabel = (group: { - label?: unknown - protocol?: unknown - rounds?: unknown - durationMinutes?: unknown -}): string => { - if (group.label) return group.label as string - const protocol = group.protocol as string - const rounds = group.rounds as string | null | undefined - const durationMinutes = group.durationMinutes as number | null | undefined - if (protocol === 'emom') return rounds ? `${PROTOCOL_LABEL.emom} · ${rounds} min` : PROTOCOL_LABEL.emom - if (protocol === 'amrap') - return durationMinutes ? `${PROTOCOL_LABEL.amrap} · ${durationMinutes} min` : PROTOCOL_LABEL.amrap - if (protocol === 'for_time') return rounds ? `${PROTOCOL_LABEL.for_time} · ${rounds} rund` : PROTOCOL_LABEL.for_time - if (protocol === 'tabata') return PROTOCOL_LABEL.tabata - return rounds ? `${rounds} serie` : '' -} - -export const setLogToFormValues = (set: SetLog, fields: MetricField[]): Values => { - const initial: Values = { note: set.note ?? '' } - if (set.isBodyweight) initial[BODYWEIGHT_KEY] = 'true' - - for (const field of fields) { - const raw = (set as Record)[field] - if (raw == null) { - initial[field] = '' - } else if (METRIC_FIELDS[field].composite === 'duration') { - const base = Number(raw) - initial[minKey(field)] = String(Math.floor(base / 60)) - initial[secKey(field)] = String(base % 60) - } else if (METRIC_FIELDS[field].units) { - const conv = toDefaultUnit(field, Number(raw)) - initial[field] = conv.value - initial[unitKey(field)] = conv.unit - } else { - initial[field] = String(raw) - } - } - return initial -} diff --git a/src/loaders/load-plans-items.ts b/src/loaders/load-plans-items.ts deleted file mode 100644 index 1d6b4d6..0000000 --- a/src/loaders/load-plans-items.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { getPayload } from 'payload' - -import { STATUS_LABEL } from '@/types/constants' -import { buildExerciseMeta, workoutGroupLabel } from '@/lib/metrics' -import type { TPlanAccordionItem } from '@/types/plan' -import type { TBlock, TGroup, TWorkout } from '@/types/workout' - -type Payload = Awaited> - -export type PlanLabels = { - seriesPrefix: string - repsPrefix: string - durationPrefix: string - restPrefix: string -} - -export async function loadPlansItems( - payload: Payload, - planIds: (number | string)[], - labels: PlanLabels, - overrideAccess = false, -): Promise { - if (!planIds.length) return [] - - const plans = await payload.find({ - collection: 'plans', - where: { id: { in: planIds } }, - sort: '-createdAt', - depth: 0, - limit: 100, - overrideAccess, - }) - - if (!plans.docs.length) return [] - - const microcycles = await payload.find({ - collection: 'microcycles', - where: { plan: { in: planIds } }, - sort: 'order', - depth: 0, - limit: 500, - overrideAccess, - }) - - const microcycleIds = microcycles.docs.map((m) => m.id) - - const workouts = microcycleIds.length - ? await payload.find({ - collection: 'workouts', - where: { microcycle: { in: microcycleIds } }, - sort: 'order', - depth: 0, - limit: 1000, - overrideAccess, - }) - : { docs: [] } - - const workoutIds = workouts.docs.map((w) => w.id) - - const workoutGroups = workoutIds.length - ? await payload.find({ - collection: 'workout-groups', - where: { workout: { in: workoutIds } }, - sort: 'order', - depth: 0, - limit: 10000, - overrideAccess, - }) - : { docs: [] } - - const groupIds = workoutGroups.docs.map((g) => g.id) - - const exerciseRows = groupIds.length - ? await payload.find({ - collection: 'workout-exercise-rows', - where: { group: { in: groupIds } }, - sort: 'order', - depth: 1, - limit: 10000, - overrideAccess, - }) - : { docs: [] } - - const resolveId = (rel: unknown): number | string | undefined => - rel && typeof rel === 'object' ? (rel as { id: number | string }).id : (rel as number | string) - - const microcyclesByPlan = (planId: number | string) => - microcycles.docs.filter((microcycle) => resolveId(microcycle.plan) === planId) - const workoutsByMicrocycle = (microcycleId: number | string) => - workouts.docs.filter((workout) => resolveId(workout.microcycle) === microcycleId) - const groupsByWorkout = (workoutId: number | string) => - workoutGroups.docs.filter((group) => resolveId(group.workout) === workoutId) - const rowsByGroup = (groupId: number | string) => - exerciseRows.docs.filter((row) => resolveId(row.group) === groupId) - - const serializeWorkout = (workout: (typeof workouts.docs)[number]): TWorkout => { - const sections = (workout.sections ?? []) as Array<{ - id?: string - title?: string | null - subtitle?: string | null - }> - const groups = groupsByWorkout(workout.id) - - const serializeGroup = (group: (typeof groups)[number]): TGroup => ({ - protocol: (group.protocol as string) ?? 'standard', - label: workoutGroupLabel(group), - exercises: rowsByGroup(group.id).map((ex) => { - const cat = - ex.exercise && typeof ex.exercise === 'object' - ? (ex.exercise as { - id: number - name?: string - trackingType?: string - videoUrl?: string - }) - : null - const name = cat?.name || ex.note || '' - const extraNote = cat && ex.note && ex.note !== cat.name ? ex.note : null - return { - rowId: String(ex.id), - numer: (ex.numer as string | null) ?? null, - name, - note: extraNote as string | null, - exerciseId: cat?.id ?? null, - exerciseName: name, - trackingType: cat?.trackingType ?? null, - videoUrl: (cat?.videoUrl as string | null | undefined) ?? null, - rounds: (ex.rounds as string | null) ?? null, - meta: buildExerciseMeta(ex as Parameters[0], labels), - prefill: { - reps: (ex.reps as string | null) ?? null, - rir: (ex.rir as string | null) ?? null, - }, - setParameters: - (ex.setParameters as - | Array<{ setNumber: number; reps?: string | null; kg?: string | null }> - | null - | undefined) ?? null, - } - }), - }) - - return { - id: workout.id, - title: workout.title, - rpe: workout.rpe ?? null, - sections: sections.map((section) => { - const sectionGroups = groups - .filter((group) => (group.sectionRowId as string | null | undefined) === section.id) - .sort((a, b) => ((a.order as number) ?? 0) - ((b.order as number) ?? 0)) - // Bundle consecutive groups into colored blocks: a new block starts at - // the first group of the section or whenever a group does not bundle - // with the previous one. Block index resets per section. - const blocks: TBlock[] = [] - sectionGroups.forEach((group, groupIndexInSection) => { - if (groupIndexInSection === 0 || !group.bundleWithPrevious) { - blocks.push({ index: blocks.length, groups: [] }) - } - blocks[blocks.length - 1].groups.push(serializeGroup(group)) - }) - return { - title: section.title ?? null, - subtitle: section.subtitle ?? null, - blocks, - } - }), - } - } - - return plans.docs.map((plan) => { - const status = (plan.status as string) || 'active' - return { - id: plan.id, - title: plan.title, - status, - statusLabel: STATUS_LABEL[status] || status, - dateRange: - plan.startDate || plan.endDate - ? [plan.startDate, plan.endDate] - .map((date) => (date ? new Date(date).toLocaleDateString('pl-PL') : '…')) - .join(' – ') - : null, - description: plan.description ?? null, - microcycles: microcyclesByPlan(plan.id).map((microcycle) => ({ - id: microcycle.id, - title: microcycle.title, - rpe: microcycle.rpe ?? null, - workouts: workoutsByMicrocycle(microcycle.id).map((workout) => serializeWorkout(workout)), - })), - } - }) -} diff --git a/src/migrations/20260724_065545.json b/src/migrations/20260724_065545.json new file mode 100644 index 0000000..e5eece3 --- /dev/null +++ b/src/migrations/20260724_065545.json @@ -0,0 +1,4417 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients_sessions": { + "name": "clients_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "clients_sessions_order_idx": { + "name": "clients_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "clients_sessions_parent_id_idx": { + "name": "clients_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "clients_sessions_parent_id_fk": { + "name": "clients_sessions_parent_id_fk", + "tableFrom": "clients_sessions", + "tableTo": "clients", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients": { + "name": "clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "clients_updated_at_idx": { + "name": "clients_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "clients_created_at_idx": { + "name": "clients_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "clients_email_idx": { + "name": "clients_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._clients_v_version_sessions": { + "name": "_clients_v_version_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_clients_v_version_sessions_order_idx": { + "name": "_clients_v_version_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_sessions_parent_id_idx": { + "name": "_clients_v_version_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_clients_v_version_sessions_parent_id_fk": { + "name": "_clients_v_version_sessions_parent_id_fk", + "tableFrom": "_clients_v_version_sessions", + "tableTo": "_clients_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._clients_v": { + "name": "_clients_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_name": { + "name": "version_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_notes": { + "name": "version_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_email": { + "name": "version_email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "version_reset_password_token": { + "name": "version_reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_reset_password_expiration": { + "name": "version_reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_salt": { + "name": "version_salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_hash": { + "name": "version_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_login_attempts": { + "name": "version_login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "version_lock_until": { + "name": "version_lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "_clients_v_parent_idx": { + "name": "_clients_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_version_updated_at_idx": { + "name": "_clients_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_version_created_at_idx": { + "name": "_clients_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_version_email_idx": { + "name": "_clients_v_version_version_email_idx", + "columns": [ + { + "expression": "version_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_created_at_idx": { + "name": "_clients_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_updated_at_idx": { + "name": "_clients_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_clients_v_parent_id_clients_id_fk": { + "name": "_clients_v_parent_id_clients_id_fk", + "tableFrom": "_clients_v", + "tableTo": "clients", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_plans_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "start_date": { + "name": "start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plans_client_idx": { + "name": "plans_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plans_updated_at_idx": { + "name": "plans_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plans_created_at_idx": { + "name": "plans_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plans_client_id_clients_id_fk": { + "name": "plans_client_id_clients_id_fk", + "tableFrom": "plans", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._plans_v": { + "name": "_plans_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_client_id": { + "name": "version_client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_status": { + "name": "version_status", + "type": "enum__plans_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "version_start_date": { + "name": "version_start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_end_date": { + "name": "version_end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "version_description": { + "name": "version_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_source": { + "name": "version_source", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "_plans_v_parent_idx": { + "name": "_plans_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_version_version_client_idx": { + "name": "_plans_v_version_version_client_idx", + "columns": [ + { + "expression": "version_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_version_version_updated_at_idx": { + "name": "_plans_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_version_version_created_at_idx": { + "name": "_plans_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_created_at_idx": { + "name": "_plans_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_updated_at_idx": { + "name": "_plans_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_plans_v_parent_id_plans_id_fk": { + "name": "_plans_v_parent_id_plans_id_fk", + "tableFrom": "_plans_v", + "tableTo": "plans", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_plans_v_version_client_id_clients_id_fk": { + "name": "_plans_v_version_client_id_clients_id_fk", + "tableFrom": "_plans_v", + "tableTo": "clients", + "columnsFrom": [ + "version_client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microcycles": { + "name": "microcycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rpe": { + "name": "rpe", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "microcycles_plan_idx": { + "name": "microcycles_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "microcycles_updated_at_idx": { + "name": "microcycles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "microcycles_created_at_idx": { + "name": "microcycles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microcycles_plan_id_plans_id_fk": { + "name": "microcycles_plan_id_plans_id_fk", + "tableFrom": "microcycles", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workouts_sections": { + "name": "workouts_sections", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workouts_sections_order_idx": { + "name": "workouts_sections_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workouts_sections_parent_id_idx": { + "name": "workouts_sections_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workouts_sections_parent_id_fk": { + "name": "workouts_sections_parent_id_fk", + "tableFrom": "workouts_sections", + "tableTo": "workouts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workouts": { + "name": "workouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "microcycle_id": { + "name": "microcycle_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rpe": { + "name": "rpe", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workouts_microcycle_idx": { + "name": "workouts_microcycle_idx", + "columns": [ + { + "expression": "microcycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workouts_updated_at_idx": { + "name": "workouts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workouts_created_at_idx": { + "name": "workouts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workouts_microcycle_id_microcycles_id_fk": { + "name": "workouts_microcycle_id_microcycles_id_fk", + "tableFrom": "workouts", + "tableTo": "microcycles", + "columnsFrom": [ + "microcycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_groups": { + "name": "workout_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "workout_id": { + "name": "workout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "section_row_id": { + "name": "section_row_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "bundle_with_previous": { + "name": "bundle_with_previous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "protocol": { + "name": "protocol", + "type": "enum_workout_groups_protocol", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "rounds": { + "name": "rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "work_seconds": { + "name": "work_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "rest_seconds": { + "name": "rest_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "rest_between_rounds": { + "name": "rest_between_rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workout_groups_workout_idx": { + "name": "workout_groups_workout_idx", + "columns": [ + { + "expression": "workout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_groups_updated_at_idx": { + "name": "workout_groups_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_groups_created_at_idx": { + "name": "workout_groups_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_groups_workout_id_workouts_id_fk": { + "name": "workout_groups_workout_id_workouts_id_fk", + "tableFrom": "workout_groups", + "tableTo": "workouts", + "columnsFrom": [ + "workout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_exercise_rows_set_parameters": { + "name": "workout_exercise_rows_set_parameters", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "set_number": { + "name": "set_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "reps": { + "name": "reps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "kg": { + "name": "kg", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workout_exercise_rows_set_parameters_order_idx": { + "name": "workout_exercise_rows_set_parameters_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_set_parameters_parent_id_idx": { + "name": "workout_exercise_rows_set_parameters_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_exercise_rows_set_parameters_parent_id_fk": { + "name": "workout_exercise_rows_set_parameters_parent_id_fk", + "tableFrom": "workout_exercise_rows_set_parameters", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_exercise_rows": { + "name": "workout_exercise_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "numer": { + "name": "numer", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rounds": { + "name": "rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps": { + "name": "reps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "kg": { + "name": "kg", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "tut": { + "name": "tut", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rir": { + "name": "rir", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rest": { + "name": "rest", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "duration_min": { + "name": "duration_min", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "duration_sec": { + "name": "duration_sec", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_protocol": { + "name": "override_protocol", + "type": "enum_workout_exercise_rows_override_protocol", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "override_rounds": { + "name": "override_rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "override_duration_minutes": { + "name": "override_duration_minutes", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_interval_seconds": { + "name": "override_interval_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_work_seconds": { + "name": "override_work_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_rest_seconds": { + "name": "override_rest_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workout_exercise_rows_group_idx": { + "name": "workout_exercise_rows_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_exercise_idx": { + "name": "workout_exercise_rows_exercise_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_updated_at_idx": { + "name": "workout_exercise_rows_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_created_at_idx": { + "name": "workout_exercise_rows_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_exercise_rows_group_id_workout_groups_id_fk": { + "name": "workout_exercise_rows_group_id_workout_groups_id_fk", + "tableFrom": "workout_exercise_rows", + "tableTo": "workout_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workout_exercise_rows_exercise_id_exercises_id_fk": { + "name": "workout_exercise_rows_exercise_id_exercises_id_fk", + "tableFrom": "workout_exercise_rows", + "tableTo": "exercises", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_logs": { + "name": "workout_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "workout_id": { + "name": "workout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workout_logs_workout_idx": { + "name": "workout_logs_workout_idx", + "columns": [ + { + "expression": "workout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_logs_client_idx": { + "name": "workout_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_logs_updated_at_idx": { + "name": "workout_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_logs_created_at_idx": { + "name": "workout_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_logs_workout_id_workouts_id_fk": { + "name": "workout_logs_workout_id_workouts_id_fk", + "tableFrom": "workout_logs", + "tableTo": "workouts", + "columnsFrom": [ + "workout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workout_logs_client_id_clients_id_fk": { + "name": "workout_logs_client_id_clients_id_fk", + "tableFrom": "workout_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.round_logs": { + "name": "round_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "round_number": { + "name": "round_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_round_logs_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'completed'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "round_logs_session_idx": { + "name": "round_logs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_group_idx": { + "name": "round_logs_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_client_idx": { + "name": "round_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_updated_at_idx": { + "name": "round_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_created_at_idx": { + "name": "round_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "round_logs_session_id_workout_logs_id_fk": { + "name": "round_logs_session_id_workout_logs_id_fk", + "tableFrom": "round_logs", + "tableTo": "workout_logs", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "round_logs_group_id_workout_groups_id_fk": { + "name": "round_logs_group_id_workout_groups_id_fk", + "tableFrom": "round_logs", + "tableTo": "workout_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "round_logs_client_id_clients_id_fk": { + "name": "round_logs_client_id_clients_id_fk", + "tableFrom": "round_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.set_logs": { + "name": "set_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_name": { + "name": "exercise_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "exercise_row_id": { + "name": "exercise_row_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "round_log_id": { + "name": "round_log_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_number": { + "name": "set_number", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "weight_left": { + "name": "weight_left", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "weight_right": { + "name": "weight_right", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "is_bodyweight": { + "name": "is_bodyweight", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "distance_m": { + "name": "distance_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "duration_sec": { + "name": "duration_sec", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "reps": { + "name": "reps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps_left": { + "name": "reps_left", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps_right": { + "name": "reps_right", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rir": { + "name": "rir", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "set_logs_session_idx": { + "name": "set_logs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_client_idx": { + "name": "set_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_exercise_idx": { + "name": "set_logs_exercise_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_exercise_row_idx": { + "name": "set_logs_exercise_row_idx", + "columns": [ + { + "expression": "exercise_row_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_round_log_idx": { + "name": "set_logs_round_log_idx", + "columns": [ + { + "expression": "round_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_updated_at_idx": { + "name": "set_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_created_at_idx": { + "name": "set_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "set_logs_session_id_workout_logs_id_fk": { + "name": "set_logs_session_id_workout_logs_id_fk", + "tableFrom": "set_logs", + "tableTo": "workout_logs", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_client_id_clients_id_fk": { + "name": "set_logs_client_id_clients_id_fk", + "tableFrom": "set_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_exercise_id_exercises_id_fk": { + "name": "set_logs_exercise_id_exercises_id_fk", + "tableFrom": "set_logs", + "tableTo": "exercises", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_exercise_row_id_workout_exercise_rows_id_fk": { + "name": "set_logs_exercise_row_id_workout_exercise_rows_id_fk", + "tableFrom": "set_logs", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "exercise_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_round_log_id_round_logs_id_fk": { + "name": "set_logs_round_log_id_round_logs_id_fk", + "tableFrom": "set_logs", + "tableTo": "round_logs", + "columnsFrom": [ + "round_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exercise_logs": { + "name": "exercise_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_name": { + "name": "exercise_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "exercise_row_id": { + "name": "exercise_row_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round_log_id": { + "name": "round_log_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "exercise_logs_session_idx": { + "name": "exercise_logs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_client_idx": { + "name": "exercise_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_exercise_idx": { + "name": "exercise_logs_exercise_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_exercise_row_idx": { + "name": "exercise_logs_exercise_row_idx", + "columns": [ + { + "expression": "exercise_row_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_round_log_idx": { + "name": "exercise_logs_round_log_idx", + "columns": [ + { + "expression": "round_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_updated_at_idx": { + "name": "exercise_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_created_at_idx": { + "name": "exercise_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "exercise_logs_session_id_workout_logs_id_fk": { + "name": "exercise_logs_session_id_workout_logs_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "workout_logs", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_client_id_clients_id_fk": { + "name": "exercise_logs_client_id_clients_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_exercise_id_exercises_id_fk": { + "name": "exercise_logs_exercise_id_exercises_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "exercises", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_exercise_row_id_workout_exercise_rows_id_fk": { + "name": "exercise_logs_exercise_row_id_workout_exercise_rows_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "exercise_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_round_log_id_round_logs_id_fk": { + "name": "exercise_logs_round_log_id_round_logs_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "round_logs", + "columnsFrom": [ + "round_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exercises": { + "name": "exercises", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tracking_type": { + "name": "tracking_type", + "type": "enum_exercises_tracking_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'strength'" + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "video_url": { + "name": "video_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "muscle_group": { + "name": "muscle_group", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "equipment": { + "name": "equipment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "exercises_updated_at_idx": { + "name": "exercises_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercises_created_at_idx": { + "name": "exercises_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_links_permissions": { + "name": "share_links_permissions", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_share_links_permissions", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "share_links_permissions_order_idx": { + "name": "share_links_permissions_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_permissions_parent_idx": { + "name": "share_links_permissions_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_links_permissions_parent_fk": { + "name": "share_links_permissions_parent_fk", + "tableFrom": "share_links_permissions", + "tableTo": "share_links", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_links": { + "name": "share_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "share_links_plan_idx": { + "name": "share_links_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_token_idx": { + "name": "share_links_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_updated_at_idx": { + "name": "share_links_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_created_at_idx": { + "name": "share_links_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_links_plan_id_plans_id_fk": { + "name": "share_links_plan_id_plans_id_fk", + "tableFrom": "share_links", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "clients_id": { + "name": "clients_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "plans_id": { + "name": "plans_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "microcycles_id": { + "name": "microcycles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workouts_id": { + "name": "workouts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workout_groups_id": { + "name": "workout_groups_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workout_exercise_rows_id": { + "name": "workout_exercise_rows_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workout_logs_id": { + "name": "workout_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "round_logs_id": { + "name": "round_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_logs_id": { + "name": "set_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_logs_id": { + "name": "exercise_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercises_id": { + "name": "exercises_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "share_links_id": { + "name": "share_links_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_clients_id_idx": { + "name": "payload_locked_documents_rels_clients_id_idx", + "columns": [ + { + "expression": "clients_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_plans_id_idx": { + "name": "payload_locked_documents_rels_plans_id_idx", + "columns": [ + { + "expression": "plans_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_microcycles_id_idx": { + "name": "payload_locked_documents_rels_microcycles_id_idx", + "columns": [ + { + "expression": "microcycles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workouts_id_idx": { + "name": "payload_locked_documents_rels_workouts_id_idx", + "columns": [ + { + "expression": "workouts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workout_groups_id_idx": { + "name": "payload_locked_documents_rels_workout_groups_id_idx", + "columns": [ + { + "expression": "workout_groups_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workout_exercise_rows_id_idx": { + "name": "payload_locked_documents_rels_workout_exercise_rows_id_idx", + "columns": [ + { + "expression": "workout_exercise_rows_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workout_logs_id_idx": { + "name": "payload_locked_documents_rels_workout_logs_id_idx", + "columns": [ + { + "expression": "workout_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_round_logs_id_idx": { + "name": "payload_locked_documents_rels_round_logs_id_idx", + "columns": [ + { + "expression": "round_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_set_logs_id_idx": { + "name": "payload_locked_documents_rels_set_logs_id_idx", + "columns": [ + { + "expression": "set_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_exercise_logs_id_idx": { + "name": "payload_locked_documents_rels_exercise_logs_id_idx", + "columns": [ + { + "expression": "exercise_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_exercises_id_idx": { + "name": "payload_locked_documents_rels_exercises_id_idx", + "columns": [ + { + "expression": "exercises_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_share_links_id_idx": { + "name": "payload_locked_documents_rels_share_links_id_idx", + "columns": [ + { + "expression": "share_links_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_clients_fk": { + "name": "payload_locked_documents_rels_clients_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "clients", + "columnsFrom": [ + "clients_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_plans_fk": { + "name": "payload_locked_documents_rels_plans_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "plans", + "columnsFrom": [ + "plans_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_microcycles_fk": { + "name": "payload_locked_documents_rels_microcycles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "microcycles", + "columnsFrom": [ + "microcycles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workouts_fk": { + "name": "payload_locked_documents_rels_workouts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workouts", + "columnsFrom": [ + "workouts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workout_groups_fk": { + "name": "payload_locked_documents_rels_workout_groups_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workout_groups", + "columnsFrom": [ + "workout_groups_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workout_exercise_rows_fk": { + "name": "payload_locked_documents_rels_workout_exercise_rows_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "workout_exercise_rows_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workout_logs_fk": { + "name": "payload_locked_documents_rels_workout_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workout_logs", + "columnsFrom": [ + "workout_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_round_logs_fk": { + "name": "payload_locked_documents_rels_round_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "round_logs", + "columnsFrom": [ + "round_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_set_logs_fk": { + "name": "payload_locked_documents_rels_set_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "set_logs", + "columnsFrom": [ + "set_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_exercise_logs_fk": { + "name": "payload_locked_documents_rels_exercise_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "exercise_logs", + "columnsFrom": [ + "exercise_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_exercises_fk": { + "name": "payload_locked_documents_rels_exercises_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "exercises", + "columnsFrom": [ + "exercises_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_share_links_fk": { + "name": "payload_locked_documents_rels_share_links_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "share_links", + "columnsFrom": [ + "share_links_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "clients_id": { + "name": "clients_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_clients_id_idx": { + "name": "payload_preferences_rels_clients_id_idx", + "columns": [ + { + "expression": "clients_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_clients_fk": { + "name": "payload_preferences_rels_clients_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "clients", + "columnsFrom": [ + "clients_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_plans_status": { + "name": "enum_plans_status", + "schema": "public", + "values": [ + "active", + "paused", + "completed" + ] + }, + "public.enum__plans_v_version_status": { + "name": "enum__plans_v_version_status", + "schema": "public", + "values": [ + "active", + "paused", + "completed" + ] + }, + "public.enum_workout_groups_protocol": { + "name": "enum_workout_groups_protocol", + "schema": "public", + "values": [ + "standard", + "emom", + "amrap", + "for_time", + "tabata" + ] + }, + "public.enum_workout_exercise_rows_override_protocol": { + "name": "enum_workout_exercise_rows_override_protocol", + "schema": "public", + "values": [ + "", + "standard", + "emom", + "amrap", + "for_time", + "tabata" + ] + }, + "public.enum_round_logs_status": { + "name": "enum_round_logs_status", + "schema": "public", + "values": [ + "completed", + "partial", + "skipped" + ] + }, + "public.enum_exercises_tracking_type": { + "name": "enum_exercises_tracking_type", + "schema": "public", + "values": [ + "strength", + "cardio" + ] + }, + "public.enum_share_links_permissions": { + "name": "enum_share_links_permissions", + "schema": "public", + "values": [ + "plan", + "results" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "bea176a0-e300-47ad-8c17-9242553f797e", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260724_065545.ts b/src/migrations/20260724_065545.ts new file mode 100644 index 0000000..aac634a --- /dev/null +++ b/src/migrations/20260724_065545.ts @@ -0,0 +1,17 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "set_logs" ADD COLUMN "weight_left" numeric; + ALTER TABLE "set_logs" ADD COLUMN "weight_right" numeric; + ALTER TABLE "set_logs" ADD COLUMN "reps_left" varchar; + ALTER TABLE "set_logs" ADD COLUMN "reps_right" varchar;`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "set_logs" DROP COLUMN "weight_left"; + ALTER TABLE "set_logs" DROP COLUMN "weight_right"; + ALTER TABLE "set_logs" DROP COLUMN "reps_left"; + ALTER TABLE "set_logs" DROP COLUMN "reps_right";`) +} diff --git a/src/migrations/20260724_073306.json b/src/migrations/20260724_073306.json new file mode 100644 index 0000000..48f77e3 --- /dev/null +++ b/src/migrations/20260724_073306.json @@ -0,0 +1,4429 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients_sessions": { + "name": "clients_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "clients_sessions_order_idx": { + "name": "clients_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "clients_sessions_parent_id_idx": { + "name": "clients_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "clients_sessions_parent_id_fk": { + "name": "clients_sessions_parent_id_fk", + "tableFrom": "clients_sessions", + "tableTo": "clients", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients": { + "name": "clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "clients_updated_at_idx": { + "name": "clients_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "clients_created_at_idx": { + "name": "clients_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "clients_email_idx": { + "name": "clients_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._clients_v_version_sessions": { + "name": "_clients_v_version_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "_clients_v_version_sessions_order_idx": { + "name": "_clients_v_version_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_sessions_parent_id_idx": { + "name": "_clients_v_version_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_clients_v_version_sessions_parent_id_fk": { + "name": "_clients_v_version_sessions_parent_id_fk", + "tableFrom": "_clients_v_version_sessions", + "tableTo": "_clients_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._clients_v": { + "name": "_clients_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_name": { + "name": "version_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_notes": { + "name": "version_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_email": { + "name": "version_email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "version_reset_password_token": { + "name": "version_reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_reset_password_expiration": { + "name": "version_reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_salt": { + "name": "version_salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_hash": { + "name": "version_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_login_attempts": { + "name": "version_login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "version_lock_until": { + "name": "version_lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "_clients_v_parent_idx": { + "name": "_clients_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_version_updated_at_idx": { + "name": "_clients_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_version_created_at_idx": { + "name": "_clients_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_version_version_email_idx": { + "name": "_clients_v_version_version_email_idx", + "columns": [ + { + "expression": "version_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_created_at_idx": { + "name": "_clients_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_clients_v_updated_at_idx": { + "name": "_clients_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_clients_v_parent_id_clients_id_fk": { + "name": "_clients_v_parent_id_clients_id_fk", + "tableFrom": "_clients_v", + "tableTo": "clients", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_plans_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "start_date": { + "name": "start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plans_client_idx": { + "name": "plans_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plans_updated_at_idx": { + "name": "plans_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plans_created_at_idx": { + "name": "plans_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plans_client_id_clients_id_fk": { + "name": "plans_client_id_clients_id_fk", + "tableFrom": "plans", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._plans_v": { + "name": "_plans_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_client_id": { + "name": "version_client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_status": { + "name": "version_status", + "type": "enum__plans_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "version_start_date": { + "name": "version_start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_end_date": { + "name": "version_end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "version_description": { + "name": "version_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_source": { + "name": "version_source", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "_plans_v_parent_idx": { + "name": "_plans_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_version_version_client_idx": { + "name": "_plans_v_version_version_client_idx", + "columns": [ + { + "expression": "version_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_version_version_updated_at_idx": { + "name": "_plans_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_version_version_created_at_idx": { + "name": "_plans_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_created_at_idx": { + "name": "_plans_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_plans_v_updated_at_idx": { + "name": "_plans_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_plans_v_parent_id_plans_id_fk": { + "name": "_plans_v_parent_id_plans_id_fk", + "tableFrom": "_plans_v", + "tableTo": "plans", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_plans_v_version_client_id_clients_id_fk": { + "name": "_plans_v_version_client_id_clients_id_fk", + "tableFrom": "_plans_v", + "tableTo": "clients", + "columnsFrom": [ + "version_client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microcycles": { + "name": "microcycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rpe": { + "name": "rpe", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "microcycles_plan_idx": { + "name": "microcycles_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "microcycles_updated_at_idx": { + "name": "microcycles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "microcycles_created_at_idx": { + "name": "microcycles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microcycles_plan_id_plans_id_fk": { + "name": "microcycles_plan_id_plans_id_fk", + "tableFrom": "microcycles", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workouts_sections": { + "name": "workouts_sections", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workouts_sections_order_idx": { + "name": "workouts_sections_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workouts_sections_parent_id_idx": { + "name": "workouts_sections_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workouts_sections_parent_id_fk": { + "name": "workouts_sections_parent_id_fk", + "tableFrom": "workouts_sections", + "tableTo": "workouts", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workouts": { + "name": "workouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "microcycle_id": { + "name": "microcycle_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rpe": { + "name": "rpe", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workouts_microcycle_idx": { + "name": "workouts_microcycle_idx", + "columns": [ + { + "expression": "microcycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workouts_updated_at_idx": { + "name": "workouts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workouts_created_at_idx": { + "name": "workouts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workouts_microcycle_id_microcycles_id_fk": { + "name": "workouts_microcycle_id_microcycles_id_fk", + "tableFrom": "workouts", + "tableTo": "microcycles", + "columnsFrom": [ + "microcycle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_groups": { + "name": "workout_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "workout_id": { + "name": "workout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "section_row_id": { + "name": "section_row_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "bundle_with_previous": { + "name": "bundle_with_previous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "protocol": { + "name": "protocol", + "type": "enum_workout_groups_protocol", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "rounds": { + "name": "rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "work_seconds": { + "name": "work_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 20 + }, + "rest_seconds": { + "name": "rest_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "rest_between_rounds": { + "name": "rest_between_rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workout_groups_workout_idx": { + "name": "workout_groups_workout_idx", + "columns": [ + { + "expression": "workout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_groups_updated_at_idx": { + "name": "workout_groups_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_groups_created_at_idx": { + "name": "workout_groups_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_groups_workout_id_workouts_id_fk": { + "name": "workout_groups_workout_id_workouts_id_fk", + "tableFrom": "workout_groups", + "tableTo": "workouts", + "columnsFrom": [ + "workout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_exercise_rows_set_parameters": { + "name": "workout_exercise_rows_set_parameters", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "set_number": { + "name": "set_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "reps": { + "name": "reps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "kg": { + "name": "kg", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workout_exercise_rows_set_parameters_order_idx": { + "name": "workout_exercise_rows_set_parameters_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_set_parameters_parent_id_idx": { + "name": "workout_exercise_rows_set_parameters_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_exercise_rows_set_parameters_parent_id_fk": { + "name": "workout_exercise_rows_set_parameters_parent_id_fk", + "tableFrom": "workout_exercise_rows_set_parameters", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_exercise_rows": { + "name": "workout_exercise_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "numer": { + "name": "numer", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rounds": { + "name": "rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps": { + "name": "reps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps_left": { + "name": "reps_left", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps_right": { + "name": "reps_right", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "kg": { + "name": "kg", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "tut": { + "name": "tut", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rir": { + "name": "rir", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rest": { + "name": "rest", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "duration_min": { + "name": "duration_min", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "duration_sec": { + "name": "duration_sec", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_protocol": { + "name": "override_protocol", + "type": "enum_workout_exercise_rows_override_protocol", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "override_rounds": { + "name": "override_rounds", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "override_duration_minutes": { + "name": "override_duration_minutes", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_interval_seconds": { + "name": "override_interval_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_work_seconds": { + "name": "override_work_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "override_rest_seconds": { + "name": "override_rest_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workout_exercise_rows_group_idx": { + "name": "workout_exercise_rows_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_exercise_idx": { + "name": "workout_exercise_rows_exercise_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_updated_at_idx": { + "name": "workout_exercise_rows_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_exercise_rows_created_at_idx": { + "name": "workout_exercise_rows_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_exercise_rows_group_id_workout_groups_id_fk": { + "name": "workout_exercise_rows_group_id_workout_groups_id_fk", + "tableFrom": "workout_exercise_rows", + "tableTo": "workout_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workout_exercise_rows_exercise_id_exercises_id_fk": { + "name": "workout_exercise_rows_exercise_id_exercises_id_fk", + "tableFrom": "workout_exercise_rows", + "tableTo": "exercises", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workout_logs": { + "name": "workout_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "workout_id": { + "name": "workout_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workout_logs_workout_idx": { + "name": "workout_logs_workout_idx", + "columns": [ + { + "expression": "workout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_logs_client_idx": { + "name": "workout_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_logs_updated_at_idx": { + "name": "workout_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workout_logs_created_at_idx": { + "name": "workout_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workout_logs_workout_id_workouts_id_fk": { + "name": "workout_logs_workout_id_workouts_id_fk", + "tableFrom": "workout_logs", + "tableTo": "workouts", + "columnsFrom": [ + "workout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workout_logs_client_id_clients_id_fk": { + "name": "workout_logs_client_id_clients_id_fk", + "tableFrom": "workout_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.round_logs": { + "name": "round_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "round_number": { + "name": "round_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_round_logs_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'completed'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "round_logs_session_idx": { + "name": "round_logs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_group_idx": { + "name": "round_logs_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_client_idx": { + "name": "round_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_updated_at_idx": { + "name": "round_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "round_logs_created_at_idx": { + "name": "round_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "round_logs_session_id_workout_logs_id_fk": { + "name": "round_logs_session_id_workout_logs_id_fk", + "tableFrom": "round_logs", + "tableTo": "workout_logs", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "round_logs_group_id_workout_groups_id_fk": { + "name": "round_logs_group_id_workout_groups_id_fk", + "tableFrom": "round_logs", + "tableTo": "workout_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "round_logs_client_id_clients_id_fk": { + "name": "round_logs_client_id_clients_id_fk", + "tableFrom": "round_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.set_logs": { + "name": "set_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_name": { + "name": "exercise_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "exercise_row_id": { + "name": "exercise_row_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "round_log_id": { + "name": "round_log_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_number": { + "name": "set_number", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "weight_left": { + "name": "weight_left", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "weight_right": { + "name": "weight_right", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "is_bodyweight": { + "name": "is_bodyweight", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "distance_m": { + "name": "distance_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "duration_sec": { + "name": "duration_sec", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "reps": { + "name": "reps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps_left": { + "name": "reps_left", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reps_right": { + "name": "reps_right", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "rir": { + "name": "rir", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "set_logs_session_idx": { + "name": "set_logs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_client_idx": { + "name": "set_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_exercise_idx": { + "name": "set_logs_exercise_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_exercise_row_idx": { + "name": "set_logs_exercise_row_idx", + "columns": [ + { + "expression": "exercise_row_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_round_log_idx": { + "name": "set_logs_round_log_idx", + "columns": [ + { + "expression": "round_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_updated_at_idx": { + "name": "set_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "set_logs_created_at_idx": { + "name": "set_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "set_logs_session_id_workout_logs_id_fk": { + "name": "set_logs_session_id_workout_logs_id_fk", + "tableFrom": "set_logs", + "tableTo": "workout_logs", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_client_id_clients_id_fk": { + "name": "set_logs_client_id_clients_id_fk", + "tableFrom": "set_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_exercise_id_exercises_id_fk": { + "name": "set_logs_exercise_id_exercises_id_fk", + "tableFrom": "set_logs", + "tableTo": "exercises", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_exercise_row_id_workout_exercise_rows_id_fk": { + "name": "set_logs_exercise_row_id_workout_exercise_rows_id_fk", + "tableFrom": "set_logs", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "exercise_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "set_logs_round_log_id_round_logs_id_fk": { + "name": "set_logs_round_log_id_round_logs_id_fk", + "tableFrom": "set_logs", + "tableTo": "round_logs", + "columnsFrom": [ + "round_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exercise_logs": { + "name": "exercise_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_name": { + "name": "exercise_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "exercise_row_id": { + "name": "exercise_row_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round_log_id": { + "name": "round_log_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "exercise_logs_session_idx": { + "name": "exercise_logs_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_client_idx": { + "name": "exercise_logs_client_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_exercise_idx": { + "name": "exercise_logs_exercise_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_exercise_row_idx": { + "name": "exercise_logs_exercise_row_idx", + "columns": [ + { + "expression": "exercise_row_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_round_log_idx": { + "name": "exercise_logs_round_log_idx", + "columns": [ + { + "expression": "round_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_updated_at_idx": { + "name": "exercise_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercise_logs_created_at_idx": { + "name": "exercise_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "exercise_logs_session_id_workout_logs_id_fk": { + "name": "exercise_logs_session_id_workout_logs_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "workout_logs", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_client_id_clients_id_fk": { + "name": "exercise_logs_client_id_clients_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_exercise_id_exercises_id_fk": { + "name": "exercise_logs_exercise_id_exercises_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "exercises", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_exercise_row_id_workout_exercise_rows_id_fk": { + "name": "exercise_logs_exercise_row_id_workout_exercise_rows_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "exercise_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "exercise_logs_round_log_id_round_logs_id_fk": { + "name": "exercise_logs_round_log_id_round_logs_id_fk", + "tableFrom": "exercise_logs", + "tableTo": "round_logs", + "columnsFrom": [ + "round_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exercises": { + "name": "exercises", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tracking_type": { + "name": "tracking_type", + "type": "enum_exercises_tracking_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'strength'" + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "video_url": { + "name": "video_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "muscle_group": { + "name": "muscle_group", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "equipment": { + "name": "equipment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "exercises_updated_at_idx": { + "name": "exercises_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exercises_created_at_idx": { + "name": "exercises_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_links_permissions": { + "name": "share_links_permissions", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_share_links_permissions", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "share_links_permissions_order_idx": { + "name": "share_links_permissions_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_permissions_parent_idx": { + "name": "share_links_permissions_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_links_permissions_parent_fk": { + "name": "share_links_permissions_parent_fk", + "tableFrom": "share_links_permissions", + "tableTo": "share_links", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_links": { + "name": "share_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "share_links_plan_idx": { + "name": "share_links_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_token_idx": { + "name": "share_links_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_updated_at_idx": { + "name": "share_links_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_links_created_at_idx": { + "name": "share_links_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_links_plan_id_plans_id_fk": { + "name": "share_links_plan_id_plans_id_fk", + "tableFrom": "share_links", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "clients_id": { + "name": "clients_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "plans_id": { + "name": "plans_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "microcycles_id": { + "name": "microcycles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workouts_id": { + "name": "workouts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workout_groups_id": { + "name": "workout_groups_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workout_exercise_rows_id": { + "name": "workout_exercise_rows_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "workout_logs_id": { + "name": "workout_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "round_logs_id": { + "name": "round_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "set_logs_id": { + "name": "set_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercise_logs_id": { + "name": "exercise_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "exercises_id": { + "name": "exercises_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "share_links_id": { + "name": "share_links_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_clients_id_idx": { + "name": "payload_locked_documents_rels_clients_id_idx", + "columns": [ + { + "expression": "clients_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_plans_id_idx": { + "name": "payload_locked_documents_rels_plans_id_idx", + "columns": [ + { + "expression": "plans_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_microcycles_id_idx": { + "name": "payload_locked_documents_rels_microcycles_id_idx", + "columns": [ + { + "expression": "microcycles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workouts_id_idx": { + "name": "payload_locked_documents_rels_workouts_id_idx", + "columns": [ + { + "expression": "workouts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workout_groups_id_idx": { + "name": "payload_locked_documents_rels_workout_groups_id_idx", + "columns": [ + { + "expression": "workout_groups_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workout_exercise_rows_id_idx": { + "name": "payload_locked_documents_rels_workout_exercise_rows_id_idx", + "columns": [ + { + "expression": "workout_exercise_rows_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_workout_logs_id_idx": { + "name": "payload_locked_documents_rels_workout_logs_id_idx", + "columns": [ + { + "expression": "workout_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_round_logs_id_idx": { + "name": "payload_locked_documents_rels_round_logs_id_idx", + "columns": [ + { + "expression": "round_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_set_logs_id_idx": { + "name": "payload_locked_documents_rels_set_logs_id_idx", + "columns": [ + { + "expression": "set_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_exercise_logs_id_idx": { + "name": "payload_locked_documents_rels_exercise_logs_id_idx", + "columns": [ + { + "expression": "exercise_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_exercises_id_idx": { + "name": "payload_locked_documents_rels_exercises_id_idx", + "columns": [ + { + "expression": "exercises_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_share_links_id_idx": { + "name": "payload_locked_documents_rels_share_links_id_idx", + "columns": [ + { + "expression": "share_links_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_clients_fk": { + "name": "payload_locked_documents_rels_clients_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "clients", + "columnsFrom": [ + "clients_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_plans_fk": { + "name": "payload_locked_documents_rels_plans_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "plans", + "columnsFrom": [ + "plans_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_microcycles_fk": { + "name": "payload_locked_documents_rels_microcycles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "microcycles", + "columnsFrom": [ + "microcycles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workouts_fk": { + "name": "payload_locked_documents_rels_workouts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workouts", + "columnsFrom": [ + "workouts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workout_groups_fk": { + "name": "payload_locked_documents_rels_workout_groups_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workout_groups", + "columnsFrom": [ + "workout_groups_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workout_exercise_rows_fk": { + "name": "payload_locked_documents_rels_workout_exercise_rows_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workout_exercise_rows", + "columnsFrom": [ + "workout_exercise_rows_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_workout_logs_fk": { + "name": "payload_locked_documents_rels_workout_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "workout_logs", + "columnsFrom": [ + "workout_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_round_logs_fk": { + "name": "payload_locked_documents_rels_round_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "round_logs", + "columnsFrom": [ + "round_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_set_logs_fk": { + "name": "payload_locked_documents_rels_set_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "set_logs", + "columnsFrom": [ + "set_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_exercise_logs_fk": { + "name": "payload_locked_documents_rels_exercise_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "exercise_logs", + "columnsFrom": [ + "exercise_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_exercises_fk": { + "name": "payload_locked_documents_rels_exercises_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "exercises", + "columnsFrom": [ + "exercises_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_share_links_fk": { + "name": "payload_locked_documents_rels_share_links_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "share_links", + "columnsFrom": [ + "share_links_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "clients_id": { + "name": "clients_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_clients_id_idx": { + "name": "payload_preferences_rels_clients_id_idx", + "columns": [ + { + "expression": "clients_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_clients_fk": { + "name": "payload_preferences_rels_clients_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "clients", + "columnsFrom": [ + "clients_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_plans_status": { + "name": "enum_plans_status", + "schema": "public", + "values": [ + "active", + "paused", + "completed" + ] + }, + "public.enum__plans_v_version_status": { + "name": "enum__plans_v_version_status", + "schema": "public", + "values": [ + "active", + "paused", + "completed" + ] + }, + "public.enum_workout_groups_protocol": { + "name": "enum_workout_groups_protocol", + "schema": "public", + "values": [ + "standard", + "emom", + "amrap", + "for_time", + "tabata" + ] + }, + "public.enum_workout_exercise_rows_override_protocol": { + "name": "enum_workout_exercise_rows_override_protocol", + "schema": "public", + "values": [ + "", + "standard", + "emom", + "amrap", + "for_time", + "tabata" + ] + }, + "public.enum_round_logs_status": { + "name": "enum_round_logs_status", + "schema": "public", + "values": [ + "completed", + "partial", + "skipped" + ] + }, + "public.enum_exercises_tracking_type": { + "name": "enum_exercises_tracking_type", + "schema": "public", + "values": [ + "strength", + "cardio" + ] + }, + "public.enum_share_links_permissions": { + "name": "enum_share_links_permissions", + "schema": "public", + "values": [ + "plan", + "results" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "5c1c5040-bd3a-4adb-899d-7b6d9896822e", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260724_073306.ts b/src/migrations/20260724_073306.ts new file mode 100644 index 0000000..30cc507 --- /dev/null +++ b/src/migrations/20260724_073306.ts @@ -0,0 +1,13 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "workout_exercise_rows" ADD COLUMN "reps_left" varchar; + ALTER TABLE "workout_exercise_rows" ADD COLUMN "reps_right" varchar;`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "workout_exercise_rows" DROP COLUMN "reps_left"; + ALTER TABLE "workout_exercise_rows" DROP COLUMN "reps_right";`) +} diff --git a/src/migrations/20260724_090318.ts b/src/migrations/20260724_090318.ts new file mode 100644 index 0000000..61d4cec --- /dev/null +++ b/src/migrations/20260724_090318.ts @@ -0,0 +1,13 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TYPE "public"."enum_workout_exercise_rows_target_type" AS ENUM('repetitions', 'duration'); + ALTER TABLE "workout_exercise_rows" ADD COLUMN "target_type" "enum_workout_exercise_rows_target_type" DEFAULT 'repetitions';`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "workout_exercise_rows" DROP COLUMN "target_type"; + DROP TYPE "public"."enum_workout_exercise_rows_target_type";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index e373aed..0359c8c 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -6,6 +6,9 @@ import * as migration_20260618_172305_drop_share_links_versions from './20260618 import * as migration_20260620_195052_exercise_logs from './20260620_195052_exercise_logs'; import * as migration_20260620_200543_exercise_logs_relations from './20260620_200543_exercise_logs_relations'; import * as migration_20260622_112303_bundle_with_previous from './20260622_112303_bundle_with_previous'; +import * as migration_20260724_065545 from './20260724_065545'; +import * as migration_20260724_073306 from './20260724_073306'; +import * as migration_20260724_090318 from './20260724_090318'; export const migrations = [ { @@ -46,6 +49,21 @@ export const migrations = [ { up: migration_20260622_112303_bundle_with_previous.up, down: migration_20260622_112303_bundle_with_previous.down, - name: '20260622_112303_bundle_with_previous' + name: '20260622_112303_bundle_with_previous', + }, + { + up: migration_20260724_065545.up, + down: migration_20260724_065545.down, + name: '20260724_065545', + }, + { + up: migration_20260724_073306.up, + down: migration_20260724_073306.down, + name: '20260724_073306', + }, + { + up: migration_20260724_090318.up, + down: migration_20260724_090318.down, + name: '20260724_090318' }, ]; diff --git a/src/app/(payload)/admin/modules/share-link-url/index.ts b/src/modules/sharing/admin/share-link-url/index.ts similarity index 100% rename from src/app/(payload)/admin/modules/share-link-url/index.ts rename to src/modules/sharing/admin/share-link-url/index.ts diff --git a/src/app/(payload)/admin/modules/share-link-url/share-link-url.tsx b/src/modules/sharing/admin/share-link-url/share-link-url.tsx similarity index 100% rename from src/app/(payload)/admin/modules/share-link-url/share-link-url.tsx rename to src/modules/sharing/admin/share-link-url/share-link-url.tsx diff --git a/src/modules/sharing/index.ts b/src/modules/sharing/index.ts new file mode 100644 index 0000000..a03db4a --- /dev/null +++ b/src/modules/sharing/index.ts @@ -0,0 +1 @@ +export type { LoadShareLinkOutput } from './types' diff --git a/src/modules/sharing/server/index.ts b/src/modules/sharing/server/index.ts new file mode 100644 index 0000000..11bbf63 --- /dev/null +++ b/src/modules/sharing/server/index.ts @@ -0,0 +1,4 @@ +import 'server-only' + +export { loadShareLink } from './load-share-link' +export type { LoadShareLinkOutput } from '../types' diff --git a/src/loaders/share-link-loader.ts b/src/modules/sharing/server/load-share-link.ts similarity index 69% rename from src/loaders/share-link-loader.ts rename to src/modules/sharing/server/load-share-link.ts index ea33da3..d2e8303 100644 --- a/src/loaders/share-link-loader.ts +++ b/src/modules/sharing/server/load-share-link.ts @@ -1,20 +1,14 @@ +import 'server-only' + import { getTranslations } from 'next-intl/server' import { getPayload } from 'payload' import config from '@/payload.config' -import { loadPlansItems } from '@/loaders/load-plans-items' -import type { TPlanAccordionItem } from '@/types/plan' - -export type TShareLinkData = { - meta: { - planTitle: string - permissions: ('plan' | 'results')[] - expiresAt: string - } - plan?: TPlanAccordionItem[] -} | null +import type { PlanTree } from '@/modules/training/plans' +import { loadPlanTree } from '@/modules/training/plans/server' +import type { LoadShareLinkOutput } from '@/modules/sharing' -export async function loadShareLink(token: string): Promise { +export async function loadShareLink(token: string): Promise { const payload = await getPayload({ config: await config }) const result = await payload.find({ @@ -38,15 +32,14 @@ export async function loadShareLink(token: string): Promise { const t = await getTranslations('share') - let planData: TPlanAccordionItem[] | undefined + let planData: PlanTree[] | undefined if (permissions.includes('plan')) { - planData = await loadPlansItems( + planData = await loadPlanTree( payload, [planId], { seriesPrefix: t('seriesPrefix'), - repsPrefix: t('repsPrefix'), durationPrefix: t('durationPrefix'), restPrefix: t('restPrefix'), }, diff --git a/src/modules/sharing/types.ts b/src/modules/sharing/types.ts new file mode 100644 index 0000000..e229937 --- /dev/null +++ b/src/modules/sharing/types.ts @@ -0,0 +1,10 @@ +import type { PlanTree } from '@/modules/training/plans' + +export type LoadShareLinkOutput = { + meta: { + planTitle: string + permissions: ('plan' | 'results')[] + expiresAt: string + } + plan?: PlanTree[] +} | null diff --git a/src/modules/training/admin/training-navigation/training-navigation.tsx b/src/modules/training/admin/training-navigation/training-navigation.tsx new file mode 100644 index 0000000..5ad564b --- /dev/null +++ b/src/modules/training/admin/training-navigation/training-navigation.tsx @@ -0,0 +1,130 @@ +import 'server-only' + +import type { Payload } from 'payload' +import React from 'react' + +type NavigationProps = { + id?: number | string + payload: Payload +} + +const containerStyle: React.CSSProperties = { + border: '1px solid var(--theme-elevation-150)', + borderRadius: 8, + marginTop: 16, + padding: 16, +} + +const linkStyle: React.CSSProperties = { + color: 'var(--theme-success-500)', + textDecoration: 'none', +} + +const listStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 8, + margin: '12px 0 0', + padding: 0, +} + +const breadcrumbStyle: React.CSSProperties = { + alignItems: 'center', + background: 'var(--theme-elevation-50)', + border: '1px solid var(--theme-border-color)', + borderRadius: 'var(--style-radius-m)', + color: 'var(--theme-elevation-500)', + display: 'flex', + flexWrap: 'wrap', + fontSize: 13, + gap: 8, + padding: '10px 12px', +} + +const breadcrumbSeparatorStyle: React.CSSProperties = { + color: 'var(--theme-elevation-300)', +} + +const isExistingDocument = (id?: number | string): id is number | string => Boolean(id && id !== 'create') + +export async function PlanMicrocycles({ id, payload }: NavigationProps) { + if (!isExistingDocument(id)) return null + + const result = await payload.find({ + collection: 'microcycles', + depth: 0, + limit: 100, + sort: 'order', + where: { plan: { equals: id } }, + }) + + return ( +
+ Microcycles + {result.docs.length === 0 ? ( +
No microcycles yet.
+ ) : ( + + )} +
+ ) +} + +export async function MicrocycleWorkouts({ id, payload }: NavigationProps) { + if (!isExistingDocument(id)) return null + + const result = await payload.find({ + collection: 'workouts', + depth: 0, + limit: 100, + sort: 'order', + where: { microcycle: { equals: id } }, + }) + + return ( +
+ Workouts + {result.docs.length === 0 ? ( +
No workouts yet.
+ ) : ( + + )} +
+ ) +} + +export async function WorkoutStructureBreadcrumb({ id, payload }: NavigationProps) { + if (!isExistingDocument(id)) return null + + const workout = await payload.findByID({ collection: 'workouts', depth: 2, id }) + const microcycle = typeof workout.microcycle === 'object' ? workout.microcycle : null + const plan = microcycle && typeof microcycle.plan === 'object' ? microcycle.plan : null + + return ( + + ) +} diff --git a/src/app/(payload)/admin/modules/workout-logs-notice/workout-logs-notice.tsx b/src/modules/training/admin/workout-logs-notice/workout-logs-notice.tsx similarity index 98% rename from src/app/(payload)/admin/modules/workout-logs-notice/workout-logs-notice.tsx rename to src/modules/training/admin/workout-logs-notice/workout-logs-notice.tsx index db9dfad..6c56fdf 100644 --- a/src/app/(payload)/admin/modules/workout-logs-notice/workout-logs-notice.tsx +++ b/src/modules/training/admin/workout-logs-notice/workout-logs-notice.tsx @@ -1,3 +1,5 @@ +import 'server-only' + import React from 'react' type WorkoutLogsNoticeProps = { diff --git a/src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx b/src/modules/training/admin/workout-structure/components/editor/editor.tsx similarity index 86% rename from src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx rename to src/modules/training/admin/workout-structure/components/editor/editor.tsx index a2eb4ce..8ade716 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx +++ b/src/modules/training/admin/workout-structure/components/editor/editor.tsx @@ -2,32 +2,35 @@ import React, { useState } from 'react' import { Button } from '@payloadcms/ui' -import type { ExerciseRow, Group, Section } from '../../types' -import { PROTOCOL_LABEL } from '../../constants' +import type { WorkoutGroup } from '@/payload-types' +import type { ExerciseRow, Section } from '../../types' +import { PROTOCOL_LABEL } from '@/modules/training/plans' import { exerciseLabel, exerciseMeta, groupLabel } from '../../utils' import { s } from '../../styles' import { GroupForm } from '../group-form' import { ExerciseForm } from '../exercise-form' import { useWorkoutMutations } from './hooks/use-workout-mutations' -type Props = { +type WorkoutStructureEditorProps = { + header?: React.ReactNode sections: Section[] - initialGroups: Group[] + initialGroups: WorkoutGroup[] initialExerciseRows: ExerciseRow[] groupIdsWithLogs?: number[] exerciseRowIdsWithLogs?: number[] } export function WorkoutStructureEditor({ + header, sections, initialGroups, initialExerciseRows, groupIdsWithLogs = [], exerciseRowIdsWithLogs = [], -}: Props) { +}: WorkoutStructureEditorProps) { const groupsWithLogs = new Set(groupIdsWithLogs) const exerciseRowsWithLogs = new Set(exerciseRowIdsWithLogs) - const [groups, setGroups] = useState(initialGroups) + const [groups, setGroups] = useState(initialGroups) const [exerciseRows, setExerciseRows] = useState(initialExerciseRows) const [addingGroupFor, setAddingGroupFor] = useState(null) const [editingGroup, setEditingGroup] = useState(null) @@ -40,14 +43,19 @@ export function WorkoutStructureEditor({ const sectionsWithFallback: Section[] = sections.length > 0 ? sections : [{ id: undefined, title: null, subtitle: null }] - const groupsForSection = (sectionId: string | undefined) => - groups.filter((g) => g.sectionRowId === (sectionId ?? '')) + const groupsForSection = (sectionId: string | null | undefined) => + groups.filter((group) => group.sectionRowId === (sectionId ?? '')) const rowsForGroup = (groupId: number) => - exerciseRows.filter((r) => r.group === groupId).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) + exerciseRows + .filter((exerciseRow) => exerciseRow.group === groupId) + .sort( + (firstRow, secondRow) => (firstRow.order ?? 0) - (secondRow.order ?? 0), + ) return (
+ {header &&
{header}
} {exerciseRowsWithLogs.size === 0 && (
@@ -82,7 +90,11 @@ export function WorkoutStructureEditor({ nextOrder={group.order ?? 0} initial={group} onSaved={(updated) => { - setGroups((prev) => prev.map((g) => g.id === updated.id ? { ...g, ...updated } : g)) + setGroups((previousGroups) => + previousGroups.map((group) => + group.id === updated.id ? { ...group, ...updated } : group, + ), + ) setEditingGroup(null) }} onCancel={() => setEditingGroup(null)} @@ -135,7 +147,13 @@ export function WorkoutStructureEditor({ nextOrder={row.order ?? 0} initial={row} onSaved={(updated) => { - setExerciseRows((prev) => prev.map((r) => r.id === updated.id ? { ...r, ...updated } : r)) + setExerciseRows((previousRows) => + previousRows.map((exerciseRow) => + exerciseRow.id === updated.id + ? { ...exerciseRow, ...updated } + : exerciseRow, + ), + ) setEditingExercise(null) }} onCancel={() => setEditingExercise(null)} diff --git a/src/app/(payload)/admin/modules/workout-structure/components/editor/hooks/use-workout-mutations.ts b/src/modules/training/admin/workout-structure/components/editor/hooks/use-workout-mutations.ts similarity index 69% rename from src/app/(payload)/admin/modules/workout-structure/components/editor/hooks/use-workout-mutations.ts rename to src/modules/training/admin/workout-structure/components/editor/hooks/use-workout-mutations.ts index 6460052..a9d6464 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/editor/hooks/use-workout-mutations.ts +++ b/src/modules/training/admin/workout-structure/components/editor/hooks/use-workout-mutations.ts @@ -3,10 +3,11 @@ import { useState } from 'react' import { toast } from '@payloadcms/ui' import { sdk } from '@/lib/sdk' -import type { ExerciseRow, Group } from '../../../types' +import type { WorkoutGroup } from '@/payload-types' +import type { ExerciseRow } from '../../../types' export function useWorkoutMutations( - setGroups: React.Dispatch>, + setGroups: React.Dispatch>, setExerciseRows: React.Dispatch>, ) { const [deletingGroup, setDeletingGroup] = useState(null) @@ -16,8 +17,10 @@ export function useWorkoutMutations( setDeletingGroup(groupId) try { await sdk.delete({ collection: 'workout-groups', id: groupId }) - setGroups((prev) => prev.filter((g) => g.id !== groupId)) - setExerciseRows((prev) => prev.filter((r) => r.group !== groupId)) + setGroups((previousGroups) => previousGroups.filter((group) => group.id !== groupId)) + setExerciseRows((previousRows) => + previousRows.filter((exerciseRow) => exerciseRow.group !== groupId), + ) toast.success('Group deleted') } catch (e) { toast.error(e instanceof Error ? e.message : 'Failed to delete group') @@ -30,7 +33,9 @@ export function useWorkoutMutations( setDeletingExercise(rowId) try { await sdk.delete({ collection: 'workout-exercise-rows', id: rowId }) - setExerciseRows((prev) => prev.filter((r) => r.id !== rowId)) + setExerciseRows((previousRows) => + previousRows.filter((exerciseRow) => exerciseRow.id !== rowId), + ) toast.success('Exercise deleted') } catch (e) { toast.error(e instanceof Error ? e.message : 'Failed to delete exercise') diff --git a/src/app/(payload)/admin/modules/workout-structure/components/editor/index.ts b/src/modules/training/admin/workout-structure/components/editor/index.ts similarity index 100% rename from src/app/(payload)/admin/modules/workout-structure/components/editor/index.ts rename to src/modules/training/admin/workout-structure/components/editor/index.ts diff --git a/src/modules/training/admin/workout-structure/components/exercise-form/exercise-form.tsx b/src/modules/training/admin/workout-structure/components/exercise-form/exercise-form.tsx new file mode 100644 index 0000000..d59a1c9 --- /dev/null +++ b/src/modules/training/admin/workout-structure/components/exercise-form/exercise-form.tsx @@ -0,0 +1,212 @@ +'use client' + +import { Button, Form, RelationshipField, SelectField, TextField, toast, useFormFields, useFormProcessing } from '@payloadcms/ui' +import type { FormState, SelectFieldClient, SingleRelationshipFieldClient } from 'payload' +import type { ExerciseRow } from '../../types' +import { s } from '../../styles' +import { sdk } from '@/lib/sdk' +import { + EXERCISE_TARGET_TYPE_OPTIONS, + type ExerciseTargetType, +} from '@/modules/training/exercises' +import { textField } from '../../utils/fields' +import { validateDuration, validateKgOrRepsSides, validateRepsSidesOrKg, validateRounds } from '../../utils' + +type ExerciseFormProps = { + groupId: number + nextOrder: number + initial?: ExerciseRow + onSaved: (row: ExerciseRow) => void + onCancel: () => void +} + +const exerciseRelField: SingleRelationshipFieldClient = { + name: 'exercise', + type: 'relationship', + relationTo: 'exercises', + hasMany: false, + label: 'Exercise (catalog)', +} as SingleRelationshipFieldClient + +const targetTypeField: SelectFieldClient = { + name: 'targetType', + type: 'select', + label: 'Target type', + options: EXERCISE_TARGET_TYPE_OPTIONS, +} as SelectFieldClient + +type FieldStateMap = Record + +function FormFields({ isEdit, onCancel }: { isEdit: boolean; onCancel: () => void }) { + const processing = useFormProcessing() + const targetType = useFormFields( + ([fields]) => ((fields as unknown as FieldStateMap).targetType?.value as string) ?? 'repetitions', + ) + + return ( + <> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + {targetType === 'duration' ? ( +
+
+ +
+
+ +
+ +
+ ) : ( +
+
+ +
+
+ +
+ +
+ )} + +
+ + +
+ + ) +} + +function ExerciseDetailsFields({ targetType }: { targetType: string }) { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) +} + +export function ExerciseForm({ + groupId, + nextOrder, + initial, + onSaved, + onCancel, +}: ExerciseFormProps) { + const isEdit = !!initial + + const initialState: FormState = { + numer: { value: initial?.numer ?? '' }, + rounds: { value: initial?.rounds ?? '' }, + exercise: { value: initial?.exercise?.id ?? null }, + note: { value: initial?.note ?? '' }, + targetType: { value: initial?.targetType ?? 'repetitions' }, + repsLeft: { value: initial?.repsLeft ?? '' }, + repsRight: { value: initial?.repsRight ?? '' }, + kg: { value: initial?.kg ?? '' }, + rir: { value: initial?.rir ?? '' }, + tut: { value: initial?.tut ?? '' }, + rest: { value: initial?.rest ?? '' }, + durationMin: { value: initial?.durationMin ?? '' }, + durationSec: { value: initial?.durationSec ?? '' }, + } + + const handleSubmit = async (_: FormState, data: Record) => { + const exerciseId = data.exercise as number | string | null + const targetType = (data.targetType as ExerciseTargetType) ?? 'repetitions' + const body = { + numer: (data.numer as string) || null, + rounds: (data.rounds as string) || null, + exercise: exerciseId ? Number(exerciseId) : null, + note: (data.note as string) || null, + targetType, + repsLeft: targetType === 'repetitions' ? (data.repsLeft as string) || null : null, + repsRight: targetType === 'repetitions' ? (data.repsRight as string) || null : null, + kg: (data.kg as string) || null, + rir: (data.rir as string) || null, + tut: (data.tut as string) || null, + rest: (data.rest as string) || null, + durationMin: targetType === 'duration' && String(data.durationMin ?? '').trim() !== '' ? Number(data.durationMin) : null, + durationSec: targetType === 'duration' && String(data.durationSec ?? '').trim() !== '' ? Number(data.durationSec) : null, + } + + try { + const doc = isEdit + ? await sdk.update({ + collection: 'workout-exercise-rows', + id: initial!.id, + data: body, + depth: 1, + }) + : await sdk.create({ + collection: 'workout-exercise-rows', + data: { ...body, group: groupId, order: nextOrder }, + depth: 1, + }) + + const exerciseDoc = doc.exercise + const exerciseObj = exerciseDoc && typeof exerciseDoc === 'object' + ? { id: exerciseDoc.id, name: exerciseDoc.name } + : null + const normalizedGroup = + typeof doc.group === 'object' ? doc.group.id : doc.group + + toast.success(isEdit ? 'Exercise updated' : 'Exercise added') + onSaved({ ...doc, group: normalizedGroup, exercise: exerciseObj }) + } catch (e) { + toast.error(e instanceof Error ? e.message : 'Save failed') + } + } + + return ( +
+
+ {isEdit ? 'Edit exercise' : 'New exercise'} +
+ +
+ + +
+ ) +} diff --git a/src/app/(payload)/admin/modules/workout-structure/components/exercise-form/index.ts b/src/modules/training/admin/workout-structure/components/exercise-form/index.ts similarity index 100% rename from src/app/(payload)/admin/modules/workout-structure/components/exercise-form/index.ts rename to src/modules/training/admin/workout-structure/components/exercise-form/index.ts diff --git a/src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx b/src/modules/training/admin/workout-structure/components/group-form/group-form.tsx similarity index 81% rename from src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx rename to src/modules/training/admin/workout-structure/components/group-form/group-form.tsx index 05e2e73..9afb3c8 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx +++ b/src/modules/training/admin/workout-structure/components/group-form/group-form.tsx @@ -2,12 +2,12 @@ import { Button, CheckboxField, Form, SelectField, TextField, toast, useDocumentInfo, useFormFields, useFormProcessing } from '@payloadcms/ui' import type { CheckboxFieldClient, FormState, SelectFieldClient } from 'payload' -import type { Group } from '../../types' -import { PROTOCOLS } from '../../constants' import { s } from '../../styles' import type { WorkoutGroup } from '@/payload-types' import { sdk } from '@/lib/sdk' -import { textField } from '@/app/(payload)/admin/utils/fields' +import { PROTOCOL_OPTIONS, type WorkoutProtocol } from '@/modules/training/plans' + +import { textField } from '../../utils/fields' import { validateDurationMinutes, validateIntervalSeconds, @@ -20,7 +20,7 @@ const protocolField: SelectFieldClient = { name: 'protocol', type: 'select', label: 'Protocol', - options: PROTOCOLS, + options: PROTOCOL_OPTIONS, } as SelectFieldClient const bundleField: CheckboxFieldClient = { @@ -29,20 +29,20 @@ const bundleField: CheckboxFieldClient = { label: 'Merge into the previous block', } as CheckboxFieldClient -type Props = { - sectionRowId: string | undefined +type GroupFormProps = { + sectionRowId: string | null | undefined nextOrder: number - initial?: Group - onSaved: (group: Group) => void + initial?: WorkoutGroup + onSaved: (group: WorkoutGroup) => void onCancel: () => void } -type AnyFields = Record +type FieldStateMap = Record function FormFields({ onCancel }: { onCancel: () => void }) { const processing = useFormProcessing() const protocol = useFormFields( - ([fields]) => ((fields as unknown as AnyFields)['protocol']?.value as string) ?? 'standard' + ([fields]) => ((fields as unknown as FieldStateMap)['protocol']?.value as string) ?? 'standard' ) return ( @@ -88,7 +88,7 @@ function FormFields({ onCancel }: { onCancel: () => void }) {
- +
)} @@ -112,7 +112,13 @@ function FormFields({ onCancel }: { onCancel: () => void }) { ) } -export function GroupForm({ sectionRowId, nextOrder, initial, onSaved, onCancel }: Props) { +export function GroupForm({ + sectionRowId, + nextOrder, + initial, + onSaved, + onCancel, +}: GroupFormProps) { const { id: docId } = useDocumentInfo() const isEdit = !!initial @@ -129,26 +135,38 @@ export function GroupForm({ sectionRowId, nextOrder, initial, onSaved, onCancel } const handleSubmit = async (_: FormState, data: Record) => { - const protocol = data.protocol as string - const body: Record = { + const body = { label: (data.label as string) || null, bundleWithPrevious: Boolean(data.bundleWithPrevious), - protocol, + protocol: data.protocol as WorkoutProtocol, rounds: (data.rounds as string) || null, durationMinutes: data.durationMinutes ? Number(data.durationMinutes) : null, intervalSeconds: data.intervalSeconds ? Number(data.intervalSeconds) : null, workSeconds: data.workSeconds ? Number(data.workSeconds) : null, restSeconds: data.restSeconds ? Number(data.restSeconds) : null, restBetweenRounds: (data.restBetweenRounds as string) || null, - ...(!isEdit && { workout: docId, sectionRowId: sectionRowId ?? '', order: nextOrder }), } try { - const doc = isEdit - ? await sdk.update({ collection: 'workout-groups', id: initial!.id, data: body as unknown as WorkoutGroup }) - : await sdk.create({ collection: 'workout-groups', data: body as unknown as WorkoutGroup }) + let doc: WorkoutGroup + + if (isEdit) { + doc = await sdk.update({ collection: 'workout-groups', id: initial!.id, data: body }) + } else { + if (docId == null) return + doc = await sdk.create({ + collection: 'workout-groups', + data: { + ...body, + workout: Number(docId), + sectionRowId: sectionRowId ?? '', + order: nextOrder, + }, + }) + } + toast.success(isEdit ? 'Group updated' : 'Group added') - onSaved(doc as unknown as Group) + onSaved(doc) } catch (e) { toast.error(e instanceof Error ? e.message : 'Save failed') } diff --git a/src/app/(payload)/admin/modules/workout-structure/components/group-form/index.ts b/src/modules/training/admin/workout-structure/components/group-form/index.ts similarity index 100% rename from src/app/(payload)/admin/modules/workout-structure/components/group-form/index.ts rename to src/modules/training/admin/workout-structure/components/group-form/index.ts diff --git a/src/modules/training/admin/workout-structure/loader.ts b/src/modules/training/admin/workout-structure/loader.ts new file mode 100644 index 0000000..afbe020 --- /dev/null +++ b/src/modules/training/admin/workout-structure/loader.ts @@ -0,0 +1,94 @@ +import 'server-only' + +import type { Payload } from 'payload' +import type { ExerciseRow, LoadWorkoutStructureOutput } from './types' + +const relationshipId = (relationship: number | { id: number }): number => + typeof relationship === 'object' ? relationship.id : relationship + +export async function loadWorkoutStructure( + payload: Payload, + docId: number | string, +): Promise { + const workout = await payload.findByID({ collection: 'workouts', id: docId, depth: 0 }) + + const groupsResult = await payload.find({ + collection: 'workout-groups', + where: { workout: { equals: docId } }, + sort: 'order', + limit: 500, + depth: 0, + }) + + const groupIds = groupsResult.docs.map((group) => group.id) + + const exerciseRowsResult = groupIds.length + ? await payload.find({ + collection: 'workout-exercise-rows', + where: { group: { in: groupIds } }, + sort: 'order', + limit: 5000, + depth: 1, + }) + : { docs: [] } + + const exerciseRowIds = exerciseRowsResult.docs.map((exerciseRow) => exerciseRow.id) + + const [roundLogsResult, setLogsResult] = await Promise.all([ + groupIds.length + ? payload.find({ + collection: 'round-logs', + where: { group: { in: groupIds } }, + limit: 5000, + depth: 0, + }) + : { docs: [] }, + exerciseRowIds.length + ? payload.find({ + collection: 'set-logs', + where: { exerciseRow: { in: exerciseRowIds } }, + limit: 5000, + depth: 0, + }) + : { docs: [] }, + ]) + + const groupIdsWithLogs: number[] = [ + ...new Set( + roundLogsResult.docs.map((roundLog) => relationshipId(roundLog.group)), + ), + ] + + const exerciseRowIdsWithLogs: number[] = [ + ...new Set( + setLogsResult.docs + .map((setLog) => setLog.exerciseRow) + .filter((exerciseRow) => exerciseRow != null) + .map((exerciseRow) => relationshipId(exerciseRow)), + ), + ] + + const sections = workout.sections ?? [] + + const initialExerciseRows: ExerciseRow[] = exerciseRowsResult.docs.map( + (exerciseRow) => ({ + ...exerciseRow, + group: relationshipId(exerciseRow.group), + exercise: + exerciseRow.exercise && typeof exerciseRow.exercise === 'object' + ? { + id: exerciseRow.exercise.id, + name: exerciseRow.exercise.name, + } + : null, + }), + ) + + return { + sections, + initialGroups: groupsResult.docs, + initialExerciseRows, + groupIdsWithLogs, + exerciseRowIdsWithLogs, + } +} diff --git a/src/app/(payload)/admin/modules/workout-structure/styles.ts b/src/modules/training/admin/workout-structure/styles.ts similarity index 89% rename from src/app/(payload)/admin/modules/workout-structure/styles.ts rename to src/modules/training/admin/workout-structure/styles.ts index 576182b..244a805 100644 --- a/src/app/(payload)/admin/modules/workout-structure/styles.ts +++ b/src/modules/training/admin/workout-structure/styles.ts @@ -2,6 +2,7 @@ import React from 'react' export const s = { container: { padding: '20px 24px', fontFamily: 'var(--font-body)' } as React.CSSProperties, + header: { marginBottom: 16 } as React.CSSProperties, sectionBlock: { marginBottom: 28 } as React.CSSProperties, sectionHeader: { fontSize: 11, @@ -58,16 +59,6 @@ export const s = { width: '100%', boxSizing: 'border-box' as const, }, - // select: { - // background: 'var(--theme-input-bg)', - // border: '1px solid var(--theme-border-color)', - // borderRadius: 'var(--style-radius-s)', - // color: 'var(--theme-text)', - // fontFamily: 'var(--font-body)', - // fontSize: 'var(--base-body-size)', - // padding: '5px 8px', - // width: '100%', - // }, formActions: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 10 } as React.CSSProperties, empty: { fontSize: 12, color: 'var(--theme-elevation-500)', padding: '8px 12px', fontStyle: 'italic' as const }, errorMsg: { fontSize: 12, color: 'var(--theme-error-500)', marginBottom: 8 }, diff --git a/src/modules/training/admin/workout-structure/types.ts b/src/modules/training/admin/workout-structure/types.ts new file mode 100644 index 0000000..7d6bb5f --- /dev/null +++ b/src/modules/training/admin/workout-structure/types.ts @@ -0,0 +1,21 @@ +import type { + Exercise as PayloadExercise, + Workout, + WorkoutExerciseRow, + WorkoutGroup, +} from '@/payload-types' + +export type Section = NonNullable[number] + +export type ExerciseRow = Omit & { + group: number + exercise?: Pick | null +} + +export type LoadWorkoutStructureOutput = { + sections: Section[] + initialGroups: WorkoutGroup[] + initialExerciseRows: ExerciseRow[] + groupIdsWithLogs: number[] + exerciseRowIdsWithLogs: number[] +} diff --git a/src/app/(payload)/admin/utils/fields.ts b/src/modules/training/admin/workout-structure/utils/fields.ts similarity index 100% rename from src/app/(payload)/admin/utils/fields.ts rename to src/modules/training/admin/workout-structure/utils/fields.ts diff --git a/src/modules/training/admin/workout-structure/utils/format.ts b/src/modules/training/admin/workout-structure/utils/format.ts new file mode 100644 index 0000000..59a6e56 --- /dev/null +++ b/src/modules/training/admin/workout-structure/utils/format.ts @@ -0,0 +1,35 @@ +import type { WorkoutGroup } from '@/payload-types' +import type { ExerciseRow } from '../types' +import { formatMinSec } from '@/lib/date' +import { formatSideReps } from '@/modules/training/exercises' + +export const groupLabel = (g: WorkoutGroup): string => { + const p = g.protocol ?? 'standard' + const r = g.rounds + const d = g.durationMinutes + if (p === 'emom') return r ? `EMOM · ${r} min` : 'EMOM' + if (p === 'amrap') return d ? `AMRAP · ${d} min` : 'AMRAP' + if (p === 'for_time') return r ? `For Time · ${r} rounds` : 'For Time' + if (p === 'tabata') return 'Tabata' + return r ? `${r} sets` : 'Standard' +} + +export const exerciseLabel = (row: ExerciseRow): string => row.exercise?.name ?? row.note ?? '—' + +export const exerciseMeta = (row: ExerciseRow): string => { + const parts: string[] = [] + if (row.rounds) parts.push(`${row.rounds} sets`) + if (row.targetType === 'duration') { + const duration = formatMinSec(row.durationMin, row.durationSec) + if (duration) parts.push(`Duration: ${duration}`) + } else { + const sideReps = formatSideReps(row.repsLeft, row.repsRight) + if (sideReps) parts.push(`Steps: ${sideReps}`) + else if (row.reps) parts.push(`Steps: ${row.reps}`) + } + if (row.kg) parts.push(`KG: ${row.kg}`) + if (row.rir) parts.push(`RIR ${row.rir}`) + if (row.tut) parts.push(`TUT ${row.tut}`) + if (row.rest) parts.push(`Rest(s): ${row.rest}`) + return parts.join(' · ') +} diff --git a/src/app/(payload)/admin/modules/workout-structure/utils/index.ts b/src/modules/training/admin/workout-structure/utils/index.ts similarity index 100% rename from src/app/(payload)/admin/modules/workout-structure/utils/index.ts rename to src/modules/training/admin/workout-structure/utils/index.ts diff --git a/src/app/(payload)/admin/modules/workout-structure/utils/validate.ts b/src/modules/training/admin/workout-structure/utils/validate.ts similarity index 59% rename from src/app/(payload)/admin/modules/workout-structure/utils/validate.ts rename to src/modules/training/admin/workout-structure/utils/validate.ts index bee2dcd..712c053 100644 --- a/src/app/(payload)/admin/modules/workout-structure/utils/validate.ts +++ b/src/modules/training/admin/workout-structure/utils/validate.ts @@ -2,18 +2,29 @@ import type { TextFieldValidation } from 'payload' type SiblingData = Record +const hasValue = (value: unknown): boolean => + value !== null && value !== undefined && String(value).trim() !== '' + export const validateRounds: TextFieldValidation = (value) => { if (value && !/^[\d\-–]+$/.test(String(value))) return 'Format: number or range (e.g. 4, 3–4)' return true } -export const validateRepsOrKg: TextFieldValidation = (value, { siblingData }) => { - if (!value && !(siblingData as SiblingData)?.kg) return 'Enter reps or load' +export const validateRepsSidesOrKg: TextFieldValidation = (value, { siblingData }) => { + const data = siblingData as SiblingData + if (!hasValue(value) && !hasValue(data.repsLeft) && !hasValue(data.repsRight) && !hasValue(data.kg)) return 'Enter reps or load' + return true +} + +export const validateKgOrRepsSides: TextFieldValidation = (value, { siblingData }) => { + const data = siblingData as SiblingData + if (!hasValue(value) && !hasValue(data.repsLeft) && !hasValue(data.repsRight)) return 'Enter reps or load' return true } -export const validateKgOrReps: TextFieldValidation = (value, { siblingData }) => { - if (!value && !(siblingData as SiblingData)?.reps) return 'Enter reps or load' +export const validateDuration: TextFieldValidation = (value, { siblingData }) => { + const data = siblingData as SiblingData + if (!hasValue(value) && !hasValue(data.durationMin) && !hasValue(data.durationSec)) return 'Enter duration' return true } diff --git a/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx b/src/modules/training/admin/workout-structure/workout-structure.tsx similarity index 66% rename from src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx rename to src/modules/training/admin/workout-structure/workout-structure.tsx index 1562fca..2c332eb 100644 --- a/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx +++ b/src/modules/training/admin/workout-structure/workout-structure.tsx @@ -1,4 +1,8 @@ +import 'server-only' + +import type { Payload } from 'payload' import React from 'react' +import { WorkoutStructureBreadcrumb } from '../training-navigation/training-navigation' import { loadWorkoutStructure } from './loader' import { WorkoutStructureEditor } from './components/editor' @@ -7,8 +11,7 @@ export async function WorkoutStructureView({ payload, }: { initPageResult?: { docID?: number | string } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - payload?: any + payload?: Payload }) { const docId = initPageResult?.docID @@ -21,5 +24,10 @@ export async function WorkoutStructureView({ } const data = await loadWorkoutStructure(payload, docId) - return + return ( + } + /> + ) } diff --git a/src/components/workout/exercise-card/components/add-set-actions/add-set-actions.tsx b/src/modules/training/components/exercise-card/components/add-set-actions/add-set-actions.tsx similarity index 100% rename from src/components/workout/exercise-card/components/add-set-actions/add-set-actions.tsx rename to src/modules/training/components/exercise-card/components/add-set-actions/add-set-actions.tsx diff --git a/src/components/workout/exercise-card/components/add-set-actions/index.ts b/src/modules/training/components/exercise-card/components/add-set-actions/index.ts similarity index 100% rename from src/components/workout/exercise-card/components/add-set-actions/index.ts rename to src/modules/training/components/exercise-card/components/add-set-actions/index.ts diff --git a/src/components/workout/exercise-card/components/exercise-header/exercise-header.tsx b/src/modules/training/components/exercise-card/components/exercise-header/exercise-header.tsx similarity index 100% rename from src/components/workout/exercise-card/components/exercise-header/exercise-header.tsx rename to src/modules/training/components/exercise-card/components/exercise-header/exercise-header.tsx diff --git a/src/components/workout/exercise-card/components/exercise-header/index.ts b/src/modules/training/components/exercise-card/components/exercise-header/index.ts similarity index 100% rename from src/components/workout/exercise-card/components/exercise-header/index.ts rename to src/modules/training/components/exercise-card/components/exercise-header/index.ts diff --git a/src/components/workout/exercise-card/components/exercise-note/exercise-note.tsx b/src/modules/training/components/exercise-card/components/exercise-note/exercise-note.tsx similarity index 90% rename from src/components/workout/exercise-card/components/exercise-note/exercise-note.tsx rename to src/modules/training/components/exercise-card/components/exercise-note/exercise-note.tsx index d710db3..e617048 100644 --- a/src/components/workout/exercise-card/components/exercise-note/exercise-note.tsx +++ b/src/modules/training/components/exercise-card/components/exercise-note/exercise-note.tsx @@ -1,7 +1,7 @@ 'use client' import { useTranslations } from 'next-intl' -import { NoteField } from '@/components/workout/note-field' +import { NoteField } from '@/modules/training/components/note-field' export function ExerciseNote({ note, diff --git a/src/components/workout/exercise-card/components/exercise-note/index.ts b/src/modules/training/components/exercise-card/components/exercise-note/index.ts similarity index 100% rename from src/components/workout/exercise-card/components/exercise-note/index.ts rename to src/modules/training/components/exercise-card/components/exercise-note/index.ts diff --git a/src/components/workout/exercise-card/components/meta-line/index.ts b/src/modules/training/components/exercise-card/components/meta-line/index.ts similarity index 100% rename from src/components/workout/exercise-card/components/meta-line/index.ts rename to src/modules/training/components/exercise-card/components/meta-line/index.ts diff --git a/src/components/workout/exercise-card/components/meta-line/meta-line.tsx b/src/modules/training/components/exercise-card/components/meta-line/meta-line.tsx similarity index 100% rename from src/components/workout/exercise-card/components/meta-line/meta-line.tsx rename to src/modules/training/components/exercise-card/components/meta-line/meta-line.tsx diff --git a/src/components/workout/exercise-card/components/series-list/index.ts b/src/modules/training/components/exercise-card/components/series-list/index.ts similarity index 100% rename from src/components/workout/exercise-card/components/series-list/index.ts rename to src/modules/training/components/exercise-card/components/series-list/index.ts diff --git a/src/components/workout/exercise-card/components/series-list/series-list.tsx b/src/modules/training/components/exercise-card/components/series-list/series-list.tsx similarity index 67% rename from src/components/workout/exercise-card/components/series-list/series-list.tsx rename to src/modules/training/components/exercise-card/components/series-list/series-list.tsx index 8b9b291..d4e8ad7 100644 --- a/src/components/workout/exercise-card/components/series-list/series-list.tsx +++ b/src/modules/training/components/exercise-card/components/series-list/series-list.tsx @@ -1,9 +1,10 @@ 'use client' import React from 'react' -import type { MetricField } from '@/collections/exercises/types' -import { SeriesRow } from '@/components/workout/series-row' -import type { SetLog, Values } from '@/types/workout' +import { SeriesRow } from '@/modules/training/components/series-row' +import type { MetricField } from '@/modules/training/exercises' +import type { MetricFormValues } from '@/modules/training/logs' +import type { SetLog } from '@/payload-types' export function SeriesList({ sets, @@ -14,7 +15,7 @@ export function SeriesList({ }: { sets: SetLog[] fields: MetricField[] - onUpdate?: (id: number, fields: MetricField[], values: Values) => Promise + onUpdate?: (id: number, fields: MetricField[], values: MetricFormValues) => Promise onDelete?: (id: number) => Promise readOnly?: boolean }) { diff --git a/src/components/workout/exercise-card/exercise-card.tsx b/src/modules/training/components/exercise-card/exercise-card.tsx similarity index 54% rename from src/components/workout/exercise-card/exercise-card.tsx rename to src/modules/training/components/exercise-card/exercise-card.tsx index bb7bd7c..38b0b2e 100644 --- a/src/components/workout/exercise-card/exercise-card.tsx +++ b/src/modules/training/components/exercise-card/exercise-card.tsx @@ -1,10 +1,14 @@ 'use client' import React, { useState } from 'react' -import { trackingFields, type MetricField } from '@/collections/exercises/types' -import { SeriesForm } from '@/components/workout/series-form' -import type { SetLog, TExercise, Values } from '@/types/workout' -import { setLogToFormValues } from '@/lib/metrics' +import { SeriesForm } from '@/modules/training/components/series-form' +import { getTrackingFields, type MetricField } from '@/modules/training/exercises' +import { getExerciseName, type WorkoutExerciseTree } from '@/modules/training/plans' +import type { SetLog } from '@/payload-types' +import { + toMetricFormValues, + type MetricFormValues, +} from '@/modules/training/logs' import { AddSetActions } from './components/add-set-actions' import { ExerciseHeader } from './components/exercise-header' import { ExerciseNote } from './components/exercise-note' @@ -21,25 +25,42 @@ export function ExerciseCard({ onSaveNote, readOnly, }: { - exercise: TExercise + exercise: WorkoutExerciseTree sets: SetLog[] clientNote?: string - onAdd?: (exercise: TExercise, fields: MetricField[], values: Values) => Promise - onUpdate?: (id: number, fields: MetricField[], values: Values) => Promise + onAdd?: ( + exercise: WorkoutExerciseTree, + fields: MetricField[], + values: MetricFormValues, + ) => Promise + onUpdate?: (id: number, fields: MetricField[], values: MetricFormValues) => Promise onDelete?: (id: number) => Promise - onSaveNote?: (exercise: TExercise, note: string) => Promise + onSaveNote?: (exercise: WorkoutExerciseTree, note: string) => Promise readOnly?: boolean }) { const [open, setOpen] = useState(false) - const fields = trackingFields(exercise.trackingType) - const prefillValues: Values = { reps: exercise.prefill.reps ?? '', rir: exercise.prefill.rir ?? '', note: '' } + const fields: MetricField[] = + exercise.targetType === 'duration' + ? ['weightLeft', 'weightRight', 'durationSec'] + : getTrackingFields(exercise.exercise?.trackingType) + const prefillValues: MetricFormValues = { + repsLeft: exercise.repsLeft ?? '', + repsRight: exercise.repsRight ?? '', + note: '', + } + const name = getExerciseName(exercise) + const note = exercise.exercise && exercise.note !== name ? exercise.note : null return (
- + {exercise.meta.length > 0 && {exercise.meta.join(' · ')}} - {exercise.note && {exercise.note}} + {note && {note}} @@ -58,7 +79,7 @@ export function ExerciseCard({ setOpen(true)} onDuplicate={ - sets.length > 0 ? () => onAdd?.(exercise, fields, setLogToFormValues(sets.at(-1)!, fields)) : undefined + sets.length > 0 ? () => onAdd?.(exercise, fields, toMetricFormValues(sets.at(-1)!, fields)) : undefined } /> ))} diff --git a/src/components/workout/exercise-card/index.ts b/src/modules/training/components/exercise-card/index.ts similarity index 100% rename from src/components/workout/exercise-card/index.ts rename to src/modules/training/components/exercise-card/index.ts diff --git a/src/components/workout/note-field/index.ts b/src/modules/training/components/note-field/index.ts similarity index 100% rename from src/components/workout/note-field/index.ts rename to src/modules/training/components/note-field/index.ts diff --git a/src/components/workout/note-field/note-field.tsx b/src/modules/training/components/note-field/note-field.tsx similarity index 97% rename from src/components/workout/note-field/note-field.tsx rename to src/modules/training/components/note-field/note-field.tsx index 245f61a..5edfde3 100644 --- a/src/components/workout/note-field/note-field.tsx +++ b/src/modules/training/components/note-field/note-field.tsx @@ -76,7 +76,7 @@ export function NoteField({ value={value} autoFocus placeholder={labels.placeholder} - onChange={(e) => setValue(e.target.value)} + onChange={(event) => setValue(event.target.value)} /> + )}
diff --git a/src/components/workout/series-row/index.ts b/src/modules/training/components/series-row/index.ts similarity index 100% rename from src/components/workout/series-row/index.ts rename to src/modules/training/components/series-row/index.ts diff --git a/src/components/workout/series-row/series-row.tsx b/src/modules/training/components/series-row/series-row.tsx similarity index 74% rename from src/components/workout/series-row/series-row.tsx rename to src/modules/training/components/series-row/series-row.tsx index 7e62017..9815ab5 100644 --- a/src/components/workout/series-row/series-row.tsx +++ b/src/modules/training/components/series-row/series-row.tsx @@ -2,12 +2,16 @@ import React, { useState } from 'react' import { Pencil, Trash2 } from 'lucide-react' -import type { MetricField } from '@/collections/exercises/types' import { joinClasses, panelClass } from '@/lib/class-names' import { Button } from '@/components/ui/button' -import { SeriesForm } from '@/components/workout/series-form' -import type { SetLog, Values } from '@/types/workout' -import { setLogToFormValues, setSummary } from '@/lib/metrics' +import { SeriesForm } from '@/modules/training/components/series-form' +import type { MetricField } from '@/modules/training/exercises' +import type { SetLog } from '@/payload-types' +import { + formatSetLogSummary, + toMetricFormValues, + type MetricFormValues, +} from '@/modules/training/logs' export function SeriesRow({ set, @@ -18,7 +22,7 @@ export function SeriesRow({ }: { set: SetLog fields: MetricField[] - onUpdate: (values: Values) => Promise + onUpdate: (values: MetricFormValues) => Promise onDelete: () => Promise readOnly?: boolean }) { @@ -29,7 +33,7 @@ export function SeriesRow({
  • { await onUpdate(values) setEditing(false) @@ -43,7 +47,7 @@ export function SeriesRow({ return (
  • - Seria {set.setNumber}: {setSummary(set)} + Seria {set.setNumber}: {formatSetLogSummary(set)} {!readOnly && ( diff --git a/src/components/workout/session-times/index.ts b/src/modules/training/components/session-times/index.ts similarity index 100% rename from src/components/workout/session-times/index.ts rename to src/modules/training/components/session-times/index.ts diff --git a/src/components/workout/session-times/session-times.tsx b/src/modules/training/components/session-times/session-times.tsx similarity index 97% rename from src/components/workout/session-times/session-times.tsx rename to src/modules/training/components/session-times/session-times.tsx index caabee7..7fc42ef 100644 --- a/src/components/workout/session-times/session-times.tsx +++ b/src/modules/training/components/session-times/session-times.tsx @@ -6,7 +6,7 @@ import { joinClasses, mutedTextClass } from '@/lib/class-names' import { Button } from '@/components/ui/button' import { Field } from '@/components/ui/field' import { Input } from '@/components/ui/input' -import type { Session } from '@/types/workout' +import type { WorkoutLog } from '@/payload-types' import { combineDateTime, formatDuration, isoToDateInput, isoToTimeInput } from '@/lib/date' export function SessionTimesBadge({ @@ -14,7 +14,7 @@ export function SessionTimesBadge({ open, onOpen, }: { - session: Session | null + session: WorkoutLog | null open: boolean onOpen: () => void }) { @@ -54,7 +54,7 @@ export function SessionTimesForm({ onSave, onClose, }: { - session: Session | null + session: WorkoutLog | null onSet: (field: 'startedAt' | 'finishedAt', iso: string | null) => void onSave: (startedAt: string | null, finishedAt: string | null) => Promise onClose: () => void diff --git a/src/components/workout/workout-plans/components/active-context-banner/active-context-banner.tsx b/src/modules/training/components/workout-plans/components/active-context-banner/active-context-banner.tsx similarity index 100% rename from src/components/workout/workout-plans/components/active-context-banner/active-context-banner.tsx rename to src/modules/training/components/workout-plans/components/active-context-banner/active-context-banner.tsx diff --git a/src/components/workout/workout-plans/components/active-context-banner/index.ts b/src/modules/training/components/workout-plans/components/active-context-banner/index.ts similarity index 100% rename from src/components/workout/workout-plans/components/active-context-banner/index.ts rename to src/modules/training/components/workout-plans/components/active-context-banner/index.ts diff --git a/src/components/workout/workout-plans/components/workout-pickers/index.ts b/src/modules/training/components/workout-plans/components/workout-pickers/index.ts similarity index 100% rename from src/components/workout/workout-plans/components/workout-pickers/index.ts rename to src/modules/training/components/workout-plans/components/workout-pickers/index.ts diff --git a/src/components/workout/workout-plans/components/workout-pickers/workout-pickers.tsx b/src/modules/training/components/workout-plans/components/workout-pickers/workout-pickers.tsx similarity index 78% rename from src/components/workout/workout-plans/components/workout-pickers/workout-pickers.tsx rename to src/modules/training/components/workout-plans/components/workout-pickers/workout-pickers.tsx index 722ac12..e1ed6ac 100644 --- a/src/components/workout/workout-plans/components/workout-pickers/workout-pickers.tsx +++ b/src/modules/training/components/workout-plans/components/workout-pickers/workout-pickers.tsx @@ -2,10 +2,8 @@ import React from 'react' import { Button } from '@/components/ui/button' -import type { TPlanAccordionItem } from '@/types/plan' -import type { TWorkout } from '@/types/workout' +import type { MicrocycleTree, WorkoutTree } from '@/modules/training/plans' -type Microcycle = TPlanAccordionItem['microcycles'][number] type Id = number | string | null | undefined export function MicrocyclePicker({ @@ -13,9 +11,9 @@ export function MicrocyclePicker({ activeMicrocycleId, onSelect, }: { - microcycles: Microcycle[] + microcycles: MicrocycleTree[] activeMicrocycleId: Id - onSelect: (microcycleId: Microcycle['id']) => void + onSelect: (microcycleId: MicrocycleTree['id']) => void }) { return (
    @@ -39,9 +37,9 @@ export function WorkoutPicker({ activeWorkoutId, onSelect, }: { - workouts: TWorkout[] + workouts: WorkoutTree[] activeWorkoutId: Id - onSelect: (workoutId: TWorkout['id']) => void + onSelect: (workoutId: WorkoutTree['id']) => void }) { return (
    diff --git a/src/components/workout/workout-plans/hooks/use-workout-selection.ts b/src/modules/training/components/workout-plans/hooks/use-workout-selection.ts similarity index 73% rename from src/components/workout/workout-plans/hooks/use-workout-selection.ts rename to src/modules/training/components/workout-plans/hooks/use-workout-selection.ts index 7a23465..60f48c5 100644 --- a/src/components/workout/workout-plans/hooks/use-workout-selection.ts +++ b/src/modules/training/components/workout-plans/hooks/use-workout-selection.ts @@ -1,13 +1,12 @@ 'use client' import { useEffect, useMemo, useState, useSyncExternalStore } from 'react' -import { STORAGE_KEY, SSR_SNAPSHOT } from '@/types/constants' -import type { TPlanAccordionItem } from '@/types/plan' -import type { TWorkout } from '@/types/workout' +import type { MicrocycleTree, PlanTree, WorkoutTree } from '@/modules/training/plans' -type Microcycle = TPlanAccordionItem['microcycles'][number] +const STORAGE_KEY = 'training-app:active-workout-selection' +const SSR_SNAPSHOT = '__SSR_SELECTION__' -type Selection = { +type WorkoutSelection = { planId: number | string | null microcycleId: number | string | null workoutId: number | string | null @@ -18,7 +17,7 @@ const subscribeToSelection = (onStoreChange: () => void) => { return () => window.removeEventListener('storage', onStoreChange) } -const firstAvailableSelection = (plans: TPlanAccordionItem[]): Selection => { +const firstAvailableSelection = (plans: PlanTree[]): WorkoutSelection => { const plan = plans[0] const microcycle = plan?.microcycles[0] const workout = microcycle?.workouts[0] @@ -30,7 +29,7 @@ const firstAvailableSelection = (plans: TPlanAccordionItem[]): Selection => { } } -const isValidSelection = (plans: TPlanAccordionItem[], selection: Selection) => { +const isValidSelection = (plans: PlanTree[], selection: WorkoutSelection) => { const plan = plans.find((item) => item.id === selection.planId) if (!plan) return false @@ -41,20 +40,20 @@ const isValidSelection = (plans: TPlanAccordionItem[], selection: Selection) => } export function useWorkoutSelection( - plans: TPlanAccordionItem[], + plans: PlanTree[], options: { readOnly?: boolean }, ): { - resolvedSelection: Selection - activePlan: TPlanAccordionItem | null - activeMicrocycle: Microcycle | null - activeWorkout: TWorkout | null - selectPlan: (plan: TPlanAccordionItem) => void - selectMicrocycle: (plan: TPlanAccordionItem, microcycleId: number | string) => void + resolvedSelection: WorkoutSelection + activePlan: PlanTree | null + activeMicrocycle: MicrocycleTree | null + activeWorkout: WorkoutTree | null + selectPlan: (plan: PlanTree) => void + selectMicrocycle: (plan: PlanTree, microcycleId: number | string) => void selectWorkout: (workoutId: number | string) => void } { const { readOnly } = options const initialSelection = useMemo(() => firstAvailableSelection(plans), [plans]) - const [selection, setSelection] = useState(null) + const [selection, setSelection] = useState(null) const storedSelectionRaw = useSyncExternalStore( subscribeToSelection, () => (readOnly ? SSR_SNAPSHOT : window.localStorage.getItem(STORAGE_KEY)), @@ -64,7 +63,7 @@ export function useWorkoutSelection( if (storedSelectionRaw === SSR_SNAPSHOT || !storedSelectionRaw) return initialSelection try { - return JSON.parse(storedSelectionRaw) as Selection + return JSON.parse(storedSelectionRaw) as WorkoutSelection } catch { window.localStorage.removeItem(STORAGE_KEY) return initialSelection @@ -72,7 +71,9 @@ export function useWorkoutSelection( }, [initialSelection, storedSelectionRaw]) const preferredSelection = selection ?? storedSelection - const resolvedSelection = isValidSelection(plans, preferredSelection) ? preferredSelection : initialSelection + const resolvedSelection = isValidSelection(plans, preferredSelection) + ? preferredSelection + : initialSelection useEffect(() => { if (readOnly) return @@ -81,7 +82,7 @@ export function useWorkoutSelection( window.localStorage.setItem(STORAGE_KEY, JSON.stringify(resolvedSelection)) }, [plans, readOnly, resolvedSelection, storedSelectionRaw]) - const selectPlan = (plan: TPlanAccordionItem) => { + const selectPlan = (plan: PlanTree) => { const nextMicrocycle = plan.microcycles[0] ?? null const nextWorkout = nextMicrocycle?.workouts[0] ?? null setSelection({ @@ -91,7 +92,7 @@ export function useWorkoutSelection( }) } - const selectMicrocycle = (plan: TPlanAccordionItem, microcycleId: number | string) => { + const selectMicrocycle = (plan: PlanTree, microcycleId: number | string) => { const microcycle = plan.microcycles.find((item) => item.id === microcycleId) ?? null const nextWorkout = microcycle?.workouts[0] ?? null setSelection({ @@ -107,7 +108,9 @@ export function useWorkoutSelection( const activePlan = plans.find((plan) => plan.id === resolvedSelection.planId) ?? null const activeMicrocycle = - activePlan?.microcycles.find((microcycle) => microcycle.id === resolvedSelection.microcycleId) ?? null + activePlan?.microcycles.find( + (microcycle) => microcycle.id === resolvedSelection.microcycleId, + ) ?? null const activeWorkout = activeMicrocycle?.workouts.find((workout) => workout.id === resolvedSelection.workoutId) ?? null diff --git a/src/components/workout/workout-plans/index.ts b/src/modules/training/components/workout-plans/index.ts similarity index 100% rename from src/components/workout/workout-plans/index.ts rename to src/modules/training/components/workout-plans/index.ts diff --git a/src/components/workout/workout-plans/workout-plans.tsx b/src/modules/training/components/workout-plans/workout-plans.tsx similarity index 95% rename from src/components/workout/workout-plans/workout-plans.tsx rename to src/modules/training/components/workout-plans/workout-plans.tsx index bd6167e..4c48286 100644 --- a/src/components/workout/workout-plans/workout-plans.tsx +++ b/src/modules/training/components/workout-plans/workout-plans.tsx @@ -1,12 +1,12 @@ 'use client' import React from 'react' -import { WorkoutTracker } from '@/components/workout/workout-tracker' +import { WorkoutTracker } from '@/modules/training/components/workout-tracker' import { Button } from '@/components/ui/button' import { StatusBadge } from '@/components/ui/status-badge' import { Surface } from '@/components/ui/surface' import { mutedTextClass } from '@/lib/class-names' -import type { TPlanAccordionItem } from '@/types/plan' +import type { PlanTree } from '@/modules/training/plans' import { ActiveContextBanner } from './components/active-context-banner' import { MicrocyclePicker, WorkoutPicker } from './components/workout-pickers' import { useWorkoutSelection } from './hooks/use-workout-selection' @@ -16,7 +16,7 @@ export function WorkoutPlans({ readOnly, showResults, }: { - plans: TPlanAccordionItem[] + plans: PlanTree[] readOnly?: boolean showResults?: boolean }) { diff --git a/src/components/workout/workout-tracker/hooks/use-workout-session.ts b/src/modules/training/components/workout-tracker/hooks/use-workout-session.ts similarity index 58% rename from src/components/workout/workout-tracker/hooks/use-workout-session.ts rename to src/modules/training/components/workout-tracker/hooks/use-workout-session.ts index 358dc50..fa521df 100644 --- a/src/components/workout/workout-tracker/hooks/use-workout-session.ts +++ b/src/modules/training/components/workout-tracker/hooks/use-workout-session.ts @@ -1,26 +1,30 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import type { MetricField } from '@/collections/exercises/types' -import { metricBody } from '@/lib/metrics' import { sdk } from '@/lib/sdk' -import type { Session, SetLog, TExercise, TWorkout, Values } from '@/types/workout' - -const toSession = (doc: unknown): Session => doc as unknown as Session -const toSetLog = (doc: unknown): SetLog => doc as unknown as SetLog - -type ExerciseNote = { id: number; exerciseRow: number; note: string | null } -const toExerciseNote = (doc: unknown): ExerciseNote => doc as unknown as ExerciseNote +import type { MetricField } from '@/modules/training/exercises' +import { + getExerciseName, + type WorkoutExerciseTree, + type WorkoutTree, +} from '@/modules/training/plans' +import { toSetLogMetricData, type MetricFormValues } from '@/modules/training/logs' +import type { ExerciseLog, SetLog, WorkoutLog } from '@/payload-types' + +const relationshipId = ( + relationship: number | { id: number } | null | undefined, +): number | null => + relationship && typeof relationship === 'object' ? relationship.id : (relationship ?? null) export function useWorkoutSession( - workout: TWorkout, + workout: WorkoutTree, options: { readOnly?: boolean; showResults?: boolean }, ) { const { readOnly, showResults } = options - const [session, setSession] = useState(null) + const [session, setSession] = useState(null) const [sets, setSets] = useState([]) - const [exerciseNotes, setExerciseNotes] = useState([]) + const [exerciseNotes, setExerciseNotes] = useState([]) const [loadedWorkoutId, setLoadedWorkoutId] = useState(null) const [error, setError] = useState(null) @@ -37,7 +41,7 @@ export function useWorkoutSession( .find({ collection: 'workout-logs', where: { workout: { equals: workout.id } }, limit: 1, depth: 0, sort: '-updatedAt' }) .then(async (result) => { if (!active) return - const loadedSession = (result.docs[0] ?? null) as Session | null + const loadedSession = result.docs[0] ?? null if (!loadedSession) { setSession(null) @@ -65,8 +69,8 @@ export function useWorkoutSession( ]) if (!active) return - setSets(setsResult.docs as unknown as SetLog[]) - setExerciseNotes(notesResult.docs.map(toExerciseNote)) + setSets(setsResult.docs) + setExerciseNotes(notesResult.docs) setLoadedWorkoutId(workout.id) }) .catch((loadError) => { @@ -89,67 +93,84 @@ export function useWorkoutSession( } } - const creating = useRef | null>(null) - const ensureSession = async (): Promise => { + const creating = useRef | null>(null) + const ensureSession = async (): Promise => { if (displayedSession) return displayedSession if (!creating.current) { creating.current = sdk - .create({ collection: 'workout-logs', data: { workout: workout.id } as never }) + .create({ collection: 'workout-logs', data: { workout: workout.id } }) .then((doc) => { - const created = toSession(doc) - setSession(created) + setSession(doc) setLoadedWorkoutId(workout.id) - return created + return doc }) } return creating.current } - const setsForRow = (rowId: string) => + const setsForRow = (rowId: number) => displayedSets - .filter((set) => String(set.exerciseRow) === rowId) - .sort((a, b) => (a.setNumber ?? 0) - (b.setNumber ?? 0)) + .filter((set) => relationshipId(set.exerciseRow) === rowId) + .sort( + (firstSet, secondSet) => (firstSet.setNumber ?? 0) - (secondSet.setNumber ?? 0), + ) - const noteForRow = (rowId: string): string => - displayedNotes.find((entry) => String(entry.exerciseRow) === rowId)?.note ?? '' + const noteForRow = (rowId: number): string => + displayedNotes.find((entry) => relationshipId(entry.exerciseRow) === rowId)?.note ?? '' const setTime = (field: 'startedAt' | 'finishedAt', iso: string | null) => runMutation(async () => { const s = await ensureSession() - const doc = await sdk.update({ collection: 'workout-logs', id: s.id, data: { [field]: iso } as never }) - setSession(toSession(doc)) + const doc = await sdk.update({ collection: 'workout-logs', id: s.id, data: { [field]: iso } }) + setSession(doc) }, 'Błąd zapisu czasu') const saveTimes = (startedAt: string | null, finishedAt: string | null) => runMutation(async () => { const s = await ensureSession() - const doc = await sdk.update({ collection: 'workout-logs', id: s.id, data: { startedAt, finishedAt } as never }) - setSession(toSession(doc)) + const doc = await sdk.update({ + collection: 'workout-logs', + id: s.id, + data: { startedAt, finishedAt }, + }) + setSession(doc) }, 'Błąd zapisu czasu') - const addSet = (ex: TExercise, fields: MetricField[], values: Values) => + const addSet = ( + exercise: WorkoutExerciseTree, + fields: MetricField[], + values: MetricFormValues, + ) => runMutation(async () => { const s = await ensureSession() - const setNumber = setsForRow(ex.rowId).length + 1 + const setNumber = setsForRow(exercise.id).length + 1 + const exerciseName = getExerciseName(exercise) const doc = await sdk.create({ collection: 'set-logs', depth: 0, data: { session: s.id, - exercise: ex.exerciseId ?? undefined, - exerciseName: ex.exerciseName, - exerciseRow: Number(ex.rowId), + exercise: exercise.exercise?.id ?? undefined, + exerciseName, + exerciseRow: exercise.id, setNumber, - ...metricBody(fields, values), - } as never, + ...toSetLogMetricData(fields, values), + }, }) - setSets((prev) => [...prev, toSetLog(doc)]) + setSets((prev) => [...prev, doc]) }, 'Błąd zapisu serii') - const updateSet = (id: number, fields: MetricField[], values: Values) => + const updateSet = (id: number, fields: MetricField[], values: MetricFormValues) => runMutation(async () => { - const doc = await sdk.update({ collection: 'set-logs', id, depth: 0, data: metricBody(fields, values) as never }) - setSets((prev) => prev.map((set) => (set.id === id ? { ...set, ...toSetLog(doc) } : set))) + const doc = await sdk.update({ + collection: 'set-logs', + id, + depth: 0, + data: toSetLogMetricData(fields, values), + }) + setSets((prev) => + prev.map((set) => (set.id === id ? doc : set)), + ) }, 'Błąd aktualizacji serii') const deleteSet = (id: number) => @@ -161,34 +182,46 @@ export function useWorkoutSession( const saveSessionNote = (note: string) => runMutation(async () => { const s = await ensureSession() - const doc = await sdk.update({ collection: 'workout-logs', id: s.id, depth: 0, data: { notes: note.trim() } as never }) - setSession(toSession(doc)) + const doc = await sdk.update({ + collection: 'workout-logs', + id: s.id, + depth: 0, + data: { notes: note.trim() }, + }) + setSession(doc) }, 'Błąd zapisu notatki') - const saveExerciseNote = (ex: TExercise, note: string) => + const saveExerciseNote = (exercise: WorkoutExerciseTree, note: string) => runMutation(async () => { const s = await ensureSession() - const rowId = Number(ex.rowId) - const existing = exerciseNotes.find((entry) => entry.exerciseRow === rowId) + const rowId = exercise.id + const exerciseName = getExerciseName(exercise) + const existing = exerciseNotes.find( + (entry) => relationshipId(entry.exerciseRow) === rowId, + ) const trimmed = note.trim() const doc = existing - ? await sdk.update({ collection: 'exercise-logs', id: existing.id, depth: 0, data: { note: trimmed } as never }) + ? await sdk.update({ + collection: 'exercise-logs', + id: existing.id, + depth: 0, + data: { note: trimmed }, + }) : await sdk.create({ collection: 'exercise-logs', depth: 0, data: { session: s.id, - exercise: ex.exerciseId ?? undefined, - exerciseName: ex.exerciseName, + exercise: exercise.exercise?.id ?? undefined, + exerciseName, exerciseRow: rowId, note: trimmed, - } as never, + }, }) - const saved = toExerciseNote(doc) setExerciseNotes((prev) => - existing ? prev.map((entry) => (entry.id === saved.id ? saved : entry)) : [...prev, saved], + existing ? prev.map((entry) => (entry.id === doc.id ? doc : entry)) : [...prev, doc], ) }, 'Błąd zapisu notatki') diff --git a/src/components/workout/workout-tracker/index.ts b/src/modules/training/components/workout-tracker/index.ts similarity index 100% rename from src/components/workout/workout-tracker/index.ts rename to src/modules/training/components/workout-tracker/index.ts diff --git a/src/components/workout/workout-tracker/workout-tracker.tsx b/src/modules/training/components/workout-tracker/workout-tracker.tsx similarity index 77% rename from src/components/workout/workout-tracker/workout-tracker.tsx rename to src/modules/training/components/workout-tracker/workout-tracker.tsx index 592b1d5..480f837 100644 --- a/src/components/workout/workout-tracker/workout-tracker.tsx +++ b/src/modules/training/components/workout-tracker/workout-tracker.tsx @@ -4,10 +4,10 @@ import { useTranslations } from 'next-intl' import React, { useState } from 'react' import { mutedTextClass, panelClass, sectionLabelClass } from '@/lib/class-names' import { Alert } from '@/components/ui/alert' -import { ExerciseCard } from '@/components/workout/exercise-card' -import { NoteField } from '@/components/workout/note-field' -import { SessionTimesBadge, SessionTimesForm } from '@/components/workout/session-times' -import type { TWorkout } from '@/types/workout' +import { ExerciseCard } from '@/modules/training/components/exercise-card' +import { NoteField } from '@/modules/training/components/note-field' +import { SessionTimesBadge, SessionTimesForm } from '@/modules/training/components/session-times' +import type { WorkoutTree } from '@/modules/training/plans' import { useWorkoutSession } from './hooks/use-workout-session' export function WorkoutTracker({ @@ -15,7 +15,7 @@ export function WorkoutTracker({ readOnly, showResults, }: { - workout: TWorkout + workout: WorkoutTree readOnly?: boolean showResults?: boolean }) { @@ -74,13 +74,22 @@ export function WorkoutTracker({ > {block.groups.map((group, groupIndex) => (
    0 ? 'mt-2' : undefined}> - {group.label &&
    {group.label}
    } + {(group.label || group.protocolLabel) && ( +
    + {group.label} + {group.label && group.protocolLabel ? ' ' : ''} + {group.protocolLabel && (group.label ? `(${group.protocolLabel})` : group.protocolLabel)} +
    + )} + {group.meta.length > 0 && ( +
    {group.meta.join(' · ')}
    + )} {group.exercises.map((exercise) => ( + +export const METRIC_FIELDS: Record = { + weightLeft: { + label: 'Weight left (kg)', + placeholder: 'KG left', + numeric: true, + bodyweightAffected: true, + }, + weightRight: { + label: 'Weight right (kg)', + placeholder: 'KG right', + numeric: true, + bodyweightAffected: true, + }, + repsLeft: { label: 'Reps left', placeholder: 'reps', numeric: false }, + repsRight: { label: 'Reps right', placeholder: 'reps', numeric: false }, + distanceM: { label: 'Distance (m)', placeholder: 'distance', numeric: true }, + durationSec: { + label: 'Duration', + placeholder: 'minutes / seconds', + numeric: true, + composite: 'duration', + }, +} + +export const ALL_METRIC_FIELDS = Object.keys(METRIC_FIELDS) as MetricField[] + +export const TRACKING: Record = { + strength: { + label: 'Strength', + fields: ['weightLeft', 'weightRight', 'repsLeft', 'repsRight'], + }, + cardio: { + label: 'Cardio', + fields: ['distanceM', 'durationSec'], + }, +} + +export const DEFAULT_TRACKING: TrackingType = 'strength' + +export const TRACKING_OPTIONS = (Object.keys(TRACKING) as TrackingType[]).map((value) => ({ + label: TRACKING[value].label, + value, +})) diff --git a/src/modules/training/exercises/formatters.ts b/src/modules/training/exercises/formatters.ts new file mode 100644 index 0000000..e42e31c --- /dev/null +++ b/src/modules/training/exercises/formatters.ts @@ -0,0 +1,15 @@ +export const hasMetricValue = (value?: string | null): boolean => { + const trimmed = value?.trim() ?? '' + return trimmed !== '' && trimmed.toLowerCase() !== 'x' +} + +export const formatSideReps = ( + repsLeft?: string | null, + repsRight?: string | null, +): string | null => { + const left = hasMetricValue(repsLeft) ? repsLeft?.trim() || null : null + const right = hasMetricValue(repsRight) ? repsRight?.trim() || null : null + + if (left && right) return `${left}+${right}` + return left ?? right +} diff --git a/src/modules/training/exercises/index.ts b/src/modules/training/exercises/index.ts new file mode 100644 index 0000000..4ec66a8 --- /dev/null +++ b/src/modules/training/exercises/index.ts @@ -0,0 +1,17 @@ +export { + ALL_METRIC_FIELDS, + DEFAULT_TRACKING, + EXERCISE_TARGET_TYPE_OPTIONS, + METRIC_FIELDS, + TRACKING, + TRACKING_OPTIONS, +} from './constants' +export { formatSideReps, hasMetricValue } from './formatters' +export { getTrackingFields } from './tracking' +export type { + ExerciseTargetType, + MetricField, + MetricMeta, + MetricUnits, + TrackingType, +} from './types' diff --git a/src/modules/training/exercises/tracking.ts b/src/modules/training/exercises/tracking.ts new file mode 100644 index 0000000..099449e --- /dev/null +++ b/src/modules/training/exercises/tracking.ts @@ -0,0 +1,7 @@ +import { DEFAULT_TRACKING, TRACKING } from './constants' +import type { MetricField, TrackingType } from './types' + +export const getTrackingFields = (trackingType?: string | null): MetricField[] => + trackingType && trackingType in TRACKING + ? TRACKING[trackingType as TrackingType].fields + : TRACKING[DEFAULT_TRACKING].fields diff --git a/src/modules/training/exercises/types.ts b/src/modules/training/exercises/types.ts new file mode 100644 index 0000000..dee2a5e --- /dev/null +++ b/src/modules/training/exercises/types.ts @@ -0,0 +1,35 @@ +import type { Exercise, WorkoutExerciseRow } from '@/payload-types' + +export type MetricField = + | 'weightLeft' + | 'weightRight' + | 'repsLeft' + | 'repsRight' + | 'distanceM' + | 'durationSec' + +export type TrackingType = NonNullable + +export type ExerciseTargetType = NonNullable + +type UnitOption = { + label: string + value: string + factor: number +} + +export type MetricMeta = { + label: string + placeholder: string + numeric: boolean + /** Input is split into minutes and seconds, then stored as total seconds. */ + composite?: 'duration' + /** Input units converted to a base value before persistence. */ + units?: { + default: string + options: UnitOption[] + } + bodyweightAffected?: boolean +} + +export type MetricUnits = NonNullable diff --git a/src/modules/training/logs/constants.ts b/src/modules/training/logs/constants.ts new file mode 100644 index 0000000..f6f7053 --- /dev/null +++ b/src/modules/training/logs/constants.ts @@ -0,0 +1,3 @@ +export const BODYWEIGHT_FORM_FIELD = 'weight__bodyweight' as const + +export const LEGACY_SET_LOG_FIELDS = ['weight', 'reps', 'rir'] as const diff --git a/src/modules/training/logs/formatters.ts b/src/modules/training/logs/formatters.ts new file mode 100644 index 0000000..b723b47 --- /dev/null +++ b/src/modules/training/logs/formatters.ts @@ -0,0 +1,23 @@ +import { formatSideReps } from '@/modules/training/exercises' +import { formatSec } from '@/lib/date' +import type { SetLog } from '@/payload-types' + +export const formatSetLogSummary = (set: SetLog): string => { + const parts: string[] = [] + + if (set.isBodyweight) { + parts.push('MC') + } else { + if (set.weightLeft != null) parts.push(`L ${set.weightLeft} kg`) + if (set.weightRight != null) parts.push(`R ${set.weightRight} kg`) + } + + if (set.distanceM != null) parts.push(`${set.distanceM} m`) + if (set.durationSec != null) parts.push(formatSec(set.durationSec)) + + const sideReps = formatSideReps(set.repsLeft, set.repsRight) + if (sideReps) parts.push(`Steps: ${sideReps}`) + if (set.note) parts.push(set.note) + + return parts.length ? parts.join(' · ') : '—' +} diff --git a/src/modules/training/logs/index.ts b/src/modules/training/logs/index.ts new file mode 100644 index 0000000..b249569 --- /dev/null +++ b/src/modules/training/logs/index.ts @@ -0,0 +1,14 @@ +export { BODYWEIGHT_FORM_FIELD, LEGACY_SET_LOG_FIELDS } from './constants' +export { formatSetLogSummary } from './formatters' +export { + getMetricMinutesField, + getMetricSecondsField, + getMetricUnitField, + toMetricFormValues, + toSetLogMetricData, +} from './metric-form' +export type { + MetricFormField, + MetricFormValues, + SetLogMetricInput, +} from './types' diff --git a/src/modules/training/logs/metric-form.ts b/src/modules/training/logs/metric-form.ts new file mode 100644 index 0000000..f05c452 --- /dev/null +++ b/src/modules/training/logs/metric-form.ts @@ -0,0 +1,136 @@ +import { + METRIC_FIELDS, + type MetricField, + type MetricUnits, +} from '@/modules/training/exercises' +import type { SetLog } from '@/payload-types' +import { BODYWEIGHT_FORM_FIELD } from './constants' +import type { MetricFormValues, SetLogMetricInput } from './types' + +export const getMetricMinutesField = (field: Field): `${Field}__min` => + `${field}__min` + +export const getMetricSecondsField = (field: Field): `${Field}__sec` => + `${field}__sec` + +export const getMetricUnitField = (field: Field): `${Field}__unit` => + `${field}__unit` + +const parseFiniteNumber = (value?: string): number | null => { + const trimmed = value?.trim() ?? '' + if (trimmed === '') return null + + const numericValue = Number(trimmed) + return Number.isFinite(numericValue) ? numericValue : null +} + +const textOrNull = (value?: string): string | null => value?.trim() || null + +const getUnitFactor = (units: MetricUnits, unit: string): number => + units.options.find((option) => option.value === unit)?.factor ?? 1 + +const toDurationSeconds = (minutesValue?: string, secondsValue?: string): number | null => { + const minutesText = minutesValue?.trim() ?? '' + const secondsText = secondsValue?.trim() ?? '' + + if (minutesText === '' && secondsText === '') return null + + const minutes = minutesText === '' ? 0 : parseFiniteNumber(minutesText) + const seconds = secondsText === '' ? 0 : parseFiniteNumber(secondsText) + + if (minutes === null || seconds === null) return null + return minutes * 60 + seconds +} + +const fromBaseUnit = (field: MetricField, baseValue: number): { value: string; unit: string } => { + const units = METRIC_FIELDS[field].units + if (!units) return { value: String(baseValue), unit: '' } + + const unit = units.default + return { + value: String(baseValue / getUnitFactor(units, unit)), + unit, + } +} + +export const toSetLogMetricData = ( + fields: MetricField[], + values: MetricFormValues, +): SetLogMetricInput => { + const isBodyweight = values[BODYWEIGHT_FORM_FIELD] === 'true' + const metricValues: { [Field in MetricField]?: SetLog[Field] } = {} + const setMetricValue = ( + field: Field, + value: SetLog[Field], + ) => { + metricValues[field] = value + } + + for (const field of fields) { + const meta = METRIC_FIELDS[field] + + if (meta.composite === 'duration') { + setMetricValue( + field, + toDurationSeconds( + values[getMetricMinutesField(field)], + values[getMetricSecondsField(field)], + ), + ) + continue + } + + if (isBodyweight && meta.bodyweightAffected) { + setMetricValue(field, null) + continue + } + + const rawValue = values[field] + if (rawValue?.trim() === '') { + setMetricValue(field, null) + } else if (meta.units) { + const numericValue = parseFiniteNumber(rawValue) + const unit = values[getMetricUnitField(field)] || meta.units.default + setMetricValue( + field, + numericValue === null ? null : numericValue * getUnitFactor(meta.units, unit) + ) + } else { + setMetricValue( + field, + meta.numeric ? parseFiniteNumber(rawValue) : textOrNull(rawValue), + ) + } + } + + return { + ...metricValues, + isBodyweight, + note: values.note?.trim() || null, + } +} + +export const toMetricFormValues = (set: SetLog, fields: MetricField[]): MetricFormValues => { + const initial: MetricFormValues = { note: set.note ?? '' } + if (set.isBodyweight) initial[BODYWEIGHT_FORM_FIELD] = 'true' + + for (const field of fields) { + const rawValue = set[field] + + if (rawValue == null) { + initial[field] = '' + } else if (METRIC_FIELDS[field].composite === 'duration') { + const baseValue = Number(rawValue) + initial[getMetricMinutesField(field)] = String(Math.floor(baseValue / 60)) + initial[getMetricSecondsField(field)] = String(baseValue % 60) + } else if (METRIC_FIELDS[field].units) { + const converted = fromBaseUnit(field, Number(rawValue)) + initial[field] = converted.value + initial[getMetricUnitField(field)] = converted.unit + } else { + initial[field] = String(rawValue) + } + } + + return initial +} diff --git a/src/modules/training/logs/types.ts b/src/modules/training/logs/types.ts new file mode 100644 index 0000000..b754a03 --- /dev/null +++ b/src/modules/training/logs/types.ts @@ -0,0 +1,19 @@ +import type { MetricField } from '@/modules/training/exercises' +import type { SetLog } from '@/payload-types' + +type BodyweightFormField = typeof import('./constants').BODYWEIGHT_FORM_FIELD + +export type MetricFormField = + | MetricField + | `${MetricField}__min` + | `${MetricField}__sec` + | `${MetricField}__unit` + | BodyweightFormField + | 'note' + +export type MetricFormValues = Partial> + +export type SetLogMetricInput = Partial> & { + isBodyweight: NonNullable + note: SetLog['note'] +} diff --git a/src/modules/training/plans/build-plan-tree.ts b/src/modules/training/plans/build-plan-tree.ts new file mode 100644 index 0000000..95fcd90 --- /dev/null +++ b/src/modules/training/plans/build-plan-tree.ts @@ -0,0 +1,133 @@ +import { + buildExerciseMeta, + buildWorkoutGroupMeta, + formatWorkoutGroupLabel, +} from './formatters' +import { STATUS_LABEL } from './constants' +import type { + ExerciseMetaLabels, + PlanDocuments, + PlanTree, + WorkoutBlock, + WorkoutExerciseTree, + WorkoutGroupTree, + WorkoutTree, +} from './types' + +type Relationship = + | RelationshipId + | { id: RelationshipId } + +const resolveRelationshipId = ( + relationship: Relationship, +): RelationshipId => (typeof relationship === 'object' ? relationship.id : relationship) + +const groupByRelationship = ( + items: Item[], + getRelationship: (item: Item) => Relationship, +): Map => { + const groupedItems = new Map() + + items.forEach((item) => { + const relationshipId = resolveRelationshipId(getRelationship(item)) + const relatedItems = groupedItems.get(relationshipId) ?? [] + relatedItems.push(item) + groupedItems.set(relationshipId, relatedItems) + }) + + return groupedItems +} + +export const buildPlanTree = ( + documents: PlanDocuments, + labels: ExerciseMetaLabels, +): PlanTree[] => { + const microcyclesByPlan = groupByRelationship( + documents.microcycles, + (microcycle) => microcycle.plan, + ) + const workoutsByMicrocycle = groupByRelationship( + documents.workouts, + (workout) => workout.microcycle, + ) + const groupsByWorkout = groupByRelationship(documents.groups, (group) => group.workout) + const exerciseRowsByGroup = groupByRelationship( + documents.exerciseRows, + (exerciseRow) => exerciseRow.group, + ) + + const buildExerciseTree = ( + exerciseRow: PlanDocuments['exerciseRows'][number], + ): WorkoutExerciseTree => ({ + ...exerciseRow, + group: resolveRelationshipId(exerciseRow.group), + exercise: + exerciseRow.exercise && typeof exerciseRow.exercise === 'object' + ? exerciseRow.exercise + : null, + meta: buildExerciseMeta(exerciseRow, labels), + }) + + const buildGroupTree = ( + group: PlanDocuments['groups'][number], + ): WorkoutGroupTree => ({ + ...group, + protocol: group.protocol ?? 'standard', + label: group.label ?? '', + protocolLabel: formatWorkoutGroupLabel(group), + meta: buildWorkoutGroupMeta(group), + exercises: (exerciseRowsByGroup.get(group.id) ?? []).map(buildExerciseTree), + }) + + const buildWorkoutTree = ( + workout: PlanDocuments['workouts'][number], + ): WorkoutTree => { + const workoutGroups = groupsByWorkout.get(workout.id) ?? [] + + return { + ...workout, + sections: (workout.sections ?? []).map((section) => { + const sectionGroups = workoutGroups + .filter((group) => group.sectionRowId === section.id) + .sort( + (firstGroup, secondGroup) => + (firstGroup.order ?? 0) - (secondGroup.order ?? 0), + ) + const blocks: WorkoutBlock[] = [] + + sectionGroups.forEach((group, groupIndexInSection) => { + if (groupIndexInSection === 0 || !group.bundleWithPrevious) { + blocks.push({ index: blocks.length, groups: [] }) + } + + blocks[blocks.length - 1].groups.push(buildGroupTree(group)) + }) + + return { + ...section, + blocks, + } + }), + } + } + + return documents.plans.map((plan) => { + const status = plan.status ?? 'active' + + return { + ...plan, + status, + statusLabel: STATUS_LABEL[status] || status, + dateRange: + plan.startDate || plan.endDate + ? [plan.startDate, plan.endDate] + .map((date) => (date ? new Date(date).toLocaleDateString('pl-PL') : '...')) + .join(' - ') + : null, + microcycles: (microcyclesByPlan.get(plan.id) ?? []).map((microcycle) => ({ + ...microcycle, + workouts: (workoutsByMicrocycle.get(microcycle.id) ?? []).map(buildWorkoutTree), + })), + } + }) +} diff --git a/src/modules/training/plans/constants.ts b/src/modules/training/plans/constants.ts new file mode 100644 index 0000000..6c67062 --- /dev/null +++ b/src/modules/training/plans/constants.ts @@ -0,0 +1,20 @@ +import type { WorkoutProtocol } from './types' + +export const STATUS_LABEL: Record = { + active: 'Aktywny', + paused: 'Wstrzymany', + completed: 'Zakończony', +} + +export const PROTOCOL_LABEL: Record = { + standard: 'Standard', + emom: 'EMOM', + amrap: 'AMRAP', + for_time: 'For Time', + tabata: 'Tabata', +} + +export const PROTOCOL_OPTIONS = (Object.keys(PROTOCOL_LABEL) as WorkoutProtocol[]).map((value) => ({ + value, + label: PROTOCOL_LABEL[value], +})) diff --git a/src/modules/training/plans/formatters.ts b/src/modules/training/plans/formatters.ts new file mode 100644 index 0000000..2e77366 --- /dev/null +++ b/src/modules/training/plans/formatters.ts @@ -0,0 +1,76 @@ +import { formatSideReps, hasMetricValue } from '@/modules/training/exercises' +import { formatMinSec } from '@/lib/date' +import { PROTOCOL_LABEL } from './constants' +import type { + BuildExerciseMetaInput, + BuildWorkoutGroupMetaInput, + ExerciseMetaLabels, + FormatWorkoutGroupLabelInput, + WorkoutExerciseTree, +} from './types' + +export const getExerciseName = ( + exercise: Pick, +): string => exercise.exercise?.name ?? exercise.note ?? '' + +export const buildExerciseMeta = ( + exercise: BuildExerciseMetaInput, + labels: ExerciseMetaLabels, +): string[] => { + const parts: string[] = [] + + if (hasMetricValue(exercise.rounds)) { + parts.push(`${labels.seriesPrefix}: ${exercise.rounds}`) + } + + if (exercise.targetType === 'duration') { + const duration = formatMinSec(exercise.durationMin, exercise.durationSec) + if (duration) parts.push(`${labels.durationPrefix}: ${duration}`) + } else { + const sideReps = formatSideReps(exercise.repsLeft, exercise.repsRight) + if (sideReps) parts.push(`Steps: ${sideReps}`) + else if (hasMetricValue(exercise.reps)) parts.push(`Steps: ${exercise.reps}`) + } + + if (hasMetricValue(exercise.rest)) parts.push(`${labels.restPrefix}: ${exercise.rest}`) + if (hasMetricValue(exercise.tut)) parts.push(`TUT: ${exercise.tut}`) + if (hasMetricValue(exercise.rir)) parts.push(`RIR: ${exercise.rir}`) + if (hasMetricValue(exercise.kg)) parts.push(`KG: ${exercise.kg}`) + + return parts +} + +export const formatWorkoutGroupLabel = (group: FormatWorkoutGroupLabelInput): string => { + const protocol = group.protocol + return protocol && protocol !== 'standard' ? PROTOCOL_LABEL[protocol] : '' +} + +export const buildWorkoutGroupMeta = (group: BuildWorkoutGroupMetaInput): string[] => { + const restValue = group.restBetweenRounds?.trim() ?? '' + const rest = hasMetricValue(restValue) + ? /^\d+(?:\.\d+)?$/.test(restValue) + ? `Rest: ${restValue} s` + : `Rest: ${restValue}` + : null + + const parts: string[] = [] + + if (group.protocol === 'standard') { + if (group.rounds) parts.push(`Sets: ${group.rounds}`) + if (rest) parts.push(rest) + return parts + } + + if (group.protocol === 'emom') { + if (group.rounds) parts.push(`Duration: ${group.rounds} min`) + if (group.intervalSeconds != null) parts.push(`Interval: ${group.intervalSeconds} s`) + return parts + } + + if (group.protocol === 'tabata') { + if (group.workSeconds != null) parts.push(`Work: ${group.workSeconds} s`) + if (group.restSeconds != null) parts.push(`Rest: ${group.restSeconds} s`) + } + + return parts +} diff --git a/src/modules/training/plans/index.ts b/src/modules/training/plans/index.ts new file mode 100644 index 0000000..243a7c4 --- /dev/null +++ b/src/modules/training/plans/index.ts @@ -0,0 +1,24 @@ +export { PROTOCOL_LABEL, PROTOCOL_OPTIONS, STATUS_LABEL } from './constants' +export { + buildExerciseMeta, + buildWorkoutGroupMeta, + formatWorkoutGroupLabel, + getExerciseName, +} from './formatters' +export { buildPlanTree } from './build-plan-tree' +export type { + BuildExerciseMetaInput, + BuildWorkoutGroupMetaInput, + ExerciseMetaLabels, + FormatWorkoutGroupLabelInput, + LoadTrainingPlansOutput, + MicrocycleTree, + PlanDocuments, + PlanTree, + WorkoutBlock, + WorkoutExerciseTree, + WorkoutGroupTree, + WorkoutProtocol, + WorkoutSectionTree, + WorkoutTree, +} from './types' diff --git a/src/modules/training/plans/server/index.ts b/src/modules/training/plans/server/index.ts new file mode 100644 index 0000000..cbae8fa --- /dev/null +++ b/src/modules/training/plans/server/index.ts @@ -0,0 +1,5 @@ +import 'server-only' + +export { loadPlanDocuments } from './load-plan-documents' +export { loadPlanTree } from './load-plan-tree' +export { loadTrainingPlans } from './load-training-plans' diff --git a/src/modules/training/plans/server/load-plan-documents.ts b/src/modules/training/plans/server/load-plan-documents.ts new file mode 100644 index 0000000..0ae788a --- /dev/null +++ b/src/modules/training/plans/server/load-plan-documents.ts @@ -0,0 +1,83 @@ +import 'server-only' + +import type { getPayload } from 'payload' + +import type { PlanDocuments } from '@/modules/training/plans' + +type PayloadInstance = Awaited> + +export async function loadPlanDocuments( + payload: PayloadInstance, + planIds: (number | string)[], + overrideAccess = false, +): Promise { + if (!planIds.length) { + return { plans: [], microcycles: [], workouts: [], groups: [], exerciseRows: [] } + } + + const plans = await payload.find({ + collection: 'plans', + where: { id: { in: planIds } }, + sort: '-createdAt', + depth: 0, + limit: 100, + overrideAccess, + }) + + if (!plans.docs.length) { + return { plans: [], microcycles: [], workouts: [], groups: [], exerciseRows: [] } + } + + const microcycles = await payload.find({ + collection: 'microcycles', + where: { plan: { in: planIds } }, + sort: 'order', + depth: 0, + limit: 500, + overrideAccess, + }) + const microcycleIds = microcycles.docs.map((microcycle) => microcycle.id) + + const workouts = microcycleIds.length + ? await payload.find({ + collection: 'workouts', + where: { microcycle: { in: microcycleIds } }, + sort: 'order', + depth: 0, + limit: 1000, + overrideAccess, + }) + : { docs: [] } + const workoutIds = workouts.docs.map((workout) => workout.id) + + const groups = workoutIds.length + ? await payload.find({ + collection: 'workout-groups', + where: { workout: { in: workoutIds } }, + sort: 'order', + depth: 0, + limit: 10000, + overrideAccess, + }) + : { docs: [] } + const groupIds = groups.docs.map((group) => group.id) + + const exerciseRows = groupIds.length + ? await payload.find({ + collection: 'workout-exercise-rows', + where: { group: { in: groupIds } }, + sort: 'order', + depth: 1, + limit: 10000, + overrideAccess, + }) + : { docs: [] } + + return { + plans: plans.docs, + microcycles: microcycles.docs, + workouts: workouts.docs, + groups: groups.docs, + exerciseRows: exerciseRows.docs, + } +} diff --git a/src/modules/training/plans/server/load-plan-tree.ts b/src/modules/training/plans/server/load-plan-tree.ts new file mode 100644 index 0000000..f9511c7 --- /dev/null +++ b/src/modules/training/plans/server/load-plan-tree.ts @@ -0,0 +1,20 @@ +import 'server-only' + +import type { getPayload } from 'payload' + +import { buildPlanTree, type ExerciseMetaLabels, type PlanTree } from '@/modules/training/plans' + +import { loadPlanDocuments } from './load-plan-documents' + +type PayloadInstance = Awaited> + +export async function loadPlanTree( + payload: PayloadInstance, + planIds: (number | string)[], + labels: ExerciseMetaLabels, + overrideAccess = false, +): Promise { + const documents = await loadPlanDocuments(payload, planIds, overrideAccess) + + return buildPlanTree(documents, labels) +} diff --git a/src/loaders/training-plan-loader.ts b/src/modules/training/plans/server/load-training-plans.ts similarity index 64% rename from src/loaders/training-plan-loader.ts rename to src/modules/training/plans/server/load-training-plans.ts index ba1a7e6..d733935 100644 --- a/src/loaders/training-plan-loader.ts +++ b/src/modules/training/plans/server/load-training-plans.ts @@ -1,16 +1,15 @@ +import 'server-only' + import { headers as getHeaders } from 'next/headers.js' import { getTranslations } from 'next-intl/server' import { getPayload } from 'payload' import config from '@/payload.config' -import { loadPlansItems } from '@/loaders/load-plans-items' -import type { TPlanAccordionItem } from '@/types/plan' +import type { LoadTrainingPlansOutput } from '@/modules/training/plans' -type LoadResult = - | { user: { id: number | string; name?: string | null; email?: string | null }; plans: TPlanAccordionItem[] } - | { user: null } +import { loadPlanTree } from './load-plan-tree' -export async function loadTrainingPlans(): Promise { +export async function loadTrainingPlans(): Promise { const headers = await getHeaders() const payload = await getPayload({ config: await config }) const { user } = await payload.auth({ headers }) @@ -29,14 +28,13 @@ export async function loadTrainingPlans(): Promise { limit: 100, }) - const planIds = plans.docs.map((p) => p.id) + const planIds = plans.docs.map((plan) => plan.id) - const accordionPlans = await loadPlansItems( + const planTree = await loadPlanTree( payload, planIds, { seriesPrefix: t('seriesPrefix'), - repsPrefix: t('repsPrefix'), durationPrefix: t('durationPrefix'), restPrefix: t('restPrefix'), }, @@ -45,6 +43,6 @@ export async function loadTrainingPlans(): Promise { return { user: { id: user.id, name: user.name ?? null, email: user.email ?? null }, - plans: accordionPlans, + plans: planTree, } } diff --git a/src/modules/training/plans/types.ts b/src/modules/training/plans/types.ts new file mode 100644 index 0000000..e188376 --- /dev/null +++ b/src/modules/training/plans/types.ts @@ -0,0 +1,99 @@ +import type { + Exercise as PayloadExercise, + Microcycle as PayloadMicrocycle, + Plan as PayloadPlan, + Workout as PayloadWorkout, + WorkoutExerciseRow, + WorkoutGroup, +} from '@/payload-types' + +export type PlanDocuments = { + plans: PayloadPlan[] + microcycles: PayloadMicrocycle[] + workouts: PayloadWorkout[] + groups: WorkoutGroup[] + exerciseRows: WorkoutExerciseRow[] +} + +export type WorkoutExerciseTree = WorkoutExerciseRow & { + group: WorkoutGroup['id'] + exercise: PayloadExercise | null + meta: string[] +} + +export type WorkoutGroupTree = WorkoutGroup & { + protocol: NonNullable + label: string + protocolLabel: string + meta: string[] + exercises: WorkoutExerciseTree[] +} + +// A block bundles consecutive groups that share one colored band in the tracker. +export type WorkoutBlock = { + index: number + groups: WorkoutGroupTree[] +} + +type PayloadWorkoutSection = NonNullable[number] + +export type WorkoutSectionTree = PayloadWorkoutSection & { + blocks: WorkoutBlock[] +} + +export type WorkoutTree = Omit & { + sections: WorkoutSectionTree[] +} + +export type MicrocycleTree = PayloadMicrocycle & { + workouts: WorkoutTree[] +} + +export type PlanTree = PayloadPlan & { + status: NonNullable + statusLabel: string + dateRange: string | null + microcycles: MicrocycleTree[] +} + +export type LoadTrainingPlansOutput = + | { + user: { id: number | string; name?: string | null; email?: string | null } + plans: PlanTree[] + } + | { user: null } + +export type ExerciseMetaLabels = { + seriesPrefix: string + durationPrefix: string + restPrefix: string +} + +export type BuildExerciseMetaInput = Pick< + WorkoutExerciseRow, + | 'rounds' + | 'reps' + | 'repsLeft' + | 'repsRight' + | 'targetType' + | 'durationMin' + | 'durationSec' + | 'rest' + | 'tut' + | 'rir' + | 'kg' +> + +export type BuildWorkoutGroupMetaInput = Pick< + WorkoutGroup, + | 'protocol' + | 'rounds' + | 'intervalSeconds' + | 'workSeconds' + | 'restSeconds' + | 'restBetweenRounds' +> + +export type FormatWorkoutGroupLabelInput = Pick + +export type WorkoutProtocol = NonNullable diff --git a/src/payload-types.ts b/src/payload-types.ts index f6407fd..5869d3e 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -366,11 +366,14 @@ export interface WorkoutExerciseRow { */ exercise?: (number | null) | Exercise; note?: string | null; + targetType?: ('repetitions' | 'duration') | null; /** * e.g. 4, 3-4 */ rounds?: string | null; reps?: string | null; + repsLeft?: string | null; + repsRight?: string | null; kg?: string | null; tut?: string | null; rir?: string | null; @@ -471,10 +474,14 @@ export interface SetLog { roundLog?: (number | null) | RoundLog; setNumber?: number | null; weight?: number | null; + weightLeft?: number | null; + weightRight?: number | null; isBodyweight?: boolean | null; distanceM?: number | null; durationSec?: number | null; reps?: string | null; + repsLeft?: string | null; + repsRight?: string | null; rir?: string | null; note?: string | null; completedAt?: string | null; @@ -794,8 +801,11 @@ export interface WorkoutExerciseRowsSelect { numer?: T; exercise?: T; note?: T; + targetType?: T; rounds?: T; reps?: T; + repsLeft?: T; + repsRight?: T; kg?: T; tut?: T; rir?: T; @@ -865,10 +875,14 @@ export interface SetLogsSelect { roundLog?: T; setNumber?: T; weight?: T; + weightLeft?: T; + weightRight?: T; isBodyweight?: T; distanceM?: T; durationSec?: T; reps?: T; + repsLeft?: T; + repsRight?: T; rir?: T; note?: T; completedAt?: T; diff --git a/src/scripts/seed.ts b/src/scripts/seed.ts index 3dbf3ed..cab5229 100644 --- a/src/scripts/seed.ts +++ b/src/scripts/seed.ts @@ -174,7 +174,7 @@ async function run() { data: { ...pick(row, [ 'order', 'numer', 'note', - 'rounds', 'reps', 'kg', + 'rounds', 'reps', 'repsLeft', 'repsRight', 'kg', 'tut', 'rir', 'rest', 'durationMin', 'durationSec', 'setParameters', 'override', diff --git a/src/types/constants.ts b/src/types/constants.ts deleted file mode 100644 index 372dad5..0000000 --- a/src/types/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const STATUS_LABEL: Record = { - active: 'Aktywny', - paused: 'Wstrzymany', - completed: 'Zakończony', -} - -export type Protocol = 'emom' | 'amrap' | 'for_time' | 'tabata' - -export const PROTOCOL_LABEL: Record = { - emom: 'EMOM', - amrap: 'AMRAP', - for_time: 'For Time', - tabata: 'Tabata', -} - -export const STORAGE_KEY = 'training-app:active-workout-selection' -export const SSR_SNAPSHOT = '__SSR_SELECTION__' diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 0fca32b..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './workout' -export * from './plan' -export * from './constants' diff --git a/src/types/plan.ts b/src/types/plan.ts deleted file mode 100644 index 0a359cf..0000000 --- a/src/types/plan.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { TWorkout } from './workout' - -export type TPlanAccordionItem = { - id: number | string - title: string - status: string - statusLabel: string - dateRange?: string | null - description?: string | null - microcycles: Array<{ - id: number | string - title: string - rpe?: number | null - workouts: TWorkout[] - }> -} diff --git a/src/types/workout.ts b/src/types/workout.ts deleted file mode 100644 index ed444ff..0000000 --- a/src/types/workout.ts +++ /dev/null @@ -1,43 +0,0 @@ -export type TExercise = { - rowId: string - numer?: string | null - name: string - note?: string | null - exerciseId?: number | null - exerciseName: string - trackingType?: string | null - videoUrl?: string | null - rounds?: string | null - meta: string[] - prefill: { reps?: string | null; rir?: string | null } - setParameters?: Array<{ setNumber: number; reps?: string | null; kg?: string | null }> | null -} - -export type TGroup = { - protocol: string - label: string - exercises: TExercise[] -} - -// A block bundles consecutive groups that share one colored band in the tracker. -export type TBlock = { index: number; groups: TGroup[] } - -export type TSection = { title?: string | null; subtitle?: string | null; blocks: TBlock[] } -export type TWorkout = { id: number; title: string; rpe?: number | null; sections: TSection[] } - -export type Session = { id: number; startedAt?: string | null; finishedAt?: string | null; notes?: string | null } - -export type SetLog = { - id: number - exerciseRow?: number | null - setNumber?: number | null - weight?: number | null - isBodyweight?: boolean | null - distanceM?: number | null - durationSec?: number | null - reps?: string | null - rir?: string | null - note?: string | null -} - -export type Values = Record