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

@@ -399,6 +399,8 @@
<string name="notifications_property_shared_desc">When someone shares a property with you</string>
<string name="notifications_warranty_expiring">Warranty Expiring</string>
<string name="notifications_warranty_expiring_desc">Reminders for expiring warranties</string>
<string name="notifications_daily_digest">Daily Summary</string>
<string name="notifications_daily_digest_desc">Daily overview of tasks due and overdue</string>
<!-- Email Notifications -->
<string name="notifications_email_section">Email Notifications</string>

View File

@@ -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

View File

@@ -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<Int?>(null) }
var taskOverdueHour by remember { mutableStateOf<Int?>(null) }
var warrantyExpiringHour by remember { mutableStateOf<Int?>(null) }
var dailyDigestHour by remember { mutableStateOf<Int?>(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),

View File

@@ -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) {