diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index d8b834a..c1e5f3d 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -399,6 +399,8 @@
When someone shares a property with you
Warranty Expiring
Reminders for expiring warranties
+ Daily Summary
+ Daily overview of tasks due and overdue
Email Notifications
diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt
index 3d1d842..ef4e0f5 100644
--- a/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt
+++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/Notification.kt
@@ -41,6 +41,8 @@ data class NotificationPreference(
val residenceShared: Boolean = true,
@SerialName("warranty_expiring")
val warrantyExpiring: Boolean = true,
+ @SerialName("daily_digest")
+ val dailyDigest: Boolean = true,
// Email preferences
@SerialName("email_task_completed")
val emailTaskCompleted: Boolean = true,
@@ -50,7 +52,9 @@ data class NotificationPreference(
@SerialName("task_overdue_hour")
val taskOverdueHour: Int? = null,
@SerialName("warranty_expiring_hour")
- val warrantyExpiringHour: Int? = null
+ val warrantyExpiringHour: Int? = null,
+ @SerialName("daily_digest_hour")
+ val dailyDigestHour: Int? = null
)
@Serializable
@@ -67,6 +71,8 @@ data class UpdateNotificationPreferencesRequest(
val residenceShared: Boolean? = null,
@SerialName("warranty_expiring")
val warrantyExpiring: Boolean? = null,
+ @SerialName("daily_digest")
+ val dailyDigest: Boolean? = null,
// Email preferences
@SerialName("email_task_completed")
val emailTaskCompleted: Boolean? = null,
@@ -76,7 +82,9 @@ data class UpdateNotificationPreferencesRequest(
@SerialName("task_overdue_hour")
val taskOverdueHour: Int? = null,
@SerialName("warranty_expiring_hour")
- val warrantyExpiringHour: Int? = null
+ val warrantyExpiringHour: Int? = null,
+ @SerialName("daily_digest_hour")
+ val dailyDigestHour: Int? = null
)
@Serializable
diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt
index 689146e..484a19a 100644
--- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/NotificationPreferencesScreen.kt
@@ -37,20 +37,24 @@ fun NotificationPreferencesScreen(
var taskAssigned by remember { mutableStateOf(true) }
var residenceShared by remember { mutableStateOf(true) }
var warrantyExpiring by remember { mutableStateOf(true) }
+ var dailyDigest by remember { mutableStateOf(true) }
var emailTaskCompleted by remember { mutableStateOf(true) }
// Custom notification times (local hours)
var taskDueSoonHour by remember { mutableStateOf(null) }
var taskOverdueHour by remember { mutableStateOf(null) }
var warrantyExpiringHour by remember { mutableStateOf(null) }
+ var dailyDigestHour by remember { mutableStateOf(null) }
// Time picker dialog states
var showTaskDueSoonTimePicker by remember { mutableStateOf(false) }
var showTaskOverdueTimePicker by remember { mutableStateOf(false) }
+ var showDailyDigestTimePicker by remember { mutableStateOf(false) }
// Default local hours when user first enables custom time
val defaultTaskDueSoonLocalHour = 14 // 2 PM local
val defaultTaskOverdueLocalHour = 9 // 9 AM local
+ val defaultDailyDigestLocalHour = 8 // 8 AM local
// Load preferences on first render
LaunchedEffect(Unit) {
@@ -67,6 +71,7 @@ fun NotificationPreferencesScreen(
taskAssigned = prefs.taskAssigned
residenceShared = prefs.residenceShared
warrantyExpiring = prefs.warrantyExpiring
+ dailyDigest = prefs.dailyDigest
emailTaskCompleted = prefs.emailTaskCompleted
// Load custom notification times (convert from UTC to local)
@@ -79,6 +84,9 @@ fun NotificationPreferencesScreen(
prefs.warrantyExpiringHour?.let { utcHour ->
warrantyExpiringHour = DateUtils.utcHourToLocal(utcHour)
}
+ prefs.dailyDigestHour?.let { utcHour ->
+ dailyDigestHour = DateUtils.utcHourToLocal(utcHour)
+ }
}
}
@@ -374,9 +382,54 @@ fun NotificationPreferencesScreen(
viewModel.updatePreference(warrantyExpiring = it)
}
)
+
+ HorizontalDivider(
+ modifier = Modifier.padding(horizontal = AppSpacing.lg),
+ color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
+ )
+
+ NotificationToggle(
+ title = stringResource(Res.string.notifications_daily_digest),
+ description = stringResource(Res.string.notifications_daily_digest_desc),
+ icon = Icons.Default.Summarize,
+ iconTint = MaterialTheme.colorScheme.secondary,
+ checked = dailyDigest,
+ onCheckedChange = {
+ dailyDigest = it
+ viewModel.updatePreference(dailyDigest = it)
+ }
+ )
+
+ // Time picker for Daily Digest
+ if (dailyDigest) {
+ NotificationTimePickerRow(
+ currentHour = dailyDigestHour,
+ onSetCustomTime = {
+ val localHour = defaultDailyDigestLocalHour
+ dailyDigestHour = localHour
+ val utcHour = DateUtils.localHourToUtc(localHour)
+ viewModel.updatePreference(dailyDigestHour = utcHour)
+ },
+ onChangeTime = { showDailyDigestTimePicker = true }
+ )
+ }
}
}
+ // Daily Digest time picker dialog
+ if (showDailyDigestTimePicker) {
+ HourPickerDialog(
+ currentHour = dailyDigestHour ?: defaultDailyDigestLocalHour,
+ onHourSelected = { hour ->
+ dailyDigestHour = hour
+ val utcHour = DateUtils.localHourToUtc(hour)
+ viewModel.updatePreference(dailyDigestHour = utcHour)
+ showDailyDigestTimePicker = false
+ },
+ onDismiss = { showDailyDigestTimePicker = false }
+ )
+ }
+
// Email Notifications Section
Text(
stringResource(Res.string.notifications_email_section),
diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt
index 8c99e88..78f7172 100644
--- a/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/NotificationPreferencesViewModel.kt
@@ -37,10 +37,12 @@ class NotificationPreferencesViewModel : ViewModel() {
taskAssigned: Boolean? = null,
residenceShared: Boolean? = null,
warrantyExpiring: Boolean? = null,
+ dailyDigest: Boolean? = null,
emailTaskCompleted: Boolean? = null,
taskDueSoonHour: Int? = null,
taskOverdueHour: Int? = null,
- warrantyExpiringHour: Int? = null
+ warrantyExpiringHour: Int? = null,
+ dailyDigestHour: Int? = null
) {
viewModelScope.launch {
_updateState.value = ApiResult.Loading
@@ -51,10 +53,12 @@ class NotificationPreferencesViewModel : ViewModel() {
taskAssigned = taskAssigned,
residenceShared = residenceShared,
warrantyExpiring = warrantyExpiring,
+ dailyDigest = dailyDigest,
emailTaskCompleted = emailTaskCompleted,
taskDueSoonHour = taskDueSoonHour,
taskOverdueHour = taskOverdueHour,
- warrantyExpiringHour = warrantyExpiringHour
+ warrantyExpiringHour = warrantyExpiringHour,
+ dailyDigestHour = dailyDigestHour
)
val result = APILayer.updateNotificationPreferences(request)
_updateState.value = when (result) {
diff --git a/iosApp/iosApp/Helpers/L10n.swift b/iosApp/iosApp/Helpers/L10n.swift
index b1906d3..0a536be 100644
--- a/iosApp/iosApp/Helpers/L10n.swift
+++ b/iosApp/iosApp/Helpers/L10n.swift
@@ -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
diff --git a/iosApp/iosApp/Localizable.xcstrings b/iosApp/iosApp/Localizable.xcstrings
index dc4b16d..3ddb004 100644
--- a/iosApp/iosApp/Localizable.xcstrings
+++ b/iosApp/iosApp/Localizable.xcstrings
@@ -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" : {
diff --git a/iosApp/iosApp/Profile/NotificationPreferencesView.swift b/iosApp/iosApp/Profile/NotificationPreferencesView.swift
index 7b05755..1303bfe 100644
--- a/iosApp/iosApp/Profile/NotificationPreferencesView.swift
+++ b/iosApp/iosApp/Profile/NotificationPreferencesView.swift
@@ -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