From 4d7378bf3fdde4d58a54604ce36feb85227c8650 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Fri, 24 Jul 2026 10:22:05 +0200 Subject: [PATCH 01/11] feat: support side-specific workout sets --- .changeset/green-sides-log.md | 5 + AGENTS.md | 21 +- README.md | 8 + messages/en.json | 4 +- messages/pl.json | 4 +- package.json | 4 + .../exercise-form/exercise-form.tsx | 17 +- .../admin/modules/workout-structure/loader.ts | 2 + .../admin/modules/workout-structure/types.ts | 2 + .../modules/workout-structure/utils/format.ts | 9 +- .../workout-structure/utils/validate.ts | 10 +- src/collections/exercises/types.ts | 14 +- src/collections/set-logs/index.ts | 32 +- .../workout-exercise-rows/index.ts | 12 +- .../workout/exercise-card/exercise-card.tsx | 6 +- .../bodyweight-field/bodyweight-field.tsx | 64 - .../components/bodyweight-field/index.ts | 1 - .../metric-field-input/metric-field-input.tsx | 19 - .../workout/series-form/series-form.tsx | 18 +- .../workout-tracker/workout-tracker.tsx | 8 +- src/lib/metrics.ts | 77 +- src/loaders/load-plans-items.ts | 87 +- src/loaders/share-link-loader.ts | 1 - src/loaders/training-plan-loader.ts | 1 - src/migrations/20260724_065545.json | 4417 ++++++++++++++++ src/migrations/20260724_065545.ts | 17 + src/migrations/20260724_073306.json | 4429 +++++++++++++++++ src/migrations/20260724_073306.ts | 13 + src/migrations/index.ts | 14 +- src/payload-types.ts | 12 + src/scripts/backfill-set-log-sides.ts | 55 + .../backfill-workout-exercise-reps-sides.ts | 60 + src/scripts/normalize-reps-sides.ts | 111 + src/scripts/normalize-workout-exercise-kg.ts | 65 + src/scripts/seed.ts | 2 +- src/types/workout.ts | 7 +- 36 files changed, 9438 insertions(+), 190 deletions(-) create mode 100644 .changeset/green-sides-log.md delete mode 100644 src/components/workout/series-form/components/bodyweight-field/bodyweight-field.tsx delete mode 100644 src/components/workout/series-form/components/bodyweight-field/index.ts create mode 100644 src/migrations/20260724_065545.json create mode 100644 src/migrations/20260724_065545.ts create mode 100644 src/migrations/20260724_073306.json create mode 100644 src/migrations/20260724_073306.ts create mode 100644 src/scripts/backfill-set-log-sides.ts create mode 100644 src/scripts/backfill-workout-exercise-reps-sides.ts create mode 100644 src/scripts/normalize-reps-sides.ts create mode 100644 src/scripts/normalize-workout-exercise-kg.ts 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/AGENTS.md b/AGENTS.md index 769eee8..d1c04a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ 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. @@ -123,14 +124,32 @@ src/ ```bash yarn dev # start dev server yarn build # production build — run after every implementation -yarn payload migrate # run pending DB migrations +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 backfill:set-log-sides # backfill legacy set-log sides - run manually +yarn backfill:workout-exercise-reps-sides # backfill legacy exercise-row reps - run manually +yarn normalize:reps-sides # split legacy left+right reps - run manually +yarn normalize:workout-exercise-kg # normalize legacy exercise-row KG - run manually yarn seed # seed demo data 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..f4eee62 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,12 @@ PAYLOAD_SECRET=your-long-random-secret-here Run migrations: ```bash +yarn payload migrate:create # after a schema change yarn payload migrate ``` +Run both migration commands manually. Agents do not generate or apply database migrations. + ### Run ```bash @@ -263,7 +266,12 @@ src/ | `yarn dev` | Start 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 backfill:set-log-sides` | Copy legacy set-log values into left and right fields. Run manually. | +| `yarn backfill:workout-exercise-reps-sides` | Copy legacy exercise-row reps into left and right fields. Run manually. | +| `yarn normalize:reps-sides` | Split legacy left and right rep values. Run manually. | +| `yarn normalize:workout-exercise-kg` | Normalize legacy exercise-row KG values. Run manually. | | `yarn generate:types` | Regenerate `payload-types.ts` from collection configs | | `yarn generate:importmap` | Regenerate Payload admin import map (run after adding custom views) | | `yarn seed` | Seed database with demo data | 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/package.json b/package.json index 9210b87..3afa218 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", "seed:export": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" tsx src/scripts/export-seed.ts", + "backfill:set-log-sides": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" tsx src/scripts/backfill-set-log-sides.ts", + "backfill:workout-exercise-reps-sides": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" tsx src/scripts/backfill-workout-exercise-reps-sides.ts", + "normalize:reps-sides": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" tsx src/scripts/normalize-reps-sides.ts", + "normalize:workout-exercise-kg": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" tsx src/scripts/normalize-workout-exercise-kg.ts", "seed": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" tsx src/scripts/seed.ts", "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint .", "payload": "cross-env NODE_OPTIONS=--no-deprecation tsx node_modules/payload/bin.js --disable-transpile", 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 index cfe1367..8eaf839 100644 --- 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 @@ -6,7 +6,7 @@ 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' +import { validateKgOrRepsSides, validateRepsSidesOrKg, validateRounds } from '../../utils' type Props = { groupId: number @@ -49,10 +49,13 @@ function FormFields({ isEdit, onCancel }: { isEdit: boolean; onCancel: () => voi
- +
- + +
+
+
@@ -61,7 +64,7 @@ function FormFields({ isEdit, onCancel }: { isEdit: boolean; onCancel: () => voi
- +
@@ -85,7 +88,8 @@ export function ExerciseForm({ groupId, nextOrder, initial, onSaved, onCancel }: rounds: { value: initial?.rounds ?? '' }, exercise: { value: initial?.exercise ? { value: initial.exercise.id, relationTo: 'exercises' } : null }, note: { value: initial?.note ?? '' }, - reps: { value: initial?.reps ?? '' }, + repsLeft: { value: initial?.repsLeft ?? '' }, + repsRight: { value: initial?.repsRight ?? '' }, kg: { value: initial?.kg ?? '' }, rir: { value: initial?.rir ?? '' }, tut: { value: initial?.tut ?? '' }, @@ -99,7 +103,8 @@ export function ExerciseForm({ groupId, nextOrder, initial, onSaved, onCancel }: rounds: (data.rounds as string) || null, exercise: exerciseRaw ? Number(exerciseRaw.value) : null, note: (data.note as string) || null, - reps: (data.reps as string) || null, + repsLeft: (data.repsLeft as string) || null, + repsRight: (data.repsRight as string) || null, kg: (data.kg as string) || null, rir: (data.rir as string) || null, tut: (data.tut as string) || null, diff --git a/src/app/(payload)/admin/modules/workout-structure/loader.ts b/src/app/(payload)/admin/modules/workout-structure/loader.ts index d56d577..fb7b506 100644 --- a/src/app/(payload)/admin/modules/workout-structure/loader.ts +++ b/src/app/(payload)/admin/modules/workout-structure/loader.ts @@ -83,6 +83,8 @@ export async function loadWorkoutStructure( : null, note: r.note ?? null, reps: r.reps ?? null, + repsLeft: r.repsLeft ?? null, + repsRight: r.repsRight ?? null, kg: r.kg ?? null, tut: r.tut ?? null, rir: r.rir ?? null, diff --git a/src/app/(payload)/admin/modules/workout-structure/types.ts b/src/app/(payload)/admin/modules/workout-structure/types.ts index d4b7c5d..6827d39 100644 --- a/src/app/(payload)/admin/modules/workout-structure/types.ts +++ b/src/app/(payload)/admin/modules/workout-structure/types.ts @@ -24,6 +24,8 @@ export type ExerciseRow = { note?: string | null rounds?: string | null reps?: string | null + repsLeft?: string | null + repsRight?: string | null kg?: string | null tut?: string | null rir?: string | null diff --git a/src/app/(payload)/admin/modules/workout-structure/utils/format.ts b/src/app/(payload)/admin/modules/workout-structure/utils/format.ts index 00fe314..d6c18dd 100644 --- a/src/app/(payload)/admin/modules/workout-structure/utils/format.ts +++ b/src/app/(payload)/admin/modules/workout-structure/utils/format.ts @@ -1,4 +1,5 @@ import type { ExerciseRow, Group } from '../types' +import { formatSideReps } from '@/lib/metrics' export const groupLabel = (g: Group): string => { const p = g.protocol ?? 'standard' @@ -17,10 +18,12 @@ export const exerciseLabel = (row: ExerciseRow): string => 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`) + 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 ${row.rest}`) + if (row.rest) parts.push(`Rest(s): ${row.rest}`) return parts.join(' · ') } diff --git a/src/app/(payload)/admin/modules/workout-structure/utils/validate.ts b/src/app/(payload)/admin/modules/workout-structure/utils/validate.ts index bee2dcd..5b93b0a 100644 --- a/src/app/(payload)/admin/modules/workout-structure/utils/validate.ts +++ b/src/app/(payload)/admin/modules/workout-structure/utils/validate.ts @@ -7,13 +7,15 @@ export const validateRounds: TextFieldValidation = (value) => { 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 (!value && !data.repsLeft && !data.repsRight && !data.kg) 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 validateKgOrRepsSides: TextFieldValidation = (value, { siblingData }) => { + const data = siblingData as SiblingData + if (!value && !data.repsLeft && !data.repsRight) return 'Enter reps or load' return true } diff --git a/src/collections/exercises/types.ts b/src/collections/exercises/types.ts index dd4d420..e3bec11 100644 --- a/src/collections/exercises/types.ts +++ b/src/collections/exercises/types.ts @@ -3,7 +3,7 @@ * 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 MetricField = 'weightLeft' | 'weightRight' | 'repsLeft' | 'repsRight' | 'distanceM' | 'durationSec' export type TrackingType = 'strength' | 'cardio' export type UnitOption = { label: string; value: string; factor: number } @@ -15,20 +15,24 @@ export type MetricMeta = { 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[] } + bodyweightAffected?: boolean } 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 }, + 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 LEGACY_SET_LOG_FIELDS = ['weight', 'reps', 'rir'] as const + export const TRACKING: Record = { - strength: { label: 'Strength', fields: ['weight', 'reps', 'rir'] }, + strength: { label: 'Strength', fields: ['weightLeft', 'weightRight', 'repsLeft', 'repsRight'] }, cardio: { label: 'Cardio', fields: ['distanceM', 'durationSec'] }, } diff --git a/src/collections/set-logs/index.ts b/src/collections/set-logs/index.ts index afa7b90..3b8c955 100644 --- a/src/collections/set-logs/index.ts +++ b/src/collections/set-logs/index.ts @@ -1,12 +1,12 @@ import type { CollectionConfig } from 'payload' import { adminOrOwnByClient, canReadViaShareToken } from '../../access' -import { trackingFields, ALL_METRIC_FIELDS } from '../exercises/types' +import { trackingFields, ALL_METRIC_FIELDS, LEGACY_SET_LOG_FIELDS } from '../exercises/types' 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: { @@ -41,8 +41,9 @@ export const SetLogs: CollectionConfig = { 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 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) data[field] = null } } return data @@ -106,6 +107,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 +139,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/workout-exercise-rows/index.ts b/src/collections/workout-exercise-rows/index.ts index c6eab78..e4585e5 100644 --- a/src/collections/workout-exercise-rows/index.ts +++ b/src/collections/workout-exercise-rows/index.ts @@ -14,7 +14,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: { @@ -75,8 +75,10 @@ export const WorkoutExerciseRows: CollectionConfig = { 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: { width: '25%' } }, + { name: 'repsRight', type: 'text', label: 'Reps right', admin: { width: '25%' } }, + { name: 'kg', type: 'text', label: 'KG', admin: { width: '25%' } }, ], }, { @@ -84,7 +86,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%' } }, ], }, { @@ -138,7 +140,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/components/workout/exercise-card/exercise-card.tsx b/src/components/workout/exercise-card/exercise-card.tsx index bb7bd7c..f319b4c 100644 --- a/src/components/workout/exercise-card/exercise-card.tsx +++ b/src/components/workout/exercise-card/exercise-card.tsx @@ -32,7 +32,11 @@ export function ExerciseCard({ }) { const [open, setOpen] = useState(false) const fields = trackingFields(exercise.trackingType) - const prefillValues: Values = { reps: exercise.prefill.reps ?? '', rir: exercise.prefill.rir ?? '', note: '' } + const prefillValues: Values = { + repsLeft: exercise.prefill.repsLeft ?? '', + repsRight: exercise.prefill.repsRight ?? '', + note: '', + } return (
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/components/workout/series-form/components/metric-field-input/metric-field-input.tsx b/src/components/workout/series-form/components/metric-field-input/metric-field-input.tsx index d1a1bb0..7aaed76 100644 --- a/src/components/workout/series-form/components/metric-field-input/metric-field-input.tsx +++ b/src/components/workout/series-form/components/metric-field-input/metric-field-input.tsx @@ -8,7 +8,6 @@ import { minKey, secKey, unitKey } from '@/lib/metric-keys' import { Field } from '@/components/ui/field' import { Input, Select } from '@/components/ui/input' import type { Values } from '@/types/workout' -import { BodyweightField } from '../bodyweight-field' import { DurationInput } from '../duration-input' export function MetricFieldInput({ @@ -20,8 +19,6 @@ export function MetricFieldInput({ control, setValue, validate, - isBodyweight, - onToggleBodyweight, }: { field: MetricField isFirst: boolean @@ -31,8 +28,6 @@ export function MetricFieldInput({ control: Control setValue: UseFormSetValue validate: () => true | string - isBodyweight: boolean - onToggleBodyweight: () => void }) { const meta = METRIC_FIELDS[field] const firstFieldOptions: RegisterOptions = isFirst ? { validate } : {} @@ -88,20 +83,6 @@ export function MetricFieldInput({ ) } - if (field === 'weight') { - return ( - - ) - } - return ( ({ defaultValues: initial }) const isBodyweight = useWatch({ control, name: BODYWEIGHT_KEY }) === 'true' + const visibleFields = fields.filter((field) => !isBodyweight || !METRIC_FIELDS[field].bodyweightAffected) + const hasBodyweightFields = fields.some((field) => METRIC_FIELDS[field].bodyweightAffected) const toggleBodyweight = useCallback(() => { setValue(BODYWEIGHT_KEY, isBodyweight ? 'false' : 'true') - if (!isBodyweight) setValue('weight', '') + if (!isBodyweight) { + setValue('weightLeft', '') + setValue('weightRight', '') + } }, [isBodyweight, setValue]) const validateAtLeastOne = () => { @@ -56,12 +61,12 @@ export function SeriesForm({ await onSubmit(data) }) - const firstField = fields[0] + const firstField = visibleFields[0] return (
- {fields.map((field, index) => ( + {visibleFields.map((field, index) => ( ))} + {hasBodyweightFields && ( + + )}
diff --git a/src/components/workout/workout-tracker/workout-tracker.tsx b/src/components/workout/workout-tracker/workout-tracker.tsx index 592b1d5..c850462 100644 --- a/src/components/workout/workout-tracker/workout-tracker.tsx +++ b/src/components/workout/workout-tracker/workout-tracker.tsx @@ -74,7 +74,13 @@ 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.exercises.map((exercise) => ( { + 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 + /** 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 @@ -23,10 +31,20 @@ export const isValidValue = (value?: string | null): boolean => { return trimmed !== '' && trimmed.toLowerCase() !== 'x' } +export const formatSideReps = (repsLeft?: string | null, repsRight?: string | null): string | null => { + const left = isValidValue(repsLeft) ? repsLeft!.trim() : null + const right = isValidValue(repsRight) ? repsRight!.trim() : null + + if (left && right) return `${left}+${right}` + return left ?? right +} + export const buildExerciseMeta = ( ex: { rounds?: string | null reps?: string | null + repsLeft?: string | null + repsRight?: string | null durationMin?: number | null durationSec?: number | null rest?: string | null @@ -38,13 +56,15 @@ export const buildExerciseMeta = ( ): 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 sideReps = formatSideReps(ex.repsLeft, ex.repsRight) + if (sideReps) parts.push(`Steps: ${sideReps}`) + else if (isValidValue(ex.reps)) parts.push(`Steps: ${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`) + if (isValidValue(ex.kg)) parts.push(`KG: ${ex.kg}`) return parts } @@ -61,8 +81,8 @@ export const metricBody = (fields: MetricField[], values: Values): Record { const parts: string[] = [] if (set.isBodyweight) parts.push('MC') - else if (set.weight != null) parts.push(`${set.weight} kg`) + 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)) - // 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}`) + 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(' · ') : '—' } 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 protocol = group.protocol as keyof typeof PROTOCOL_LABEL | undefined + return protocol ? (PROTOCOL_LABEL[protocol] ?? protocol) : '' +} + +export const workoutGroupMeta = (group: { + protocol?: unknown + rounds?: unknown + intervalSeconds?: unknown + workSeconds?: unknown + restSeconds?: unknown +}): string[] => { + const protocol = group.protocol as string | undefined 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` : '' + const intervalSeconds = group.intervalSeconds as number | null | undefined + const workSeconds = group.workSeconds as number | null | undefined + const restSeconds = group.restSeconds as number | null | undefined + if (protocol !== 'emom') return [] + + const parts: string[] = [] + if (rounds) parts.push(`Duration: ${rounds} min`) + if (intervalSeconds != null) parts.push(`Interval: ${intervalSeconds} s`) + if (workSeconds != null) parts.push(`Work: ${workSeconds} s`) + if (restSeconds != null) parts.push(`Rest: ${restSeconds} s`) + return parts } export const setLogToFormValues = (set: SetLog, fields: MetricField[]): Values => { diff --git a/src/loaders/load-plans-items.ts b/src/loaders/load-plans-items.ts index 1d6b4d6..f4cef4a 100644 --- a/src/loaders/load-plans-items.ts +++ b/src/loaders/load-plans-items.ts @@ -1,7 +1,7 @@ import { getPayload } from 'payload' import { STATUS_LABEL } from '@/types/constants' -import { buildExerciseMeta, workoutGroupLabel } from '@/lib/metrics' +import { buildExerciseMeta, workoutGroupLabel, workoutGroupMeta } from '@/lib/metrics' import type { TPlanAccordionItem } from '@/types/plan' import type { TBlock, TGroup, TWorkout } from '@/types/workout' @@ -9,7 +9,6 @@ type Payload = Awaited> export type PlanLabels = { seriesPrefix: string - repsPrefix: string durationPrefix: string restPrefix: string } @@ -101,44 +100,52 @@ export async function loadPlansItems( }> 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, - } - }), - }) + const serializeGroup = (group: (typeof groups)[number]): TGroup => { + const groupMeta = workoutGroupMeta(group) + + return { + protocol: (group.protocol as string) ?? 'standard', + label: (group.label as string | null) ?? '', + protocolLabel: 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), + ...groupMeta, + ], + prefill: { + repsLeft: (ex.repsLeft as string | null) ?? null, + repsRight: (ex.repsRight as string | null) ?? null, + }, + setParameters: + (ex.setParameters as + | Array<{ setNumber: number; reps?: string | null; kg?: string | null }> + | null + | undefined) ?? null, + } + }), + } + } return { id: workout.id, diff --git a/src/loaders/share-link-loader.ts b/src/loaders/share-link-loader.ts index ea33da3..2b56acc 100644 --- a/src/loaders/share-link-loader.ts +++ b/src/loaders/share-link-loader.ts @@ -46,7 +46,6 @@ export async function loadShareLink(token: string): Promise { [planId], { seriesPrefix: t('seriesPrefix'), - repsPrefix: t('repsPrefix'), durationPrefix: t('durationPrefix'), restPrefix: t('restPrefix'), }, diff --git a/src/loaders/training-plan-loader.ts b/src/loaders/training-plan-loader.ts index ba1a7e6..c64a53b 100644 --- a/src/loaders/training-plan-loader.ts +++ b/src/loaders/training-plan-loader.ts @@ -36,7 +36,6 @@ export async function loadTrainingPlans(): Promise { planIds, { seriesPrefix: t('seriesPrefix'), - repsPrefix: t('repsPrefix'), durationPrefix: t('durationPrefix'), restPrefix: t('restPrefix'), }, 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/index.ts b/src/migrations/index.ts index e373aed..1fdf19c 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -6,6 +6,8 @@ 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'; export const migrations = [ { @@ -46,6 +48,16 @@ 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' }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index f6407fd..4053a53 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -371,6 +371,8 @@ export interface WorkoutExerciseRow { */ rounds?: string | null; reps?: string | null; + repsLeft?: string | null; + repsRight?: string | null; kg?: string | null; tut?: string | null; rir?: string | null; @@ -471,10 +473,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; @@ -796,6 +802,8 @@ export interface WorkoutExerciseRowsSelect { note?: T; rounds?: T; reps?: T; + repsLeft?: T; + repsRight?: T; kg?: T; tut?: T; rir?: T; @@ -865,10 +873,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/backfill-set-log-sides.ts b/src/scripts/backfill-set-log-sides.ts new file mode 100644 index 0000000..c75918d --- /dev/null +++ b/src/scripts/backfill-set-log-sides.ts @@ -0,0 +1,55 @@ +import 'dotenv/config' +import { getPayload } from 'payload' +import config from '../payload.config.js' + +const pageSize = 100 + +const isMissing = (value: number | string | null | undefined): boolean => value == null + +async function run() { + const payload = await getPayload({ config }) + let page = 1 + let updatedCount = 0 + + while (true) { + const result = await payload.find({ + collection: 'set-logs', + depth: 0, + limit: pageSize, + page, + sort: 'id', + overrideAccess: true, + }) + + for (const setLog of result.docs) { + const hasLegacyValues = !isMissing(setLog.weight) || !isMissing(setLog.reps) + const hasNoSideValues = [setLog.weightLeft, setLog.weightRight, setLog.repsLeft, setLog.repsRight].every(isMissing) + + if (!hasLegacyValues || !hasNoSideValues) continue + + await payload.update({ + collection: 'set-logs', + id: setLog.id, + depth: 0, + overrideAccess: true, + data: { + weightLeft: setLog.weight, + weightRight: setLog.weight, + repsLeft: setLog.reps, + repsRight: setLog.reps, + }, + }) + updatedCount += 1 + } + + if (!result.hasNextPage) break + page += 1 + } + + payload.logger.info(`Backfilled ${updatedCount} set logs.`) +} + +run().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/src/scripts/backfill-workout-exercise-reps-sides.ts b/src/scripts/backfill-workout-exercise-reps-sides.ts new file mode 100644 index 0000000..562294c --- /dev/null +++ b/src/scripts/backfill-workout-exercise-reps-sides.ts @@ -0,0 +1,60 @@ +import 'dotenv/config' +import { getPayload } from 'payload' +import config from '../payload.config.js' + +const pageSize = 100 + +const isMissing = (value: string | null | undefined): boolean => value == null + +async function run() { + const payload = await getPayload({ config }) + let page = 1 + let processedCount = 0 + let updatedCount = 0 + + payload.logger.info('Starting workout exercise reps backfill.') + + while (true) { + const result = await payload.find({ + collection: 'workout-exercise-rows', + depth: 0, + limit: pageSize, + page, + sort: 'id', + overrideAccess: true, + }) + + for (const exerciseRow of result.docs) { + const hasLegacyReps = !isMissing(exerciseRow.reps) + const hasNoSideReps = isMissing(exerciseRow.repsLeft) && isMissing(exerciseRow.repsRight) + + if (!hasLegacyReps || !hasNoSideReps) continue + + await payload.update({ + collection: 'workout-exercise-rows', + id: exerciseRow.id, + depth: 0, + overrideAccess: true, + data: { + repsLeft: exerciseRow.reps, + repsRight: exerciseRow.reps, + }, + }) + updatedCount += 1 + } + + processedCount += result.docs.length + payload.logger.info(`Processed ${processedCount} of ${result.totalDocs} workout exercise rows. Updated ${updatedCount}.`) + + if (!result.hasNextPage) break + page += 1 + } + + payload.logger.info(`Backfilled ${updatedCount} workout exercise rows.`) + process.exit(0) +} + +run().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/src/scripts/normalize-reps-sides.ts b/src/scripts/normalize-reps-sides.ts new file mode 100644 index 0000000..6f45864 --- /dev/null +++ b/src/scripts/normalize-reps-sides.ts @@ -0,0 +1,111 @@ +import 'dotenv/config' +import { getPayload } from 'payload' +import config from '../payload.config.js' + +const pageSize = 100 +const sideRepsPattern = /^\s*(\d+(?:[.,]\d+)?)\s*\+\s*(\d+(?:[.,]\d+)?)(?:\s*pow\.?)?\s*$/i + +const splitSideReps = (repsLeft: string | null | undefined, repsRight: string | null | undefined) => { + if (!repsLeft || repsLeft !== repsRight) return null + + const match = repsLeft.match(sideRepsPattern) + if (!match) return null + + return { repsLeft: match[1], repsRight: match[2] } +} + +async function normalizeSetLogs() { + const payload = await getPayload({ config }) + let page = 1 + let processedCount = 0 + let updatedCount = 0 + + while (true) { + const result = await payload.find({ + collection: 'set-logs', + depth: 0, + limit: pageSize, + page, + sort: 'id', + overrideAccess: true, + }) + + for (const setLog of result.docs) { + const normalizedReps = splitSideReps(setLog.repsLeft, setLog.repsRight) + if (!normalizedReps) continue + + await payload.update({ + collection: 'set-logs', + id: setLog.id, + depth: 0, + overrideAccess: true, + data: normalizedReps, + }) + updatedCount += 1 + } + + processedCount += result.docs.length + payload.logger.info(`Processed ${processedCount} of ${result.totalDocs} set logs. Updated ${updatedCount}.`) + + if (!result.hasNextPage) break + page += 1 + } + + return updatedCount +} + +async function normalizeWorkoutExerciseRows() { + const payload = await getPayload({ config }) + let page = 1 + let processedCount = 0 + let updatedCount = 0 + + while (true) { + const result = await payload.find({ + collection: 'workout-exercise-rows', + depth: 0, + limit: pageSize, + page, + sort: 'id', + overrideAccess: true, + }) + + for (const exerciseRow of result.docs) { + const normalizedReps = splitSideReps(exerciseRow.repsLeft, exerciseRow.repsRight) + if (!normalizedReps) continue + + await payload.update({ + collection: 'workout-exercise-rows', + id: exerciseRow.id, + depth: 0, + overrideAccess: true, + data: normalizedReps, + }) + updatedCount += 1 + } + + processedCount += result.docs.length + payload.logger.info(`Processed ${processedCount} of ${result.totalDocs} workout exercise rows. Updated ${updatedCount}.`) + + if (!result.hasNextPage) break + page += 1 + } + + return updatedCount +} + +async function run() { + const payload = await getPayload({ config }) + payload.logger.info('Starting left and right reps normalization.') + + const normalizedSetLogs = await normalizeSetLogs() + const normalizedExerciseRows = await normalizeWorkoutExerciseRows() + + payload.logger.info(`Normalized ${normalizedSetLogs} set logs and ${normalizedExerciseRows} workout exercise rows.`) + process.exit(0) +} + +run().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/src/scripts/normalize-workout-exercise-kg.ts b/src/scripts/normalize-workout-exercise-kg.ts new file mode 100644 index 0000000..2ad84c6 --- /dev/null +++ b/src/scripts/normalize-workout-exercise-kg.ts @@ -0,0 +1,65 @@ +import 'dotenv/config' +import { getPayload } from 'payload' +import config from '../payload.config.js' + +const pageSize = 100 +const kgAmountPattern = '\\d+(?:[.,]\\d+)?(?:\\s*kg\\.?)?' +const kgOptionPattern = `${kgAmountPattern}(?:\\s*[+-]\\s*${kgAmountPattern})*` +const kgValuePattern = new RegExp(`^\\s*${kgOptionPattern}(?:\\s+lub\\s+${kgOptionPattern})*\\s*$`, 'i') + +const normalizeKgValue = (value: string | null | undefined): string | null => { + if (!value || !kgValuePattern.test(value)) return null + return value + .replace(/\s*kg\.?\s*/gi, '') + .replace(/\s*([+-])\s*/g, (_matchedDelimiter, delimiter) => delimiter) + .replace(/\s*lub\s*/gi, ' lub ') + .trim() +} + +async function run() { + const payload = await getPayload({ config }) + let page = 1 + let processedCount = 0 + let updatedCount = 0 + + payload.logger.info('Starting workout exercise KG normalization.') + + while (true) { + const result = await payload.find({ + collection: 'workout-exercise-rows', + depth: 0, + limit: pageSize, + page, + sort: 'id', + overrideAccess: true, + }) + + for (const exerciseRow of result.docs) { + const normalizedKg = normalizeKgValue(exerciseRow.kg) + if (!normalizedKg || normalizedKg === exerciseRow.kg) continue + + await payload.update({ + collection: 'workout-exercise-rows', + id: exerciseRow.id, + depth: 0, + overrideAccess: true, + data: { kg: normalizedKg }, + }) + updatedCount += 1 + } + + processedCount += result.docs.length + payload.logger.info(`Processed ${processedCount} of ${result.totalDocs} workout exercise rows. Updated ${updatedCount}.`) + + if (!result.hasNextPage) break + page += 1 + } + + payload.logger.info(`Normalized ${updatedCount} workout exercise rows.`) + process.exit(0) +} + +run().catch((error) => { + console.error(error) + process.exit(1) +}) 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/workout.ts b/src/types/workout.ts index ed444ff..2c3c3c9 100644 --- a/src/types/workout.ts +++ b/src/types/workout.ts @@ -9,13 +9,14 @@ export type TExercise = { videoUrl?: string | null rounds?: string | null meta: string[] - prefill: { reps?: string | null; rir?: string | null } + prefill: { repsLeft?: string | null; repsRight?: string | null } setParameters?: Array<{ setNumber: number; reps?: string | null; kg?: string | null }> | null } export type TGroup = { protocol: string label: string + protocolLabel: string exercises: TExercise[] } @@ -32,10 +33,14 @@ export type SetLog = { exerciseRow?: number | null 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 } From 2b1669df7c78644a4d6d0d0e043a3097148d1ff5 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Fri, 24 Jul 2026 10:59:47 +0200 Subject: [PATCH 02/11] feat: improve training structure navigation --- .changeset/admin-training-navigation.md | 5 + src/app/(payload)/admin/importMap.js | 4 + .../training-navigation.tsx | 128 ++++++++++++++++++ .../components/editor/editor.tsx | 3 + .../exercise-form/exercise-form.tsx | 8 +- .../admin/modules/workout-structure/styles.ts | 1 + .../workout-structure/workout-structure.tsx | 8 +- src/collections/microcycles/index.ts | 13 ++ src/collections/plans/index.ts | 13 ++ 9 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 .changeset/admin-training-navigation.md create mode 100644 src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx 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/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 0b66e61..a6c6629 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,3 +1,5 @@ +import { PlanMicrocycles as PlanMicrocycles_97d8c339b0a7b72f91585277739d5b79 } from '@/app/(payload)/admin/modules/training-navigation/training-navigation' +import { MicrocycleWorkouts as MicrocycleWorkouts_97d8c339b0a7b72f91585277739d5b79 } from '@/app/(payload)/admin/modules/training-navigation/training-navigation' 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' @@ -5,6 +7,8 @@ import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } f /** @type import('payload').ImportMap */ export const importMap = { + "@/app/(payload)/admin/modules/training-navigation/training-navigation#PlanMicrocycles": PlanMicrocycles_97d8c339b0a7b72f91585277739d5b79, + "@/app/(payload)/admin/modules/training-navigation/training-navigation#MicrocycleWorkouts": MicrocycleWorkouts_97d8c339b0a7b72f91585277739d5b79, "@/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, diff --git a/src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx b/src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx new file mode 100644 index 0000000..5a4dd59 --- /dev/null +++ b/src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx @@ -0,0 +1,128 @@ +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-structure/components/editor/editor.tsx b/src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx index a2eb4ce..1c850db 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx +++ b/src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx @@ -11,6 +11,7 @@ import { ExerciseForm } from '../exercise-form' import { useWorkoutMutations } from './hooks/use-workout-mutations' type Props = { + header?: React.ReactNode sections: Section[] initialGroups: Group[] initialExerciseRows: ExerciseRow[] @@ -19,6 +20,7 @@ type Props = { } export function WorkoutStructureEditor({ + header, sections, initialGroups, initialExerciseRows, @@ -48,6 +50,7 @@ export function WorkoutStructureEditor({ return (
+ {header &&
{header}
} {exerciseRowsWithLogs.size === 0 && (
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 index 8eaf839..2a5f4d4 100644 --- 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 @@ -1,7 +1,7 @@ 'use client' import { Button, Form, RelationshipField, TextField, toast, useFormProcessing } from '@payloadcms/ui' -import type { FormState, SingleRelationshipFieldClient, ValueWithRelation } from 'payload' +import type { FormState, SingleRelationshipFieldClient } from 'payload' import type { ExerciseRow } from '../../types' import { s } from '../../styles' import { sdk } from '@/lib/sdk' @@ -86,7 +86,7 @@ export function ExerciseForm({ groupId, nextOrder, initial, onSaved, onCancel }: const initialState: FormState = { numer: { value: initial?.numer ?? '' }, rounds: { value: initial?.rounds ?? '' }, - exercise: { value: initial?.exercise ? { value: initial.exercise.id, relationTo: 'exercises' } : null }, + exercise: { value: initial?.exercise?.id ?? null }, note: { value: initial?.note ?? '' }, repsLeft: { value: initial?.repsLeft ?? '' }, repsRight: { value: initial?.repsRight ?? '' }, @@ -97,11 +97,11 @@ export function ExerciseForm({ groupId, nextOrder, initial, onSaved, onCancel }: } const handleSubmit = async (_: FormState, data: Record) => { - const exerciseRaw = data.exercise as ValueWithRelation | null + const exerciseId = data.exercise as number | string | null const body = { numer: (data.numer as string) || null, rounds: (data.rounds as string) || null, - exercise: exerciseRaw ? Number(exerciseRaw.value) : null, + exercise: exerciseId ? Number(exerciseId) : null, note: (data.note as string) || null, repsLeft: (data.repsLeft as string) || null, repsRight: (data.repsRight as string) || null, diff --git a/src/app/(payload)/admin/modules/workout-structure/styles.ts b/src/app/(payload)/admin/modules/workout-structure/styles.ts index 576182b..9291674 100644 --- a/src/app/(payload)/admin/modules/workout-structure/styles.ts +++ b/src/app/(payload)/admin/modules/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, diff --git a/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx b/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx index 1562fca..80bd38a 100644 --- a/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx +++ b/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { WorkoutStructureBreadcrumb } from '../training-navigation/training-navigation' import { loadWorkoutStructure } from './loader' import { WorkoutStructureEditor } from './components/editor' @@ -21,5 +22,10 @@ export async function WorkoutStructureView({ } const data = await loadWorkoutStructure(payload, docId) - return + return ( + } + /> + ) } diff --git a/src/collections/microcycles/index.ts b/src/collections/microcycles/index.ts index 77eea2a..7849359 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: '@/app/(payload)/admin/modules/training-navigation/training-navigation', + exportName: 'MicrocycleWorkouts', + }, + }, + }, + }, ], } diff --git a/src/collections/plans/index.ts b/src/collections/plans/index.ts index 05490e0..7b47ddc 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: '@/app/(payload)/admin/modules/training-navigation/training-navigation', + exportName: 'PlanMicrocycles', + }, + }, + }, + }, ], } From 9cffd93317de9b745146161f4e3480fc31c349fd Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Fri, 24 Jul 2026 11:01:33 +0200 Subject: [PATCH 03/11] feat: update label for restSeconds field to improve clarity --- .../workout-structure/components/group-form/group-form.tsx | 2 +- src/collections/workout-groups/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx b/src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx index 05e2e73..dd44ceb 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx +++ b/src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx @@ -88,7 +88,7 @@ function FormFields({ onCancel }: { onCancel: () => void }) {
- +
)} 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, From 2d3214f0b53219b412838686a6d74bd78a77bc23 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Fri, 24 Jul 2026 13:37:11 +0200 Subject: [PATCH 04/11] feat: support duration exercise targets --- .changeset/exercise-target-type.md | 5 + .../exercise-form/exercise-form.tsx | 94 ++++++++++++++----- .../admin/modules/workout-structure/loader.ts | 1 + .../admin/modules/workout-structure/types.ts | 1 + .../modules/workout-structure/utils/format.ts | 12 ++- .../workout-structure/utils/validate.ts | 13 ++- src/collections/set-logs/index.ts | 20 +++- .../workout-exercise-rows/index.ts | 40 +++++++- .../workout/exercise-card/exercise-card.tsx | 5 +- .../metric-field-input/metric-field-input.tsx | 44 +++++---- .../workout/series-form/series-form.tsx | 2 - src/lib/metrics.ts | 14 ++- src/loaders/load-plans-items.ts | 1 + src/migrations/20260724_090318.ts | 13 +++ src/migrations/index.ts | 8 +- src/payload-types.ts | 2 + src/types/workout.ts | 1 + 17 files changed, 215 insertions(+), 61 deletions(-) create mode 100644 .changeset/exercise-target-type.md create mode 100644 src/migrations/20260724_090318.ts 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/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 index 2a5f4d4..384f186 100644 --- 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 @@ -1,12 +1,12 @@ 'use client' -import { Button, Form, RelationshipField, TextField, toast, useFormProcessing } from '@payloadcms/ui' -import type { FormState, SingleRelationshipFieldClient } from 'payload' +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 { textField } from '@/app/(payload)/admin/utils/fields' -import { validateKgOrRepsSides, validateRepsSidesOrKg, validateRounds } from '../../utils' +import { validateDuration, validateKgOrRepsSides, validateRepsSidesOrKg, validateRounds } from '../../utils' type Props = { groupId: number @@ -24,8 +24,23 @@ const exerciseRelField: SingleRelationshipFieldClient = { label: 'Exercise (catalog)', } as SingleRelationshipFieldClient +const targetTypeField: SelectFieldClient = { + name: 'targetType', + type: 'select', + label: 'Target type', + options: [ + { label: 'Repetitions', value: 'repetitions' }, + { label: 'Duration', value: 'duration' }, + ], +} as SelectFieldClient + +type FormFieldsState = Record + function FormFields({ isEdit, onCancel }: { isEdit: boolean; onCancel: () => void }) { const processing = useFormProcessing() + const targetType = useFormFields( + ([fields]) => ((fields as unknown as FormFieldsState).targetType?.value as string) ?? 'repetitions', + ) return ( <> @@ -48,25 +63,32 @@ function FormFields({ isEdit, onCancel }: { isEdit: boolean; onCancel: () => voi
-
- -
-
- -
-
- +
+
-
- -
-
- +
+ + {targetType === 'duration' ? ( +
+
+ +
+
+ +
+
-
- + ) : ( +
+
+ +
+
+ +
+
-
+ )}
)} + {group.meta.length > 0 && ( +
{group.meta.join(' · ')}
+ )} {group.exercises.map((exercise) => ( { const protocol = group.protocol as string | undefined const rounds = group.rounds as string | null | undefined const intervalSeconds = group.intervalSeconds as number | null | undefined const workSeconds = group.workSeconds as number | null | undefined const restSeconds = group.restSeconds as number | null | undefined - if (protocol !== 'emom') return [] + const restBetweenRounds = group.restBetweenRounds as string | null | undefined + const rest = isValidValue(restBetweenRounds) + ? /^\d+(?:\.\d+)?$/.test(restBetweenRounds!.trim()) + ? `Rest: ${restBetweenRounds!.trim()} s` + : `Rest: ${restBetweenRounds!.trim()}` + : null const parts: string[] = [] - if (rounds) parts.push(`Duration: ${rounds} min`) - if (intervalSeconds != null) parts.push(`Interval: ${intervalSeconds} s`) - if (workSeconds != null) parts.push(`Work: ${workSeconds} s`) - if (restSeconds != null) parts.push(`Rest: ${restSeconds} s`) + if (protocol === 'standard') { + if (rounds) parts.push(`Sets: ${rounds}`) + if (rest) parts.push(rest) + return parts + } + + if (protocol === 'emom') { + if (rounds) parts.push(`Duration: ${rounds} min`) + if (intervalSeconds != null) parts.push(`Interval: ${intervalSeconds} s`) + return parts + } + + if (protocol === 'tabata') { + if (workSeconds != null) parts.push(`Work: ${workSeconds} s`) + if (restSeconds != null) parts.push(`Rest: ${restSeconds} s`) + } return parts } diff --git a/src/loaders/load-plans-items.ts b/src/loaders/load-plans-items.ts index 7661be1..662228b 100644 --- a/src/loaders/load-plans-items.ts +++ b/src/loaders/load-plans-items.ts @@ -107,6 +107,7 @@ export async function loadPlansItems( protocol: (group.protocol as string) ?? 'standard', label: (group.label as string | null) ?? '', protocolLabel: workoutGroupLabel(group), + meta: groupMeta, exercises: rowsByGroup(group.id).map((ex) => { const cat = ex.exercise && typeof ex.exercise === 'object' @@ -130,10 +131,7 @@ export async function loadPlansItems( targetType: (ex.targetType as 'repetitions' | 'duration' | null) ?? 'repetitions', videoUrl: (cat?.videoUrl as string | null | undefined) ?? null, rounds: (ex.rounds as string | null) ?? null, - meta: [ - ...buildExerciseMeta(ex as Parameters[0], labels), - ...groupMeta, - ], + meta: buildExerciseMeta(ex as Parameters[0], labels), prefill: { repsLeft: (ex.repsLeft as string | null) ?? null, repsRight: (ex.repsRight as string | null) ?? null, diff --git a/src/types/workout.ts b/src/types/workout.ts index dc1b512..cfca3ff 100644 --- a/src/types/workout.ts +++ b/src/types/workout.ts @@ -18,6 +18,7 @@ export type TGroup = { protocol: string label: string protocolLabel: string + meta: string[] exercises: TExercise[] } From 3e802cf368fe34191fa70e97331561a0fe789918 Mon Sep 17 00:00:00 2001 From: Krzysztof Polak Date: Sat, 25 Jul 2026 13:52:29 +0200 Subject: [PATCH 06/11] refactor: organize training code into modules --- .ai/specs/2026-06-16-share-links.md | 21 +- .ai/specs/2026-06-20-exercise-client-note.md | 12 +- AGENTS.md | 51 ++++- README.md | 80 +++++-- src/app/(payload)/admin/importMap.js | 20 +- .../modules/workout-structure/constants.ts | 15 -- .../admin/modules/workout-structure/loader.ts | 98 --------- src/app/[locale]/(frontend)/page.tsx | 4 +- .../(frontend)/share/[token]/page.tsx | 4 +- src/collections/exercises/index.ts | 4 +- src/collections/exercises/types.ts | 49 ----- src/collections/microcycles/index.ts | 2 +- src/collections/plans/index.ts | 2 +- src/collections/set-logs/index.ts | 5 +- src/collections/share-links/index.ts | 2 +- src/collections/workouts/index.ts | 4 +- src/lib/metric-keys.ts | 12 -- src/lib/metrics.ts | 199 ------------------ .../sharing/admin}/share-link-url/index.ts | 0 .../admin}/share-link-url/share-link-url.tsx | 0 src/modules/sharing/index.ts | 1 + src/modules/sharing/server/index.ts | 4 + .../sharing/server/load-share-link.ts} | 20 +- src/modules/sharing/types.ts | 10 + .../training-navigation.tsx | 2 + .../workout-logs-notice.tsx | 2 + .../components/editor/editor.tsx | 24 ++- .../editor/hooks/use-workout-mutations.ts | 10 +- .../components/editor/index.ts | 0 .../exercise-form/exercise-form.tsx | 2 +- .../components/exercise-form/index.ts | 0 .../components/group-form/group-form.tsx | 7 +- .../components/group-form/index.ts | 0 .../admin/workout-structure/loader.ts | 122 +++++++++++ .../admin}/workout-structure/styles.ts | 10 - .../admin}/workout-structure/types.ts | 4 +- .../admin/workout-structure}/utils/fields.ts | 0 .../admin}/workout-structure/utils/format.ts | 5 +- .../admin}/workout-structure/utils/index.ts | 0 .../workout-structure/utils/validate.ts | 0 .../workout-structure/workout-structure.tsx | 2 + .../add-set-actions/add-set-actions.tsx | 0 .../components/add-set-actions/index.ts | 0 .../exercise-header/exercise-header.tsx | 0 .../components/exercise-header/index.ts | 0 .../exercise-note/exercise-note.tsx | 2 +- .../components/exercise-note/index.ts | 0 .../components/meta-line/index.ts | 0 .../components/meta-line/meta-line.tsx | 0 .../components/series-list/index.ts | 0 .../components/series-list/series-list.tsx | 8 +- .../exercise-card/exercise-card.tsx | 18 +- .../components}/exercise-card/index.ts | 0 .../training/components}/note-field/index.ts | 0 .../components}/note-field/note-field.tsx | 2 +- .../duration-input/duration-input.tsx | 0 .../components/duration-input/index.ts | 0 .../components/metric-field-input/index.ts | 0 .../metric-field-input/metric-field-input.tsx | 26 ++- .../training/components}/series-form/index.ts | 0 .../components}/series-form/series-form.tsx | 22 +- .../training/components}/series-row/index.ts | 0 .../components}/series-row/series-row.tsx | 18 +- .../components}/session-times/index.ts | 0 .../session-times/session-times.tsx | 2 +- .../active-context-banner.tsx | 0 .../components/active-context-banner/index.ts | 0 .../components/workout-pickers/index.ts | 0 .../workout-pickers/workout-pickers.tsx | 3 +- .../hooks/use-workout-selection.ts | 15 +- .../components}/workout-plans/index.ts | 0 .../workout-plans/workout-plans.tsx | 4 +- .../hooks/use-workout-session.ts | 23 +- .../components}/workout-tracker/index.ts | 0 .../workout-tracker/workout-tracker.tsx | 8 +- src/modules/training/exercises/constants.ts | 45 ++++ src/modules/training/exercises/formatters.ts | 15 ++ src/modules/training/exercises/index.ts | 10 + src/modules/training/exercises/tracking.ts | 7 + src/modules/training/exercises/types.ts | 31 +++ src/modules/training/logs/constants.ts | 3 + src/modules/training/logs/formatters.ts | 23 ++ src/modules/training/logs/index.ts | 10 + src/modules/training/logs/metric-form.ts | 121 +++++++++++ src/modules/training/logs/types.ts | 42 ++++ src/modules/training/plans/constants.ts | 20 ++ src/modules/training/plans/formatters.ts | 71 +++++++ src/modules/training/plans/index.ts | 17 ++ src/modules/training/plans/server/index.ts | 4 + .../training/plans/server/load-plan-items.ts} | 93 ++++---- .../plans/server/load-training-plans.ts} | 15 +- src/modules/training/plans/types.ts | 103 +++++++++ src/types/constants.ts | 17 -- src/types/index.ts | 3 - src/types/plan.ts | 16 -- src/types/workout.ts | 50 ----- 96 files changed, 1007 insertions(+), 669 deletions(-) delete mode 100644 src/app/(payload)/admin/modules/workout-structure/constants.ts delete mode 100644 src/app/(payload)/admin/modules/workout-structure/loader.ts delete mode 100644 src/collections/exercises/types.ts delete mode 100644 src/lib/metric-keys.ts delete mode 100644 src/lib/metrics.ts rename src/{app/(payload)/admin/modules => modules/sharing/admin}/share-link-url/index.ts (100%) rename src/{app/(payload)/admin/modules => modules/sharing/admin}/share-link-url/share-link-url.tsx (100%) create mode 100644 src/modules/sharing/index.ts create mode 100644 src/modules/sharing/server/index.ts rename src/{loaders/share-link-loader.ts => modules/sharing/server/load-share-link.ts} (74%) create mode 100644 src/modules/sharing/types.ts rename src/{app/(payload)/admin/modules => modules/training/admin}/training-navigation/training-navigation.tsx (99%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-logs-notice/workout-logs-notice.tsx (98%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/editor/editor.tsx (91%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/editor/hooks/use-workout-mutations.ts (79%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/editor/index.ts (100%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/exercise-form/exercise-form.tsx (99%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/exercise-form/index.ts (100%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/group-form/group-form.tsx (97%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/components/group-form/index.ts (100%) create mode 100644 src/modules/training/admin/workout-structure/loader.ts rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/styles.ts (90%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/types.ts (92%) rename src/{app/(payload)/admin => modules/training/admin/workout-structure}/utils/fields.ts (100%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/utils/format.ts (88%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/utils/index.ts (100%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/utils/validate.ts (100%) rename src/{app/(payload)/admin/modules => modules/training/admin}/workout-structure/workout-structure.tsx (97%) rename src/{components/workout => modules/training/components}/exercise-card/components/add-set-actions/add-set-actions.tsx (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/add-set-actions/index.ts (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/exercise-header/exercise-header.tsx (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/exercise-header/index.ts (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/exercise-note/exercise-note.tsx (90%) rename src/{components/workout => modules/training/components}/exercise-card/components/exercise-note/index.ts (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/meta-line/index.ts (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/meta-line/meta-line.tsx (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/series-list/index.ts (100%) rename src/{components/workout => modules/training/components}/exercise-card/components/series-list/series-list.tsx (70%) rename src/{components/workout => modules/training/components}/exercise-card/exercise-card.tsx (76%) rename src/{components/workout => modules/training/components}/exercise-card/index.ts (100%) rename src/{components/workout => modules/training/components}/note-field/index.ts (100%) rename src/{components/workout => modules/training/components}/note-field/note-field.tsx (97%) rename src/{components/workout => modules/training/components}/series-form/components/duration-input/duration-input.tsx (100%) rename src/{components/workout => modules/training/components}/series-form/components/duration-input/index.ts (100%) rename src/{components/workout => modules/training/components}/series-form/components/metric-field-input/index.ts (100%) rename src/{components/workout => modules/training/components}/series-form/components/metric-field-input/metric-field-input.tsx (77%) rename src/{components/workout => modules/training/components}/series-form/index.ts (100%) rename src/{components/workout => modules/training/components}/series-form/series-form.tsx (81%) rename src/{components/workout => modules/training/components}/series-row/index.ts (100%) rename src/{components/workout => modules/training/components}/series-row/series-row.tsx (76%) rename src/{components/workout => modules/training/components}/session-times/index.ts (100%) rename src/{components/workout => modules/training/components}/session-times/session-times.tsx (98%) rename src/{components/workout => modules/training/components}/workout-plans/components/active-context-banner/active-context-banner.tsx (100%) rename src/{components/workout => modules/training/components}/workout-plans/components/active-context-banner/index.ts (100%) rename src/{components/workout => modules/training/components}/workout-plans/components/workout-pickers/index.ts (100%) rename src/{components/workout => modules/training/components}/workout-plans/components/workout-pickers/workout-pickers.tsx (92%) rename src/{components/workout => modules/training/components}/workout-plans/hooks/use-workout-selection.ts (91%) rename src/{components/workout => modules/training/components}/workout-plans/index.ts (100%) rename src/{components/workout => modules/training/components}/workout-plans/workout-plans.tsx (95%) rename src/{components/workout => modules/training/components}/workout-tracker/hooks/use-workout-session.ts (93%) rename src/{components/workout => modules/training/components}/workout-tracker/index.ts (100%) rename src/{components/workout => modules/training/components}/workout-tracker/workout-tracker.tsx (93%) create mode 100644 src/modules/training/exercises/constants.ts create mode 100644 src/modules/training/exercises/formatters.ts create mode 100644 src/modules/training/exercises/index.ts create mode 100644 src/modules/training/exercises/tracking.ts create mode 100644 src/modules/training/exercises/types.ts create mode 100644 src/modules/training/logs/constants.ts create mode 100644 src/modules/training/logs/formatters.ts create mode 100644 src/modules/training/logs/index.ts create mode 100644 src/modules/training/logs/metric-form.ts create mode 100644 src/modules/training/logs/types.ts create mode 100644 src/modules/training/plans/constants.ts create mode 100644 src/modules/training/plans/formatters.ts create mode 100644 src/modules/training/plans/index.ts create mode 100644 src/modules/training/plans/server/index.ts rename src/{loaders/load-plans-items.ts => modules/training/plans/server/load-plan-items.ts} (65%) rename src/{loaders/training-plan-loader.ts => modules/training/plans/server/load-training-plans.ts} (68%) create mode 100644 src/modules/training/plans/types.ts delete mode 100644 src/types/constants.ts delete mode 100644 src/types/index.ts delete mode 100644 src/types/plan.ts delete mode 100644 src/types/workout.ts 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/AGENTS.md b/AGENTS.md index d1c04a9..97ea0fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,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` | @@ -98,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 @@ -119,6 +122,40 @@ 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/`. + +--- + ## Key Commands ```bash diff --git a/README.md b/README.md index f4eee62..dfd22cd 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,16 +190,23 @@ 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 +``` + +Apply existing migrations: + +```bash +yarn payload migrate ``` -Run migrations: +Create a new migration only after changing the Payload schema: ```bash -yarn payload migrate:create # after a schema change +yarn payload migrate:create yarn payload migrate ``` -Run both migration commands manually. Agents do not generate or apply database migrations. +Maintainers run migration commands manually. Agents do not generate or apply database migrations. ### Run @@ -242,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 +├── modules/ # Vertical business modules +│ ├── training/ +│ │ ├── exercises/ # Exercise types, constants, tracking rules, formatters +│ │ ├── plans/ # Plan types, constants, formatters, server queries +│ │ ├── 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/ # One-off CLI scripts (seed, import-plan…) -├── types/ # Shared TypeScript types -├── middleware.ts +├── 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 @@ -259,11 +274,26 @@ 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. | @@ -273,17 +303,22 @@ src/ | `yarn normalize:reps-sides` | Split legacy left and right rep values. Run manually. | | `yarn normalize:workout-exercise-kg` | Normalize legacy exercise-row KG values. Run manually. | | `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: @@ -293,12 +328,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). @@ -309,6 +355,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/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index a6c6629..199db91 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,16 +1,16 @@ -import { PlanMicrocycles as PlanMicrocycles_97d8c339b0a7b72f91585277739d5b79 } from '@/app/(payload)/admin/modules/training-navigation/training-navigation' -import { MicrocycleWorkouts as MicrocycleWorkouts_97d8c339b0a7b72f91585277739d5b79 } from '@/app/(payload)/admin/modules/training-navigation/training-navigation' -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/training-navigation/training-navigation#PlanMicrocycles": PlanMicrocycles_97d8c339b0a7b72f91585277739d5b79, - "@/app/(payload)/admin/modules/training-navigation/training-navigation#MicrocycleWorkouts": MicrocycleWorkouts_97d8c339b0a7b72f91585277739d5b79, - "@/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/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 1a8efed..0000000 --- a/src/app/(payload)/admin/modules/workout-structure/loader.ts +++ /dev/null @@ -1,98 +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, - targetType: (r as { targetType?: 'repetitions' | 'duration' | null }).targetType ?? 'repetitions', - reps: r.reps ?? null, - repsLeft: r.repsLeft ?? null, - repsRight: r.repsRight ?? 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/[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 e3bec11..0000000 --- a/src/collections/exercises/types.ts +++ /dev/null @@ -1,49 +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 = 'weightLeft' | 'weightRight' | 'repsLeft' | 'repsRight' | '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[] } - bodyweightAffected?: boolean -} - -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 LEGACY_SET_LOG_FIELDS = ['weight', 'reps', 'rir'] as const - -export const TRACKING: Record = { - strength: { label: 'Strength', fields: ['weightLeft', 'weightRight', 'repsLeft', 'repsRight'] }, - 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 7849359..cb48224 100644 --- a/src/collections/microcycles/index.ts +++ b/src/collections/microcycles/index.ts @@ -48,7 +48,7 @@ export const Microcycles: CollectionConfig = { admin: { components: { Field: { - path: '@/app/(payload)/admin/modules/training-navigation/training-navigation', + 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 7b47ddc..bbd1cba 100644 --- a/src/collections/plans/index.ts +++ b/src/collections/plans/index.ts @@ -77,7 +77,7 @@ export const Plans: CollectionConfig = { admin: { components: { Field: { - path: '@/app/(payload)/admin/modules/training-navigation/training-navigation', + 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 7617f0a..b13796e 100644 --- a/src/collections/set-logs/index.ts +++ b/src/collections/set-logs/index.ts @@ -1,6 +1,7 @@ import type { CollectionConfig } from 'payload' import { adminOrOwnByClient, canReadViaShareToken } from '../../access' -import { trackingFields, ALL_METRIC_FIELDS, LEGACY_SET_LOG_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', @@ -54,7 +55,7 @@ export const SetLogs: CollectionConfig = { id: data.exercise, depth: 0, }) - const allowed = trackingFields(ex?.trackingType) + 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 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/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/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 79ccc5d..0000000 --- a/src/lib/metrics.ts +++ /dev/null @@ -1,199 +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 - durationPrefix: string - restPrefix: string -} - -type UnitMeta = { options: { value: string; factor: number }[]; default: string } - -const numericOrNull = (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 - -/** 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 formatSideReps = (repsLeft?: string | null, repsRight?: string | null): string | null => { - const left = isValidValue(repsLeft) ? repsLeft!.trim() : null - const right = isValidValue(repsRight) ? repsRight!.trim() : null - - if (left && right) return `${left}+${right}` - return left ?? right -} - -export const buildExerciseMeta = ( - ex: { - rounds?: string | null - reps?: string | null - repsLeft?: string | null - repsRight?: string | null - targetType?: 'repetitions' | 'duration' | 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 (ex.targetType === 'duration') { - const duration = formatMinSec(ex.durationMin, ex.durationSec) - if (duration) parts.push(`${labels.durationPrefix}: ${duration}`) - } else { - const sideReps = formatSideReps(ex.repsLeft, ex.repsRight) - if (sideReps) parts.push(`Steps: ${sideReps}`) - else if (isValidValue(ex.reps)) parts.push(`Steps: ${ex.reps}`) - } - 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(`KG: ${ex.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 (isBodyweight && meta.bodyweightAffected) { - body[field] = 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 ? numericOrNull(raw) : textOrNull(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.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(' · ') : '—' -} - -export const workoutGroupLabel = (group: { - protocol?: unknown -}): string => { - const protocol = group.protocol as keyof typeof PROTOCOL_LABEL | undefined - return protocol ? (PROTOCOL_LABEL[protocol] ?? protocol) : '' -} - -export const workoutGroupMeta = (group: { - protocol?: unknown - rounds?: unknown - intervalSeconds?: unknown - workSeconds?: unknown - restSeconds?: unknown - restBetweenRounds?: unknown -}): string[] => { - const protocol = group.protocol as string | undefined - const rounds = group.rounds as string | null | undefined - const intervalSeconds = group.intervalSeconds as number | null | undefined - const workSeconds = group.workSeconds as number | null | undefined - const restSeconds = group.restSeconds as number | null | undefined - const restBetweenRounds = group.restBetweenRounds as string | null | undefined - const rest = isValidValue(restBetweenRounds) - ? /^\d+(?:\.\d+)?$/.test(restBetweenRounds!.trim()) - ? `Rest: ${restBetweenRounds!.trim()} s` - : `Rest: ${restBetweenRounds!.trim()}` - : null - - const parts: string[] = [] - if (protocol === 'standard') { - if (rounds) parts.push(`Sets: ${rounds}`) - if (rest) parts.push(rest) - return parts - } - - if (protocol === 'emom') { - if (rounds) parts.push(`Duration: ${rounds} min`) - if (intervalSeconds != null) parts.push(`Interval: ${intervalSeconds} s`) - return parts - } - - if (protocol === 'tabata') { - if (workSeconds != null) parts.push(`Work: ${workSeconds} s`) - if (restSeconds != null) parts.push(`Rest: ${restSeconds} s`) - } - return parts -} - -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/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..1ed1965 --- /dev/null +++ b/src/modules/sharing/index.ts @@ -0,0 +1 @@ +export type { ShareLinkData } from './types' diff --git a/src/modules/sharing/server/index.ts b/src/modules/sharing/server/index.ts new file mode 100644 index 0000000..7bd83bd --- /dev/null +++ b/src/modules/sharing/server/index.ts @@ -0,0 +1,4 @@ +import 'server-only' + +export { loadShareLink } from './load-share-link' +export type { ShareLinkData } from '../types' diff --git a/src/loaders/share-link-loader.ts b/src/modules/sharing/server/load-share-link.ts similarity index 74% rename from src/loaders/share-link-loader.ts rename to src/modules/sharing/server/load-share-link.ts index 2b56acc..73514ac 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 { TPlanAccordionItem } from '@/modules/training/plans' +import { loadPlanItems } from '@/modules/training/plans/server' +import type { ShareLinkData } 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({ @@ -41,7 +35,7 @@ export async function loadShareLink(token: string): Promise { let planData: TPlanAccordionItem[] | undefined if (permissions.includes('plan')) { - planData = await loadPlansItems( + planData = await loadPlanItems( payload, [planId], { diff --git a/src/modules/sharing/types.ts b/src/modules/sharing/types.ts new file mode 100644 index 0000000..f207797 --- /dev/null +++ b/src/modules/sharing/types.ts @@ -0,0 +1,10 @@ +import type { TPlanAccordionItem } from '@/modules/training/plans' + +export type ShareLinkData = { + meta: { + planTitle: string + permissions: ('plan' | 'results')[] + expiresAt: string + } + plan?: TPlanAccordionItem[] +} | null diff --git a/src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx b/src/modules/training/admin/training-navigation/training-navigation.tsx similarity index 99% rename from src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx rename to src/modules/training/admin/training-navigation/training-navigation.tsx index 5a4dd59..5ad564b 100644 --- a/src/app/(payload)/admin/modules/training-navigation/training-navigation.tsx +++ b/src/modules/training/admin/training-navigation/training-navigation.tsx @@ -1,3 +1,5 @@ +import 'server-only' + import type { Payload } from 'payload' import React from 'react' 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 91% 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 1c850db..3b6230f 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx +++ b/src/modules/training/admin/workout-structure/components/editor/editor.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { Button } from '@payloadcms/ui' import type { ExerciseRow, Group, Section } from '../../types' -import { PROTOCOL_LABEL } from '../../constants' +import { PROTOCOL_LABEL } from '@/modules/training/plans' import { exerciseLabel, exerciseMeta, groupLabel } from '../../utils' import { s } from '../../styles' import { GroupForm } from '../group-form' @@ -43,10 +43,14 @@ export function WorkoutStructureEditor({ sections.length > 0 ? sections : [{ id: undefined, title: null, subtitle: null }] const groupsForSection = (sectionId: string | undefined) => - groups.filter((g) => g.sectionRowId === (sectionId ?? '')) + 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 (
@@ -85,7 +89,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)} @@ -138,7 +146,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 79% 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..0bd47ca 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 @@ -16,8 +16,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 +32,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/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx b/src/modules/training/admin/workout-structure/components/exercise-form/exercise-form.tsx similarity index 99% rename from src/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx rename to src/modules/training/admin/workout-structure/components/exercise-form/exercise-form.tsx index 384f186..19eebe5 100644 --- a/src/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx +++ b/src/modules/training/admin/workout-structure/components/exercise-form/exercise-form.tsx @@ -5,7 +5,7 @@ import type { FormState, SelectFieldClient, SingleRelationshipFieldClient } from import type { ExerciseRow } from '../../types' import { s } from '../../styles' import { sdk } from '@/lib/sdk' -import { textField } from '@/app/(payload)/admin/utils/fields' +import { textField } from '../../utils/fields' import { validateDuration, validateKgOrRepsSides, validateRepsSidesOrKg, validateRounds } from '../../utils' type Props = { 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 97% 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 dd44ceb..473480b 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 @@ -3,11 +3,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 } from '@/modules/training/plans' + +import { textField } from '../../utils/fields' import { validateDurationMinutes, validateIntervalSeconds, @@ -20,7 +21,7 @@ const protocolField: SelectFieldClient = { name: 'protocol', type: 'select', label: 'Protocol', - options: PROTOCOLS, + options: PROTOCOL_OPTIONS, } as SelectFieldClient const bundleField: CheckboxFieldClient = { 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..49869db --- /dev/null +++ b/src/modules/training/admin/workout-structure/loader.ts @@ -0,0 +1,122 @@ +import 'server-only' + +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((group: 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: RawExerciseRow) => 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: { group: number | { id: number } }) => + typeof roundLog.group === 'object' ? roundLog.group.id : roundLog.group + ) as number[], + ), + ] + + const exerciseRowIdsWithLogs: number[] = [ + ...new Set( + setLogsResult.docs.map((setLog: { exerciseRow: number | { id: number } }) => + typeof setLog.exerciseRow === 'object' ? setLog.exerciseRow.id : setLog.exerciseRow + ) as number[], + ), + ] + + const sections: Section[] = (workout.sections ?? []) as Section[] + + const initialGroups: Group[] = groupsResult.docs.map((group: Group) => ({ + id: group.id, + sectionRowId: group.sectionRowId ?? null, + order: group.order ?? 0, + label: group.label ?? null, + bundleWithPrevious: group.bundleWithPrevious ?? false, + protocol: group.protocol ?? 'standard', + rounds: group.rounds ?? null, + durationMinutes: group.durationMinutes ?? null, + intervalSeconds: group.intervalSeconds ?? null, + workSeconds: group.workSeconds ?? null, + restSeconds: group.restSeconds ?? null, + restBetweenRounds: group.restBetweenRounds ?? null, + })) + + const initialExerciseRows: ExerciseRow[] = exerciseRowsResult.docs.map( + (exerciseRow: RawExerciseRow) => ({ + id: exerciseRow.id, + group: + typeof exerciseRow.group === 'object' && exerciseRow.group !== null + ? exerciseRow.group.id + : (exerciseRow.group ?? null), + order: exerciseRow.order ?? 0, + numer: exerciseRow.numer ?? null, + rounds: exerciseRow.rounds ?? null, + exercise: + exerciseRow.exercise && typeof exerciseRow.exercise === 'object' + ? { + id: (exerciseRow.exercise as { id: number }).id, + name: (exerciseRow.exercise as { name?: string | null }).name ?? null, + } + : null, + note: exerciseRow.note ?? null, + targetType: + (exerciseRow as { targetType?: 'repetitions' | 'duration' | null }).targetType ?? + 'repetitions', + reps: exerciseRow.reps ?? null, + repsLeft: exerciseRow.repsLeft ?? null, + repsRight: exerciseRow.repsRight ?? null, + kg: exerciseRow.kg ?? null, + tut: exerciseRow.tut ?? null, + rir: exerciseRow.rir ?? null, + rest: exerciseRow.rest ?? null, + durationMin: exerciseRow.durationMin ?? null, + durationSec: exerciseRow.durationSec ?? null, + }), + ) + + return { sections, initialGroups, 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 90% rename from src/app/(payload)/admin/modules/workout-structure/styles.ts rename to src/modules/training/admin/workout-structure/styles.ts index 9291674..244a805 100644 --- a/src/app/(payload)/admin/modules/workout-structure/styles.ts +++ b/src/modules/training/admin/workout-structure/styles.ts @@ -59,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/app/(payload)/admin/modules/workout-structure/types.ts b/src/modules/training/admin/workout-structure/types.ts similarity index 92% rename from src/app/(payload)/admin/modules/workout-structure/types.ts rename to src/modules/training/admin/workout-structure/types.ts index 6c391a7..557b204 100644 --- a/src/app/(payload)/admin/modules/workout-structure/types.ts +++ b/src/modules/training/admin/workout-structure/types.ts @@ -1,3 +1,5 @@ +import type { WorkoutProtocol } from '@/modules/training/plans' + export type Section = { id?: string; title?: string | null; subtitle?: string | null } export type Group = { @@ -6,7 +8,7 @@ export type Group = { order?: number | null label?: string | null bundleWithPrevious?: boolean | null - protocol?: string | null + protocol?: WorkoutProtocol | null rounds?: string | null durationMinutes?: number | null intervalSeconds?: number | null 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/app/(payload)/admin/modules/workout-structure/utils/format.ts b/src/modules/training/admin/workout-structure/utils/format.ts similarity index 88% rename from src/app/(payload)/admin/modules/workout-structure/utils/format.ts rename to src/modules/training/admin/workout-structure/utils/format.ts index 81a87d7..3c6f3fa 100644 --- a/src/app/(payload)/admin/modules/workout-structure/utils/format.ts +++ b/src/modules/training/admin/workout-structure/utils/format.ts @@ -1,6 +1,6 @@ import type { ExerciseRow, Group } from '../types' import { formatMinSec } from '@/lib/date' -import { formatSideReps } from '@/lib/metrics' +import { formatSideReps } from '@/modules/training/exercises' export const groupLabel = (g: Group): string => { const p = g.protocol ?? 'standard' @@ -13,8 +13,7 @@ export const groupLabel = (g: Group): string => { return r ? `${r} sets` : 'Standard' } -export const exerciseLabel = (row: ExerciseRow): string => - row.exercise?.name ?? row.note ?? '—' +export const exerciseLabel = (row: ExerciseRow): string => row.exercise?.name ?? row.note ?? '—' export const exerciseMeta = (row: ExerciseRow): string => { const parts: string[] = [] 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 100% rename from src/app/(payload)/admin/modules/workout-structure/utils/validate.ts rename to src/modules/training/admin/workout-structure/utils/validate.ts 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 97% rename from src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx rename to src/modules/training/admin/workout-structure/workout-structure.tsx index 80bd38a..0f2d91c 100644 --- a/src/app/(payload)/admin/modules/workout-structure/workout-structure.tsx +++ b/src/modules/training/admin/workout-structure/workout-structure.tsx @@ -1,3 +1,5 @@ +import 'server-only' + import React from 'react' import { WorkoutStructureBreadcrumb } from '../training-navigation/training-navigation' import { loadWorkoutStructure } from './loader' 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 70% 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..72362d9 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,9 @@ '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, SetLog } from '@/modules/training/logs' export function SeriesList({ sets, @@ -14,7 +14,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 76% rename from src/components/workout/exercise-card/exercise-card.tsx rename to src/modules/training/components/exercise-card/exercise-card.tsx index b40dbb4..3154599 100644 --- a/src/components/workout/exercise-card/exercise-card.tsx +++ b/src/modules/training/components/exercise-card/exercise-card.tsx @@ -1,10 +1,10 @@ '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 type { TExercise } from '@/modules/training/plans' +import { toMetricFormValues, type MetricFormValues, type SetLog } from '@/modules/training/logs' import { AddSetActions } from './components/add-set-actions' import { ExerciseHeader } from './components/exercise-header' import { ExerciseNote } from './components/exercise-note' @@ -24,8 +24,8 @@ export function ExerciseCard({ exercise: TExercise sets: SetLog[] clientNote?: string - onAdd?: (exercise: TExercise, fields: MetricField[], values: Values) => Promise - onUpdate?: (id: number, fields: MetricField[], values: Values) => Promise + onAdd?: (exercise: TExercise, fields: MetricField[], values: MetricFormValues) => Promise + onUpdate?: (id: number, fields: MetricField[], values: MetricFormValues) => Promise onDelete?: (id: number) => Promise onSaveNote?: (exercise: TExercise, note: string) => Promise readOnly?: boolean @@ -34,8 +34,8 @@ export function ExerciseCard({ const fields: MetricField[] = exercise.targetType === 'duration' ? ['weightLeft', 'weightRight', 'durationSec'] - : trackingFields(exercise.trackingType) - const prefillValues: Values = { + : getTrackingFields(exercise.trackingType) + const prefillValues: MetricFormValues = { repsLeft: exercise.prefill.repsLeft ?? '', repsRight: exercise.prefill.repsRight ?? '', note: '', @@ -65,7 +65,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)} />