fix: iOS fitness models — UUID strings, snapshot_ fields, convertFromSnakeCase compatibility
All checks were successful
Security Checks / dependency-audit (push) Successful in 12s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 3s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 01:44:53 -05:00
parent 4592e35732
commit e852e98812
23 changed files with 1745 additions and 512 deletions

View File

@@ -38,6 +38,6 @@ struct MainTabView: View {
}
.tag(1)
}
.tint(.accent)
.tint(.accentWarm)
}
}

View File

@@ -33,10 +33,10 @@ struct AssistantChatView: View {
} label: {
Text(tab.rawValue)
.font(.subheadline.bold())
.foregroundStyle(selectedTab == tab ? Color.accent : Color.textSecondary)
.foregroundStyle(selectedTab == tab ? Color.accentWarm : Color.textSecondary)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(selectedTab == tab ? Color.accent.opacity(0.1) : Color.clear)
.background(selectedTab == tab ? Color.accentWarm.opacity(0.1) : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
@@ -83,7 +83,7 @@ struct AssistantChatView: View {
if vm.isLoading {
HStack {
ProgressView()
.tint(Color.accent)
.tint(Color.accentWarm)
Text("Thinking...")
.font(.caption)
.foregroundStyle(Color.textSecondary)
@@ -122,7 +122,7 @@ struct AssistantChatView: View {
), matching: .images) {
Image(systemName: vm.photoData != nil ? "photo.fill" : "photo")
.font(.title3)
.foregroundStyle(vm.photoData != nil ? Color.accent : Color.textSecondary)
.foregroundStyle(vm.photoData != nil ? Color.accentWarm : Color.textSecondary)
}
TextField("Ask anything...", text: $vm.inputText)
@@ -141,7 +141,7 @@ struct AssistantChatView: View {
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.title2)
.foregroundStyle(vm.inputText.isEmpty && vm.photoData == nil ? Color.textTertiary : Color.accent)
.foregroundStyle(vm.inputText.isEmpty && vm.photoData == nil ? Color.textTertiary : Color.accentWarm)
}
.disabled(vm.inputText.isEmpty && vm.photoData == nil)
}
@@ -212,10 +212,10 @@ struct AssistantChatView: View {
Spacer()
Text(draft.mealType.capitalized)
.font(.caption2.bold())
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Color.accent.opacity(0.1))
.background(Color.accentWarm.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 6))
}
@@ -276,10 +276,10 @@ struct AssistantChatView: View {
.font(.caption2)
.lineLimit(1)
}
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.accent.opacity(0.08))
.background(Color.accentWarm.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}

View File

