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:
Trey t
2025-12-02 02:02:00 -06:00
parent e62e7d4371
commit c726320c1e
59 changed files with 19839 additions and 757 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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