diff --git a/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift b/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift index eb32971..da87595 100644 --- a/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift +++ b/ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift @@ -72,6 +72,10 @@ struct FitnessAPI { try await api.delete("\(basePath)/foods/\(id)") } + func updateFood(id: String, body: UpdateFoodRequest) async throws -> Food { + try await api.patch("\(basePath)/foods/\(id)", body: body) + } + // MARK: - Templates func getTemplates() async throws -> [MealTemplate] { diff --git a/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift b/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift index 9de1f1e..04a9ecc 100644 --- a/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift +++ b/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift @@ -235,6 +235,20 @@ struct UpdateEntryRequest: Encodable { } } +// MARK: - Update Food Request + +struct UpdateFoodRequest: Encodable { + var name: String? + var brand: String? + var caloriesPerBase: Double? + var proteinPerBase: Double? + var carbsPerBase: Double? + var fatPerBase: Double? + var sugarPerBase: Double? + var fiberPerBase: Double? + var baseUnit: String? +} + // MARK: - Delete Response struct SuccessResponse: Decodable { diff --git a/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift b/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift index 52f4850..33df71b 100644 --- a/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift +++ b/ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift @@ -3,6 +3,7 @@ import SwiftUI struct FoodLibraryView: View { @State private var vm = FoodSearchViewModel() @State private var selectedFood: Food? + @State private var editingFood: Food? var body: some View { VStack(spacing: 0) { @@ -77,6 +78,11 @@ struct FoodLibraryView: View { .padding(.vertical, 10) } .contextMenu { + Button { + editingFood = food + } label: { + Label("Edit", systemImage: "pencil") + } Button(role: .destructive) { Task { _ = try? await FitnessAPI().deleteFood(id: food.id) @@ -102,5 +108,133 @@ struct FoodLibraryView: View { .sheet(item: $selectedFood) { food in AddFoodSheet(food: food) } + .sheet(item: $editingFood) { food in + EditFoodSheet(food: food) { + Task { await vm.loadInitial() } + } + } + } +} + +// MARK: - Edit Food Sheet + +struct EditFoodSheet: View { + let food: Food + var onSave: () -> Void = {} + @Environment(\.dismiss) private var dismiss + + @State private var name: String + @State private var brand: String + @State private var calories: String + @State private var protein: String + @State private var carbs: String + @State private var fat: String + @State private var sugar: String + @State private var fiber: String + @State private var baseUnit: String + @State private var isSaving = false + @State private var error: String? + + init(food: Food, onSave: @escaping () -> Void = {}) { + self.food = food + self.onSave = onSave + _name = State(initialValue: food.name) + _brand = State(initialValue: food.brand ?? "") + _calories = State(initialValue: String(Int(food.caloriesPerBase))) + _protein = State(initialValue: String(Int(food.proteinPerBase))) + _carbs = State(initialValue: String(Int(food.carbsPerBase))) + _fat = State(initialValue: String(Int(food.fatPerBase))) + _sugar = State(initialValue: String(Int(food.sugarPerBase))) + _fiber = State(initialValue: String(Int(food.fiberPerBase))) + _baseUnit = State(initialValue: food.baseUnit) + } + + var body: some View { + NavigationStack { + Form { + Section("Name") { + TextField("Food name", text: $name) + TextField("Brand (optional)", text: $brand) + } + + Section("Nutrition per \(baseUnit)") { + macroField("Calories", text: $calories, color: .emerald) + macroField("Protein (g)", text: $protein, color: .macroProtein) + macroField("Carbs (g)", text: $carbs, color: .macroCarbs) + macroField("Fat (g)", text: $fat, color: .macroFat) + macroField("Sugar (g)", text: $sugar, color: .orange) + macroField("Fiber (g)", text: $fiber, color: .green) + } + + Section("Unit") { + TextField("Base unit", text: $baseUnit) + } + + if let error { + Section { + Text(error) + .foregroundStyle(.red) + .font(.caption) + } + } + } + .navigationTitle("Edit Food") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button { + save() + } label: { + if isSaving { + ProgressView().controlSize(.small) + } else { + Text("Save") + } + } + .disabled(name.isEmpty || isSaving) + } + } + } + } + + private func macroField(_ label: String, text: Binding, color: Color) -> some View { + HStack { + Text(label) + .foregroundStyle(color) + Spacer() + TextField("0", text: text) + .keyboardType(.numberPad) + .multilineTextAlignment(.trailing) + .frame(width: 80) + } + } + + private func save() { + isSaving = true + error = nil + Task { + do { + let request = UpdateFoodRequest( + name: name, + brand: brand.isEmpty ? nil : brand, + caloriesPerBase: Double(calories), + proteinPerBase: Double(protein), + carbsPerBase: Double(carbs), + fatPerBase: Double(fat), + sugarPerBase: Double(sugar), + fiberPerBase: Double(fiber), + baseUnit: baseUnit + ) + _ = try await FitnessAPI().updateFood(id: food.id, body: request) + onSave() + dismiss() + } catch { + self.error = error.localizedDescription + } + isSaving = false + } } }