From a4dfb38feb5df8f5c48553334a854d0cb218999b Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Thu, 30 Apr 2026 00:00:53 +0200 Subject: [PATCH] 1.0.6: Priority dishes, day rules, auto-assign CTA, tap-to-replace, export incomplete weeks, Sunday notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- MealMood.xcodeproj/project.pbxproj | 4 +- .../UserInterfaceState.xcuserstate | Bin 45796 -> 39889 bytes .../xcschemes/xcschememanagement.plist | 12 +++ MealMood/Models/Dish.swift | 5 +- MealMood/Models/Tag.swift | 12 +++ MealMood/Resources/Info.plist | 4 +- .../Resources/de.lproj/Localizable.strings | 14 +++ .../Resources/en.lproj/Localizable.strings | 14 +++ .../Resources/es.lproj/Localizable.strings | 14 +++ .../Resources/fr.lproj/Localizable.strings | 14 +++ .../Resources/it.lproj/Localizable.strings | 14 +++ .../Resources/pt-BR.lproj/Localizable.strings | 14 +++ MealMood/Services/AutocompleteEngine.swift | 16 ++- MealMood/Services/NotificationService.swift | 47 +++++++-- MealMood/ViewModels/DishViewModel.swift | 7 +- MealMood/ViewModels/TagViewModel.swift | 3 + MealMood/Views/Dishes/DishFormView.swift | 24 +++++ MealMood/Views/Home/DishDrawerView.swift | 25 ++++- MealMood/Views/Home/HomeView.swift | 96 +++++++++++++++++- MealMood/Views/Home/WeekCalendarView.swift | 7 ++ MealMood/Views/Home/WeekPlanShareView.swift | 6 +- MealMood/Views/Tags/TagRulesEditView.swift | 18 ++++ 22 files changed, 344 insertions(+), 26 deletions(-) diff --git a/MealMood.xcodeproj/project.pbxproj b/MealMood.xcodeproj/project.pbxproj index 84a2798..4f75ac7 100644 --- a/MealMood.xcodeproj/project.pbxproj +++ b/MealMood.xcodeproj/project.pbxproj @@ -860,7 +860,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = MealMood/Resources/Info.plist; @@ -965,7 +965,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MealMood/Resources/MealMood.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 24; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = MealMood/Resources/Info.plist; diff --git a/MealMood.xcodeproj/project.xcworkspace/xcuserdata/alexandrev.xcuserdatad/UserInterfaceState.xcuserstate b/MealMood.xcodeproj/project.xcworkspace/xcuserdata/alexandrev.xcuserdatad/UserInterfaceState.xcuserstate index 6d010367431d4073906d8e0ba90ae34006e16f69..dd15f395c4a69290b4f8ea9195f71706354bb6cd 100644 GIT binary patch literal 39889 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 gZ)|Yv#n{WSv9avfb%7 literal 45796 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 isShown + orderHint + 1 + + MealMood.xcscheme_^#shared#^_ + + orderHint + 0 + + MealMoodWidget.xcscheme_^#shared#^_ + + orderHint + 2 diff --git a/MealMood/Models/Dish.swift b/MealMood/Models/Dish.swift index 75808df..6ffc44e 100644 --- a/MealMood/Models/Dish.swift +++ b/MealMood/Models/Dish.swift @@ -8,19 +8,22 @@ final class Dish { var descriptionText: String? var tagIds: [UUID] var createdAt: Date + var isPriority: Bool init( id: UUID = UUID(), name: String, descriptionText: String? = nil, tagIds: [UUID] = [], - createdAt: Date = Date() + createdAt: Date = Date(), + isPriority: Bool = false ) { self.id = id self.name = name self.descriptionText = descriptionText self.tagIds = tagIds self.createdAt = createdAt + self.isPriority = isPriority } } diff --git a/MealMood/Models/Tag.swift b/MealMood/Models/Tag.swift index 0012b1c..5e6ca2d 100644 --- a/MealMood/Models/Tag.swift +++ b/MealMood/Models/Tag.swift @@ -11,6 +11,7 @@ final class Tag { var noConsecutive: Bool var noDuplicateInDay: Bool var mealTypeRestriction: String? // "lunch", "dinner", nil + var dayRestriction: String? // nil = any day, "weekdays" = Mon-Fri, "weekend" = Sat-Sun var isDefault: Bool var sortOrder: Int @@ -23,6 +24,7 @@ final class Tag { noConsecutive: Bool = false, noDuplicateInDay: Bool = false, mealTypeRestriction: String? = nil, + dayRestriction: String? = nil, isDefault: Bool = true, sortOrder: Int = 0 ) { @@ -34,6 +36,7 @@ final class Tag { self.noConsecutive = noConsecutive self.noDuplicateInDay = noDuplicateInDay self.mealTypeRestriction = mealTypeRestriction + self.dayRestriction = dayRestriction self.isDefault = isDefault self.sortOrder = sortOrder } @@ -76,6 +79,15 @@ final class Tag { parts.append(restriction == "lunch" ? "lunch only" : "dinner only") } } + if let day = dayRestriction { + switch (day, resolvedLanguage) { + case ("weekdays", .spanish): parts.append("entre semana") + case ("weekdays", _): parts.append("weekdays") + case ("weekend", .spanish): parts.append("fin de semana") + case ("weekend", _): parts.append("weekend") + default: break + } + } return parts.isEmpty ? (resolvedLanguage == .spanish ? "Sin límite" : "No limit") : parts.joined(separator: ", ") } } diff --git a/MealMood/Resources/Info.plist b/MealMood/Resources/Info.plist index f4ab0fc..f006633 100644 --- a/MealMood/Resources/Info.plist +++ b/MealMood/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.5 + 1.0.6 CFBundleVersion - 24 + 32 GADApplicationIdentifier ca-app-pub-1549720748100858~9985112590 LSRequiresIPhoneOS diff --git a/MealMood/Resources/de.lproj/Localizable.strings b/MealMood/Resources/de.lproj/Localizable.strings index a13b378..06a548c 100644 --- a/MealMood/Resources/de.lproj/Localizable.strings +++ b/MealMood/Resources/de.lproj/Localizable.strings @@ -310,3 +310,17 @@ "violations_fix" = "Slot leeren"; "violations_ignore" = "Ignorieren"; "violations_badge" = "%d Konflikt(e)"; + +// 1.0.6 +"dish_priority" = "Prioritätsgericht"; +"dish_priority_desc" = "Wird bei der Auto-Vervollständigung bevorzugt"; +"tags_day_restriction" = "Tageseinschränkung"; +"tags_day_any" = "Beliebiger Tag"; +"tags_day_weekdays" = "Nur Wochentage"; +"tags_day_weekend" = "Nur Wochenende"; +"share_slot_empty" = "Noch offen"; +"home_auto_assign_cta_title" = "Woche automatisch füllen"; +"home_auto_assign_cta_slots" = "%d freie Slots"; +"home_auto_assign_cta_button" = "Auto-ausfüllen"; +"notification_sunday_title" = "Plane deine nächste Woche"; +"notification_sunday_body" = "Es ist Sonntag — nimm dir 2 Minuten, um die Mahlzeiten der nächsten Woche zu planen."; diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings index a8023d3..97e360c 100644 --- a/MealMood/Resources/en.lproj/Localizable.strings +++ b/MealMood/Resources/en.lproj/Localizable.strings @@ -310,3 +310,17 @@ "violations_fix" = "Clear slot"; "violations_ignore" = "Ignore"; "violations_badge" = "%d conflict(s)"; + +// 1.0.6 +"dish_priority" = "Priority dish"; +"dish_priority_desc" = "Will be prioritized in auto-assign"; +"tags_day_restriction" = "Day restriction"; +"tags_day_any" = "Any day"; +"tags_day_weekdays" = "Weekdays only"; +"tags_day_weekend" = "Weekend only"; +"share_slot_empty" = "TBD"; +"home_auto_assign_cta_title" = "Fill your week automatically"; +"home_auto_assign_cta_slots" = "%d empty slots"; +"home_auto_assign_cta_button" = "Auto-assign"; +"notification_sunday_title" = "Plan your next week"; +"notification_sunday_body" = "Sunday is here — take 2 minutes to plan next week's meals."; diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings index ea72431..fd3fb89 100644 --- a/MealMood/Resources/es.lproj/Localizable.strings +++ b/MealMood/Resources/es.lproj/Localizable.strings @@ -310,3 +310,17 @@ "violations_fix" = "Vaciar hueco"; "violations_ignore" = "Ignorar"; "violations_badge" = "%d conflicto(s)"; + +// 1.0.6 +"dish_priority" = "Plato prioritario"; +"dish_priority_desc" = "Se priorizará en la asignación automática"; +"tags_day_restriction" = "Restricción de días"; +"tags_day_any" = "Cualquier día"; +"tags_day_weekdays" = "Solo entre semana"; +"tags_day_weekend" = "Solo fin de semana"; +"share_slot_empty" = "Por definir"; +"home_auto_assign_cta_title" = "Rellena tu semana automáticamente"; +"home_auto_assign_cta_slots" = "%d huecos vacíos"; +"home_auto_assign_cta_button" = "Auto-rellenar"; +"notification_sunday_title" = "Planifica la semana que viene"; +"notification_sunday_body" = "Es domingo — tómate 2 minutos para planificar las comidas de la próxima semana."; diff --git a/MealMood/Resources/fr.lproj/Localizable.strings b/MealMood/Resources/fr.lproj/Localizable.strings index c77d3fd..59f81e3 100644 --- a/MealMood/Resources/fr.lproj/Localizable.strings +++ b/MealMood/Resources/fr.lproj/Localizable.strings @@ -310,3 +310,17 @@ "violations_fix" = "Vider le créneau"; "violations_ignore" = "Ignorer"; "violations_badge" = "%d conflit(s)"; + +// 1.0.6 +"dish_priority" = "Plat prioritaire"; +"dish_priority_desc" = "Sera priorisé dans l'auto-complétion"; +"tags_day_restriction" = "Restriction de jours"; +"tags_day_any" = "N'importe quel jour"; +"tags_day_weekdays" = "Jours de semaine seulement"; +"tags_day_weekend" = "Week-end seulement"; +"share_slot_empty" = "À définir"; +"home_auto_assign_cta_title" = "Remplir la semaine automatiquement"; +"home_auto_assign_cta_slots" = "%d créneaux vides"; +"home_auto_assign_cta_button" = "Auto-remplir"; +"notification_sunday_title" = "Planifiez votre prochaine semaine"; +"notification_sunday_body" = "C'est dimanche — prenez 2 minutes pour planifier vos repas de la semaine prochaine."; diff --git a/MealMood/Resources/it.lproj/Localizable.strings b/MealMood/Resources/it.lproj/Localizable.strings index 9aa0a53..e7bfdf9 100644 --- a/MealMood/Resources/it.lproj/Localizable.strings +++ b/MealMood/Resources/it.lproj/Localizable.strings @@ -310,3 +310,17 @@ "violations_fix" = "Svuota slot"; "violations_ignore" = "Ignora"; "violations_badge" = "%d conflitto/i"; + +// 1.0.6 +"dish_priority" = "Piatto prioritario"; +"dish_priority_desc" = "Verrà prioritizzato nell'auto-completamento"; +"tags_day_restriction" = "Restrizione giorni"; +"tags_day_any" = "Qualsiasi giorno"; +"tags_day_weekdays" = "Solo giorni feriali"; +"tags_day_weekend" = "Solo weekend"; +"share_slot_empty" = "Da definire"; +"home_auto_assign_cta_title" = "Riempi la settimana automaticamente"; +"home_auto_assign_cta_slots" = "%d slot vuoti"; +"home_auto_assign_cta_button" = "Auto-assegna"; +"notification_sunday_title" = "Pianifica la prossima settimana"; +"notification_sunday_body" = "È domenica — prenditi 2 minuti per pianificare i pasti della prossima settimana."; diff --git a/MealMood/Resources/pt-BR.lproj/Localizable.strings b/MealMood/Resources/pt-BR.lproj/Localizable.strings index 139de64..5a332ae 100644 --- a/MealMood/Resources/pt-BR.lproj/Localizable.strings +++ b/MealMood/Resources/pt-BR.lproj/Localizable.strings @@ -310,3 +310,17 @@ "violations_fix" = "Limpar slot"; "violations_ignore" = "Ignorar"; "violations_badge" = "%d conflito(s)"; + +// 1.0.6 +"dish_priority" = "Prato prioritário"; +"dish_priority_desc" = "Será priorizado na atribuição automática"; +"tags_day_restriction" = "Restrição de dias"; +"tags_day_any" = "Qualquer dia"; +"tags_day_weekdays" = "Apenas dias úteis"; +"tags_day_weekend" = "Apenas fim de semana"; +"share_slot_empty" = "A definir"; +"home_auto_assign_cta_title" = "Preencher a semana automaticamente"; +"home_auto_assign_cta_slots" = "%d slots vazios"; +"home_auto_assign_cta_button" = "Auto-atribuir"; +"notification_sunday_title" = "Planeje a próxima semana"; +"notification_sunday_body" = "É domingo — reserve 2 minutos para planejar as refeições da próxima semana."; diff --git a/MealMood/Services/AutocompleteEngine.swift b/MealMood/Services/AutocompleteEngine.swift index 330886d..a425d6f 100644 --- a/MealMood/Services/AutocompleteEngine.swift +++ b/MealMood/Services/AutocompleteEngine.swift @@ -166,6 +166,15 @@ struct AutocompleteEngine { if let restriction = tag.mealTypeRestriction, slot.mealType != restriction { return true } + + // Day restriction + if let dayRestriction = tag.dayRestriction { + switch dayRestriction { + case "weekdays" where slot.dayOfWeek > 4: return true + case "weekend" where slot.dayOfWeek < 5: return true + default: break + } + } } return false } @@ -187,10 +196,11 @@ struct AutocompleteEngine { guard !candidates.isEmpty else { return nil } let weights: [Double] = candidates.map { dish in let usage = plan.slots.filter { $0.dishId == dish.id }.count + let priorityBoost: Double = dish.isPriority ? 2.5 : 1.0 switch usage { - case 0: return 3.0 - case 1: return 2.0 - default: return 1.0 + case 0: return 3.0 * priorityBoost + case 1: return 2.0 * priorityBoost + default: return 1.0 * priorityBoost } } let total = weights.reduce(0, +) diff --git a/MealMood/Services/NotificationService.swift b/MealMood/Services/NotificationService.swift index c252899..5d977d3 100644 --- a/MealMood/Services/NotificationService.swift +++ b/MealMood/Services/NotificationService.swift @@ -18,23 +18,52 @@ final class NotificationService { let center = UNUserNotificationCenter.current() let identifier = "mealmood.next-week-planning" - let isComplete = nextWeekPlan?.slots.allSatisfy { $0.dishId != nil } ?? false + let isComplete = nextWeekPlan?.slots.allSatisfy { $0.dishId != nil || $0.isEatingOut } ?? false if isComplete { center.removePendingNotificationRequests(withIdentifiers: [identifier]) + } else { + let nextWeekStart = Date().startOfWeek().addingDays(7) + let reminderDay = nextWeekStart.addingDays(-1) + let calendar = Calendar.current + var components = calendar.dateComponents([.year, .month, .day], from: reminderDay) + components.hour = 19 + components.minute = 0 + + let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) + let content = UNMutableNotificationContent() + content.title = localizedString("notification_planning_title", language: language) + content.body = localizedString("notification_planning_body", language: language) + content.sound = .default + + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) + center.removePendingNotificationRequests(withIdentifiers: [identifier]) + center.add(request) + } + + scheduleSundayReminderIfNeeded(nextWeekPlan: nextWeekPlan, language: language) + } + + private func scheduleSundayReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) { + let center = UNUserNotificationCenter.current() + let identifier = "mealmood.sunday-planning" + + // Cancel if next week is already planned + let hasAnySlot = nextWeekPlan?.slots.contains { $0.dishId != nil || $0.isEatingOut } ?? false + if hasAnySlot { + center.removePendingNotificationRequests(withIdentifiers: [identifier]) return } - let nextWeekStart = Date().startOfWeek().addingDays(7) - let reminderDay = nextWeekStart.addingDays(-1) - let calendar = Calendar.current - var components = calendar.dateComponents([.year, .month, .day], from: reminderDay) - components.hour = 19 + // Schedule for next Sunday at 17:00 + var components = DateComponents() + components.weekday = 1 // Sunday + components.hour = 17 components.minute = 0 - let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) + let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) let content = UNMutableNotificationContent() - content.title = localizedString("notification_planning_title", language: language) - content.body = localizedString("notification_planning_body", language: language) + content.title = localizedString("notification_sunday_title", language: language) + content.body = localizedString("notification_sunday_body", language: language) content.sound = .default let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) diff --git a/MealMood/ViewModels/DishViewModel.swift b/MealMood/ViewModels/DishViewModel.swift index 4c345da..0e60188 100644 --- a/MealMood/ViewModels/DishViewModel.swift +++ b/MealMood/ViewModels/DishViewModel.swift @@ -6,6 +6,7 @@ final class DishViewModel: ObservableObject { @Published var name: String = "" @Published var descriptionText: String = "" @Published var selectedTagIds: Set = [] + @Published var isPriority: Bool = false @Published var showTagSelector: Bool = false @Published var showDeleteAlert: Bool = false @Published var showAssignedDeleteAlert: Bool = false @@ -23,6 +24,7 @@ final class DishViewModel: ObservableObject { name = dish.name descriptionText = dish.descriptionText ?? "" selectedTagIds = Set(dish.tagIds) + isPriority = dish.isPriority } func reset() { @@ -30,6 +32,7 @@ final class DishViewModel: ObservableObject { name = "" descriptionText = "" selectedTagIds = [] + isPriority = false } func save(context: ModelContext) { @@ -40,11 +43,13 @@ final class DishViewModel: ObservableObject { 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) + tagIds: Array(selectedTagIds), + isPriority: isPriority ) context.insert(dish) AnalyticsService.logDishAdded(tagCount: selectedTagIds.count) diff --git a/MealMood/ViewModels/TagViewModel.swift b/MealMood/ViewModels/TagViewModel.swift index 2cca259..69104f9 100644 --- a/MealMood/ViewModels/TagViewModel.swift +++ b/MealMood/ViewModels/TagViewModel.swift @@ -8,6 +8,7 @@ final class TagViewModel: ObservableObject { @Published var noConsecutive: Bool = false @Published var noDuplicateInDay: Bool = false @Published var mealTypeRestriction: String? = nil + @Published var dayRestriction: String? = nil @Published var useMaxLimit: Bool = true func loadTag(_ tag: Tag) { @@ -16,6 +17,7 @@ final class TagViewModel: ObservableObject { noConsecutive = tag.noConsecutive noDuplicateInDay = tag.noDuplicateInDay mealTypeRestriction = tag.mealTypeRestriction + dayRestriction = tag.dayRestriction useMaxLimit = tag.maxPerWeek != nil } @@ -25,6 +27,7 @@ final class TagViewModel: ObservableObject { tag.noConsecutive = noConsecutive tag.noDuplicateInDay = noDuplicateInDay tag.mealTypeRestriction = mealTypeRestriction + tag.dayRestriction = dayRestriction } func reset() { diff --git a/MealMood/Views/Dishes/DishFormView.swift b/MealMood/Views/Dishes/DishFormView.swift index 623d529..66348d2 100644 --- a/MealMood/Views/Dishes/DishFormView.swift +++ b/MealMood/Views/Dishes/DishFormView.swift @@ -130,6 +130,30 @@ struct DishFormView: View { } } + // Priority toggle + Toggle(isOn: Binding( + get: { viewModel.isPriority }, + set: { viewModel.isPriority = $0 } + )) { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 6) { + Image(systemName: "star.fill") + .foregroundColor(.mealMoodCoral) + .font(.system(size: 13)) + Text("dish_priority") + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + } + Text("dish_priority_desc") + .font(.mealMoodCaption) + .foregroundColor(.mealMoodTextSecondary) + } + } + .tint(.mealMoodCoral) + .padding(16) + .background(Color.mealMoodSurface) + .cornerRadius(14) + if showDishLimitUpsell && reachedFreeDishLimit { PremiumUpsellBanner( messageKey: "premium_limit_dishes", diff --git a/MealMood/Views/Home/DishDrawerView.swift b/MealMood/Views/Home/DishDrawerView.swift index 623ad29..3b0e919 100644 --- a/MealMood/Views/Home/DishDrawerView.swift +++ b/MealMood/Views/Home/DishDrawerView.swift @@ -5,6 +5,7 @@ struct DishDrawerView: View { let tags: [Tag] let language: AppLanguage let usedDishIds: Set + var usageRanking: [UUID: Int] = [:] var onAddDish: () -> Void var onQuickAssignDish: (Dish) -> Void var onEditDish: (Dish) -> Void @@ -12,10 +13,17 @@ struct DishDrawerView: View { @Binding var draggedDish: Dish? @State private var searchText: String = "" @State private var hideUsedThisWeek: Bool = false - + private var filteredDishes: [Dish] { let term = searchText.trimmingCharacters(in: .whitespacesAndNewlines) - return Dish.stableSortedForDisplay(dishes).filter { dish in + let sorted = dishes.sorted { lhs, rhs in + if lhs.isPriority != rhs.isPriority { return lhs.isPriority } + let lhsUsage = usageRanking[lhs.id] ?? 0 + let rhsUsage = usageRanking[rhs.id] ?? 0 + if lhsUsage != rhsUsage { return lhsUsage > rhsUsage } + return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending + } + return sorted.filter { dish in let matchesSearch = term.isEmpty || dish.name.localizedCaseInsensitiveContains(term) let matchesUsedFilter = !hideUsedThisWeek || !usedDishIds.contains(dish.id) return matchesSearch && matchesUsedFilter @@ -159,9 +167,16 @@ struct DishCardView: View { var body: some View { HStack { VStack(alignment: .leading, spacing: 4) { - Text(dish.name) - .font(.mealMoodBodyBold) - .foregroundColor(.mealMoodTextPrimary) + HStack(spacing: 4) { + if dish.isPriority { + Image(systemName: "star.fill") + .font(.system(size: 11)) + .foregroundColor(.mealMoodCoral) + } + Text(dish.name) + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + } if let desc = dish.descriptionText, !desc.isEmpty { Text(desc) diff --git a/MealMood/Views/Home/HomeView.swift b/MealMood/Views/Home/HomeView.swift index b688fe9..2aa0130 100644 --- a/MealMood/Views/Home/HomeView.swift +++ b/MealMood/Views/Home/HomeView.swift @@ -17,6 +17,7 @@ struct HomeView: View { @State private var showWeekLimitUpsell: Bool = false @State private var wasWeekComplete: Bool = false @State private var selectedEmptySlotId: UUID? + @State private var selectedFilledSlotId: UUID? @State private var showWeekPicker: Bool = false @State private var weekPickerDate: Date = Date() @State private var showCopyPreviousConfirm: Bool = false @@ -225,13 +226,22 @@ struct HomeView: View { viewModel: viewModel, onTapEmptySlot: { slot in selectedEmptySlotId = slot.id + }, + onTapFilledSlot: { slot in + selectedFilledSlotId = slot.id } ) - if isWeekComplete(plan: plan) { + let filledCount = plan.slots.filter { $0.dishId != nil || $0.isEatingOut }.count + if filledCount > 0 { exportCallout(plan: plan, settings: settings) } + let emptyCount = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut }.count + if viewModel.canEditCurrentWeek && !dishes.isEmpty && emptyCount > 0 { + autoAssignBanner(emptyCount: emptyCount, plan: plan, settings: settings) + } + ScrollView(showsIndicators: true) { dishDrawer(plan: plan, settings: settings) .padding(.bottom, 0) @@ -403,6 +413,36 @@ struct HomeView: View { ) } } + .sheet( + isPresented: Binding( + get: { selectedFilledSlotId != nil }, + set: { isPresented in + if !isPresented { selectedFilledSlotId = nil } + } + ) + ) { + if let slotId = selectedFilledSlotId, + plan.slots.contains(where: { $0.id == slotId }) { + SlotDishPickerSheet( + dishes: dishes, + tags: tags, + onPickDish: { dish in + guard let freshSlot = plan.slots.first(where: { $0.id == slotId }) else { + selectedFilledSlotId = nil + return + } + viewModel.assignDish(dish, to: freshSlot, plan: plan, settings: settings, isOverride: true) + selectedFilledSlotId = nil + }, + onCreateDish: { + selectedFilledSlotId = nil + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + viewModel.showDishForm = true + } + } + ) + } + } .sheet(item: $editingDish) { dish in DishFormView(dish: dish) } @@ -591,6 +631,7 @@ struct HomeView: View { tags: tags, language: settings.languageEnum.resolved(), usedDishIds: Set(plan.slots.compactMap(\.dishId)), + usageRanking: dishUsageCounts, onAddDish: { viewModel.showDishForm = true }, onQuickAssignDish: { dish in viewModel.assignDishToFirstFreeSlot( @@ -651,6 +692,59 @@ struct HomeView: View { plan.slots.allSatisfy { $0.dishId != nil || $0.isEatingOut } } + private var dishUsageCounts: [UUID: Int] { + var counts: [UUID: Int] = [:] + for plan in weekPlans { + for slot in plan.slots { + if let dishId = slot.dishId { + counts[dishId, default: 0] += 1 + } + } + } + return counts + } + + @ViewBuilder + private func autoAssignBanner(emptyCount: Int, plan: WeekPlan, settings: AppSettings) -> some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text("home_auto_assign_cta_title") + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + Text(String(format: String(localized: "home_auto_assign_cta_slots"), emptyCount)) + .font(.mealMoodCaption) + .foregroundColor(.mealMoodTextSecondary) + } + Spacer() + Button { + viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings) + } label: { + HStack(spacing: 6) { + Image(systemName: "wand.and.stars") + Text("home_auto_assign_cta_button") + .font(.mealMoodSmall.weight(.semibold)) + } + .foregroundColor(.white) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.mealMoodCoral) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + .disabled(viewModel.isAutoCompleting) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.mealMoodCoral.opacity(0.08)) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.mealMoodCoral.opacity(0.25), lineWidth: 1) + ) + .padding(.horizontal, 16) + } + private func updateWidget(settings: AppSettings) { let todayPlan = fetchWeekPlan(for: Date().startOfWeek()) WidgetDataStore.update(plan: todayPlan, dishes: dishes, settings: settings) diff --git a/MealMood/Views/Home/WeekCalendarView.swift b/MealMood/Views/Home/WeekCalendarView.swift index b40589d..8003faf 100644 --- a/MealMood/Views/Home/WeekCalendarView.swift +++ b/MealMood/Views/Home/WeekCalendarView.swift @@ -11,6 +11,7 @@ struct WeekCalendarView: View { let tags: [Tag] @ObservedObject var viewModel: HomeViewModel var onTapEmptySlot: ((MealSlot) -> Void)? + var onTapFilledSlot: ((MealSlot) -> Void)? private var dayRange: ClosedRange { settings.includeWeekends ? 0...6 : 0...4 @@ -279,6 +280,12 @@ struct WeekCalendarView: View { viewModel.removeDish(from: live, plan: plan, settings: settings) } : nil ) + .contentShape(Rectangle()) + .onTapGesture { + guard viewModel.canEditCurrentWeek, onTapFilledSlot != nil else { return } + guard let live = liveSlot(for: slot) else { return } + onTapFilledSlot?(live) + } .draggable("slot:\(slot.slotId.uuidString)") } else if let slot = slot, slot.isEatingOut { EatingOutSlotView(mealType: mealType) diff --git a/MealMood/Views/Home/WeekPlanShareView.swift b/MealMood/Views/Home/WeekPlanShareView.swift index f6d3e23..21178bd 100644 --- a/MealMood/Views/Home/WeekPlanShareView.swift +++ b/MealMood/Views/Home/WeekPlanShareView.swift @@ -502,8 +502,10 @@ struct WeekPlanShareView: View { private func dishName(day: Int, mealType: MealType) -> String { let slot = plan.slots.first { $0.dayOfWeek == day && $0.mealType == mealType.rawValue } - guard let slot, let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else { - return "—" + guard let slot else { return String(localized: "share_slot_empty") } + if slot.isEatingOut { return String(localized: "slot_eating_out") } + guard let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else { + return String(localized: "share_slot_empty") } return dish.name } diff --git a/MealMood/Views/Tags/TagRulesEditView.swift b/MealMood/Views/Tags/TagRulesEditView.swift index 7f73f0a..640a93d 100644 --- a/MealMood/Views/Tags/TagRulesEditView.swift +++ b/MealMood/Views/Tags/TagRulesEditView.swift @@ -147,6 +147,24 @@ struct TagRulesEditView: View { .background(Color.mealMoodSurface) .cornerRadius(14) + // Day restriction + VStack(alignment: .leading, spacing: 12) { + Text("tags_day_restriction") + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + + Picker("Días", selection: $viewModel.dayRestriction) { + Text("tags_day_any").tag(nil as String?) + Text("tags_day_weekdays").tag("weekdays" as String?) + Text("tags_day_weekend").tag("weekend" as String?) + } + .pickerStyle(.segmented) + .disabled(!isPremium) + } + .padding(16) + .background(Color.mealMoodSurface) + .cornerRadius(14) + if !isPremium { HStack(spacing: 8) { Image(systemName: "star.circle")