fix: #29 — edit food name, macros, unit in food library
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

Long-press a food → "Edit" opens EditFoodSheet with:
- Name and brand fields
- All macros: calories, protein, carbs, fat, sugar, fiber
- Base unit field
- Save calls PATCH /api/fitness/foods/{id}
- List refreshes after save

Also added updateFood() to FitnessAPI and UpdateFoodRequest model.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 22:31:46 -05:00
parent 55ef010370
commit b5d734efe1
3 changed files with 152 additions and 0 deletions

View File

@@ -72,6 +72,10 @@ struct FitnessAPI {
try await api.delete("\(basePath)/foods/\(id)") 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 // MARK: - Templates
func getTemplates() async throws -> [MealTemplate] { func getTemplates() async throws -> [MealTemplate] {

View File

@@ -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 // MARK: - Delete Response
struct SuccessResponse: Decodable { struct SuccessResponse: Decodable {

View File

@@ -3,6 +3,7 @@ import SwiftUI
struct FoodLibraryView: View { struct FoodLibraryView: View {
@State private var vm = FoodSearchViewModel() @State private var vm = FoodSearchViewModel()
@State private var selectedFood: Food? @State private var selectedFood: Food?
@State private var editingFood: Food?
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -77,6 +78,11 @@ struct FoodLibraryView: View {
.padding(.vertical, 10) .padding(.vertical, 10)
} }
.contextMenu { .contextMenu {
Button {
editingFood = food
} label: {
Label("Edit", systemImage: "pencil")
}
Button(role: .destructive) { Button(role: .destructive) {
Task { Task {
_ = try? await FitnessAPI().deleteFood(id: food.id) _ = try? await FitnessAPI().deleteFood(id: food.id)
@@ -102,5 +108,133 @@ struct FoodLibraryView: View {
.sheet(item: $selectedFood) { food in .sheet(item: $selectedFood) { food in
AddFoodSheet(food: food) 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<String>, 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
}
} }
} }