Add Daily Digest notification preferences with custom time support

- Add dailyDigest and dailyDigestHour fields to Kotlin NotificationPreference model
- Update NotificationPreferencesViewModel to support new fields
- Add Daily Summary toggle with time picker to Android NotificationPreferencesScreen
- Add Daily Summary toggle with time picker to iOS NotificationPreferencesView
- Add localized strings for Daily Summary in all 10 languages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-07 22:51:50 -06:00
parent bb4ff216b1
commit 6cbcff116f
7 changed files with 261 additions and 6 deletions

View File

@@ -547,6 +547,8 @@ enum L10n {
static var propertySharedDescription: String { String(localized: "profile_property_shared_description") }
static var warrantyExpiring: String { String(localized: "profile_warranty_expiring") }
static var warrantyExpiringDescription: String { String(localized: "profile_warranty_expiring_description") }
static var dailyDigest: String { String(localized: "profile_daily_digest") }
static var dailyDigestDescription: String { String(localized: "profile_daily_digest_description") }
static var otherNotifications: String { String(localized: "profile_other_notifications") }
// Email Notifications

View File

@@ -20543,6 +20543,136 @@
}
}
},
"profile_daily_digest" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tägliche Zusammenfassung"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Daily Summary"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resumen diario"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Résumé quotidien"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Riepilogo giornaliero"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "デイリーサマリー"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "일일 요약"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dagelijkse samenvatting"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resumo diário"
}
},
"zh" : {
"stringUnit" : {
"state" : "translated",
"value" : "每日摘要"
}
}
}
},
"profile_daily_digest_description" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tägliche Übersicht über fällige und überfällige Aufgaben"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Daily overview of tasks due and overdue"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resumen diario de tareas pendientes y vencidas"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aperçu quotidien des tâches à faire et en retard"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Panoramica giornaliera delle attività in scadenza e scadute"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "期限が近いタスクと期限切れのタスクの毎日の概要"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "마감 예정 및 지연된 작업의 일일 개요"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dagelijks overzicht van taken die binnenkort verlopen en achterstallig zijn"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resumo diário de tarefas a vencer e atrasadas"
}
},
"zh" : {
"stringUnit" : {
"state" : "translated",
"value" : "即将到期和逾期任务的每日概览"
}
}
}
},
"properties_add_button" : {
"extractionState" : "manual",
"localizations" : {

View File

@@ -217,6 +217,40 @@ struct NotificationPreferencesView: View {
.onChange(of: viewModel.warrantyExpiring) { _, newValue in
viewModel.updatePreference(warrantyExpiring: newValue)
}
Toggle(isOn: $viewModel.dailyDigest) {
Label {
VStack(alignment: .leading, spacing: 2) {
Text(L10n.Profile.dailyDigest)
.foregroundColor(Color.appTextPrimary)
Text(L10n.Profile.dailyDigestDescription)
.font(.caption)
.foregroundColor(Color.appTextSecondary)
}
} icon: {
Image(systemName: "list.bullet.clipboard.fill")
.foregroundColor(Color.appSecondary)
}
}
.tint(Color.appPrimary)
.onChange(of: viewModel.dailyDigest) { _, newValue in
viewModel.updatePreference(dailyDigest: newValue)
}
// Time picker for Daily Digest
if viewModel.dailyDigest {
NotificationTimePickerRow(
isEnabled: $viewModel.dailyDigestTimeEnabled,
selectedHour: $viewModel.dailyDigestHour,
onEnableCustomTime: {
viewModel.enableCustomTime(for: .dailyDigest)
},
onTimeChange: { hour in
viewModel.updateCustomTime(hour, for: .dailyDigest)
},
formatHour: viewModel.formatHour
)
}
} header: {
Text(L10n.Profile.otherNotifications)
}
@@ -278,17 +312,20 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
@Published var taskAssigned: Bool = true
@Published var residenceShared: Bool = true
@Published var warrantyExpiring: Bool = true
@Published var dailyDigest: Bool = true
@Published var emailTaskCompleted: Bool = true
// Custom notification times (local hours, 0-23)
@Published var taskDueSoonHour: Int? = nil
@Published var taskOverdueHour: Int? = nil
@Published var warrantyExpiringHour: Int? = nil
@Published var dailyDigestHour: Int? = nil
// Track if user has enabled custom times
@Published var taskDueSoonTimeEnabled: Bool = false
@Published var taskOverdueTimeEnabled: Bool = false
@Published var warrantyExpiringTimeEnabled: Bool = false
@Published var dailyDigestTimeEnabled: Bool = false
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@@ -298,6 +335,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
private let defaultTaskDueSoonLocalHour = 14 // 2 PM local
private let defaultTaskOverdueLocalHour = 9 // 9 AM local
private let defaultWarrantyExpiringLocalHour = 10 // 10 AM local
private let defaultDailyDigestLocalHour = 8 // 8 AM local
func loadPreferences() {
isLoading = true
@@ -314,6 +352,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
self.taskAssigned = prefs.taskAssigned
self.residenceShared = prefs.residenceShared
self.warrantyExpiring = prefs.warrantyExpiring
self.dailyDigest = prefs.dailyDigest
self.emailTaskCompleted = prefs.emailTaskCompleted
// Load custom notification times (convert from UTC to local)
@@ -329,6 +368,10 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
self.warrantyExpiringHour = DateUtils.utcHourToLocal(Int(utcHour))
self.warrantyExpiringTimeEnabled = true
}
if let utcHour = prefs.dailyDigestHour?.intValue {
self.dailyDigestHour = DateUtils.utcHourToLocal(Int(utcHour))
self.dailyDigestTimeEnabled = true
}
self.isLoading = false
self.errorMessage = nil
@@ -350,10 +393,12 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
taskAssigned: Bool? = nil,
residenceShared: Bool? = nil,
warrantyExpiring: Bool? = nil,
dailyDigest: Bool? = nil,
emailTaskCompleted: Bool? = nil,
taskDueSoonHour: Int? = nil,
taskOverdueHour: Int? = nil,
warrantyExpiringHour: Int? = nil
warrantyExpiringHour: Int? = nil,
dailyDigestHour: Int? = nil
) {
isSaving = true
@@ -363,6 +408,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
let taskDueSoonUtc = taskDueSoonHour.map { DateUtils.localHourToUtc($0) }
let taskOverdueUtc = taskOverdueHour.map { DateUtils.localHourToUtc($0) }
let warrantyExpiringUtc = warrantyExpiringHour.map { DateUtils.localHourToUtc($0) }
let dailyDigestUtc = dailyDigestHour.map { DateUtils.localHourToUtc($0) }
let request = UpdateNotificationPreferencesRequest(
taskDueSoon: taskDueSoon.map { KotlinBoolean(bool: $0) },
@@ -371,10 +417,12 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
taskAssigned: taskAssigned.map { KotlinBoolean(bool: $0) },
residenceShared: residenceShared.map { KotlinBoolean(bool: $0) },
warrantyExpiring: warrantyExpiring.map { KotlinBoolean(bool: $0) },
dailyDigest: dailyDigest.map { KotlinBoolean(bool: $0) },
emailTaskCompleted: emailTaskCompleted.map { KotlinBoolean(bool: $0) },
taskDueSoonHour: taskDueSoonUtc.map { KotlinInt(int: Int32($0)) },
taskOverdueHour: taskOverdueUtc.map { KotlinInt(int: Int32($0)) },
warrantyExpiringHour: warrantyExpiringUtc.map { KotlinInt(int: Int32($0)) }
warrantyExpiringHour: warrantyExpiringUtc.map { KotlinInt(int: Int32($0)) },
dailyDigestHour: dailyDigestUtc.map { KotlinInt(int: Int32($0)) }
)
let result = try await APILayer.shared.updateNotificationPreferences(request: request)
@@ -406,6 +454,10 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
warrantyExpiringHour = defaultWarrantyExpiringLocalHour
warrantyExpiringTimeEnabled = true
updatePreference(warrantyExpiringHour: defaultWarrantyExpiringLocalHour)
case .dailyDigest:
dailyDigestHour = defaultDailyDigestLocalHour
dailyDigestTimeEnabled = true
updatePreference(dailyDigestHour: defaultDailyDigestLocalHour)
}
}
@@ -421,6 +473,9 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
case .warrantyExpiring:
warrantyExpiringHour = hour
updatePreference(warrantyExpiringHour: hour)
case .dailyDigest:
dailyDigestHour = hour
updatePreference(dailyDigestHour: hour)
}
}
@@ -428,6 +483,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
case taskDueSoon
case taskOverdue
case warrantyExpiring
case dailyDigest
}
// Format hour to display string