package repository import ( "context" "go-server/internal/models" "gorm.io/gorm" ) type DailyOverviewRepository struct { db *gorm.DB } func NewDailyOverviewRepository(db *gorm.DB) *DailyOverviewRepository { return &DailyOverviewRepository{db: db} } // NutrientTotal represents the aggregated total for a nutrient across all supplements type NutrientTotal struct { NutrientID models.ULID `json:"nutrientId" db:"nutrient_id"` NutrientName string `json:"nutrientName" db:"nutrient_name"` Description string `json:"description" db:"description"` TotalAmount string `json:"totalAmount" db:"total_amount"` Unit string `json:"unit" db:"unit"` Categories []string `json:"categories" db:"categories"` } // SupplementNutrientDetails represents detailed breakdown by supplement type SupplementNutrientDetails struct { SupplementID models.ULID `json:"supplementId" db:"supplement_id"` SupplementName string `json:"supplementName" db:"supplement_name"` NutrientID models.ULID `json:"nutrientId" db:"nutrient_id"` NutrientName string `json:"nutrientName" db:"nutrient_name"` Amount string `json:"amount" db:"amount"` Unit string `json:"unit" db:"unit"` ServingSize string `json:"servingSize" db:"serving_size"` ReferenceIntake string `json:"referenceIntake" db:"reference_intake"` } // GetNutrientTotals returns aggregated nutrient totals across all supplements // This is perfect for a daily overview where you want to see total intake func (r *DailyOverviewRepository) GetNutrientTotals(ctx context.Context) ([]NutrientTotal, error) { var results []NutrientTotal query := ` SELECT n.id as nutrient_id, n.name as nutrient_name, n.description, sn.per_serving_reference_intake, -- For now, we'll concatenate amounts (later we can parse and sum numeric values) STRING_AGG(sn.per_serving, ' + ') as total_amount, -- Extract unit from first entry (assumption: same nutrient has same unit) SPLIT_PART(MIN(sn.per_serving), ' ', 2) as unit, -- Get all categories for this nutrient ARRAY_AGG(DISTINCT c.name) as categories FROM nutrients n LEFT JOIN supplement_nutrients sn ON n.id = sn.nutrient_id LEFT JOIN supplements s ON sn.supplement_id = s.id LEFT JOIN nutrient_categories nc ON n.id = nc.nutrient_id LEFT JOIN categories c ON nc.category_id = c.id WHERE sn.id IS NOT NULL -- Only nutrients that are in supplements GROUP BY n.id, n.name, n.description, sn.per_serving_reference_intake ORDER BY n.name ` err := r.db.WithContext(ctx).Raw(query).Scan(&results).Error return results, err } // GetSupplementBreakdown returns detailed breakdown of nutrients by supplement func (r *DailyOverviewRepository) GetSupplementBreakdown(ctx context.Context) ([]SupplementNutrientDetails, error) { var results []SupplementNutrientDetails query := ` SELECT s.id as supplement_id, s.name as supplement_name, n.id as nutrient_id, n.name as nutrient_name, sn.per_serving as amount, SPLIT_PART(sn.per_serving, ' ', 2) as unit, sn.serving_size, sn.per_serving_reference_intake as reference_intake FROM supplements s JOIN supplement_nutrients sn ON s.id = sn.supplement_id JOIN nutrients n ON sn.nutrient_id = n.id ORDER BY s.name, n.name ` err := r.db.WithContext(ctx).Raw(query).Scan(&results).Error return results, err } // GetNutrientsByCategory returns nutrients grouped by category with totals func (r *DailyOverviewRepository) GetNutrientsByCategory(ctx context.Context) (map[string][]NutrientTotal, error) { var results []struct { CategoryName string `db:"category_name"` CategoryID models.ULID `db:"category_id"` NutrientID models.ULID `db:"nutrient_id"` NutrientName string `db:"nutrient_name"` Description string `db:"description"` TotalAmount string `db:"total_amount"` Unit string `db:"unit"` } query := ` SELECT c.name as category_name, c.id as category_id, n.id as nutrient_id, n.name as nutrient_name, n.description, STRING_AGG(sn.per_serving, ' + ') as total_amount, SPLIT_PART(MIN(sn.per_serving), ' ', 2) as unit FROM categories c JOIN nutrient_categories nc ON c.id = nc.category_id JOIN nutrients n ON nc.nutrient_id = n.id LEFT JOIN supplement_nutrients sn ON n.id = sn.nutrient_id WHERE sn.id IS NOT NULL GROUP BY c.name, n.id, n.name, n.description, c.id ORDER BY c.name, n.name ` err := r.db.WithContext(ctx).Raw(query).Scan(&results).Error if err != nil { return nil, err } // Group by category categoryMap := make(map[string][]NutrientTotal) for _, result := range results { nutrient := NutrientTotal{ NutrientID: result.NutrientID, NutrientName: result.NutrientName, Description: result.Description, TotalAmount: result.TotalAmount, Unit: result.Unit, Categories: []string{result.CategoryID.String()}, } categoryMap[result.CategoryName] = append(categoryMap[result.CategoryName], nutrient) } return categoryMap, nil } // ExecuteRawQuery allows executing arbitrary SQL queries and scanning into any struct // This gives you full flexibility for custom queries func (r *DailyOverviewRepository) ExecuteRawQuery(ctx context.Context, query string, dest interface{}, args ...interface{}) error { return r.db.WithContext(ctx).Raw(query, args...).Scan(dest).Error } // ExecuteRawQueryWithResult executes a raw query and returns the result as a map // Useful for dynamic queries where you don't know the structure ahead of time func (r *DailyOverviewRepository) ExecuteRawQueryWithResult(ctx context.Context, query string, args ...interface{}) ([]map[string]interface{}, error) { rows, err := r.db.WithContext(ctx).Raw(query, args...).Rows() if err != nil { return nil, err } defer rows.Close() columns, err := rows.Columns() if err != nil { return nil, err } var results []map[string]interface{} for rows.Next() { values := make([]interface{}, len(columns)) valuePtrs := make([]interface{}, len(columns)) for i := range columns { valuePtrs[i] = &values[i] } if err := rows.Scan(valuePtrs...); err != nil { return nil, err } result := make(map[string]interface{}) for i, col := range columns { result[col] = values[i] } results = append(results, result) } return results, nil }