fix: #29 — edit food name, macros, unit in food library
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:
@@ -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] {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user