all repos — go-lift @ 387721fc5e90ba268efbae885c9baf8e0a543f28

Lightweight workout tracker prototype..

src/api/crud.go (view raw)

  1package api
  2
  3import (
  4	"encoding/json"
  5	"net/http"
  6	"strconv"
  7
  8	"github.com/birabittoh/go-lift/src/database"
  9	"gorm.io/gorm"
 10)
 11
 12// User handlers
 13func getUserHandler(db *gorm.DB) http.HandlerFunc {
 14	return func(w http.ResponseWriter, r *http.Request) {
 15		id, err := getIDFromPath(r)
 16		if err != nil {
 17			jsonError(w, http.StatusBadRequest, "Invalid user ID")
 18			return
 19		}
 20
 21		var user database.User
 22		if err := db.First(&user, id).Error; err != nil {
 23			if err == gorm.ErrRecordNotFound {
 24				jsonError(w, http.StatusNotFound, "User not found")
 25				return
 26			}
 27			jsonError(w, http.StatusInternalServerError, "Database error")
 28			return
 29		}
 30
 31		jsonResponse(w, http.StatusOK, user)
 32	}
 33}
 34
 35func updateUserHandler(db *gorm.DB) http.HandlerFunc {
 36	return func(w http.ResponseWriter, r *http.Request) {
 37		id, err := getIDFromPath(r)
 38		if err != nil {
 39			jsonError(w, http.StatusBadRequest, "Invalid user ID")
 40			return
 41		}
 42
 43		var user database.User
 44		if err := db.First(&user, id).Error; err != nil {
 45			if err == gorm.ErrRecordNotFound {
 46				jsonError(w, http.StatusNotFound, "User not found")
 47				return
 48			}
 49			jsonError(w, http.StatusInternalServerError, "Database error")
 50			return
 51		}
 52
 53		var updateData database.User
 54		if err := json.NewDecoder(r.Body).Decode(&updateData); err != nil {
 55			jsonError(w, http.StatusBadRequest, "Invalid JSON")
 56			return
 57		}
 58
 59		// Update specific fields
 60		user.Name = updateData.Name
 61		user.IsFemale = updateData.IsFemale
 62		user.Height = updateData.Height
 63		user.Weight = updateData.Weight
 64		user.BirthDate = updateData.BirthDate
 65
 66		if err := db.Save(&user).Error; err != nil {
 67			jsonError(w, http.StatusInternalServerError, "Failed to update user")
 68			return
 69		}
 70
 71		jsonResponse(w, http.StatusOK, user)
 72	}
 73}
 74
 75// Exercise handlers (read-only)
 76func getExercisesHandler(db *gorm.DB) http.HandlerFunc {
 77	return func(w http.ResponseWriter, r *http.Request) {
 78		var exercises []database.Exercise
 79		query := db.Preload("PrimaryMuscles").Preload("SecondaryMuscles")
 80
 81		// Optional filtering
 82		if category := r.URL.Query().Get("category"); category != "" {
 83			query = query.Where("category = ?", category)
 84		}
 85		if level := r.URL.Query().Get("level"); level != "" {
 86			query = query.Where("level = ?", level)
 87		}
 88
 89		if err := query.Find(&exercises).Error; err != nil {
 90			jsonError(w, http.StatusInternalServerError, "Database error")
 91			return
 92		}
 93
 94		jsonResponse(w, http.StatusOK, exercises)
 95	}
 96}
 97
 98func getExerciseHandler(db *gorm.DB) http.HandlerFunc {
 99	return func(w http.ResponseWriter, r *http.Request) {
100		id, err := getIDFromPath(r)
101		if err != nil {
102			jsonError(w, http.StatusBadRequest, "Invalid exercise ID")
103			return
104		}
105
106		var exercise database.Exercise
107		if err := db.Preload("PrimaryMuscles").Preload("SecondaryMuscles").First(&exercise, id).Error; err != nil {
108			if err == gorm.ErrRecordNotFound {
109				jsonError(w, http.StatusNotFound, "Exercise not found")
110				return
111			}
112			jsonError(w, http.StatusInternalServerError, "Database error")
113			return
114		}
115
116		jsonResponse(w, http.StatusOK, exercise)
117	}
118}
119
120// Muscle handlers (read-only)
121func getMusclesHandler(db *gorm.DB) http.HandlerFunc {
122	return func(w http.ResponseWriter, r *http.Request) {
123		var muscles []database.Muscle
124		if err := db.Find(&muscles).Error; err != nil {
125			jsonError(w, http.StatusInternalServerError, "Database error")
126			return
127		}
128
129		jsonResponse(w, http.StatusOK, muscles)
130	}
131}
132
133// Routine handlers
134func getRoutinesHandler(db *gorm.DB) http.HandlerFunc {
135	return func(w http.ResponseWriter, r *http.Request) {
136		var routines []database.Routine
137		if err := db.Find(&routines).Error; err != nil {
138			jsonError(w, http.StatusInternalServerError, "Database error")
139			return
140		}
141
142		jsonResponse(w, http.StatusOK, routines)
143	}
144}
145
146func getRoutineHandler(db *gorm.DB) http.HandlerFunc {
147	return func(w http.ResponseWriter, r *http.Request) {
148		id, err := getIDFromPath(r)
149		if err != nil {
150			jsonError(w, http.StatusBadRequest, "Invalid routine ID")
151			return
152		}
153
154		var routine database.Routine
155		if err := db.Preload("Items.ExerciseItems.Exercise.PrimaryMuscles").
156			Preload("Items.ExerciseItems.Exercise.SecondaryMuscles").
157			Preload("Items.ExerciseItems.Sets").
158			Order("Items.order_index, Items.ExerciseItems.order_index, Items.ExerciseItems.Sets.order_index").
159			First(&routine, id).Error; err != nil {
160			if err == gorm.ErrRecordNotFound {
161				jsonError(w, http.StatusNotFound, "Routine not found")
162				return
163			}
164			jsonError(w, http.StatusInternalServerError, "Database error")
165			return
166		}
167
168		jsonResponse(w, http.StatusOK, routine)
169	}
170}
171
172func createRoutineHandler(db *gorm.DB) http.HandlerFunc {
173	return func(w http.ResponseWriter, r *http.Request) {
174		var routine database.Routine
175		if err := json.NewDecoder(r.Body).Decode(&routine); err != nil {
176			jsonError(w, http.StatusBadRequest, "Invalid JSON")
177			return
178		}
179
180		if err := db.Create(&routine).Error; err != nil {
181			jsonError(w, http.StatusInternalServerError, "Failed to create routine")
182			return
183		}
184
185		jsonResponse(w, http.StatusCreated, routine)
186	}
187}
188
189func updateRoutineHandler(db *gorm.DB) http.HandlerFunc {
190	return func(w http.ResponseWriter, r *http.Request) {
191		id, err := getIDFromPath(r)
192		if err != nil {
193			jsonError(w, http.StatusBadRequest, "Invalid routine ID")
194			return
195		}
196
197		var routine database.Routine
198		if err := json.NewDecoder(r.Body).Decode(&routine); err != nil {
199			jsonError(w, http.StatusBadRequest, "Invalid JSON")
200			return
201		}
202
203		routine.ID = uint(id)
204		if err := db.Save(&routine).Error; err != nil {
205			jsonError(w, http.StatusInternalServerError, "Failed to update routine")
206			return
207		}
208
209		jsonResponse(w, http.StatusOK, routine)
210	}
211}
212
213func deleteRoutineHandler(db *gorm.DB) http.HandlerFunc {
214	return func(w http.ResponseWriter, r *http.Request) {
215		id, err := getIDFromPath(r)
216		if err != nil {
217			jsonError(w, http.StatusBadRequest, "Invalid routine ID")
218			return
219		}
220
221		if err := db.Delete(&database.Routine{}, id).Error; err != nil {
222			jsonError(w, http.StatusInternalServerError, "Failed to delete routine")
223			return
224		}
225
226		jsonResponse(w, http.StatusOK, map[string]string{"message": "Routine deleted"})
227	}
228}
229
230// Record routine handlers (workout sessions)
231func getRecordRoutinesHandler(db *gorm.DB) http.HandlerFunc {
232	return func(w http.ResponseWriter, r *http.Request) {
233		var records []database.RecordRoutine
234		query := db.Preload("Routine").Order("created_at DESC")
235
236		// Optional limit for recent workouts
237		if limit := r.URL.Query().Get("limit"); limit != "" {
238			if l, err := strconv.Atoi(limit); err == nil {
239				query = query.Limit(l)
240			}
241		}
242
243		if err := query.Find(&records).Error; err != nil {
244			jsonError(w, http.StatusInternalServerError, "Database error")
245			return
246		}
247
248		jsonResponse(w, http.StatusOK, records)
249	}
250}
251
252func getRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
253	return func(w http.ResponseWriter, r *http.Request) {
254		id, err := getIDFromPath(r)
255		if err != nil {
256			jsonError(w, http.StatusBadRequest, "Invalid record ID")
257			return
258		}
259
260		var record database.RecordRoutine
261		if err := db.Preload("Routine").
262			Preload("RecordItems.RoutineItem").
263			Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.PrimaryMuscles").
264			Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.SecondaryMuscles").
265			Preload("RecordItems.RecordExerciseItems.RecordSets.Set").
266			Order("RecordItems.order_index, RecordItems.RecordExerciseItems.order_index, RecordItems.RecordExerciseItems.RecordSets.order_index").
267			First(&record, id).Error; err != nil {
268			if err == gorm.ErrRecordNotFound {
269				jsonError(w, http.StatusNotFound, "Record not found")
270				return
271			}
272			jsonError(w, http.StatusInternalServerError, "Database error")
273			return
274		}
275
276		jsonResponse(w, http.StatusOK, record)
277	}
278}
279
280func createRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
281	return func(w http.ResponseWriter, r *http.Request) {
282		var record database.RecordRoutine
283		if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
284			jsonError(w, http.StatusBadRequest, "Invalid JSON")
285			return
286		}
287
288		if err := db.Create(&record).Error; err != nil {
289			jsonError(w, http.StatusInternalServerError, "Failed to create record")
290			return
291		}
292
293		jsonResponse(w, http.StatusCreated, record)
294	}
295}
296
297func updateRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
298	return func(w http.ResponseWriter, r *http.Request) {
299		id, err := getIDFromPath(r)
300		if err != nil {
301			jsonError(w, http.StatusBadRequest, "Invalid record ID")
302			return
303		}
304
305		var record database.RecordRoutine
306		if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
307			jsonError(w, http.StatusBadRequest, "Invalid JSON")
308			return
309		}
310
311		record.ID = uint(id)
312		if err := db.Save(&record).Error; err != nil {
313			jsonError(w, http.StatusInternalServerError, "Failed to update record")
314			return
315		}
316
317		jsonResponse(w, http.StatusOK, record)
318	}
319}
320
321func deleteRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
322	return func(w http.ResponseWriter, r *http.Request) {
323		id, err := getIDFromPath(r)
324		if err != nil {
325			jsonError(w, http.StatusBadRequest, "Invalid record ID")
326			return
327		}
328
329		if err := db.Delete(&database.RecordRoutine{}, id).Error; err != nil {
330			jsonError(w, http.StatusInternalServerError, "Failed to delete record")
331			return
332		}
333
334		jsonResponse(w, http.StatusOK, map[string]string{"message": "Record deleted"})
335	}
336}
337
338// Stats handler
339func getStatsHandler(db *gorm.DB) http.HandlerFunc {
340	return func(w http.ResponseWriter, r *http.Request) {
341		stats := WorkoutStats{}
342
343		// Total workouts
344		db.Model(&database.RecordRoutine{}).Count(&stats.TotalWorkouts)
345
346		// Total minutes (sum of all workout durations)
347		var totalSeconds uint
348		db.Model(&database.RecordRoutine{}).Select("COALESCE(SUM(duration), 0)").Scan(&totalSeconds)
349		stats.TotalMinutes = int(totalSeconds / 60)
350
351		// Total exercises completed
352		db.Model(&database.RecordExerciseItem{}).Count(&stats.TotalExercises)
353
354		// Most frequent exercise
355		var exerciseStats struct {
356			ExerciseName string `json:"exercise_name"`
357			Count        int    `json:"count"`
358		}
359		db.Raw(`
360			SELECT e.name as exercise_name, COUNT(*) as count
361			FROM record_exercise_items rei
362			JOIN exercise_items ei ON rei.exercise_item_id = ei.id
363			JOIN exercises e ON ei.exercise_id = e.id
364			GROUP BY e.id, e.name
365			ORDER BY count DESC
366			LIMIT 1
367		`).Scan(&exerciseStats)
368
369		if exerciseStats.Count > 0 {
370			stats.MostFrequentExercise = &struct {
371				Name  string `json:"name"`
372				Count int    `json:"count"`
373			}{
374				Name:  exerciseStats.ExerciseName,
375				Count: exerciseStats.Count,
376			}
377		}
378
379		// Most frequent routine
380		var routineStats struct {
381			RoutineName string `json:"routine_name"`
382			Count       int    `json:"count"`
383		}
384		db.Raw(`
385			SELECT r.name as routine_name, COUNT(*) as count
386			FROM record_routines rr
387			JOIN routines r ON rr.routine_id = r.id
388			GROUP BY r.id, r.name
389			ORDER BY count DESC
390			LIMIT 1
391		`).Scan(&routineStats)
392
393		if routineStats.Count > 0 {
394			stats.MostFrequentRoutine = &struct {
395				Name  string `json:"name"`
396				Count int    `json:"count"`
397			}{
398				Name:  routineStats.RoutineName,
399				Count: routineStats.Count,
400			}
401		}
402
403		// Recent workouts (last 5)
404		db.Preload("Routine").
405			Order("created_at DESC").
406			Limit(5).
407			Find(&stats.RecentWorkouts)
408
409		jsonResponse(w, http.StatusOK, stats)
410	}
411}