Add meal plan feature with cross-device sync and automatic stale data cleanup
Introduces weekly meal planning with a calendar-based tab view, per-recipe date assignments synced via Nextcloud Cookbook custom metadata, and 30-day automatic pruning of old entries on load, save, and sync merge. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
83
Nextcloud Cookbook iOS Client/Data/MealPlanModels.swift
Normal file
83
Nextcloud Cookbook iOS Client/Data/MealPlanModels.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// MealPlanModels.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Tracks meal plan assignments for a recipe, stored as `_mealPlanAssignment` in the recipe JSON on the server.
|
||||
struct MealPlanAssignment: Codable {
|
||||
var version: Int = 1
|
||||
var lastModified: String
|
||||
var dates: [String: MealPlanDateEntry]
|
||||
|
||||
init(lastModified: String = MealPlanDate.now(), dates: [String: MealPlanDateEntry] = [:]) {
|
||||
self.version = 1
|
||||
self.lastModified = lastModified
|
||||
self.dates = dates
|
||||
}
|
||||
}
|
||||
|
||||
struct MealPlanDateEntry: Codable {
|
||||
enum Status: String, Codable {
|
||||
case assigned
|
||||
case removed
|
||||
}
|
||||
|
||||
var status: Status
|
||||
var mealType: String?
|
||||
var modifiedAt: String
|
||||
|
||||
init(status: Status, mealType: String? = nil, modifiedAt: String = MealPlanDate.now()) {
|
||||
self.status = status
|
||||
self.mealType = mealType
|
||||
self.modifiedAt = modifiedAt
|
||||
}
|
||||
}
|
||||
|
||||
/// ISO 8601 date helpers for meal plan dates.
|
||||
enum MealPlanDate {
|
||||
private static let isoFormatter: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
return f
|
||||
}()
|
||||
|
||||
private static let dayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "yyyy-MM-dd"
|
||||
f.timeZone = .current
|
||||
return f
|
||||
}()
|
||||
|
||||
static func now() -> String {
|
||||
isoFormatter.string(from: Date())
|
||||
}
|
||||
|
||||
static func date(from string: String) -> Date? {
|
||||
isoFormatter.date(from: string)
|
||||
}
|
||||
|
||||
static func string(from date: Date) -> String {
|
||||
isoFormatter.string(from: date)
|
||||
}
|
||||
|
||||
static func dayString(from date: Date) -> String {
|
||||
dayFormatter.string(from: date)
|
||||
}
|
||||
|
||||
static func dateFromDay(_ dayString: String) -> Date? {
|
||||
dayFormatter.date(from: dayString)
|
||||
}
|
||||
}
|
||||
|
||||
/// Local-only aggregated view struct used by the UI.
|
||||
struct MealPlanEntry: Identifiable {
|
||||
let recipeId: String
|
||||
let recipeName: String
|
||||
let date: Date
|
||||
let dateString: String
|
||||
let mealType: String?
|
||||
|
||||
var id: String { "\(recipeId)-\(dateString)" }
|
||||
}
|
||||
Reference in New Issue
Block a user