all repos — go-lift @ 35d89fd489cd99e63cbf7b49fe5e9088f5b13317

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.Model(&database.Exercise{})
 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.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// Routine handlers
121func getRoutinesHandler(db *gorm.DB) http.HandlerFunc {
122	return func(w http.ResponseWriter, r *http.Request) {
123		var routines []database.Routine
124		if err := db.Find(&routines).Error; err != nil {
125			jsonError(w, http.StatusInternalServerError, "Database error")
126			return
127		}
128
129		jsonResponse(w, http.StatusOK, routines)
130	}
131}
132
133func getRoutineHandler(db *gorm.DB) http.HandlerFunc {
134	return func(w http.ResponseWriter, r *http.Request) {
135		id, err := getIDFromPath(r)
136		if err != nil {
137			jsonError(w, http.StatusBadRequest, "Invalid routine ID")
138			return
139		}
140
141		var routine database.Routine
142		if err := db.Preload("Items.ExerciseItems.Exercise").
143			Preload("Items.ExerciseItems.Sets").
144			Order("Items.order_index, Items.ExerciseItems.order_index, Items.ExerciseItems.Sets.order_index").
145			First(&routine, id).Error; err != nil {
146			if err == gorm.ErrRecordNotFound {
147				jsonError(w, http.StatusNotFound, "Routine not found")
148				return
149			}
150			jsonError(w, http.StatusInternalServerError, "Database error")
151			return
152		}
153
154		jsonResponse(w, http.StatusOK, routine)
155	}
156}
157
158func createRoutineHandler(db *gorm.DB) http.HandlerFunc {
159	return func(w http.ResponseWriter, r *http.Request) {
160		var routine database.Routine
161		if err := json.NewDecoder(r.Body).Decode(&routine); err != nil {
162			jsonError(w, http.StatusBadRequest, "Invalid JSON")
163			return
164		}
165
166		if err := db.Create(&routine).Error; err != nil {
167			jsonError(w, http.StatusInternalServerError, "Failed to create routine")
168			return
169		}
170
171		jsonResponse(w, http.StatusCreated, routine)
172	}
173}
174
175func updateRoutineHandler(db *gorm.DB) http.HandlerFunc {
176	return func(w http.ResponseWriter, r *http.Request) {
177		id, err := getIDFromPath(r)
178		if err != nil {
179			jsonError(w, http.StatusBadRequest, "Invalid routine ID")
180			return
181		}
182
183		var routine database.Routine
184		if err := json.NewDecoder(r.Body).Decode(&routine); err != nil {
185			jsonError(w, http.StatusBadRequest, "Invalid JSON")
186			return
187		}
188
189		routine.ID = uint(id)
190		if err := db.Save(&routine).Error; err != nil {
191			jsonError(w, http.StatusInternalServerError, "Failed to update routine")
192			return
193		}
194
195		jsonResponse(w, http.StatusOK, routine)
196	}
197}
198
199func deleteRoutineHandler(db *gorm.DB) http.HandlerFunc {
200	return func(w http.ResponseWriter, r *http.Request) {
201		id, err := getIDFromPath(r)
202		if err != nil {
203			jsonError(w, http.StatusBadRequest, "Invalid routine ID")
204			return
205		}
206
207		if err := db.Delete(&database.Routine{}, id).Error; err != nil {
208			jsonError(w, http.StatusInternalServerError, "Failed to delete routine")
209			return
210		}
211
212		jsonResponse(w, http.StatusOK, map[string]string{"message": "Routine deleted"})
213	}
214}
215
216// Record routine handlers (workout sessions)
217func getRecordRoutinesHandler(db *gorm.DB) http.HandlerFunc {
218	return func(w http.ResponseWriter, r *http.Request) {
219		var records []database.RecordRoutine
220		query := db.Preload("Routine").Order("created_at DESC")
221
222		// Optional limit for recent workouts
223		if limit := r.URL.Query().Get("limit"); limit != "" {
224			if l, err := strconv.Atoi(limit); err == nil {
225				query = query.Limit(l)
226			}
227		}
228
229		if err := query.Find(&records).Error; err != nil {
230			jsonError(w, http.StatusInternalServerError, "Database error")
231			return
232		}
233
234		jsonResponse(w, http.StatusOK, records)
235	}
236}
237
238func getRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
239	return func(w http.ResponseWriter, r *http.Request) {
240		id, err := getIDFromPath(r)
241		if err != nil {
242			jsonError(w, http.StatusBadRequest, "Invalid record ID")
243			return
244		}
245
246		var record database.RecordRoutine
247		if err := db.Preload("Routine").
248			Preload("RecordItems.RoutineItem").
249			Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise").
250			Preload("RecordItems.RecordExerciseItems.RecordSets.Set").
251			Order("RecordItems.order_index, RecordItems.RecordExerciseItems.order_index, RecordItems.RecordExerciseItems.RecordSets.order_index").
252			First(&record, id).Error; err != nil {
253			if err == gorm.ErrRecordNotFound {
254				jsonError(w, http.StatusNotFound, "Record not found")
255				return
256			}
257			jsonError(w, http.StatusInternalServerError, "Database error")
258			return
259		}
260
261		jsonResponse(w, http.StatusOK, record)
262	}
263}
264
265func createRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
266	return func(w http.ResponseWriter, r *http.Request) {
267		var record database.RecordRoutine
268		if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
269			jsonError(w, http.StatusBadRequest, "Invalid JSON")
270			return
271		}
272
273		if err := db.Create(&record).Error; err != nil {
274			jsonError(w, http.StatusInternalServerError, "Failed to create record")
275			return
276		}
277
278		jsonResponse(w, http.StatusCreated, record)
279	}
280}
281
282func updateRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
283	return func(w http.ResponseWriter, r *http.Request) {
284		id, err := getIDFromPath(r)
285		if err != nil {
286			jsonError(w, http.StatusBadRequest, "Invalid record ID")
287			return
288		}
289
290		var record database.RecordRoutine
291		if err := json.NewDecoder(r.Body).Decode(&record); err != nil {
292			jsonError(w, http.StatusBadRequest, "Invalid JSON")
293			return
294		}
295
296		record.ID = uint(id)
297		if err := db.Save(&record).Error; err != nil {
298			jsonError(w, http.StatusInternalServerError, "Failed to update record")
299			return
300		}
301
302		jsonResponse(w, http.StatusOK, record)
303	}
304}
305
306func deleteRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
307	return func(w http.ResponseWriter, r *http.Request) {
308		id, err := getIDFromPath(r)
309		if err != nil {
310			jsonError(w, http.StatusBadRequest, "Invalid record ID")
311			return
312		}
313
314		if err := db.Delete(&database.RecordRoutine{}, id).Error; err != nil {
315			jsonError(w, http.StatusInternalServerError, "Failed to delete record")
316			return
317		}
318
319		jsonResponse(w, http.StatusOK, map[string]string{"message": "Record deleted"})
320	}
321}
322
323// Stats handler
324func getStatsHandler(db *gorm.DB) http.HandlerFunc {
325	return func(w http.ResponseWriter, r *http.Request) {
326		stats := WorkoutStats{}
327
328		// Total workouts
329		db.Model(&database.RecordRoutine{}).Count(&stats.TotalWorkouts)
330
331		// Total minutes (sum of all workout durations)
332		var totalSeconds uint
333		db.Model(&database.RecordRoutine{}).Select("COALESCE(SUM(duration), 0)").Scan(&totalSeconds)
334		stats.TotalMinutes = int(totalSeconds / 60)
335
336		// Total exercises completed
337		db.Model(&database.RecordExerciseItem{}).Count(&stats.TotalExercises)
338
339		// Most frequent exercise
340		var exerciseStats struct {
341			ExerciseName string `json:"exercise_name"`
342			Count        int    `json:"count"`
343		}
344		db.Raw(`
345			SELECT e.name as exercise_name, COUNT(*) as count
346			FROM record_exercise_items rei
347			JOIN exercise_items ei ON rei.exercise_item_id = ei.id
348			JOIN exercises e ON ei.exercise_id = e.id
349			GROUP BY e.id, e.name
350			ORDER BY count DESC
351			LIMIT 1
352		`).Scan(&exerciseStats)
353
354		if exerciseStats.Count > 0 {
355			stats.MostFrequentExercise = &struct {
356				Name  string `json:"name"`
357				Count int    `json:"count"`
358			}{
359				Name:  exerciseStats.ExerciseName,
360				Count: exerciseStats.Count,
361			}
362		}
363
364		// Most frequent routine
365		var routineStats struct {
366			RoutineName string `json:"routine_name"`
367			Count       int    `json:"count"`
368		}
369		db.Raw(`
370			SELECT r.name as routine_name, COUNT(*) as count
371			FROM record_routines rr
372			JOIN routines r ON rr.routine_id = r.id
373			GROUP BY r.id, r.name
374			ORDER BY count DESC
375			LIMIT 1
376		`).Scan(&routineStats)
377
378		if routineStats.Count > 0 {
379			stats.MostFrequentRoutine = &struct {
380				Name  string `json:"name"`
381				Count int    `json:"count"`
382			}{
383				Name:  routineStats.RoutineName,
384				Count: routineStats.Count,
385			}
386		}
387
388		// Recent workouts (last 5)
389		db.Preload("Routine").
390			Order("created_at DESC").
391			Limit(5).
392			Find(&stats.RecentWorkouts)
393
394		jsonResponse(w, http.StatusOK, stats)
395	}
396}