Add email notification preference for task completion

- Add emailTaskCompleted field to NotificationPreference model
- Add email preference toggle to notification settings UI (iOS & Android)
- Add localized strings for email notifications section
- Update ViewModel to support email preference updates

🤖 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-04 20:04:42 -06:00
parent 7c0238bdf8
commit 22bf109cf7
7 changed files with 118 additions and 6 deletions

View File

@@ -390,6 +390,11 @@
<string name="notifications_warranty_expiring">Warranty Expiring</string> <string name="notifications_warranty_expiring">Warranty Expiring</string>
<string name="notifications_warranty_expiring_desc">Reminders for expiring warranties</string> <string name="notifications_warranty_expiring_desc">Reminders for expiring warranties</string>
<!-- Email Notifications -->
<string name="notifications_email_section">Email Notifications</string>
<string name="notifications_email_task_completed">Task Completed Email</string>
<string name="notifications_email_task_completed_desc">Receive email when a task is completed</string>
<!-- Common --> <!-- Common -->
<string name="common_save">Save</string> <string name="common_save">Save</string>
<string name="common_cancel">Cancel</string> <string name="common_cancel">Cancel</string>

View File

@@ -40,7 +40,10 @@ data class NotificationPreference(
@SerialName("residence_shared") @SerialName("residence_shared")
val residenceShared: Boolean = true, val residenceShared: Boolean = true,
@SerialName("warranty_expiring") @SerialName("warranty_expiring")
val warrantyExpiring: Boolean = true val warrantyExpiring: Boolean = true,
// Email preferences
@SerialName("email_task_completed")
val emailTaskCompleted: Boolean = true
) )
@Serializable @Serializable
@@ -56,7 +59,10 @@ data class UpdateNotificationPreferencesRequest(
@SerialName("residence_shared") @SerialName("residence_shared")
val residenceShared: Boolean? = null, val residenceShared: Boolean? = null,
@SerialName("warranty_expiring") @SerialName("warranty_expiring")
val warrantyExpiring: Boolean? = null val warrantyExpiring: Boolean? = null,
// Email preferences
@SerialName("email_task_completed")
val emailTaskCompleted: Boolean? = null
) )
@Serializable @Serializable

View File

@@ -35,6 +35,7 @@ fun NotificationPreferencesScreen(
var taskAssigned by remember { mutableStateOf(true) } var taskAssigned by remember { mutableStateOf(true) }
var residenceShared by remember { mutableStateOf(true) } var residenceShared by remember { mutableStateOf(true) }
var warrantyExpiring by remember { mutableStateOf(true) } var warrantyExpiring by remember { mutableStateOf(true) }
var emailTaskCompleted by remember { mutableStateOf(true) }
// Load preferences on first render // Load preferences on first render
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -51,6 +52,7 @@ fun NotificationPreferencesScreen(
taskAssigned = prefs.taskAssigned taskAssigned = prefs.taskAssigned
residenceShared = prefs.residenceShared residenceShared = prefs.residenceShared
warrantyExpiring = prefs.warrantyExpiring warrantyExpiring = prefs.warrantyExpiring
emailTaskCompleted = prefs.emailTaskCompleted
} }
} }
@@ -294,6 +296,36 @@ fun NotificationPreferencesScreen(
} }
} }
// Email Notifications Section
Text(
stringResource(Res.string.notifications_email_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column {
NotificationToggle(
title = stringResource(Res.string.notifications_email_task_completed),
description = stringResource(Res.string.notifications_email_task_completed_desc),
icon = Icons.Default.Email,
iconTint = MaterialTheme.colorScheme.primary,
checked = emailTaskCompleted,
onCheckedChange = {
emailTaskCompleted = it
viewModel.updatePreference(emailTaskCompleted = it)
}
)
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl)) Spacer(modifier = Modifier.height(AppSpacing.xl))
} }
} }

View File

