a4dfb38feb
- Dish.isPriority: mark dishes as priority; auto-assign gives them 2.5x weight - Tag.dayRestriction: restrict tags to weekdays or weekend only - Auto-assign CTA banner shown when there are empty slots (was hidden in context menu) - Tap on filled slot opens picker to replace dish directly (no need to remove first) - Export callout visible as soon as ≥1 slot is filled (not only when week is complete) - WeekPlanShareView shows localized "TBD" / "Eating out" for empty/eating-out slots - Second Sunday 17:00 push notification to plan next week - DishDrawer sorts: priority first, then by historical usage, then alphabetically - Search in SlotDishPickerSheet already shipped in 1.0.5 (verified ✓) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
97 lines
2.9 KiB
Swift
97 lines
2.9 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
@MainActor
|
|
final class DishViewModel: ObservableObject {
|
|
@Published var name: String = ""
|
|
@Published var descriptionText: String = ""
|
|
@Published var selectedTagIds: Set<UUID> = []
|
|
@Published var isPriority: Bool = false
|
|
@Published var showTagSelector: Bool = false
|
|
@Published var showDeleteAlert: Bool = false
|
|
@Published var showAssignedDeleteAlert: Bool = false
|
|
|
|
var editingDish: Dish?
|
|
|
|
var isEditing: Bool { editingDish != nil }
|
|
|
|
var isValid: Bool {
|
|
!name.trimmingCharacters(in: .whitespaces).isEmpty
|
|
}
|
|
|
|
func loadDish(_ dish: Dish) {
|
|
editingDish = dish
|
|
name = dish.name
|
|
descriptionText = dish.descriptionText ?? ""
|
|
selectedTagIds = Set(dish.tagIds)
|
|
isPriority = dish.isPriority
|
|
}
|
|
|
|
func reset() {
|
|
editingDish = nil
|
|
name = ""
|
|
descriptionText = ""
|
|
selectedTagIds = []
|
|
isPriority = false
|
|
}
|
|
|
|
func save(context: ModelContext) {
|
|
let trimmedName = name.trimmingCharacters(in: .whitespaces)
|
|
guard !trimmedName.isEmpty else { return }
|
|
|
|
if let dish = editingDish {
|
|
dish.name = trimmedName
|
|
dish.descriptionText = descriptionText.isEmpty ? nil : descriptionText
|
|
dish.tagIds = Array(selectedTagIds)
|
|
dish.isPriority = isPriority
|
|
} else {
|
|
let dish = Dish(
|
|
name: trimmedName,
|
|
descriptionText: descriptionText.isEmpty ? nil : descriptionText,
|
|
tagIds: Array(selectedTagIds),
|
|
isPriority: isPriority
|
|
)
|
|
context.insert(dish)
|
|
AnalyticsService.logDishAdded(tagCount: selectedTagIds.count)
|
|
}
|
|
|
|
try? context.save()
|
|
reset()
|
|
}
|
|
|
|
func deleteDish(context: ModelContext) -> Bool {
|
|
guard let dish = editingDish else { return false }
|
|
|
|
if isDishAssignedInCurrentWeek(dishId: dish.id, context: context) {
|
|
showAssignedDeleteAlert = true
|
|
return false
|
|
}
|
|
|
|
context.delete(dish)
|
|
try? context.save()
|
|
AnalyticsService.logDishDeleted()
|
|
reset()
|
|
return true
|
|
}
|
|
|
|
func canDelete(currentPlan: WeekPlan?) -> Bool {
|
|
guard let dish = editingDish, let plan = currentPlan else { return true }
|
|
return !plan.slots.contains { $0.dishId == dish.id }
|
|
}
|
|
|
|
private func isDishAssignedInCurrentWeek(dishId: UUID, context: ModelContext) -> Bool {
|
|
let currentWeekStart = Date().startOfWeek()
|
|
let descriptor = FetchDescriptor<WeekPlan>(
|
|
predicate: #Predicate<WeekPlan> { plan in
|
|
plan.weekStartDate == currentWeekStart
|
|
}
|
|
)
|
|
|
|
guard let currentPlan = try? context.fetch(descriptor).first else {
|
|
return false
|
|
}
|
|
|
|
return currentPlan.slots.contains { $0.dishId == dishId }
|
|
}
|
|
}
|