Changeset 708af9e


Ignore:
Timestamp:
04/27/26 22:01:12 (2 months ago)
Author:
Andrej <asumanovski@…>
Branches:
master
Children:
8449b9c
Parents:
89156c1
Message:

Weight tracking feature complete

Files:
20 added
5 edited

Legend:

Unmodified
Added
Removed
  • backend/src/main/java/com/trekr/backend/entity/weight/WeightUser.java

    r89156c1 r708af9e  
    8181    }
    8282
     83
    8384    public BigDecimal getGoalCalories() {
    8485        return goalCalories;
  • backend/src/main/java/com/trekr/backend/repository/TrainingSessionRepository.java

    r89156c1 r708af9e  
    1010public interface TrainingSessionRepository extends JpaRepository<TrainingSession, Long> {
    1111    Page<TrainingSession> findByTrainingUser_UserIdOrderByDateDesc(Long userId, Pageable pageable);
     12    java.util.List<TrainingSession> findByTrainingUser_UserIdAndDate(Long userId, java.time.LocalDate date);
    1213}
  • db-scripts/ddl.sql

    r89156c1 r708af9e  
    5555    user_id BIGINT PRIMARY KEY
    5656        REFERENCES USERS(user_id) ON DELETE CASCADE,
    57     weight NUMERIC,
    58     height NUMERIC,
    59     goal_weight NUMERIC,
    60     goal_calories NUMERIC
     57    weight NUMERIC NOT NULL,
     58    height NUMERIC NOT NULL,
     59    goal_weight NUMERIC NOT NULL,
     60    goal_calories NUMERIC NOT NULL
    6161);
    6262
     
    6565    user_id BIGINT
    6666        REFERENCES WEIGHT_USERS(user_id) ON DELETE CASCADE,
    67     calories NUMERIC,
    68     date DATE
     67    calories NUMERIC NOT NULL,
     68    date DATE NOT NULL,
     69
     70    CONSTRAINT daily_intakes_unique_user_date UNIQUE (user_id, date)
    6971);
    7072
  • frontend/src/App.jsx

    r89156c1 r708af9e  
    1010import NewTrainingSession from "./pages/Dashboard/pages/Training/NewTrainingSession.jsx";
    1111import Weight from "./pages/Dashboard/pages/Weight/Weight.jsx";
     12import WeightTracking from "./pages/Dashboard/pages/Weight/WeightTracking.jsx";
     13import NewWeightIntake from "./pages/Dashboard/pages/Weight/NewWeightIntake.jsx";
    1214import Finance from "./pages/Dashboard/pages/Finance/Finance.jsx";
    1315import Investing from "./pages/Dashboard/pages/Investing/Investing.jsx";
     
    4648          />
    4749          <Route path="weight" element={<Weight />} />
     50          <Route path="weight/tracking" element={<WeightTracking />} />
     51          <Route path="weight/intakes/new" element={<NewWeightIntake />} />
    4852          <Route path="finance" element={<Finance />} />
    4953          <Route path="investing" element={<Investing />} />
  • frontend/src/pages/Dashboard/pages/Weight/Weight.jsx

    r89156c1 r708af9e  
    1 import React from "react";
     1import React, { useEffect, useMemo, useState } from "react";
     2import { useNavigate } from "react-router-dom";
     3
     4import api from "../../../../api/axios";
     5
     6import WeightCenteredCard from "./components/WeightCenteredCard.jsx";
     7import WeightStartCtaCard from "./components/WeightStartCtaCard.jsx";
     8import WeightStartForm from "./components/WeightStartForm.jsx";
     9
     10function estimateGoalTimeWeeks(currentWeight, goalWeight) {
     11  const current = Number(currentWeight);
     12  const goal = Number(goalWeight);
     13  if (!Number.isFinite(current) || !Number.isFinite(goal) || current <= 0 || goal <= 0) {
     14    return null;
     15  }
     16  const difference = Math.abs(goal - current);
     17  return Math.round((difference / 0.5) * 100) / 100;
     18}
     19
     20function estimateGoalCalories(currentWeight, height, goalWeight) {
     21  const current = Number(currentWeight);
     22  const h = Number(height);
     23  const goal = Number(goalWeight);
     24  if (!Number.isFinite(current) || !Number.isFinite(h) || !Number.isFinite(goal)) {
     25    return null;
     26  }
     27
     28  const maintenance = current * 10 + h * 6.25 + 50;
     29  if (goal < current) return Math.max(0, Math.round((maintenance - 500) * 100) / 100);
     30  if (goal > current) return Math.round((maintenance + 300) * 100) / 100;
     31  return Math.round(maintenance * 100) / 100;
     32}
    233
    334const Weight = () => {
     35  const navigate = useNavigate();
     36
     37  const [isLoading, setIsLoading] = useState(true);
     38  const [isTracking, setIsTracking] = useState(false);
     39  const [step, setStep] = useState("cta");
     40  const [error, setError] = useState("");
     41  const [isSubmitting, setIsSubmitting] = useState(false);
     42  const [autoCalculateTargets, setAutoCalculateTargets] = useState(true);
     43
     44  const initialForm = useMemo(
     45    () => ({
     46      weight: "",
     47      height: "",
     48      goalWeight: "",
     49      goalTimeWeeks: "",
     50      goalCalories: "",
     51    }),
     52    [],
     53  );
     54  const [form, setForm] = useState(initialForm);
     55
     56  const estimatedGoalTimeWeeks = useMemo(
     57    () => estimateGoalTimeWeeks(form.weight, form.goalWeight),
     58    [form.weight, form.goalWeight],
     59  );
     60  const estimatedGoalCalories = useMemo(
     61    () => estimateGoalCalories(form.weight, form.height, form.goalWeight),
     62    [form.weight, form.height, form.goalWeight],
     63  );
     64
     65  useEffect(() => {
     66    let isMounted = true;
     67    const run = async () => {
     68      setIsLoading(true);
     69      setError("");
     70      try {
     71        const res = await api.get("/weight/status");
     72        const tracking = Boolean(res?.data?.tracking);
     73        if (!isMounted) return;
     74        setIsTracking(tracking);
     75        if (tracking) {
     76          navigate("/dashboard/weight/tracking", { replace: true });
     77        }
     78      } catch (err) {
     79        if (!isMounted) return;
     80        const message = err?.response?.data?.message || "Failed to load weight status";
     81        setError(message);
     82      } finally {
     83        if (isMounted) setIsLoading(false);
     84      }
     85    };
     86
     87    run();
     88    return () => {
     89      isMounted = false;
     90    };
     91  }, [navigate]);
     92
     93  const onStart = () => {
     94    setError("");
     95    setStep("form");
     96  };
     97
     98  const onCancel = () => {
     99    setStep("cta");
     100    setForm(initialForm);
     101    setAutoCalculateTargets(true);
     102    setError("");
     103  };
     104
     105  const onSubmit = async (e) => {
     106    e.preventDefault();
     107    setError("");
     108    setIsSubmitting(true);
     109
     110    try {
     111      await api.post("/weight/start", {
     112        weight: form.weight === "" ? null : Number(form.weight),
     113        height: form.height === "" ? null : Number(form.height),
     114        goalWeight: form.goalWeight === "" ? null : Number(form.goalWeight),
     115        goalCalories: autoCalculateTargets
     116          ? null
     117          : form.goalCalories === ""
     118            ? null
     119            : Number(form.goalCalories),
     120        autoCalculateTargets,
     121      });
     122
     123      setIsTracking(true);
     124      navigate("/dashboard/weight/tracking", { replace: true });
     125    } catch (err) {
     126      const message =
     127        err?.response?.data?.message ||
     128        Object.values(err?.response?.data?.errors ?? {})[0] ||
     129        "Failed to start weight tracking";
     130      setError(message);
     131    } finally {
     132      setIsSubmitting(false);
     133    }
     134  };
     135
     136  if (isLoading) {
     137    return <WeightCenteredCard title="Weight" message="Loading…" />;
     138  }
     139
     140  if (isTracking) {
     141    return <WeightCenteredCard title="Weight" message="Redirecting…" />;
     142  }
     143
    4144  return (
    5     <div className="space-y-4">
    6       <h1 className="text-2xl font-bold">Weight</h1>
    7       <div className="card bg-base-200 border border-base-300">
    8         <div className="card-body">
    9           <p className="opacity-80">Weight dashboard content goes here.</p>
    10         </div>
     145    <div className="min-h-[70vh] w-full flex items-center justify-center">
     146      <div className="w-full max-w-4xl">
     147        {step === "cta" ? (
     148          <WeightStartCtaCard error={error} onStart={onStart} isSubmitting={isSubmitting} />
     149        ) : (
     150          <WeightStartForm
     151            form={form}
     152            setForm={setForm}
     153            onSubmit={onSubmit}
     154            onCancel={onCancel}
     155            error={error}
     156            isSubmitting={isSubmitting}
     157            autoCalculateTargets={autoCalculateTargets}
     158            setAutoCalculateTargets={setAutoCalculateTargets}
     159            estimatedGoalTimeWeeks={estimatedGoalTimeWeeks}
     160            estimatedGoalCalories={estimatedGoalCalories}
     161          />
     162        )}
    11163      </div>
    12164    </div>
Note: See TracChangeset for help on using the changeset viewer.