Files
2026-10-11 14:12:10 +03:00

198 lines
6.7 KiB
Python

from __future__ import annotations
from datetime import date, timedelta
from sqlalchemy.orm import Session
from app.models import AccessoryState, Workout, WorkoutExercise, WorkoutSet
from app.services.program import load_program
from app.services.weights import bench_weight
WEEKDAY_NAMES = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
def next_dates_for_weekdays(start: date, weekdays: list[int], count: int) -> list[date]:
"""Return `count` dates on selected weekdays starting from start (inclusive if matches)."""
selected = set(weekdays)
dates: list[date] = []
cursor = start
# safety limit
for _ in range(400):
if cursor.weekday() in selected:
dates.append(cursor)
if len(dates) >= count:
break
cursor += timedelta(days=1)
if len(dates) < count:
raise ValueError("Не удалось построить расписание")
return dates
def clear_cycle_workouts(db: Session, cycle_number: int) -> None:
workouts = db.query(Workout).filter(Workout.cycle_number == cycle_number).all()
for w in workouts:
db.delete(w)
db.query(AccessoryState).filter(AccessoryState.cycle_number == cycle_number).delete()
db.flush()
def generate_schedule(
db: Session,
*,
one_rm_kg: float,
training_weekdays: list[int],
start_date: date,
cycle_number: int,
) -> list[Workout]:
program = load_program()
total_days = sum(len(w["days"]) for w in program["weeks"])
dates = next_dates_for_weekdays(start_date, training_weekdays, total_days)
# Map chronological day_index within week order from template (day 0,1,2 of each week)
date_idx = 0
created: list[Workout] = []
for week in program["weeks"]:
for day_index, day in enumerate(week["days"]):
scheduled = dates[date_idx]
date_idx += 1
workout = Workout(
cycle_number=cycle_number,
week_number=week["week"],
day_index=day_index,
scheduled_date=scheduled,
status="planned",
)
db.add(workout)
db.flush()
for order_index, ex in enumerate(day["exercises"]):
kind = ex["kind"]
accessory_state = None
needs_weight = False
prescribed = None
locked = False
if kind == "accessory":
accessory_state = (
db.query(AccessoryState)
.filter(
AccessoryState.cycle_number == cycle_number,
AccessoryState.exercise_name == ex["name"],
)
.first()
)
if accessory_state and accessory_state.locked and accessory_state.weight_kg is not None:
prescribed = accessory_state.weight_kg
locked = True
elif accessory_state and accessory_state.pending_weight_kg is not None:
prescribed = accessory_state.pending_weight_kg
needs_weight = False
elif accessory_state and accessory_state.weight_kg is not None and not accessory_state.locked:
prescribed = accessory_state.weight_kg
needs_weight = False
else:
needs_weight = True
we = WorkoutExercise(
workout_id=workout.id,
order_index=order_index,
name=ex["name"],
kind=kind,
prescribed_weight_kg=prescribed,
weight_locked=locked,
needs_weight_input=needs_weight,
)
db.add(we)
db.flush()
for set_index, s in enumerate(ex["sets"]):
if kind == "bench":
tw = bench_weight(one_rm_kg, s["percent"])
ws = WorkoutSet(
exercise_id=we.id,
set_index=set_index,
target_percent=s["percent"],
target_reps=s["reps"],
target_weight_kg=tw,
is_failure=False,
)
elif kind == "failure":
ws = WorkoutSet(
exercise_id=we.id,
set_index=set_index,
target_reps=None,
target_weight_kg=None,
is_failure=True,
)
else:
reps = s.get("reps") if isinstance(s, dict) else None
ws = WorkoutSet(
exercise_id=we.id,
set_index=set_index,
target_reps=reps,
target_weight_kg=prescribed,
is_failure=False,
)
db.add(ws)
created.append(workout)
db.flush()
return created
def apply_accessory_weight_to_future(
db: Session,
*,
cycle_number: int,
exercise_name: str,
weight_kg: float,
from_date: date,
lock: bool,
) -> None:
state = (
db.query(AccessoryState)
.filter(
AccessoryState.cycle_number == cycle_number,
AccessoryState.exercise_name == exercise_name,
)
.first()
)
if not state:
state = AccessoryState(cycle_number=cycle_number, exercise_name=exercise_name)
db.add(state)
if lock:
state.weight_kg = weight_kg
state.locked = True
state.pending_weight_kg = None
else:
state.pending_weight_kg = weight_kg
state.weight_kg = weight_kg
state.locked = False
# Update future workouts' exercises of same name
future = (
db.query(WorkoutExercise)
.join(Workout)
.filter(
Workout.cycle_number == cycle_number,
Workout.scheduled_date >= from_date,
Workout.status != "completed",
WorkoutExercise.name == exercise_name,
WorkoutExercise.kind == "accessory",
)
.all()
)
for we in future:
if we.sets and all(s.completed for s in we.sets):
continue
we.prescribed_weight_kg = weight_kg
we.weight_locked = lock
we.needs_weight_input = False
for s in we.sets:
if not s.completed:
s.target_weight_kg = weight_kg