@@ -36,7 +36,8 @@ class NotificationPreferencesViewModel : ViewModel() {
taskCompleted: Boolean? = null, taskCompleted: Boolean? = null,
taskAssigned: Boolean? = null, taskAssigned: Boolean? = null,
residenceShared: Boolean? = null, residenceShared: Boolean? = null,
warrantyExpiring: Boolean? = null warrantyExpiring: Boolean? = null,
emailTaskCompleted: Boolean? = null
) { ) {
viewModelScope.launch { viewModelScope.launch {
_updateState.value = ApiResult.Loading _updateState.value = ApiResult.Loading
@@ -46,7 +47,8 @@ class NotificationPreferencesViewModel : ViewModel() {
taskCompleted = taskCompleted, taskCompleted = taskCompleted,
taskAssigned = taskAssigned, taskAssigned = taskAssigned,
residenceShared = residenceShared, residenceShared = residenceShared,
warrantyExpiring = warrantyExpiring warrantyExpiring = warrantyExpiring,
emailTaskCompleted = emailTaskCompleted
) )
val result = APILayer.updateNotificationPreferences(request) val result = APILayer.updateNotificationPreferences(request)
_updateState.value = when (result) { _updateState.value = when (result) {

View File

@@ -540,6 +540,11 @@ enum L10n {
static var warrantyExpiring: String { String(localized: "profile_warranty_expiring") } static var warrantyExpiring: String { String(localized: "profile_warranty_expiring") }
static var warrantyExpiringDescription: String { String(localized: "profile_warranty_expiring_description") } static var warrantyExpiringDescription: String { String(localized: "profile_warranty_expiring_description") }
static var otherNotifications: String { String(localized: "profile_other_notifications") } static var otherNotifications: String { String(localized: "profile_other_notifications") }
// Email Notifications
static var emailNotifications: String { String(localized: "profile_email_notifications") }
static var emailTaskCompleted: String { String(localized: "profile_email_task_completed") }
static var emailTaskCompletedDescription: String { String(localized: "profile_email_task_completed_description") }
} }
// MARK: - Settings // MARK: - Settings

View File

@@ -17844,6 +17844,17 @@
} }
} }
}, },
"profile_email_notifications" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Email Notifications"
}
}
}
},
"profile_email_required_unique" : { "profile_email_required_unique" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {
@@ -17909,6 +17920,28 @@
} }
} }
}, },
"profile_email_task_completed" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Task Completed Email"
}
}
}
},
"profile_email_task_completed_description" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Receive email when a task is completed"
}
}
}
},
"profile_first_name" : { "profile_first_name" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {

View File

@@ -191,6 +191,31 @@ struct NotificationPreferencesView: View {
Text(L10n.Profile.otherNotifications) Text(L10n.Profile.otherNotifications)
} }
.listRowBackground(Color.appBackgroundSecondary) .listRowBackground(Color.appBackgroundSecondary)
// Email Notifications
Section {
Toggle(isOn: $viewModel.emailTaskCompleted) {
Label {
VStack(alignment: .leading, spacing: 2) {
Text(L10n.Profile.emailTaskCompleted)
.foregroundColor(Color.appTextPrimary)
Text(L10n.Profile.emailTaskCompletedDescription)
.font(.caption)
.foregroundColor(Color.appTextSecondary)
}
} icon: {
Image(systemName: "envelope.fill")
.foregroundColor(Color.appPrimary)
}
}
.tint(Color.appPrimary)
.onChange(of: viewModel.emailTaskCompleted) { _, newValue in
viewModel.updatePreference(emailTaskCompleted: newValue)
}
} header: {
Text(L10n.Profile.emailNotifications)
}
.listRowBackground(Color.appBackgroundSecondary)
} }
} }
.listStyle(.plain) .listStyle(.plain)
@@ -223,6 +248,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
@Published var taskAssigned: Bool = true @Published var taskAssigned: Bool = true
@Published var residenceShared: Bool = true @Published var residenceShared: Bool = true
@Published var warrantyExpiring: Bool = true @Published var warrantyExpiring: Bool = true
@Published var emailTaskCompleted: Bool = true
@Published var isLoading: Bool = false @Published var isLoading: Bool = false
@Published var errorMessage: String? @Published var errorMessage: String?
@@ -243,6 +269,7 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
self.taskAssigned = prefs.taskAssigned self.taskAssigned = prefs.taskAssigned
self.residenceShared = prefs.residenceShared self.residenceShared = prefs.residenceShared
self.warrantyExpiring = prefs.warrantyExpiring self.warrantyExpiring = prefs.warrantyExpiring
self.emailTaskCompleted = prefs.emailTaskCompleted
self.isLoading = false self.isLoading = false
self.errorMessage = nil self.errorMessage = nil
} else if let error = result as? ApiResultError { } else if let error = result as? ApiResultError {
@@ -262,7 +289,8 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
taskCompleted: Bool? = nil, taskCompleted: Bool? = nil,
taskAssigned: Bool? = nil, taskAssigned: Bool? = nil,
residenceShared: Bool? = nil, residenceShared: Bool? = nil,
warrantyExpiring: Bool? = nil warrantyExpiring: Bool? = nil,
emailTaskCompleted: Bool? = nil
) { ) {
isSaving = true isSaving = true
@@ -274,7 +302,8 @@ class NotificationPreferencesViewModelWrapper: ObservableObject {
taskCompleted: taskCompleted.map { KotlinBoolean(bool: $0) }, taskCompleted: taskCompleted.map { KotlinBoolean(bool: $0) },
taskAssigned: taskAssigned.map { KotlinBoolean(bool: $0) }, taskAssigned: taskAssigned.map { KotlinBoolean(bool: $0) },
residenceShared: residenceShared.map { KotlinBoolean(bool: $0) }, residenceShared: residenceShared.map { KotlinBoolean(bool: $0) },
warrantyExpiring: warrantyExpiring.map { KotlinBoolean(bool: $0) } warrantyExpiring: warrantyExpiring.map { KotlinBoolean(bool: $0) },
emailTaskCompleted: emailTaskCompleted.map { KotlinBoolean(bool: $0) }
) )
let result = try await APILayer.shared.updateNotificationPreferences(request: request) let result = try await APILayer.shared.updateNotificationPreferences(request: request)