@@ -13,7 +13,7 @@ struct LoginView: View {
VStack(spacing: 8) {
Image(systemName: "square.grid.2x2.fill")
.font(.system(size: 48))
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
Text("Platform")
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundStyle(Color.textPrimary)
@@ -67,7 +67,7 @@ struct LoginView: View {
.frame(height: 48)
}
.buttonStyle(.borderedProminent)
.tint(Color.accent)
.tint(Color.accentWarm)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 32)
.disabled(username.isEmpty || password.isEmpty || isLoading)

View File

@@ -11,12 +11,12 @@ struct FitnessAPI {
try await api.post("/api/fitness/entries", body: req)
}
func updateEntry(id: Int, quantity: Double) async throws -> FoodEntry {
func updateEntry(id: String, quantity: Double) async throws -> FoodEntry {
struct Body: Encodable { let quantity: Double }
return try await api.patch("/api/fitness/entries/\(id)", body: Body(quantity: quantity))
}
func deleteEntry(id: Int) async throws {
func deleteEntry(id: String) async throws {
try await api.delete("/api/fitness/entries/\(id)")
}
@@ -33,7 +33,7 @@ struct FitnessAPI {
try await api.get("/api/fitness/foods/recent?limit=\(limit)")
}
func getFood(id: Int) async throws -> FoodItem {
func getFood(id: String) async throws -> FoodItem {
try await api.get("/api/fitness/foods/\(id)")
}
@@ -49,7 +49,7 @@ struct FitnessAPI {
try await api.get("/api/fitness/templates")
}
func logTemplate(id: Int, date: String) async throws {
func logTemplate(id: String, date: String) async throws {
struct Empty: Decodable {}
let _: Empty = try await api.post("/api/fitness/templates/\(id)/log?date=\(date)")
}

View File

@@ -32,175 +32,227 @@ enum MealType: String, Codable, CaseIterable, Identifiable {
}
// MARK: - Food Entry
// API fields (snake_case) are auto-converted to camelCase by decoder.
// The API returns snapshot_food_name, snapshot_calories, etc. no top-level food_name/calories.
struct FoodEntry: Identifiable, Codable {
let id: Int
let userId: Int?
let foodId: Int?
let id: String
let userId: String?
let foodId: String?
let mealType: MealType
let quantity: Double
let entryDate: String
let foodName: String
let calories: Double
let protein: Double
let carbs: Double
let fat: Double
let sugar: Double?
let fiber: Double?
let servingSize: String?
let imageFilename: String?
let entryType: String?
let unit: String?
let servingDescription: String?
let snapshotFoodName: String?
let snapshotServingLabel: String?
let snapshotCalories: Double
let snapshotProtein: Double
let snapshotCarbs: Double
let snapshotFat: Double
let snapshotSugar: Double?
let snapshotFiber: Double?
let foodImagePath: String?
let note: String?
let entryMethod: String?
enum CodingKeys: String, CodingKey {
case id
case userId = "user_id"
case foodId = "food_id"
case mealType = "meal_type"
case quantity
case entryDate = "entry_date"
case foodName = "food_name"
case snapshotFoodName = "snapshot_food_name"
case calories, protein, carbs, fat, sugar, fiber
case servingSize = "serving_size"
case imageFilename = "image_filename"
}
// Computed convenience accessors used by views
var foodName: String { snapshotFoodName ?? "Unknown" }
var calories: Double { snapshotCalories }
var protein: Double { snapshotProtein }
var carbs: Double { snapshotCarbs }
var fat: Double { snapshotFat }
var sugar: Double? { snapshotSugar }
var fiber: Double? { snapshotFiber }
var imageFilename: String? { foodImagePath }
// No CodingKeys needed convertFromSnakeCase handles all mappings.
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try Self.decodeIntFlex(c, .id)
userId = try? Self.decodeIntFlex(c, .userId)
foodId = try? Self.decodeIntFlex(c, .foodId)
mealType = try c.decode(MealType.self, forKey: .mealType)
quantity = try Self.decodeDoubleFlex(c, .quantity)
entryDate = try c.decode(String.self, forKey: .entryDate)
// Handle food_name or snapshot_food_name
if let name = try? c.decode(String.self, forKey: .foodName) {
foodName = name
} else if let name = try? c.decode(String.self, forKey: .snapshotFoodName) {
foodName = name
} else {
foodName = "Unknown"
}
calories = try Self.decodeDoubleFlex(c, .calories)
protein = try Self.decodeDoubleFlex(c, .protein)
carbs = try Self.decodeDoubleFlex(c, .carbs)
fat = try Self.decodeDoubleFlex(c, .fat)
sugar = try? Self.decodeDoubleFlex(c, .sugar)
fiber = try? Self.decodeDoubleFlex(c, .fiber)
servingSize = try? c.decode(String.self, forKey: .servingSize)
imageFilename = try? c.decode(String.self, forKey: .imageFilename)
let c = try decoder.container(keyedBy: AnyCodingKey.self)
id = Self.decodeStringFlex(c, "id") ?? ""
userId = Self.decodeStringFlex(c, "userId")
foodId = Self.decodeStringFlex(c, "foodId")
mealType = (try? c.decode(MealType.self, forKey: AnyCodingKey("mealType"))) ?? .snack
quantity = Self.decodeDoubleFlex(c, "quantity") ?? 1.0
entryDate = (try? c.decode(String.self, forKey: AnyCodingKey("entryDate"))) ?? ""
entryType = try? c.decode(String.self, forKey: AnyCodingKey("entryType"))
unit = try? c.decode(String.self, forKey: AnyCodingKey("unit"))
servingDescription = try? c.decode(String.self, forKey: AnyCodingKey("servingDescription"))
snapshotFoodName = try? c.decode(String.self, forKey: AnyCodingKey("snapshotFoodName"))
snapshotServingLabel = try? c.decode(String.self, forKey: AnyCodingKey("snapshotServingLabel"))
snapshotCalories = Self.decodeDoubleFlex(c, "snapshotCalories") ?? 0
snapshotProtein = Self.decodeDoubleFlex(c, "snapshotProtein") ?? 0
snapshotCarbs = Self.decodeDoubleFlex(c, "snapshotCarbs") ?? 0
snapshotFat = Self.decodeDoubleFlex(c, "snapshotFat") ?? 0
snapshotSugar = Self.decodeDoubleFlex(c, "snapshotSugar")
snapshotFiber = Self.decodeDoubleFlex(c, "snapshotFiber")
foodImagePath = try? c.decode(String.self, forKey: AnyCodingKey("foodImagePath"))
note = try? c.decode(String.self, forKey: AnyCodingKey("note"))
entryMethod = try? c.decode(String.self, forKey: AnyCodingKey("entryMethod"))
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(id, forKey: .id)
try c.encodeIfPresent(userId, forKey: .userId)
try c.encodeIfPresent(foodId, forKey: .foodId)
try c.encode(mealType, forKey: .mealType)
try c.encode(quantity, forKey: .quantity)
try c.encode(entryDate, forKey: .entryDate)
try c.encode(foodName, forKey: .foodName)
try c.encode(calories, forKey: .calories)
try c.encode(protein, forKey: .protein)
try c.encode(carbs, forKey: .carbs)
try c.encode(fat, forKey: .fat)
try c.encodeIfPresent(sugar, forKey: .sugar)
try c.encodeIfPresent(fiber, forKey: .fiber)
try c.encodeIfPresent(servingSize, forKey: .servingSize)
try c.encodeIfPresent(imageFilename, forKey: .imageFilename)
var c = encoder.container(keyedBy: AnyCodingKey.self)
try c.encode(id, forKey: AnyCodingKey("id"))
try c.encodeIfPresent(userId, forKey: AnyCodingKey("userId"))
try c.encodeIfPresent(foodId, forKey: AnyCodingKey("foodId"))
try c.encode(mealType, forKey: AnyCodingKey("mealType"))
try c.encode(quantity, forKey: AnyCodingKey("quantity"))
try c.encode(entryDate, forKey: AnyCodingKey("entryDate"))
try c.encodeIfPresent(snapshotFoodName, forKey: AnyCodingKey("snapshotFoodName"))
try c.encode(snapshotCalories, forKey: AnyCodingKey("snapshotCalories"))
try c.encode(snapshotProtein, forKey: AnyCodingKey("snapshotProtein"))
try c.encode(snapshotCarbs, forKey: AnyCodingKey("snapshotCarbs"))
try c.encode(snapshotFat, forKey: AnyCodingKey("snapshotFat"))
try c.encodeIfPresent(snapshotSugar, forKey: AnyCodingKey("snapshotSugar"))
try c.encodeIfPresent(snapshotFiber, forKey: AnyCodingKey("snapshotFiber"))
try c.encodeIfPresent(foodImagePath, forKey: AnyCodingKey("foodImagePath"))
}
// Flexible Int decoding
private static func decodeIntFlex(_ c: KeyedDecodingContainer<CodingKeys>, _ key: CodingKeys) throws -> Int {
if let v = try? c.decode(Int.self, forKey: key) { return v }
if let v = try? c.decode(Double.self, forKey: key) { return Int(v) }
if let v = try? c.decode(String.self, forKey: key), let i = Int(v) { return i }
throw DecodingError.typeMismatch(Int.self, .init(codingPath: [key], debugDescription: "Expected numeric"))
// Flexible decoding helpers
private static func decodeDoubleFlex(_ c: KeyedDecodingContainer<AnyCodingKey>, _ key: String) -> Double? {
let k = AnyCodingKey(key)
if let v = try? c.decode(Double.self, forKey: k) { return v }
if let v = try? c.decode(Int.self, forKey: k) { return Double(v) }
if let v = try? c.decode(String.self, forKey: k), let d = Double(v) { return d }
return nil
}
// Flexible Double decoding
private static func decodeDoubleFlex(_ c: KeyedDecodingContainer<CodingKeys>, _ key: CodingKeys) throws -> Double {
if let v = try? c.decode(Double.self, forKey: key) { return v }
if let v = try? c.decode(Int.self, forKey: key) { return Double(v) }
if let v = try? c.decode(String.self, forKey: key), let d = Double(v) { return d }
throw DecodingError.typeMismatch(Double.self, .init(codingPath: [key], debugDescription: "Expected numeric"))
private static func decodeStringFlex(_ c: KeyedDecodingContainer<AnyCodingKey>, _ key: String) -> String? {
let k = AnyCodingKey(key)
if let v = try? c.decode(String.self, forKey: k) { return v }
if let v = try? c.decode(Int.self, forKey: k) { return String(v) }
if let v = try? c.decode(Double.self, forKey: k) { return String(Int(v)) }
return nil
}
}
// MARK: - Food Item
// API: id (UUID string), name, brand, base_unit, calories_per_base, protein_per_base, etc.
struct FoodItem: Identifiable, Codable {
let id: Int
let id: String
let name: String
let calories: Double
let protein: Double
let carbs: Double
let fat: Double
let sugar: Double?
let fiber: Double?
let servingSize: String?
let brand: String?
let baseUnit: String?
let caloriesPerBase: Double
let proteinPerBase: Double
let carbsPerBase: Double
let fatPerBase: Double
let sugarPerBase: Double?
let fiberPerBase: Double?
let status: String?
let imageFilename: String?
let favorite: Bool?
enum CodingKeys: String, CodingKey {
case id, name, calories, protein, carbs, fat, sugar, fiber
case servingSize = "serving_size"
case imageFilename = "image_filename"
}
// Computed convenience accessors (used by views that reference .calories, .protein, etc.)
var calories: Double { caloriesPerBase }
var protein: Double { proteinPerBase }
var carbs: Double { carbsPerBase }
var fat: Double { fatPerBase }
var sugar: Double? { sugarPerBase }
var fiber: Double? { fiberPerBase }
var servingSize: String? { baseUnit }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
if let v = try? c.decode(Int.self, forKey: .id) { id = v }
else if let v = try? c.decode(Double.self, forKey: .id) { id = Int(v) }
else { id = 0 }
name = try c.decode(String.self, forKey: .name)
calories = (try? c.decode(Double.self, forKey: .calories)) ?? Double((try? c.decode(Int.self, forKey: .calories)) ?? 0)
protein = (try? c.decode(Double.self, forKey: .protein)) ?? Double((try? c.decode(Int.self, forKey: .protein)) ?? 0)
carbs = (try? c.decode(Double.self, forKey: .carbs)) ?? Double((try? c.decode(Int.self, forKey: .carbs)) ?? 0)
fat = (try? c.decode(Double.self, forKey: .fat)) ?? Double((try? c.decode(Int.self, forKey: .fat)) ?? 0)
sugar = (try? c.decode(Double.self, forKey: .sugar)) ?? (try? c.decode(Int.self, forKey: .sugar)).map { Double($0) }
fiber = (try? c.decode(Double.self, forKey: .fiber)) ?? (try? c.decode(Int.self, forKey: .fiber)).map { Double($0) }
servingSize = try? c.decode(String.self, forKey: .servingSize)
imageFilename = try? c.decode(String.self, forKey: .imageFilename)
let c = try decoder.container(keyedBy: AnyCodingKey.self)
// id can be String or Int
if let v = try? c.decode(String.self, forKey: AnyCodingKey("id")) {
id = v
} else if let v = try? c.decode(Int.self, forKey: AnyCodingKey("id")) {
id = String(v)
} else {
id = ""
}
name = (try? c.decode(String.self, forKey: AnyCodingKey("name"))) ?? ""
brand = try? c.decode(String.self, forKey: AnyCodingKey("brand"))
baseUnit = try? c.decode(String.self, forKey: AnyCodingKey("baseUnit"))
caloriesPerBase = Self.doubleFlex(c, "caloriesPerBase")
proteinPerBase = Self.doubleFlex(c, "proteinPerBase")
carbsPerBase = Self.doubleFlex(c, "carbsPerBase")
fatPerBase = Self.doubleFlex(c, "fatPerBase")
sugarPerBase = Self.doubleFlexOpt(c, "sugarPerBase")
fiberPerBase = Self.doubleFlexOpt(c, "fiberPerBase")
status = try? c.decode(String.self, forKey: AnyCodingKey("status"))
imageFilename = try? c.decode(String.self, forKey: AnyCodingKey("imageFilename"))
favorite = try? c.decode(Bool.self, forKey: AnyCodingKey("favorite"))
}
private static func doubleFlex(_ c: KeyedDecodingContainer<AnyCodingKey>, _ key: String) -> Double {
let k = AnyCodingKey(key)
if let v = try? c.decode(Double.self, forKey: k) { return v }
if let v = try? c.decode(Int.self, forKey: k) { return Double(v) }
if let v = try? c.decode(String.self, forKey: k), let d = Double(v) { return d }
return 0
}
private static func doubleFlexOpt(_ c: KeyedDecodingContainer<AnyCodingKey>, _ key: String) -> Double? {
let k = AnyCodingKey(key)
if let v = try? c.decode(Double.self, forKey: k) { return v }
if let v = try? c.decode(Int.self, forKey: k) { return Double(v) }
if let v = try? c.decode(String.self, forKey: k), let d = Double(v) { return d }
return nil
}
}
// MARK: - Daily Goal
// API: id (UUID), calories, protein, carbs, fat, sugar, fiber, is_active
struct DailyGoal: Codable {
let id: String?
let calories: Double
let protein: Double
let carbs: Double
let fat: Double
let sugar: Double
let fiber: Double
enum CodingKeys: String, CodingKey {
case calories, protein, carbs, fat, sugar, fiber
}
let isActive: Int?
init(calories: Double = 2000, protein: Double = 150, carbs: Double = 250, fat: Double = 65, sugar: Double = 50, fiber: Double = 30) {
self.id = nil
self.calories = calories
self.protein = protein
self.carbs = carbs
self.fat = fat
self.sugar = sugar
self.fiber = fiber
self.isActive = nil
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
calories = (try? c.decode(Double.self, forKey: .calories)) ?? Double((try? c.decode(Int.self, forKey: .calories)) ?? 2000)
protein = (try? c.decode(Double.self, forKey: .protein)) ?? Double((try? c.decode(Int.self, forKey: .protein)) ?? 150)
carbs = (try? c.decode(Double.self, forKey: .carbs)) ?? Double((try? c.decode(Int.self, forKey: .carbs)) ?? 250)
fat = (try? c.decode(Double.self, forKey: .fat)) ?? Double((try? c.decode(Int.self, forKey: .fat)) ?? 65)
sugar = (try? c.decode(Double.self, forKey: .sugar)) ?? Double((try? c.decode(Int.self, forKey: .sugar)) ?? 50)
fiber = (try? c.decode(Double.self, forKey: .fiber)) ?? Double((try? c.decode(Int.self, forKey: .fiber)) ?? 30)
let c = try decoder.container(keyedBy: AnyCodingKey.self)
id = try? c.decode(String.self, forKey: AnyCodingKey("id"))
calories = Self.doubleFlex(c, "calories", default: 2000)
protein = Self.doubleFlex(c, "protein", default: 150)
carbs = Self.doubleFlex(c, "carbs", default: 250)
fat = Self.doubleFlex(c, "fat", default: 65)
sugar = Self.doubleFlex(c, "sugar", default: 50)
fiber = Self.doubleFlex(c, "fiber", default: 30)
if let v = try? c.decode(Int.self, forKey: AnyCodingKey("isActive")) {
isActive = v
} else if let v = try? c.decode(Double.self, forKey: AnyCodingKey("isActive")) {
isActive = Int(v)
} else {
isActive = nil
}
}
private static func doubleFlex(_ c: KeyedDecodingContainer<AnyCodingKey>, _ key: String, default def: Double) -> Double {
let k = AnyCodingKey(key)
if let v = try? c.decode(Double.self, forKey: k) { return v }
if let v = try? c.decode(Int.self, forKey: k) { return Double(v) }
if let v = try? c.decode(String.self, forKey: k), let d = Double(v) { return d }
return def
}
}
// MARK: - Meal Template
// API: id (UUID), name, meal_type, total_calories, total_protein, total_carbs, total_fat, item_count
struct MealTemplate: Identifiable, Codable {
let id: Int
let id: String
let name: String
let mealType: MealType
let totalCalories: Double?
@@ -209,35 +261,45 @@ struct MealTemplate: Identifiable, Codable {
let totalFat: Double?
let itemCount: Int?
enum CodingKeys: String, CodingKey {
case id, name
case mealType = "meal_type"
case totalCalories = "total_calories"
case totalProtein = "total_protein"
case totalCarbs = "total_carbs"
case totalFat = "total_fat"
case itemCount = "item_count"
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: AnyCodingKey.self)
if let v = try? c.decode(String.self, forKey: AnyCodingKey("id")) {
id = v
} else if let v = try? c.decode(Int.self, forKey: AnyCodingKey("id")) {
id = String(v)
} else {
id = ""
}
name = (try? c.decode(String.self, forKey: AnyCodingKey("name"))) ?? ""
mealType = (try? c.decode(MealType.self, forKey: AnyCodingKey("mealType"))) ?? .snack
totalCalories = Self.doubleFlexOpt(c, "totalCalories")
totalProtein = Self.doubleFlexOpt(c, "totalProtein")
totalCarbs = Self.doubleFlexOpt(c, "totalCarbs")
totalFat = Self.doubleFlexOpt(c, "totalFat")
if let v = try? c.decode(Int.self, forKey: AnyCodingKey("itemCount")) {
itemCount = v
} else if let v = try? c.decode(Double.self, forKey: AnyCodingKey("itemCount")) {
itemCount = Int(v)
} else {
itemCount = nil
}
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
if let v = try? c.decode(Int.self, forKey: .id) { id = v }
else if let v = try? c.decode(Double.self, forKey: .id) { id = Int(v) }
else { id = 0 }
name = try c.decode(String.self, forKey: .name)
mealType = try c.decode(MealType.self, forKey: .mealType)
totalCalories = (try? c.decode(Double.self, forKey: .totalCalories)) ?? (try? c.decode(Int.self, forKey: .totalCalories)).map { Double($0) }
totalProtein = (try? c.decode(Double.self, forKey: .totalProtein)) ?? (try? c.decode(Int.self, forKey: .totalProtein)).map { Double($0) }
totalCarbs = (try? c.decode(Double.self, forKey: .totalCarbs)) ?? (try? c.decode(Int.self, forKey: .totalCarbs)).map { Double($0) }
totalFat = (try? c.decode(Double.self, forKey: .totalFat)) ?? (try? c.decode(Int.self, forKey: .totalFat)).map { Double($0) }
itemCount = (try? c.decode(Int.self, forKey: .itemCount)) ?? (try? c.decode(Double.self, forKey: .itemCount)).map { Int($0) }
private static func doubleFlexOpt(_ c: KeyedDecodingContainer<AnyCodingKey>, _ key: String) -> Double? {
let k = AnyCodingKey(key)
if let v = try? c.decode(Double.self, forKey: k) { return v }
if let v = try? c.decode(Int.self, forKey: k) { return Double(v) }
if let v = try? c.decode(String.self, forKey: k), let d = Double(v) { return d }
return nil
}
}
// MARK: - Requests
// Note: APIClient uses encoder.keyEncodingStrategy = .convertToSnakeCase
// so camelCase properties are auto-converted to snake_case in JSON.
struct CreateEntryRequest: Encodable {
let foodId: Int?
let foodId: String?
let foodName: String
let mealType: String
let quantity: Double
@@ -248,15 +310,6 @@ struct CreateEntryRequest: Encodable {
let fat: Double
let sugar: Double?
let fiber: Double?
enum CodingKeys: String, CodingKey {
case foodId = "food_id"
case foodName = "food_name"
case mealType = "meal_type"
case quantity
case entryDate = "entry_date"
case calories, protein, carbs, fat, sugar, fiber
}
}
struct UpdateGoalsRequest: Encodable {
@@ -326,3 +379,25 @@ struct SourceLink: Identifiable {
self.href = (dict["href"] as? String) ?? ""
}
}
// MARK: - AnyCodingKey (flexible key lookup)
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init(_ string: String) {
self.stringValue = string
self.intValue = nil
}
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}

View File

@@ -25,7 +25,7 @@ final class FitnessRepository {
isLoading = false
}
func deleteEntry(id: Int) async {
func deleteEntry(id: String) async {
do {
try await api.deleteEntry(id: id)
entries.removeAll { $0.id == id }
@@ -43,7 +43,7 @@ final class FitnessRepository {
}
}
func updateEntry(id: Int, quantity: Double) async -> FoodEntry? {
func updateEntry(id: String, quantity: Double) async -> FoodEntry? {
do {
let updated = try await api.updateEntry(id: id, quantity: quantity)
if let idx = entries.firstIndex(where: { $0.id == id }) {

View File

@@ -60,7 +60,7 @@ struct AddFoodSheet: View {
Button { if quantity > 0.5 { quantity -= 0.5 } } label: {
Image(systemName: "minus.circle.fill")
.font(.title2)
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
Text(String(format: "%.1f", quantity))
.font(.title2.bold())
@@ -68,7 +68,7 @@ struct AddFoodSheet: View {
Button { quantity += 0.5 } label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
}
}
@@ -141,7 +141,7 @@ struct AddFoodSheet: View {
.frame(height: 48)
}
.buttonStyle(.borderedProminent)
.tint(Color.accent)
.tint(Color.accentWarm)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.padding(20)

View File

@@ -51,7 +51,7 @@ struct EntryDetailView: View {
Button { if quantity > 0.5 { quantity -= 0.5 } } label: {
Image(systemName: "minus.circle.fill")
.font(.title2)
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
Text(String(format: "%.1f", quantity))
.font(.title2.bold())
@@ -59,7 +59,7 @@ struct EntryDetailView: View {
Button { quantity += 0.5 } label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
}
}
@@ -85,7 +85,7 @@ struct EntryDetailView: View {
.frame(height: 44)
}
.buttonStyle(.borderedProminent)
.tint(Color.accent)
.tint(Color.accentWarm)
.clipShape(RoundedRectangle(cornerRadius: 12))
}

View File

@@ -27,12 +27,12 @@ struct FitnessTabView: View {
Text(tab.rawValue)
.font(.subheadline)
.fontWeight(selectedTab == tab ? .bold : .medium)
.foregroundStyle(selectedTab == tab ? Color.accent : Color.textSecondary)
.foregroundStyle(selectedTab == tab ? Color.accentWarm : Color.textSecondary)
.padding(.bottom, 8)
.overlay(alignment: .bottom) {
if selectedTab == tab {
Rectangle()
.fill(Color.accent)
.fill(Color.accentWarm)
.frame(height: 2)
}
}
@@ -64,9 +64,9 @@ struct FitnessTabView: View {
.font(.title2.bold())
.foregroundStyle(.white)
.frame(width: 56, height: 56)
.background(Color.accent)
.background(Color.accentWarm)
.clipShape(Circle())
.shadow(color: Color.accent.opacity(0.3), radius: 8, y: 4)
.shadow(color: Color.accentWarm.opacity(0.3), radius: 8, y: 4)
}
.padding(.trailing, 20)
.padding(.bottom, 20)

View File

@@ -112,7 +112,7 @@ struct FoodSearchView: View {
}
Spacer()
Image(systemName: "plus.circle")
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
}
}

View File

@@ -38,7 +38,7 @@ struct GoalsView: View {
.frame(height: 48)
}
.buttonStyle(.borderedProminent)
.tint(Color.accent)
.tint(Color.accentWarm)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 16)
.disabled(vm.isSaving)

View File

@@ -90,7 +90,7 @@ struct TemplatesView: View {
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.accent)
.background(Color.accentWarm)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}

View File

@@ -12,7 +12,7 @@ struct TodayView: View {
Button { viewModel.previousDay() } label: {
Image(systemName: "chevron.left")
.font(.headline)
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
Spacer()
Text(viewModel.displayDate)
@@ -22,7 +22,7 @@ struct TodayView: View {
Button { viewModel.nextDay() } label: {
Image(systemName: "chevron.right")
.font(.headline)
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
}
.padding(.horizontal, 20)

View File

@@ -58,7 +58,7 @@ struct HomeView: View {
} label: {
Image(systemName: "person.circle.fill")
.font(.title2)
.foregroundStyle(vm.backgroundImage != nil ? .white : Color.accent)
.foregroundStyle(vm.backgroundImage != nil ? .white : Color.accentWarm)
}
}
.padding(.horizontal, 20)
@@ -83,9 +83,9 @@ struct HomeView: View {
.font(.title2.bold())
.foregroundStyle(.white)
.frame(width: 56, height: 56)
.background(Color.accent)
.background(Color.accentWarm)
.clipShape(Circle())
.shadow(color: Color.accent.opacity(0.3), radius: 8, y: 4)
.shadow(color: Color.accentWarm.opacity(0.3), radius: 8, y: 4)
}
.padding(.trailing, 20)
.padding(.bottom, 20)

View File

@@ -6,7 +6,7 @@ struct LoadingView: View {
var body: some View {
VStack(spacing: 12) {
ProgressView()
.tint(.accent)
.tint(.accentWarm)
Text(message)
.font(.subheadline)
.foregroundStyle(Color.textSecondary)
@@ -32,7 +32,7 @@ struct ErrorBanner: View {
Task { await retry() }
}
.font(.subheadline.bold())
.foregroundStyle(Color.accent)
.foregroundStyle(Color.accentWarm)
}
}
.padding(12)

View File

@@ -27,8 +27,8 @@ extension Color {
static let canvas = Color(hex: "F5EFE6")
static let surface = Color(hex: "FFFFFF")
static let surfaceSecondary = Color(hex: "FAF7F2")
static let accent = Color(hex: "8B6914")
static let accentLight = Color(hex: "D4A843")
static let accentWarm = Color(hex: "8B6914")
static let accentWarmLight = Color(hex: "D4A843")
static let emerald = Color(hex: "059669")
static let textPrimary = Color(hex: "1C1917")
static let textSecondary = Color(hex: "78716C")