Add comprehensive i18n localization for KMM and iOS
KMM (Android/Shared): - Add strings.xml with 200+ localized strings - Add translation files for es, fr, de, pt languages - Update all screens to use stringResource() for i18n - Add Accept-Language header to API client for all platforms iOS: - Add L10n.swift helper with type-safe string accessors - Add Localizable.xcstrings with translations for all 5 languages - Update all SwiftUI views to use L10n.* for localized strings - Localize Auth, Residence, Task, Contractor, Document, and Profile views Supported languages: English, Spanish, French, German, Portuguese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,13 @@ import kotlinx.serialization.json.Json
|
||||
expect fun getLocalhostAddress(): String
|
||||
expect fun createHttpClient(): HttpClient
|
||||
|
||||
/**
|
||||
* Get the device's preferred language code (e.g., "en", "es", "fr").
|
||||
* This is used to set the Accept-Language header for API requests
|
||||
* so the server can return localized error messages and content.
|
||||
*/
|
||||
expect fun getDeviceLanguage(): String
|
||||
|
||||
object ApiClient {
|
||||
val httpClient = createHttpClient()
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import com.example.casera.ui.components.HandleErrors
|
||||
import com.example.casera.util.DateUtils
|
||||
import com.example.casera.viewmodel.ContractorViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -50,13 +52,13 @@ fun ContractorDetailScreen(
|
||||
// Handle errors for delete contractor
|
||||
deleteState.HandleErrors(
|
||||
onRetry = { viewModel.deleteContractor(contractorId) },
|
||||
errorTitle = "Failed to Delete Contractor"
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_delete)
|
||||
)
|
||||
|
||||
// Handle errors for toggle favorite
|
||||
toggleFavoriteState.HandleErrors(
|
||||
onRetry = { viewModel.toggleFavorite(contractorId) },
|
||||
errorTitle = "Failed to Update Favorite"
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_update_favorite)
|
||||
)
|
||||
|
||||
LaunchedEffect(deleteState) {
|
||||
@@ -76,10 +78,10 @@ fun ContractorDetailScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Contractor Details", fontWeight = FontWeight.Bold) },
|
||||
title = { Text(stringResource(Res.string.contractors_details), fontWeight = FontWeight.Bold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, "Back")
|
||||
Icon(Icons.Default.ArrowBack, stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@@ -88,15 +90,15 @@ fun ContractorDetailScreen(
|
||||
IconButton(onClick = { viewModel.toggleFavorite(contractorId) }) {
|
||||
Icon(
|
||||
if (state.data.isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
"Toggle favorite",
|
||||
stringResource(Res.string.contractors_toggle_favorite),
|
||||
tint = if (state.data.isFavorite) Color(0xFFF59E0B) else LocalContentColor.current
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showEditDialog = true }) {
|
||||
Icon(Icons.Default.Edit, "Edit")
|
||||
Icon(Icons.Default.Edit, stringResource(Res.string.common_edit))
|
||||
}
|
||||
IconButton(onClick = { showDeleteConfirmation = true }) {
|
||||
Icon(Icons.Default.Delete, "Delete", tint = Color(0xFFEF4444))
|
||||
Icon(Icons.Default.Delete, stringResource(Res.string.common_delete), tint = Color(0xFFEF4444))
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
@@ -120,7 +122,7 @@ fun ContractorDetailScreen(
|
||||
ApiResultHandler(
|
||||
state = contractorState,
|
||||
onRetry = { viewModel.loadContractorDetail(contractorId) },
|
||||
errorTitle = "Failed to Load Contractor",
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_load),
|
||||
loadingContent = {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
@@ -235,7 +237,7 @@ fun ContractorDetailScreen(
|
||||
if (contractor.taskCount > 0) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${contractor.taskCount} completed tasks",
|
||||
text = stringResource(Res.string.contractors_completed_tasks, contractor.taskCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -254,7 +256,7 @@ fun ContractorDetailScreen(
|
||||
contractor.phone?.let { phone ->
|
||||
QuickActionButton(
|
||||
icon = Icons.Default.Phone,
|
||||
label = "Call",
|
||||
label = stringResource(Res.string.contractors_call),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
@@ -268,7 +270,7 @@ fun ContractorDetailScreen(
|
||||
contractor.email?.let { email ->
|
||||
QuickActionButton(
|
||||
icon = Icons.Default.Email,
|
||||
label = "Email",
|
||||
label = stringResource(Res.string.contractors_send_email),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
@@ -282,7 +284,7 @@ fun ContractorDetailScreen(
|
||||
contractor.website?.let { website ->
|
||||
QuickActionButton(
|
||||
icon = Icons.Default.Language,
|
||||
label = "Website",
|
||||
label = stringResource(Res.string.contractors_website),
|
||||
color = Color(0xFFF59E0B),
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
@@ -297,7 +299,7 @@ fun ContractorDetailScreen(
|
||||
if (contractor.streetAddress != null || contractor.city != null) {
|
||||
QuickActionButton(
|
||||
icon = Icons.Default.Map,
|
||||
label = "Directions",
|
||||
label = stringResource(Res.string.contractors_directions),
|
||||
color = Color(0xFFEF4444),
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
@@ -319,11 +321,11 @@ fun ContractorDetailScreen(
|
||||
|
||||
// Contact Information
|
||||
item {
|
||||
DetailSection(title = "Contact Information") {
|
||||
DetailSection(title = stringResource(Res.string.contractors_contact_info)) {
|
||||
contractor.phone?.let { phone ->
|
||||
ClickableDetailRow(
|
||||
icon = Icons.Default.Phone,
|
||||
label = "Phone",
|
||||
label = stringResource(Res.string.contractors_phone_label),
|
||||
value = phone,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
onClick = {
|
||||
@@ -337,7 +339,7 @@ fun ContractorDetailScreen(
|
||||
contractor.email?.let { email ->
|
||||
ClickableDetailRow(
|
||||
icon = Icons.Default.Email,
|
||||
label = "Email",
|
||||
label = stringResource(Res.string.contractors_email_label),
|
||||
value = email,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
onClick = {
|
||||
@@ -351,7 +353,7 @@ fun ContractorDetailScreen(
|
||||
contractor.website?.let { website ->
|
||||
ClickableDetailRow(
|
||||
icon = Icons.Default.Language,
|
||||
label = "Website",
|
||||
label = stringResource(Res.string.contractors_website),
|
||||
value = website,
|
||||
iconTint = Color(0xFFF59E0B),
|
||||
onClick = {
|
||||
@@ -365,7 +367,7 @@ fun ContractorDetailScreen(
|
||||
|
||||
if (contractor.phone == null && contractor.email == null && contractor.website == null) {
|
||||
Text(
|
||||
text = "No contact information available",
|
||||
text = stringResource(Res.string.contractors_no_contact_info),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
@@ -377,7 +379,7 @@ fun ContractorDetailScreen(
|
||||
// Address
|
||||
if (contractor.streetAddress != null || contractor.city != null) {
|
||||
item {
|
||||
DetailSection(title = "Address") {
|
||||
DetailSection(title = stringResource(Res.string.contractors_address)) {
|
||||
val fullAddress = buildString {
|
||||
contractor.streetAddress?.let { append(it) }
|
||||
if (contractor.city != null || contractor.stateProvince != null || contractor.postalCode != null) {
|
||||
@@ -397,7 +399,7 @@ fun ContractorDetailScreen(
|
||||
if (fullAddress.isNotBlank()) {
|
||||
ClickableDetailRow(
|
||||
icon = Icons.Default.LocationOn,
|
||||
label = "Location",
|
||||
label = stringResource(Res.string.contractors_location),
|
||||
value = fullAddress,
|
||||
iconTint = Color(0xFFEF4444),
|
||||
onClick = {
|
||||
@@ -423,10 +425,10 @@ fun ContractorDetailScreen(
|
||||
?: "Property #$resId"
|
||||
|
||||
item {
|
||||
DetailSection(title = "Associated Property") {
|
||||
DetailSection(title = stringResource(Res.string.contractors_associated_property)) {
|
||||
DetailRow(
|
||||
icon = Icons.Default.Home,
|
||||
label = "Property",
|
||||
label = stringResource(Res.string.contractors_property),
|
||||
value = residenceName,
|
||||
iconTint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -437,7 +439,7 @@ fun ContractorDetailScreen(
|
||||
// Notes
|
||||
if (!contractor.notes.isNullOrBlank()) {
|
||||
item {
|
||||
DetailSection(title = "Notes") {
|
||||
DetailSection(title = stringResource(Res.string.contractors_notes)) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
@@ -460,7 +462,7 @@ fun ContractorDetailScreen(
|
||||
|
||||
// Statistics
|
||||
item {
|
||||
DetailSection(title = "Statistics") {
|
||||
DetailSection(title = stringResource(Res.string.contractors_statistics)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -470,7 +472,7 @@ fun ContractorDetailScreen(
|
||||
StatCard(
|
||||
icon = Icons.Default.CheckCircle,
|
||||
value = contractor.taskCount.toString(),
|
||||
label = "Tasks\nCompleted",
|
||||
label = stringResource(Res.string.contractors_tasks_completed),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
@@ -478,7 +480,7 @@ fun ContractorDetailScreen(
|
||||
StatCard(
|
||||
icon = Icons.Default.Star,
|
||||
value = ((contractor.rating * 10).toInt() / 10.0).toString(),
|
||||
label = "Average\nRating",
|
||||
label = stringResource(Res.string.contractors_average_rating),
|
||||
color = Color(0xFFF59E0B)
|
||||
)
|
||||
}
|
||||
@@ -488,11 +490,11 @@ fun ContractorDetailScreen(
|
||||
|
||||
// Metadata
|
||||
item {
|
||||
DetailSection(title = "Info") {
|
||||
DetailSection(title = stringResource(Res.string.contractors_info)) {
|
||||
contractor.createdBy?.let { createdBy ->
|
||||
DetailRow(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
label = "Added By",
|
||||
label = stringResource(Res.string.contractors_added_by),
|
||||
value = createdBy.username,
|
||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -501,7 +503,7 @@ fun ContractorDetailScreen(
|
||||
|
||||
DetailRow(
|
||||
icon = Icons.Default.CalendarMonth,
|
||||
label = "Member Since",
|
||||
label = stringResource(Res.string.contractors_member_since),
|
||||
value = DateUtils.formatDateMedium(contractor.createdAt),
|
||||
iconTint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -527,8 +529,8 @@ fun ContractorDetailScreen(
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirmation = false },
|
||||
icon = { Icon(Icons.Default.Warning, null, tint = Color(0xFFEF4444)) },
|
||||
title = { Text("Delete Contractor") },
|
||||
text = { Text("Are you sure you want to delete this contractor? This action cannot be undone.") },
|
||||
title = { Text(stringResource(Res.string.contractors_delete)) },
|
||||
text = { Text(stringResource(Res.string.contractors_delete_warning)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -537,12 +539,12 @@ fun ContractorDetailScreen(
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEF4444))
|
||||
) {
|
||||
Text("Delete")
|
||||
Text(stringResource(Res.string.common_delete))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteConfirmation = false }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
},
|
||||
containerColor = Color.White,
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
import com.example.casera.ui.subscription.UpgradeFeatureScreen
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -83,13 +85,13 @@ fun ContractorsScreen(
|
||||
// Handle errors for delete contractor
|
||||
deleteState.HandleErrors(
|
||||
onRetry = { /* Handled in UI */ },
|
||||
errorTitle = "Failed to Delete Contractor"
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_delete)
|
||||
)
|
||||
|
||||
// Handle errors for toggle favorite
|
||||
toggleFavoriteState.HandleErrors(
|
||||
onRetry = { /* Handled in UI */ },
|
||||
errorTitle = "Failed to Update Favorite"
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_update_favorite)
|
||||
)
|
||||
|
||||
LaunchedEffect(deleteState) {
|
||||
@@ -109,13 +111,13 @@ fun ContractorsScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Contractors", fontWeight = FontWeight.Bold) },
|
||||
title = { Text(stringResource(Res.string.contractors_title), fontWeight = FontWeight.Bold) },
|
||||
actions = {
|
||||
// Favorites filter toggle
|
||||
IconButton(onClick = { showFavoritesOnly = !showFavoritesOnly }) {
|
||||
Icon(
|
||||
if (showFavoritesOnly) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
"Filter favorites",
|
||||
stringResource(Res.string.contractors_filter_favorites),
|
||||
tint = if (showFavoritesOnly) MaterialTheme.colorScheme.tertiary else LocalContentColor.current
|
||||
)
|
||||
}
|
||||
@@ -125,7 +127,7 @@ fun ContractorsScreen(
|
||||
IconButton(onClick = { showFiltersMenu = true }) {
|
||||
Icon(
|
||||
Icons.Default.FilterList,
|
||||
"Filter by specialty",
|
||||
stringResource(Res.string.contractors_filter_specialty),
|
||||
tint = if (selectedFilter != null) MaterialTheme.colorScheme.primary else LocalContentColor.current
|
||||
)
|
||||
}
|
||||
@@ -135,7 +137,7 @@ fun ContractorsScreen(
|
||||
onDismissRequest = { showFiltersMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("All Specialties") },
|
||||
text = { Text(stringResource(Res.string.contractors_all_specialties)) },
|
||||
onClick = {
|
||||
selectedFilter = null
|
||||
showFiltersMenu = false
|
||||
@@ -186,7 +188,7 @@ fun ContractorsScreen(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
) {
|
||||
Icon(Icons.Default.Add, "Add contractor")
|
||||
Icon(Icons.Default.Add, stringResource(Res.string.contractors_add_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,12 +221,12 @@ fun ContractorsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
placeholder = { Text("Search contractors...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, "Search") },
|
||||
placeholder = { Text(stringResource(Res.string.contractors_search)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) },
|
||||
trailingIcon = {
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { searchQuery = "" }) {
|
||||
Icon(Icons.Default.Close, "Clear search")
|
||||
Icon(Icons.Default.Close, stringResource(Res.string.contractors_clear_search))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -250,7 +252,7 @@ fun ContractorsScreen(
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = { showFavoritesOnly = false },
|
||||
label = { Text("Favorites") },
|
||||
label = { Text(stringResource(Res.string.contractors_favorites)) },
|
||||
leadingIcon = { Icon(Icons.Default.Star, null, modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
@@ -270,7 +272,7 @@ fun ContractorsScreen(
|
||||
onRetry = {
|
||||
viewModel.loadContractors()
|
||||
},
|
||||
errorTitle = "Failed to Load Contractors",
|
||||
errorTitle = stringResource(Res.string.contractors_failed_to_load),
|
||||
loadingContent = {
|
||||
if (!isRefreshing) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||
@@ -296,14 +298,14 @@ fun ContractorsScreen(
|
||||
)
|
||||
Text(
|
||||
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
|
||||
"No contractors found"
|
||||
stringResource(Res.string.contractors_no_results)
|
||||
else
|
||||
"No contractors yet",
|
||||
stringResource(Res.string.contractors_empty_title),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
|
||||
Text(
|
||||
"Add your first contractor to get started",
|
||||
stringResource(Res.string.contractors_empty_subtitle_first),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
@@ -353,13 +355,13 @@ fun ContractorsScreen(
|
||||
if (showUpgradeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showUpgradeDialog = false },
|
||||
title = { Text("Upgrade Required") },
|
||||
title = { Text(stringResource(Res.string.contractors_upgrade_required)) },
|
||||
text = {
|
||||
Text("You've reached the maximum number of contractors for your current plan. Upgrade to Pro for unlimited contractors.")
|
||||
Text(stringResource(Res.string.contractors_upgrade_message))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showUpgradeDialog = false }) {
|
||||
Text("OK")
|
||||
Text(stringResource(Res.string.common_ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -422,7 +424,7 @@ fun ContractorCard(
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
contentDescription = "Favorite",
|
||||
contentDescription = stringResource(Res.string.contractors_favorite),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
@@ -490,7 +492,7 @@ fun ContractorCard(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "${contractor.taskCount} tasks",
|
||||
text = stringResource(Res.string.contractors_tasks_count, contractor.taskCount),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -505,7 +507,7 @@ fun ContractorCard(
|
||||
) {
|
||||
Icon(
|
||||
if (contractor.isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = if (contractor.isFavorite) "Remove from favorites" else "Add to favorites",
|
||||
contentDescription = if (contractor.isFavorite) stringResource(Res.string.contractors_remove_from_favorites) else stringResource(Res.string.contractors_add_to_favorites),
|
||||
tint = if (contractor.isFavorite) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -513,7 +515,7 @@ fun ContractorCard(
|
||||
// Arrow icon
|
||||
Icon(
|
||||
Icons.Default.ChevronRight,
|
||||
contentDescription = "View details",
|
||||
contentDescription = stringResource(Res.string.contractors_view_details),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import com.example.casera.util.DateUtils
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -57,7 +59,7 @@ fun DocumentDetailScreen(
|
||||
// Handle errors for document deletion
|
||||
deleteState.HandleErrors(
|
||||
onRetry = { documentViewModel.deleteDocument(documentId) },
|
||||
errorTitle = "Failed to Delete Document"
|
||||
errorTitle = stringResource(Res.string.documents_failed_to_delete)
|
||||
)
|
||||
|
||||
// Handle successful deletion
|
||||
@@ -71,20 +73,20 @@ fun DocumentDetailScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Document Details", fontWeight = FontWeight.Bold) },
|
||||
title = { Text(stringResource(Res.string.documents_details), fontWeight = FontWeight.Bold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, "Back")
|
||||
Icon(Icons.Default.ArrowBack, stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
when (documentState) {
|
||||
is ApiResult.Success -> {
|
||||
IconButton(onClick = { onNavigateToEdit(documentId) }) {
|
||||
Icon(Icons.Default.Edit, "Edit")
|
||||
Icon(Icons.Default.Edit, stringResource(Res.string.common_edit))
|
||||
}
|
||||
IconButton(onClick = { showDeleteDialog = true }) {
|
||||
Icon(Icons.Default.Delete, "Delete", tint = Color.Red)
|
||||
Icon(Icons.Default.Delete, stringResource(Res.string.common_delete), tint = Color.Red)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
@@ -101,7 +103,7 @@ fun DocumentDetailScreen(
|
||||
ApiResultHandler(
|
||||
state = documentState,
|
||||
onRetry = { documentViewModel.loadDocumentDetail(documentId) },
|
||||
errorTitle = "Failed to Load Document"
|
||||
errorTitle = stringResource(Res.string.documents_failed_to_load)
|
||||
) { document ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -136,16 +138,16 @@ fun DocumentDetailScreen(
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
"Status",
|
||||
stringResource(Res.string.documents_status),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
Text(
|
||||
when {
|
||||
!document.isActive -> "Inactive"
|
||||
daysUntilExpiration < 0 -> "Expired"
|
||||
daysUntilExpiration < 30 -> "Expiring soon"
|
||||
else -> "Active"
|
||||
!document.isActive -> stringResource(Res.string.documents_inactive)
|
||||
daysUntilExpiration < 0 -> stringResource(Res.string.documents_expired)
|
||||
daysUntilExpiration < 30 -> stringResource(Res.string.documents_expiring_soon)
|
||||
else -> stringResource(Res.string.documents_active)
|
||||
},
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -155,7 +157,7 @@ fun DocumentDetailScreen(
|
||||
if (document.isActive && daysUntilExpiration >= 0) {
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
"Days Remaining",
|
||||
stringResource(Res.string.documents_days_remaining),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
@@ -178,19 +180,19 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Basic Information",
|
||||
stringResource(Res.string.documents_basic_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
DetailRow("Title", document.title)
|
||||
DetailRow("Type", DocumentType.fromValue(document.documentType).displayName)
|
||||
DetailRow(stringResource(Res.string.documents_title_label), document.title)
|
||||
DetailRow(stringResource(Res.string.documents_type_label), DocumentType.fromValue(document.documentType).displayName)
|
||||
document.category?.let {
|
||||
DetailRow("Category", DocumentCategory.fromValue(it).displayName)
|
||||
DetailRow(stringResource(Res.string.documents_all_categories), DocumentCategory.fromValue(it).displayName)
|
||||
}
|
||||
document.description?.let {
|
||||
DetailRow("Description", it)
|
||||
DetailRow(stringResource(Res.string.documents_description_label), it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,17 +207,17 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Item Details",
|
||||
stringResource(Res.string.documents_item_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.itemName?.let { DetailRow("Item Name", it) }
|
||||
document.modelNumber?.let { DetailRow("Model Number", it) }
|
||||
document.serialNumber?.let { DetailRow("Serial Number", it) }
|
||||
document.provider?.let { DetailRow("Provider", it) }
|
||||
document.providerContact?.let { DetailRow("Provider Contact", it) }
|
||||
document.itemName?.let { DetailRow(stringResource(Res.string.documents_item_name), it) }
|
||||
document.modelNumber?.let { DetailRow(stringResource(Res.string.documents_model_number), it) }
|
||||
document.serialNumber?.let { DetailRow(stringResource(Res.string.documents_serial_number), it) }
|
||||
document.provider?.let { DetailRow(stringResource(Res.string.documents_provider), it) }
|
||||
document.providerContact?.let { DetailRow(stringResource(Res.string.documents_provider_contact), it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,15 +232,15 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Claim Information",
|
||||
stringResource(Res.string.documents_claim_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.claimPhone?.let { DetailRow("Claim Phone", it) }
|
||||
document.claimEmail?.let { DetailRow("Claim Email", it) }
|
||||
document.claimWebsite?.let { DetailRow("Claim Website", it) }
|
||||
document.claimPhone?.let { DetailRow(stringResource(Res.string.documents_claim_phone), it) }
|
||||
document.claimEmail?.let { DetailRow(stringResource(Res.string.documents_claim_email), it) }
|
||||
document.claimWebsite?.let { DetailRow(stringResource(Res.string.documents_claim_website), it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,15 +254,15 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Important Dates",
|
||||
stringResource(Res.string.documents_important_dates),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.purchaseDate?.let { DetailRow("Purchase Date", DateUtils.formatDateMedium(it)) }
|
||||
document.startDate?.let { DetailRow("Start Date", DateUtils.formatDateMedium(it)) }
|
||||
document.endDate?.let { DetailRow("End Date", DateUtils.formatDateMedium(it)) }
|
||||
document.purchaseDate?.let { DetailRow(stringResource(Res.string.documents_purchase_date), DateUtils.formatDateMedium(it)) }
|
||||
document.startDate?.let { DetailRow(stringResource(Res.string.documents_start_date), DateUtils.formatDateMedium(it)) }
|
||||
document.endDate?.let { DetailRow(stringResource(Res.string.documents_end_date), DateUtils.formatDateMedium(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,15 +274,15 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Associations",
|
||||
stringResource(Res.string.documents_associations),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.residenceAddress?.let { DetailRow("Residence", it) }
|
||||
document.contractorName?.let { DetailRow("Contractor", it) }
|
||||
document.contractorPhone?.let { DetailRow("Contractor Phone", it) }
|
||||
document.residenceAddress?.let { DetailRow(stringResource(Res.string.documents_residence), it) }
|
||||
document.contractorName?.let { DetailRow(stringResource(Res.string.documents_contractor), it) }
|
||||
document.contractorPhone?.let { DetailRow(stringResource(Res.string.documents_contractor_phone), it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,14 +294,14 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Additional Information",
|
||||
stringResource(Res.string.documents_additional_info),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.tags?.let { DetailRow("Tags", it) }
|
||||
document.notes?.let { DetailRow("Notes", it) }
|
||||
document.tags?.let { DetailRow(stringResource(Res.string.documents_tags), it) }
|
||||
document.notes?.let { DetailRow(stringResource(Res.string.documents_notes), it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,7 +314,7 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Images (${document.images.size})",
|
||||
stringResource(Res.string.documents_images, document.images.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -369,15 +371,15 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Attached File",
|
||||
stringResource(Res.string.documents_attached_file),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.fileType?.let { DetailRow("File Type", it) }
|
||||
document.fileType?.let { DetailRow(stringResource(Res.string.documents_file_type), it) }
|
||||
document.fileSize?.let {
|
||||
DetailRow("File Size", formatFileSize(it))
|
||||
DetailRow(stringResource(Res.string.documents_file_size), formatFileSize(it))
|
||||
}
|
||||
|
||||
Button(
|
||||
@@ -386,7 +388,7 @@ fun DocumentDetailScreen(
|
||||
) {
|
||||
Icon(Icons.Default.Download, null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Download File")
|
||||
Text(stringResource(Res.string.documents_download_file))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,15 +401,15 @@ fun DocumentDetailScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
"Metadata",
|
||||
stringResource(Res.string.documents_metadata),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Divider()
|
||||
|
||||
document.uploadedByUsername?.let { DetailRow("Uploaded By", it) }
|
||||
document.createdAt?.let { DetailRow("Created", DateUtils.formatDateMedium(it)) }
|
||||
document.updatedAt?.let { DetailRow("Updated", DateUtils.formatDateMedium(it)) }
|
||||
document.uploadedByUsername?.let { DetailRow(stringResource(Res.string.documents_uploaded_by), it) }
|
||||
document.createdAt?.let { DetailRow(stringResource(Res.string.documents_created), DateUtils.formatDateMedium(it)) }
|
||||
document.updatedAt?.let { DetailRow(stringResource(Res.string.documents_updated), DateUtils.formatDateMedium(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,8 +421,8 @@ fun DocumentDetailScreen(
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
title = { Text("Delete Document") },
|
||||
text = { Text("Are you sure you want to delete this document? This action cannot be undone.") },
|
||||
title = { Text(stringResource(Res.string.documents_delete)) },
|
||||
text = { Text(stringResource(Res.string.documents_delete_warning)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
@@ -428,12 +430,12 @@ fun DocumentDetailScreen(
|
||||
showDeleteDialog = false
|
||||
}
|
||||
) {
|
||||
Text("Delete", color = Color.Red)
|
||||
Text(stringResource(Res.string.common_delete), color = Color.Red)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = false }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -16,6 +16,8 @@ import com.example.casera.ui.subscription.UpgradeFeatureScreen
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.viewmodel.DocumentViewModel
|
||||
import com.example.casera.models.*
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
enum class DocumentTab {
|
||||
WARRANTIES, DOCUMENTS
|
||||
@@ -73,10 +75,10 @@ fun DocumentsScreen(
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text("Documents & Warranties", fontWeight = FontWeight.Bold) },
|
||||
title = { Text(stringResource(Res.string.documents_and_warranties), fontWeight = FontWeight.Bold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, "Back")
|
||||
Icon(Icons.Default.ArrowBack, stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@@ -85,7 +87,7 @@ fun DocumentsScreen(
|
||||
IconButton(onClick = { showActiveOnly = !showActiveOnly }) {
|
||||
Icon(
|
||||
if (showActiveOnly) Icons.Default.CheckCircle else Icons.Default.CheckCircleOutline,
|
||||
"Filter active",
|
||||
stringResource(Res.string.documents_filter_active),
|
||||
tint = if (showActiveOnly) MaterialTheme.colorScheme.secondary else LocalContentColor.current
|
||||
)
|
||||
}
|
||||
@@ -96,7 +98,7 @@ fun DocumentsScreen(
|
||||
IconButton(onClick = { showFiltersMenu = true }) {
|
||||
Icon(
|
||||
Icons.Default.FilterList,
|
||||
"Filters",
|
||||
stringResource(Res.string.documents_filters),
|
||||
tint = if (selectedCategory != null || selectedDocType != null)
|
||||
MaterialTheme.colorScheme.primary else LocalContentColor.current
|
||||
)
|
||||
@@ -108,7 +110,7 @@ fun DocumentsScreen(
|
||||
) {
|
||||
if (selectedTab == DocumentTab.WARRANTIES) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("All Categories") },
|
||||
text = { Text(stringResource(Res.string.documents_all_categories)) },
|
||||
onClick = {
|
||||
selectedCategory = null
|
||||
showFiltersMenu = false
|
||||
@@ -126,7 +128,7 @@ fun DocumentsScreen(
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text("All Types") },
|
||||
text = { Text(stringResource(Res.string.documents_all_types)) },
|
||||
onClick = {
|
||||
selectedDocType = null
|
||||
showFiltersMenu = false
|
||||
@@ -153,13 +155,13 @@ fun DocumentsScreen(
|
||||
Tab(
|
||||
selected = selectedTab == DocumentTab.WARRANTIES,
|
||||
onClick = { selectedTab = DocumentTab.WARRANTIES },
|
||||
text = { Text("Warranties") },
|
||||
text = { Text(stringResource(Res.string.documents_warranties_tab)) },
|
||||
icon = { Icon(Icons.Default.VerifiedUser, null) }
|
||||
)
|
||||
Tab(
|
||||
selected = selectedTab == DocumentTab.DOCUMENTS,
|
||||
onClick = { selectedTab = DocumentTab.DOCUMENTS },
|
||||
text = { Text("Documents") },
|
||||
text = { Text(stringResource(Res.string.documents_documents_tab)) },
|
||||
icon = { Icon(Icons.Default.Description, null) }
|
||||
)
|
||||
}
|
||||
@@ -183,7 +185,7 @@ fun DocumentsScreen(
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(Icons.Default.Add, "Add")
|
||||
Icon(Icons.Default.Add, stringResource(Res.string.documents_add_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,13 +245,13 @@ fun DocumentsScreen(
|
||||
if (showUpgradeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showUpgradeDialog = false },
|
||||
title = { Text("Upgrade Required") },
|
||||
title = { Text(stringResource(Res.string.documents_upgrade_required)) },
|
||||
text = {
|
||||
Text("You've reached the maximum number of documents for your current plan. Upgrade to Pro for unlimited documents.")
|
||||
Text(stringResource(Res.string.documents_upgrade_message))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showUpgradeDialog = false }) {
|
||||
Text("OK")
|
||||
Text(stringResource(Res.string.common_ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -17,6 +17,8 @@ import com.example.casera.viewmodel.ResidenceViewModel
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
import com.example.casera.models.*
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -53,7 +55,7 @@ fun EditTaskScreen(
|
||||
// Handle errors for task update
|
||||
updateTaskState.HandleErrors(
|
||||
onRetry = { /* Retry handled in UI */ },
|
||||
errorTitle = "Failed to Update Task"
|
||||
errorTitle = stringResource(Res.string.tasks_failed_to_update)
|
||||
)
|
||||
|
||||
// Handle update state changes
|
||||
@@ -67,18 +69,21 @@ fun EditTaskScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val titleRequiredError = stringResource(Res.string.tasks_title_error)
|
||||
val dueDateRequiredError = stringResource(Res.string.tasks_due_date_error)
|
||||
|
||||
fun validateForm(): Boolean {
|
||||
var isValid = true
|
||||
|
||||
if (title.isBlank()) {
|
||||
titleError = "Title is required"
|
||||
titleError = titleRequiredError
|
||||
isValid = false
|
||||
} else {
|
||||
titleError = ""
|
||||
}
|
||||
|
||||
if (dueDate.isNullOrBlank()) {
|
||||
dueDateError = "Due date is required"
|
||||
dueDateError = dueDateRequiredError
|
||||
isValid = false
|
||||
} else {
|
||||
dueDateError = ""
|
||||
@@ -90,10 +95,10 @@ fun EditTaskScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Edit Task") },
|
||||
title = { Text(stringResource(Res.string.tasks_edit_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -109,7 +114,7 @@ fun EditTaskScreen(
|
||||
) {
|
||||
// Required fields section
|
||||
Text(
|
||||
text = "Task Details",
|
||||
text = stringResource(Res.string.tasks_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -117,7 +122,7 @@ fun EditTaskScreen(
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text("Title *") },
|
||||
label = { Text(stringResource(Res.string.tasks_title_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = titleError.isNotEmpty(),
|
||||
supportingText = if (titleError.isNotEmpty()) {
|
||||
@@ -128,7 +133,7 @@ fun EditTaskScreen(
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Description") },
|
||||
label = { Text(stringResource(Res.string.tasks_description_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
@@ -143,7 +148,7 @@ fun EditTaskScreen(
|
||||
value = selectedCategory?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Category *") },
|
||||
label = { Text(stringResource(Res.string.tasks_category_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -175,7 +180,7 @@ fun EditTaskScreen(
|
||||
value = selectedFrequency?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Frequency *") },
|
||||
label = { Text(stringResource(Res.string.tasks_frequency_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -207,7 +212,7 @@ fun EditTaskScreen(
|
||||
value = selectedPriority?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Priority *") },
|
||||
label = { Text(stringResource(Res.string.tasks_priority_required)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = priorityExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -239,7 +244,7 @@ fun EditTaskScreen(
|
||||
value = selectedStatus?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Status *") },
|
||||
label = { Text(stringResource(Res.string.tasks_status_label)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = statusExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -265,19 +270,19 @@ fun EditTaskScreen(
|
||||
OutlinedTextField(
|
||||
value = dueDate,
|
||||
onValueChange = { dueDate = it },
|
||||
label = { Text("Due Date (YYYY-MM-DD) *") },
|
||||
label = { Text(stringResource(Res.string.tasks_due_date_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = dueDateError.isNotEmpty(),
|
||||
supportingText = if (dueDateError.isNotEmpty()) {
|
||||
{ Text(dueDateError) }
|
||||
} else null,
|
||||
placeholder = { Text("2025-01-31") }
|
||||
placeholder = { Text(stringResource(Res.string.tasks_due_date_placeholder)) }
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = estimatedCost,
|
||||
onValueChange = { estimatedCost = it },
|
||||
label = { Text("Estimated Cost") },
|
||||
label = { Text(stringResource(Res.string.tasks_estimated_cost_label)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = { Text("$") }
|
||||
@@ -325,7 +330,7 @@ fun EditTaskScreen(
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text("Update Task")
|
||||
Text(stringResource(Res.string.tasks_update))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.PasswordResetViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -60,10 +62,10 @@ fun ForgotPasswordScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Forgot Password") },
|
||||
title = { Text(stringResource(Res.string.auth_forgot_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -98,8 +100,8 @@ fun ForgotPasswordScreen(
|
||||
) {
|
||||
AuthHeader(
|
||||
icon = Icons.Default.Key,
|
||||
title = "Forgot Password?",
|
||||
subtitle = "Enter your email address and we'll send you a code to reset your password"
|
||||
title = stringResource(Res.string.auth_forgot_title),
|
||||
subtitle = stringResource(Res.string.auth_forgot_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -110,7 +112,7 @@ fun ForgotPasswordScreen(
|
||||
email = it
|
||||
viewModel.resetForgotPasswordState()
|
||||
},
|
||||
label = { Text("Email Address") },
|
||||
label = { Text(stringResource(Res.string.auth_forgot_email_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
@@ -178,7 +180,7 @@ fun ForgotPasswordScreen(
|
||||
Icon(Icons.Default.Send, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Send Reset Code",
|
||||
stringResource(Res.string.auth_forgot_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.components.HandleErrors
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.viewmodel.ResidenceViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -37,13 +39,13 @@ fun HomeScreen(
|
||||
// Handle errors for loading summary
|
||||
summaryState.HandleErrors(
|
||||
onRetry = { viewModel.loadMyResidences() },
|
||||
errorTitle = "Failed to Load Summary"
|
||||
errorTitle = stringResource(Res.string.home_failed_to_load)
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("myCrib", style = MaterialTheme.typography.headlineSmall) },
|
||||
title = { Text(stringResource(Res.string.app_name), style = MaterialTheme.typography.headlineSmall) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
@@ -51,7 +53,7 @@ fun HomeScreen(
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(
|
||||
Icons.Default.ExitToApp,
|
||||
contentDescription = "Logout",
|
||||
contentDescription = stringResource(Res.string.home_logout),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
@@ -72,12 +74,12 @@ fun HomeScreen(
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Welcome back",
|
||||
text = stringResource(Res.string.home_welcome),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "Manage your properties",
|
||||
text = stringResource(Res.string.home_manage_properties),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -127,12 +129,12 @@ fun HomeScreen(
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "Overview",
|
||||
text = stringResource(Res.string.home_overview),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "Your property stats",
|
||||
text = stringResource(Res.string.home_property_stats),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -147,7 +149,7 @@ fun HomeScreen(
|
||||
) {
|
||||
StatItem(
|
||||
value = "${summary.residences.size}",
|
||||
label = "Properties",
|
||||
label = stringResource(Res.string.home_properties),
|
||||
color = Color(0xFF3B82F6)
|
||||
)
|
||||
Divider(
|
||||
@@ -158,7 +160,7 @@ fun HomeScreen(
|
||||
)
|
||||
StatItem(
|
||||
value = "${summary.summary.totalTasks}",
|
||||
label = "Total Tasks",
|
||||
label = stringResource(Res.string.home_total_tasks),
|
||||
color = Color(0xFF8B5CF6)
|
||||
)
|
||||
Divider(
|
||||
@@ -169,7 +171,7 @@ fun HomeScreen(
|
||||
)
|
||||
StatItem(
|
||||
value = "${summary.summary.totalPending}",
|
||||
label = "Pending",
|
||||
label = stringResource(Res.string.home_pending),
|
||||
color = Color(0xFFF59E0B)
|
||||
)
|
||||
}
|
||||
@@ -197,8 +199,8 @@ fun HomeScreen(
|
||||
|
||||
// Residences Card
|
||||
NavigationCard(
|
||||
title = "Properties",
|
||||
subtitle = "Manage your residences",
|
||||
title = stringResource(Res.string.home_properties),
|
||||
subtitle = stringResource(Res.string.home_manage_residences),
|
||||
icon = Icons.Default.Home,
|
||||
iconColor = Color(0xFF3B82F6),
|
||||
onClick = onNavigateToResidences
|
||||
@@ -206,8 +208,8 @@ fun HomeScreen(
|
||||
|
||||
// Tasks Card
|
||||
NavigationCard(
|
||||
title = "Tasks",
|
||||
subtitle = "View and manage tasks",
|
||||
title = stringResource(Res.string.home_tasks),
|
||||
subtitle = stringResource(Res.string.home_view_manage_tasks),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconColor = Color(0xFF10B981),
|
||||
onClick = onNavigateToTasks
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
@@ -42,7 +44,7 @@ fun LoginScreen(
|
||||
// Handle errors for login
|
||||
loginState.HandleErrors(
|
||||
onRetry = { viewModel.login(username, password) },
|
||||
errorTitle = "Login Failed"
|
||||
errorTitle = stringResource(Res.string.auth_login_failed)
|
||||
)
|
||||
|
||||
// Handle login state changes
|
||||
@@ -88,8 +90,8 @@ fun LoginScreen(
|
||||
) {
|
||||
AuthHeader(
|
||||
icon = Icons.Default.Home,
|
||||
title = "myCrib",
|
||||
subtitle = "Manage your properties with ease"
|
||||
title = stringResource(Res.string.app_name),
|
||||
subtitle = stringResource(Res.string.app_tagline)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -97,7 +99,7 @@ fun LoginScreen(
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username or Email") },
|
||||
label = { Text(stringResource(Res.string.auth_login_username_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
@@ -113,7 +115,7 @@ fun LoginScreen(
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
label = { Text(stringResource(Res.string.auth_login_password_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
@@ -121,7 +123,7 @@ fun LoginScreen(
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (passwordVisible) "Hide password" else "Show password"
|
||||
contentDescription = if (passwordVisible) stringResource(Res.string.auth_hide_password) else stringResource(Res.string.auth_show_password)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -175,7 +177,7 @@ fun LoginScreen(
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
"Sign In",
|
||||
stringResource(Res.string.auth_login_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
@@ -189,7 +191,7 @@ fun LoginScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Forgot Password?",
|
||||
stringResource(Res.string.auth_forgot_password),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -200,7 +202,7 @@ fun LoginScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Don't have an account? Register",
|
||||
stringResource(Res.string.auth_no_account),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.example.casera.navigation.*
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
import com.example.casera.models.Residence
|
||||
import com.example.casera.storage.TokenStorage
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
@@ -35,8 +37,8 @@ fun MainScreen(
|
||||
tonalElevation = 3.dp
|
||||
) {
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Home, contentDescription = "Residences") },
|
||||
label = { Text("Residences") },
|
||||
icon = { Icon(Icons.Default.Home, contentDescription = stringResource(Res.string.properties_title)) },
|
||||
label = { Text(stringResource(Res.string.properties_title)) },
|
||||
selected = selectedTab == 0,
|
||||
onClick = {
|
||||
selectedTab = 0
|
||||
@@ -53,8 +55,8 @@ fun MainScreen(
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.CheckCircle, contentDescription = "Tasks") },
|
||||
label = { Text("Tasks") },
|
||||
icon = { Icon(Icons.Default.CheckCircle, contentDescription = stringResource(Res.string.tasks_title)) },
|
||||
label = { Text(stringResource(Res.string.tasks_title)) },
|
||||
selected = selectedTab == 1,
|
||||
onClick = {
|
||||
selectedTab = 1
|
||||
@@ -71,8 +73,8 @@ fun MainScreen(
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Build, contentDescription = "Contractors") },
|
||||
label = { Text("Contractors") },
|
||||
icon = { Icon(Icons.Default.Build, contentDescription = stringResource(Res.string.contractors_title)) },
|
||||
label = { Text(stringResource(Res.string.contractors_title)) },
|
||||
selected = selectedTab == 2,
|
||||
onClick = {
|
||||
selectedTab = 2
|
||||
@@ -89,8 +91,8 @@ fun MainScreen(
|
||||
)
|
||||
)
|
||||
NavigationBarItem(
|
||||
icon = { Icon(Icons.Default.Description, contentDescription = "Documents") },
|
||||
label = { Text("Documents") },
|
||||
icon = { Icon(Icons.Default.Description, contentDescription = stringResource(Res.string.documents_title)) },
|
||||
label = { Text(stringResource(Res.string.documents_title)) },
|
||||
selected = selectedTab == 3,
|
||||
onClick = {
|
||||
selectedTab = 3
|
||||
|
||||
@@ -17,6 +17,8 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.ui.theme.AppRadius
|
||||
import com.example.casera.ui.theme.AppSpacing
|
||||
import com.example.casera.viewmodel.NotificationPreferencesViewModel
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -55,10 +57,10 @@ fun NotificationPreferencesScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Notifications", fontWeight = FontWeight.SemiBold) },
|
||||
title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -98,13 +100,13 @@ fun NotificationPreferencesScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
"Notification Preferences",
|
||||
stringResource(Res.string.notifications_preferences),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
"Choose which notifications you'd like to receive",
|
||||
stringResource(Res.string.notifications_choose),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -157,7 +159,7 @@ fun NotificationPreferencesScreen(
|
||||
onClick = { viewModel.loadPreferences() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Retry")
|
||||
Text(stringResource(Res.string.common_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +168,7 @@ fun NotificationPreferencesScreen(
|
||||
is ApiResult.Success, is ApiResult.Idle -> {
|
||||
// Task Notifications Section
|
||||
Text(
|
||||
"Task Notifications",
|
||||
stringResource(Res.string.notifications_task_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
@@ -181,8 +183,8 @@ fun NotificationPreferencesScreen(
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = "Task Due Soon",
|
||||
description = "Reminders for upcoming tasks",
|
||||
title = stringResource(Res.string.notifications_task_due_soon),
|
||||
description = stringResource(Res.string.notifications_task_due_soon_desc),
|
||||
icon = Icons.Default.Schedule,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = taskDueSoon,
|
||||
@@ -198,8 +200,8 @@ fun NotificationPreferencesScreen(
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Task Overdue",
|
||||
description = "Alerts for overdue tasks",
|
||||
title = stringResource(Res.string.notifications_task_overdue),
|
||||
description = stringResource(Res.string.notifications_task_overdue_desc),
|
||||
icon = Icons.Default.Warning,
|
||||
iconTint = MaterialTheme.colorScheme.error,
|
||||
checked = taskOverdue,
|
||||
@@ -215,8 +217,8 @@ fun NotificationPreferencesScreen(
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Task Completed",
|
||||
description = "When someone completes a task",
|
||||
title = stringResource(Res.string.notifications_task_completed),
|
||||
description = stringResource(Res.string.notifications_task_completed_desc),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = taskCompleted,
|
||||
@@ -232,8 +234,8 @@ fun NotificationPreferencesScreen(
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Task Assigned",
|
||||
description = "When a task is assigned to you",
|
||||
title = stringResource(Res.string.notifications_task_assigned),
|
||||
description = stringResource(Res.string.notifications_task_assigned_desc),
|
||||
icon = Icons.Default.PersonAdd,
|
||||
iconTint = MaterialTheme.colorScheme.secondary,
|
||||
checked = taskAssigned,
|
||||
@@ -247,7 +249,7 @@ fun NotificationPreferencesScreen(
|
||||
|
||||
// Other Notifications Section
|
||||
Text(
|
||||
"Other Notifications",
|
||||
stringResource(Res.string.notifications_other_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = AppSpacing.md)
|
||||
@@ -262,8 +264,8 @@ fun NotificationPreferencesScreen(
|
||||
) {
|
||||
Column {
|
||||
NotificationToggle(
|
||||
title = "Property Shared",
|
||||
description = "When someone shares a property with you",
|
||||
title = stringResource(Res.string.notifications_property_shared),
|
||||
description = stringResource(Res.string.notifications_property_shared_desc),
|
||||
icon = Icons.Default.Home,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
checked = residenceShared,
|
||||
@@ -279,8 +281,8 @@ fun NotificationPreferencesScreen(
|
||||
)
|
||||
|
||||
NotificationToggle(
|
||||
title = "Warranty Expiring",
|
||||
description = "Reminders for expiring warranties",
|
||||
title = stringResource(Res.string.notifications_warranty_expiring),
|
||||
description = stringResource(Res.string.notifications_warranty_expiring_desc),
|
||||
icon = Icons.Default.Description,
|
||||
iconTint = MaterialTheme.colorScheme.tertiary,
|
||||
checked = warrantyExpiring,
|
||||
|
||||
@@ -28,6 +28,8 @@ import com.example.casera.storage.TokenStorage
|
||||
import com.example.casera.cache.SubscriptionCache
|
||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import androidx.compose.runtime.getValue
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -60,7 +62,7 @@ fun ProfileScreen(
|
||||
email = email
|
||||
)
|
||||
},
|
||||
errorTitle = "Failed to Update Profile"
|
||||
errorTitle = stringResource(Res.string.profile_update_failed)
|
||||
)
|
||||
|
||||
// Load current user data
|
||||
@@ -76,12 +78,12 @@ fun ProfileScreen(
|
||||
isLoadingUser = false
|
||||
}
|
||||
else -> {
|
||||
errorMessage = "Failed to load user data"
|
||||
errorMessage = "profile_load_failed"
|
||||
isLoadingUser = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorMessage = "Not authenticated"
|
||||
errorMessage = "profile_not_authenticated"
|
||||
isLoadingUser = false
|
||||
}
|
||||
}
|
||||
@@ -116,15 +118,15 @@ fun ProfileScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Profile", fontWeight = FontWeight.SemiBold) },
|
||||
title = { Text(stringResource(Res.string.profile_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(Icons.Default.Logout, contentDescription = "Logout")
|
||||
Icon(Icons.Default.Logout, contentDescription = stringResource(Res.string.profile_logout))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -163,7 +165,7 @@ fun ProfileScreen(
|
||||
)
|
||||
|
||||
Text(
|
||||
"Update Your Profile",
|
||||
stringResource(Res.string.profile_update_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
@@ -191,7 +193,7 @@ fun ProfileScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
text = "Appearance",
|
||||
text = stringResource(Res.string.profile_appearance),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -203,7 +205,7 @@ fun ProfileScreen(
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Palette,
|
||||
contentDescription = "Change theme",
|
||||
contentDescription = stringResource(Res.string.profile_change_theme),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
@@ -230,19 +232,19 @@ fun ProfileScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
text = "Notifications",
|
||||
text = stringResource(Res.string.profile_notifications),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "Manage notification preferences",
|
||||
text = stringResource(Res.string.profile_notifications_subtitle),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = "Notification preferences",
|
||||
contentDescription = stringResource(Res.string.profile_notifications),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
@@ -279,16 +281,16 @@ fun ProfileScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
|
||||
) {
|
||||
Text(
|
||||
text = if (SubscriptionHelper.currentTier == "pro") "Pro Plan" else "Free Plan",
|
||||
text = if (SubscriptionHelper.currentTier == "pro") stringResource(Res.string.profile_pro_plan) else stringResource(Res.string.profile_free_plan),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (SubscriptionHelper.currentTier == "pro" && currentSubscription?.expiresAt != null) {
|
||||
"Active until ${currentSubscription?.expiresAt}"
|
||||
stringResource(Res.string.profile_active_until, currentSubscription?.expiresAt ?: "")
|
||||
} else {
|
||||
"Limited features"
|
||||
stringResource(Res.string.profile_limited_features)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -309,11 +311,11 @@ fun ProfileScreen(
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(AppSpacing.sm))
|
||||
Text("Upgrade to Pro", fontWeight = FontWeight.SemiBold)
|
||||
Text(stringResource(Res.string.profile_upgrade_to_pro), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "Manage your subscription in the Google Play Store",
|
||||
text = stringResource(Res.string.profile_manage_subscription),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = AppSpacing.xs)
|
||||
@@ -326,7 +328,7 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
Text(
|
||||
"Profile Information",
|
||||
stringResource(Res.string.profile_information),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.align(Alignment.Start)
|
||||
@@ -335,7 +337,7 @@ fun ProfileScreen(
|
||||
OutlinedTextField(
|
||||
value = firstName,
|
||||
onValueChange = { firstName = it },
|
||||
label = { Text("First Name") },
|
||||
label = { Text(stringResource(Res.string.profile_first_name)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
@@ -347,7 +349,7 @@ fun ProfileScreen(
|
||||
OutlinedTextField(
|
||||
value = lastName,
|
||||
onValueChange = { lastName = it },
|
||||
label = { Text("Last Name") },
|
||||
label = { Text(stringResource(Res.string.profile_last_name)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
@@ -359,7 +361,7 @@ fun ProfileScreen(
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
label = { Text(stringResource(Res.string.profile_email)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
@@ -369,6 +371,13 @@ fun ProfileScreen(
|
||||
)
|
||||
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
val displayError = when (errorMessage) {
|
||||
"profile_load_failed" -> stringResource(Res.string.profile_load_failed)
|
||||
"profile_not_authenticated" -> stringResource(Res.string.profile_not_authenticated)
|
||||
"profile_update_coming_soon" -> stringResource(Res.string.profile_update_coming_soon)
|
||||
"profile_email_required" -> stringResource(Res.string.profile_email_required)
|
||||
else -> errorMessage
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
@@ -389,7 +398,7 @@ fun ProfileScreen(
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
errorMessage,
|
||||
displayError,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
@@ -434,9 +443,9 @@ fun ProfileScreen(
|
||||
onClick = {
|
||||
if (email.isNotEmpty()) {
|
||||
// viewModel.updateProfile not available yet
|
||||
errorMessage = "Profile update coming soon"
|
||||
errorMessage = "profile_update_coming_soon"
|
||||
} else {
|
||||
errorMessage = "Email is required"
|
||||
errorMessage = "profile_email_required"
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
@@ -458,7 +467,7 @@ fun ProfileScreen(
|
||||
) {
|
||||
Icon(Icons.Default.Save, contentDescription = null)
|
||||
Text(
|
||||
"Save Changes",
|
||||
stringResource(Res.string.profile_save),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -39,7 +41,7 @@ fun RegisterScreen(
|
||||
// Handle errors for registration
|
||||
createState.HandleErrors(
|
||||
onRetry = { viewModel.register(username, email, password) },
|
||||
errorTitle = "Registration Failed"
|
||||
errorTitle = stringResource(Res.string.error_generic)
|
||||
)
|
||||
|
||||
LaunchedEffect(createState) {
|
||||
@@ -55,10 +57,10 @@ fun RegisterScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Create Account", fontWeight = FontWeight.SemiBold) },
|
||||
title = { Text(stringResource(Res.string.auth_register_title), fontWeight = FontWeight.SemiBold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -80,8 +82,8 @@ fun RegisterScreen(
|
||||
|
||||
AuthHeader(
|
||||
icon = Icons.Default.PersonAdd,
|
||||
title = "Join myCrib",
|
||||
subtitle = "Start managing your properties today"
|
||||
title = stringResource(Res.string.auth_register_title),
|
||||
subtitle = stringResource(Res.string.auth_register_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -89,7 +91,7 @@ fun RegisterScreen(
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
label = { Text(stringResource(Res.string.auth_register_username)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
},
|
||||
@@ -101,7 +103,7 @@ fun RegisterScreen(
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("Email") },
|
||||
label = { Text(stringResource(Res.string.auth_register_email)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, contentDescription = null)
|
||||
},
|
||||
@@ -113,7 +115,7 @@ fun RegisterScreen(
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
label = { Text(stringResource(Res.string.auth_register_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
@@ -126,7 +128,7 @@ fun RegisterScreen(
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text("Confirm Password") },
|
||||
label = { Text(stringResource(Res.string.auth_register_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
@@ -140,11 +142,12 @@ fun RegisterScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match)
|
||||
Button(
|
||||
onClick = {
|
||||
when {
|
||||
password != confirmPassword -> {
|
||||
errorMessage = "Passwords do not match"
|
||||
errorMessage = passwordsDontMatchMessage
|
||||
}
|
||||
else -> {
|
||||
isLoading = true
|
||||
@@ -168,7 +171,7 @@ fun RegisterScreen(
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
"Create Account",
|
||||
stringResource(Res.string.auth_register_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.example.casera.ui.components.auth.RequirementItem
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.PasswordResetViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -59,11 +61,11 @@ fun ResetPasswordScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Reset Password") },
|
||||
title = { Text(stringResource(Res.string.auth_reset_title)) },
|
||||
navigationIcon = {
|
||||
onNavigateBack?.let { callback ->
|
||||
IconButton(onClick = callback) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -184,7 +186,7 @@ fun ResetPasswordScreen(
|
||||
newPassword = it
|
||||
viewModel.resetResetPasswordState()
|
||||
},
|
||||
label = { Text("New Password") },
|
||||
label = { Text(stringResource(Res.string.auth_reset_new_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
@@ -209,7 +211,7 @@ fun ResetPasswordScreen(
|
||||
confirmPassword = it
|
||||
viewModel.resetResetPasswordState()
|
||||
},
|
||||
label = { Text("Confirm Password") },
|
||||
label = { Text(stringResource(Res.string.auth_reset_confirm_password)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
},
|
||||
@@ -250,7 +252,7 @@ fun ResetPasswordScreen(
|
||||
Icon(Icons.Default.LockReset, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Reset Password",
|
||||
stringResource(Res.string.auth_reset_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import com.example.casera.cache.SubscriptionCache
|
||||
import com.example.casera.cache.DataCache
|
||||
import com.example.casera.util.DateUtils
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -146,7 +148,7 @@ fun ResidenceDetailScreen(
|
||||
// Handle errors for generate report
|
||||
generateReportState.HandleErrors(
|
||||
onRetry = { residenceViewModel.generateTasksReport(residenceId) },
|
||||
errorTitle = "Failed to Generate Report"
|
||||
errorTitle = stringResource(Res.string.properties_failed_to_generate_report)
|
||||
)
|
||||
|
||||
LaunchedEffect(generateReportState) {
|
||||
@@ -164,7 +166,7 @@ fun ResidenceDetailScreen(
|
||||
// Handle errors for delete residence
|
||||
deleteState.HandleErrors(
|
||||
onRetry = { residenceViewModel.deleteResidence(residenceId) },
|
||||
errorTitle = "Failed to Delete Property"
|
||||
errorTitle = stringResource(Res.string.properties_failed_to_delete)
|
||||
)
|
||||
|
||||
// Handle delete residence state
|
||||
@@ -245,8 +247,8 @@ fun ResidenceDetailScreen(
|
||||
if (showReportConfirmation) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showReportConfirmation = false },
|
||||
title = { Text("Generate Report") },
|
||||
text = { Text("This will generate and email a maintenance report for this property. Do you want to continue?") },
|
||||
title = { Text(stringResource(Res.string.properties_generate_report)) },
|
||||
text = { Text(stringResource(Res.string.properties_generate_report_confirm)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -254,12 +256,12 @@ fun ResidenceDetailScreen(
|
||||
residenceViewModel.generateTasksReport(residenceId)
|
||||
}
|
||||
) {
|
||||
Text("Generate")
|
||||
Text(stringResource(Res.string.properties_generate))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showReportConfirmation = false }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -269,8 +271,8 @@ fun ResidenceDetailScreen(
|
||||
val residence = (residenceState as ApiResult.Success<Residence>).data
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirmation = false },
|
||||
title = { Text("Delete Residence") },
|
||||
text = { Text("Are you sure you want to delete ${residence.name}? This action cannot be undone.") },
|
||||
title = { Text(stringResource(Res.string.properties_delete_residence)) },
|
||||
text = { Text(stringResource(Res.string.properties_delete_name_confirm, residence.name)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -281,12 +283,12 @@ fun ResidenceDetailScreen(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("Delete")
|
||||
Text(stringResource(Res.string.common_delete))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteConfirmation = false }) {
|
||||
Text("Cancel")
|
||||
Text(stringResource(Res.string.common_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -298,8 +300,8 @@ fun ResidenceDetailScreen(
|
||||
showCancelTaskConfirmation = false
|
||||
taskToCancel = null
|
||||
},
|
||||
title = { Text("Cancel Task") },
|
||||
text = { Text("Are you sure you want to cancel \"${taskToCancel!!.title}\"? This action cannot be undone.") },
|
||||
title = { Text(stringResource(Res.string.properties_cancel_task)) },
|
||||
text = { Text(stringResource(Res.string.properties_cancel_task_confirm, taskToCancel!!.title)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -311,7 +313,7 @@ fun ResidenceDetailScreen(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("Cancel Task")
|
||||
Text(stringResource(Res.string.properties_cancel_task))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
@@ -319,7 +321,7 @@ fun ResidenceDetailScreen(
|
||||
showCancelTaskConfirmation = false
|
||||
taskToCancel = null
|
||||
}) {
|
||||
Text("Dismiss")
|
||||
Text(stringResource(Res.string.properties_dismiss))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -331,8 +333,8 @@ fun ResidenceDetailScreen(
|
||||
showArchiveTaskConfirmation = false
|
||||
taskToArchive = null
|
||||
},
|
||||
title = { Text("Archive Task") },
|
||||
text = { Text("Are you sure you want to archive \"${taskToArchive!!.title}\"? You can unarchive it later from archived tasks.") },
|
||||
title = { Text(stringResource(Res.string.properties_archive_task)) },
|
||||
text = { Text(stringResource(Res.string.properties_archive_task_confirm, taskToArchive!!.title)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -348,7 +350,7 @@ fun ResidenceDetailScreen(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("Archive")
|
||||
Text(stringResource(Res.string.tasks_archive))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
@@ -356,7 +358,7 @@ fun ResidenceDetailScreen(
|
||||
showArchiveTaskConfirmation = false
|
||||
taskToArchive = null
|
||||
}) {
|
||||
Text("Dismiss")
|
||||
Text(stringResource(Res.string.properties_dismiss))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -375,10 +377,10 @@ fun ResidenceDetailScreen(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Property Details", fontWeight = FontWeight.Bold) },
|
||||
title = { Text(stringResource(Res.string.properties_details), fontWeight = FontWeight.Bold) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
@@ -399,7 +401,7 @@ fun ResidenceDetailScreen(
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.Description, contentDescription = "Generate Report")
|
||||
Icon(Icons.Default.Description, contentDescription = stringResource(Res.string.properties_generate_report))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,14 +410,14 @@ fun ResidenceDetailScreen(
|
||||
IconButton(onClick = {
|
||||
showManageUsersDialog = true
|
||||
}) {
|
||||
Icon(Icons.Default.People, contentDescription = "Manage Users")
|
||||
Icon(Icons.Default.People, contentDescription = stringResource(Res.string.properties_manage_users))
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
onNavigateToEditResidence(residence)
|
||||
}) {
|
||||
Icon(Icons.Default.Edit, contentDescription = "Edit Residence")
|
||||
Icon(Icons.Default.Edit, contentDescription = stringResource(Res.string.properties_edit_residence))
|
||||
}
|
||||
|
||||
// Delete button - only show for primary owners
|
||||
@@ -425,7 +427,7 @@ fun ResidenceDetailScreen(
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete Residence",
|
||||
contentDescription = stringResource(Res.string.properties_delete_residence),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
@@ -453,7 +455,7 @@ fun ResidenceDetailScreen(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Task")
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(Res.string.properties_add_task))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,7 +468,7 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
errorTitle = "Failed to Load Property",
|
||||
errorTitle = stringResource(Res.string.properties_failed_to_load),
|
||||
loadingContent = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -474,7 +476,7 @@ fun ResidenceDetailScreen(
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
text = "Loading residence...",
|
||||
text = stringResource(Res.string.properties_loading),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -528,7 +530,7 @@ fun ResidenceDetailScreen(
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.LocationOn,
|
||||
title = "Address"
|
||||
title = stringResource(Res.string.properties_address_section)
|
||||
) {
|
||||
if (residence.streetAddress != null) {
|
||||
Text(text = residence.streetAddress)
|
||||
@@ -552,7 +554,7 @@ fun ResidenceDetailScreen(
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.Info,
|
||||
title = "Property Details"
|
||||
title = stringResource(Res.string.properties_property_details_section)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -584,7 +586,7 @@ fun ResidenceDetailScreen(
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.Description,
|
||||
title = "Description"
|
||||
title = stringResource(Res.string.properties_description_section)
|
||||
) {
|
||||
Text(
|
||||
text = residence.description,
|
||||
@@ -600,7 +602,7 @@ fun ResidenceDetailScreen(
|
||||
item {
|
||||
InfoCard(
|
||||
icon = Icons.Default.AttachMoney,
|
||||
title = "Purchase Information"
|
||||
title = stringResource(Res.string.properties_purchase_info)
|
||||
) {
|
||||
residence.purchaseDate?.let {
|
||||
DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it))
|
||||
@@ -628,7 +630,7 @@ fun ResidenceDetailScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Tasks",
|
||||
text = stringResource(Res.string.tasks_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
@@ -688,12 +690,12 @@ fun ResidenceDetailScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
"No tasks yet",
|
||||
stringResource(Res.string.properties_no_tasks),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Add a task to get started",
|
||||
stringResource(Res.string.properties_add_task_start),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -767,7 +769,7 @@ fun ResidenceDetailScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Contractors",
|
||||
text = stringResource(Res.string.contractors_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
@@ -826,12 +828,12 @@ fun ResidenceDetailScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
"No contractors yet",
|
||||
stringResource(Res.string.properties_no_contractors),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Add contractors from the Contractors tab",
|
||||
stringResource(Res.string.properties_add_contractors_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ import com.example.casera.models.Residence
|
||||
import com.example.casera.models.ResidenceCreateRequest
|
||||
import com.example.casera.models.ResidenceType
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -85,11 +87,13 @@ fun ResidenceFormScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val nameRequiredError = stringResource(Res.string.properties_form_name_error)
|
||||
|
||||
fun validateForm(): Boolean {
|
||||
var isValid = true
|
||||
|
||||
if (name.isBlank()) {
|
||||
nameError = "Name is required"
|
||||
nameError = nameRequiredError
|
||||
isValid = false
|
||||
} else {
|
||||
nameError = ""
|
||||
@@ -101,10 +105,10 @@ fun ResidenceFormScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(if (isEditMode) "Edit Residence" else "Add Residence") },
|
||||
title = { Text(if (isEditMode) stringResource(Res.string.properties_edit_title) else stringResource(Res.string.properties_add_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -120,7 +124,7 @@ fun ResidenceFormScreen(
|
||||
) {
|
||||
// Basic Information section
|
||||
Text(
|
||||
text = "Property Details",
|
||||
text = stringResource(Res.string.properties_details),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -128,13 +132,13 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Property Name *") },
|
||||
label = { Text(stringResource(Res.string.properties_form_name_required)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = nameError.isNotEmpty(),
|
||||
supportingText = if (nameError.isNotEmpty()) {
|
||||
{ Text(nameError, color = MaterialTheme.colorScheme.error) }
|
||||
} else {
|
||||
{ Text("Required", color = MaterialTheme.colorScheme.error) }
|
||||
{ Text(stringResource(Res.string.properties_form_required), color = MaterialTheme.colorScheme.error) }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -146,7 +150,7 @@ fun ResidenceFormScreen(
|
||||
value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Property Type") },
|
||||
label = { Text(stringResource(Res.string.properties_type_label)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -171,7 +175,7 @@ fun ResidenceFormScreen(
|
||||
|
||||
// Address section
|
||||
Text(
|
||||
text = "Address",
|
||||
text = stringResource(Res.string.properties_address_section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -179,49 +183,49 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = streetAddress,
|
||||
onValueChange = { streetAddress = it },
|
||||
label = { Text("Street Address") },
|
||||
label = { Text(stringResource(Res.string.properties_form_street)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = apartmentUnit,
|
||||
onValueChange = { apartmentUnit = it },
|
||||
label = { Text("Apartment/Unit #") },
|
||||
label = { Text(stringResource(Res.string.properties_form_apartment)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = city,
|
||||
onValueChange = { city = it },
|
||||
label = { Text("City") },
|
||||
label = { Text(stringResource(Res.string.properties_form_city)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = stateProvince,
|
||||
onValueChange = { stateProvince = it },
|
||||
label = { Text("State/Province") },
|
||||
label = { Text(stringResource(Res.string.properties_form_state)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = postalCode,
|
||||
onValueChange = { postalCode = it },
|
||||
label = { Text("Postal Code") },
|
||||
label = { Text(stringResource(Res.string.properties_form_postal)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = country,
|
||||
onValueChange = { country = it },
|
||||
label = { Text("Country") },
|
||||
label = { Text(stringResource(Res.string.properties_form_country)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Optional fields section
|
||||
Divider()
|
||||
Text(
|
||||
text = "Optional Details",
|
||||
text = stringResource(Res.string.properties_form_optional),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
@@ -233,7 +237,7 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = bedrooms,
|
||||
onValueChange = { bedrooms = it.filter { char -> char.isDigit() } },
|
||||
label = { Text("Bedrooms") },
|
||||
label = { Text(stringResource(Res.string.properties_bedrooms)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
@@ -241,7 +245,7 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = bathrooms,
|
||||
onValueChange = { bathrooms = it },
|
||||
label = { Text("Bathrooms") },
|
||||
label = { Text(stringResource(Res.string.properties_bathrooms)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
@@ -250,7 +254,7 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = squareFootage,
|
||||
onValueChange = { squareFootage = it.filter { char -> char.isDigit() } },
|
||||
label = { Text("Square Footage") },
|
||||
label = { Text(stringResource(Res.string.properties_form_sqft)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
@@ -258,7 +262,7 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = lotSize,
|
||||
onValueChange = { lotSize = it },
|
||||
label = { Text("Lot Size (acres)") },
|
||||
label = { Text(stringResource(Res.string.properties_form_lot_size)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
@@ -266,7 +270,7 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = yearBuilt,
|
||||
onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } },
|
||||
label = { Text("Year Built") },
|
||||
label = { Text(stringResource(Res.string.properties_year_built)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
@@ -274,7 +278,7 @@ fun ResidenceFormScreen(
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Description") },
|
||||
label = { Text(stringResource(Res.string.properties_form_description)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
@@ -284,7 +288,7 @@ fun ResidenceFormScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("Primary Residence")
|
||||
Text(stringResource(Res.string.properties_form_primary))
|
||||
Switch(
|
||||
checked = isPrimary,
|
||||
onCheckedChange = { isPrimary = it }
|
||||
@@ -338,7 +342,7 @@ fun ResidenceFormScreen(
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text(if (isEditMode) "Update Residence" else "Create Residence")
|
||||
Text(if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.example.casera.network.ApiResult
|
||||
import com.example.casera.utils.SubscriptionHelper
|
||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||
import com.example.casera.cache.SubscriptionCache
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -107,7 +109,7 @@ fun ResidencesScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"My Properties",
|
||||
stringResource(Res.string.properties_title),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
@@ -123,14 +125,14 @@ fun ResidencesScreen(
|
||||
showUpgradePrompt = true
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.GroupAdd, contentDescription = "Join with Code")
|
||||
Icon(Icons.Default.GroupAdd, contentDescription = stringResource(Res.string.properties_join_title))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onNavigateToProfile) {
|
||||
Icon(Icons.Default.AccountCircle, contentDescription = "Profile")
|
||||
Icon(Icons.Default.AccountCircle, contentDescription = stringResource(Res.string.profile_title))
|
||||
}
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(Icons.Default.ExitToApp, contentDescription = "Logout")
|
||||
Icon(Icons.Default.ExitToApp, contentDescription = stringResource(Res.string.home_logout))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -163,7 +165,7 @@ fun ResidencesScreen(
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "Add Property",
|
||||
contentDescription = stringResource(Res.string.properties_add_button),
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
@@ -176,7 +178,7 @@ fun ResidencesScreen(
|
||||
state = myResidencesState,
|
||||
onRetry = { viewModel.loadMyResidences() },
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
errorTitle = "Failed to Load Properties"
|
||||
errorTitle = stringResource(Res.string.error_generic)
|
||||
) { response ->
|
||||
if (response.residences.isEmpty()) {
|
||||
Box(
|
||||
@@ -197,12 +199,12 @@ fun ResidencesScreen(
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
"No properties yet",
|
||||
stringResource(Res.string.properties_empty_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Add your first property to get started!",
|
||||
stringResource(Res.string.properties_empty_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -230,7 +232,7 @@ fun ResidencesScreen(
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Text(
|
||||
"Add Property",
|
||||
stringResource(Res.string.properties_add_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -258,7 +260,7 @@ fun ResidencesScreen(
|
||||
) {
|
||||
Icon(Icons.Default.GroupAdd, contentDescription = null)
|
||||
Text(
|
||||
"Join with Code",
|
||||
stringResource(Res.string.properties_join_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -339,7 +341,7 @@ fun ResidencesScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Overview",
|
||||
text = stringResource(Res.string.home_overview),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
@@ -353,12 +355,12 @@ fun ResidencesScreen(
|
||||
StatItem(
|
||||
icon = Icons.Default.Home,
|
||||
value = "${response.summary.totalResidences}",
|
||||
label = "Properties"
|
||||
label = stringResource(Res.string.home_properties)
|
||||
)
|
||||
StatItem(
|
||||
icon = Icons.Default.Assignment,
|
||||
value = "${response.summary.totalTasks}",
|
||||
label = "Total Tasks"
|
||||
label = stringResource(Res.string.home_total_tasks)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -373,12 +375,12 @@ fun ResidencesScreen(
|
||||
StatItem(
|
||||
icon = Icons.Default.CalendarToday,
|
||||
value = "${response.summary.tasksDueNextWeek}",
|
||||
label = "Due This Week"
|
||||
label = stringResource(Res.string.tasks_column_due_soon)
|
||||
)
|
||||
StatItem(
|
||||
icon = Icons.Default.Event,
|
||||
value = "${response.summary.tasksDueNextMonth}",
|
||||
label = "Next 30 Days"
|
||||
label = stringResource(Res.string.tasks_column_upcoming)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.utils.hexToColor
|
||||
import com.example.casera.viewmodel.TaskCompletionViewModel
|
||||
import com.example.casera.viewmodel.TaskViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -64,10 +66,10 @@ fun TasksScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("All Tasks") },
|
||||
title = { Text(stringResource(Res.string.tasks_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -108,20 +110,15 @@ fun TasksScreen(
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
"No tasks yet",
|
||||
stringResource(Res.string.tasks_empty_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Tasks are created from your properties.",
|
||||
stringResource(Res.string.tasks_empty_subtitle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"Go to Residences tab to add a property, then add tasks to it!",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.AuthViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -65,7 +67,7 @@ fun VerifyEmailScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Verify Email", fontWeight = FontWeight.SemiBold) },
|
||||
title = { Text(stringResource(Res.string.auth_verify_title), fontWeight = FontWeight.SemiBold) },
|
||||
actions = {
|
||||
TextButton(onClick = onLogout) {
|
||||
Row(
|
||||
@@ -77,7 +79,7 @@ fun VerifyEmailScreen(
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text("Logout")
|
||||
Text(stringResource(Res.string.home_logout))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -100,8 +102,8 @@ fun VerifyEmailScreen(
|
||||
|
||||
AuthHeader(
|
||||
icon = Icons.Default.MarkEmailRead,
|
||||
title = "Verify Your Email",
|
||||
subtitle = "You must verify your email address to continue"
|
||||
title = stringResource(Res.string.auth_verify_title),
|
||||
subtitle = stringResource(Res.string.auth_verify_subtitle)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -140,7 +142,7 @@ fun VerifyEmailScreen(
|
||||
code = it
|
||||
}
|
||||
},
|
||||
label = { Text("Verification Code") },
|
||||
label = { Text(stringResource(Res.string.auth_verify_code_label)) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Pin, contentDescription = null)
|
||||
},
|
||||
@@ -184,7 +186,7 @@ fun VerifyEmailScreen(
|
||||
) {
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
||||
Text(
|
||||
"Verify Email",
|
||||
stringResource(Res.string.auth_verify_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.example.casera.ui.components.auth.AuthHeader
|
||||
import com.example.casera.ui.components.common.ErrorCard
|
||||
import com.example.casera.viewmodel.PasswordResetViewModel
|
||||
import com.example.casera.network.ApiResult
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -56,10 +58,10 @@ fun VerifyResetCodeScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Verify Code") },
|
||||
title = { Text(stringResource(Res.string.auth_verify_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
@@ -140,7 +142,7 @@ fun VerifyResetCodeScreen(
|
||||
viewModel.resetVerifyCodeState()
|
||||
}
|
||||
},
|
||||
label = { Text("Verification Code") },
|
||||
label = { Text(stringResource(Res.string.auth_verify_code_label)) },
|
||||
placeholder = { Text("000000") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
@@ -210,7 +212,7 @@ fun VerifyResetCodeScreen(
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Verify Code",
|
||||
stringResource(Res.string.auth_verify_button),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -234,7 +236,7 @@ fun VerifyResetCodeScreen(
|
||||
onNavigateBack()
|
||||
}) {
|
||||
Text(
|
||||
"Send New Code",
|
||||
stringResource(Res.string.auth_verify_resend),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user