From 415799b6d04286662d329fb4ebbc46d307f2c430 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 11 Nov 2025 14:39:33 -0600 Subject: [PATCH] Refactor iOS and Android code to follow single responsibility principle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - iOS: Extracted views and helpers into separate files - Created Documents/Helpers/DocumentHelpers.swift for type/category helpers - Created Documents/Components/ with individual view files: - ImageViewerSheet.swift - WarrantyCard.swift - DocumentCard.swift - EmptyStateView.swift - WarrantiesTabContent.swift - DocumentsTabContent.swift - Cleaned up DocumentDetailView.swift and DocumentsWarrantiesView.swift - Android: Extracted composables into organized component structure - Created ui/components/documents/ package with: - DocumentCard.kt (WarrantyCardContent, RegularDocumentCardContent, formatFileSize) - DocumentStates.kt (EmptyState, ErrorState) - DocumentsTabContent.kt - Reduced DocumentsScreen.kt from 506 lines to 211 lines - Added missing imports to DocumentDetailScreen.kt and EditDocumentScreen.kt Net result: +770 insertions, -716 deletions across 15 files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../ui/components/documents/DocumentCard.kt | 241 +++++++++++++ .../ui/components/documents/DocumentStates.kt | 42 +++ .../documents/DocumentsTabContent.kt | 58 +++ .../mycrib/ui/screens/DocumentDetailScreen.kt | 2 + .../mycrib/ui/screens/DocumentsScreen.kt | 297 +--------------- .../mycrib/ui/screens/EditDocumentScreen.kt | 1 + .../Documents/Components/DocumentCard.swift | 99 ++++++ .../Components/DocumentsTabContent.swift | 50 +++ .../Documents/Components/EmptyStateView.swift | 25 ++ .../Components/ImageViewerSheet.swift | 58 +++ .../Components/WarrantiesTabContent.swift | 52 +++ .../Documents/Components/WarrantyCard.swift | 107 ++++++ .../iosApp/Documents/DocumentDetailView.swift | 91 ----- .../Documents/DocumentsWarrantiesView.swift | 329 ------------------ .../Documents/Helpers/DocumentHelpers.swift | 34 ++ 15 files changed, 770 insertions(+), 716 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentCard.kt create mode 100644 composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentStates.kt create mode 100644 composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt create mode 100644 iosApp/iosApp/Documents/Components/DocumentCard.swift create mode 100644 iosApp/iosApp/Documents/Components/DocumentsTabContent.swift create mode 100644 iosApp/iosApp/Documents/Components/EmptyStateView.swift create mode 100644 iosApp/iosApp/Documents/Components/ImageViewerSheet.swift create mode 100644 iosApp/iosApp/Documents/Components/WarrantiesTabContent.swift create mode 100644 iosApp/iosApp/Documents/Components/WarrantyCard.swift create mode 100644 iosApp/iosApp/Documents/Helpers/DocumentHelpers.swift diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentCard.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentCard.kt new file mode 100644 index 0000000..c2546b2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentCard.kt @@ -0,0 +1,241 @@ +package com.mycrib.android.ui.components.documents + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.mycrib.shared.models.Document +import com.mycrib.shared.models.DocumentCategory +import com.mycrib.shared.models.DocumentType + +@Composable +fun DocumentCard(document: Document, isWarrantyCard: Boolean = false, onClick: () -> Unit) { + if (isWarrantyCard) { + WarrantyCardContent(document = document, onClick = onClick) + } else { + RegularDocumentCardContent(document = document, onClick = onClick) + } +} + +@Composable +private fun WarrantyCardContent(document: Document, onClick: () -> Unit) { + val daysUntilExpiration = document.daysUntilExpiration ?: 0 + val statusColor = when { + !document.isActive -> Color.Gray + daysUntilExpiration < 0 -> Color.Red + daysUntilExpiration < 30 -> Color(0xFFF59E0B) + daysUntilExpiration < 90 -> Color(0xFFFBBF24) + else -> Color(0xFF10B981) + } + + Card( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + document.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(4.dp)) + document.itemName?.let { itemName -> + Text( + itemName, + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + Box( + modifier = Modifier + .background(statusColor.copy(alpha = 0.2f), RoundedCornerShape(8.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + when { + !document.isActive -> "Inactive" + daysUntilExpiration < 0 -> "Expired" + daysUntilExpiration < 30 -> "Expiring soon" + else -> "Active" + }, + style = MaterialTheme.typography.labelSmall, + color = statusColor, + fontWeight = FontWeight.Bold + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text("Provider", style = MaterialTheme.typography.labelSmall, color = Color.Gray) + Text(document.provider ?: "N/A", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) + } + Column(horizontalAlignment = Alignment.End) { + Text("Expires", style = MaterialTheme.typography.labelSmall, color = Color.Gray) + Text(document.endDate ?: "N/A", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) + } + } + + if (document.isActive && daysUntilExpiration >= 0) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + "$daysUntilExpiration days remaining", + style = MaterialTheme.typography.labelMedium, + color = statusColor + ) + } + + document.category?.let { category -> + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .background(Color(0xFFE5E7EB), RoundedCornerShape(6.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + DocumentCategory.fromValue(category).displayName, + style = MaterialTheme.typography.labelSmall, + color = Color(0xFF374151) + ) + } + } + } + } +} + +@Composable +private fun RegularDocumentCardContent(document: Document, onClick: () -> Unit) { + val typeColor = when (document.documentType) { + "warranty" -> Color(0xFF3B82F6) + "manual" -> Color(0xFF8B5CF6) + "receipt" -> Color(0xFF10B981) + "inspection" -> Color(0xFFF59E0B) + else -> Color(0xFF6B7280) + } + + Card( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Document icon + Box( + modifier = Modifier + .size(56.dp) + .background(typeColor.copy(alpha = 0.1f), RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center + ) { + Icon( + when (document.documentType) { + "photo" -> Icons.Default.Image + "warranty", "insurance" -> Icons.Default.VerifiedUser + "manual" -> Icons.Default.MenuBook + "receipt" -> Icons.Default.Receipt + else -> Icons.Default.Description + }, + contentDescription = null, + tint = typeColor, + modifier = Modifier.size(32.dp) + ) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + document.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(4.dp)) + + if (document.description?.isNotBlank() == true) { + Text( + document.description, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .background(typeColor.copy(alpha = 0.2f), RoundedCornerShape(4.dp)) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + DocumentType.fromValue(document.documentType).displayName, + style = MaterialTheme.typography.labelSmall, + color = typeColor + ) + } + + document.fileSize?.let { size -> + Text( + formatFileSize(size), + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + } + } + + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + tint = Color.Gray + ) + } + } +} + +fun formatFileSize(bytes: Int): String { + var size = bytes.toDouble() + val units = listOf("B", "KB", "MB", "GB") + var unitIndex = 0 + + while (size >= 1024 && unitIndex < units.size - 1) { + size /= 1024 + unitIndex++ + } + + return "%.1f %s".format(size, units[unitIndex]) +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentStates.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentStates.kt new file mode 100644 index 0000000..95adb8c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentStates.kt @@ -0,0 +1,42 @@ +package com.mycrib.android.ui.components.documents + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun EmptyState(icon: ImageVector, message: String) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon(icon, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.Gray) + Spacer(modifier = Modifier.height(16.dp)) + Text(message, style = MaterialTheme.typography.titleMedium, color = Color.Gray) + } +} + +@Composable +fun ErrorState(message: String, onRetry: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon(Icons.Default.Error, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.Red) + Spacer(modifier = Modifier.height(16.dp)) + Text(message, style = MaterialTheme.typography.bodyLarge, color = Color.Gray) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onRetry) { + Text("Retry") + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt new file mode 100644 index 0000000..1be0cd9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt @@ -0,0 +1,58 @@ +package com.mycrib.android.ui.components.documents + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.ReceiptLong +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mycrib.shared.models.DocumentListResponse +import com.mycrib.shared.network.ApiResult + +@Composable +fun DocumentsTabContent( + state: ApiResult, + isWarrantyTab: Boolean, + onDocumentClick: (Int) -> Unit, + onRetry: () -> Unit +) { + when (state) { + is ApiResult.Loading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + is ApiResult.Success -> { + val documents = state.data.results + if (documents.isEmpty()) { + EmptyState( + icon = if (isWarrantyTab) Icons.Default.ReceiptLong else Icons.Default.Description, + message = if (isWarrantyTab) "No warranties found" else "No documents found" + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(documents) { document -> + DocumentCard( + document = document, + isWarrantyCard = isWarrantyTab, + onClick = { document.id?.let { onDocumentClick(it) } } + ) + } + } + } + } + is ApiResult.Error -> { + ErrorState(message = state.message, onRetry = onRetry) + } + is ApiResult.Idle -> {} + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt index c6d788d..83b7f74 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentDetailScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.Image import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter import androidx.compose.ui.window.Dialog +import com.mycrib.android.ui.components.documents.ErrorState +import com.mycrib.android.ui.components.documents.formatFileSize import androidx.compose.ui.window.DialogProperties import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentsScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentsScreen.kt index 3de6138..99837a2 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/DocumentsScreen.kt @@ -1,28 +1,18 @@ package com.mycrib.android.ui.screens -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.mycrib.android.ui.components.documents.DocumentsTabContent import com.mycrib.android.viewmodel.DocumentViewModel import com.mycrib.shared.models.* -import com.mycrib.shared.network.ApiResult -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn enum class DocumentTab { WARRANTIES, DOCUMENTS @@ -219,288 +209,3 @@ fun DocumentsScreen( } } } - -@Composable -fun DocumentsTabContent( - state: ApiResult, - isWarrantyTab: Boolean, - onDocumentClick: (Int) -> Unit, - onRetry: () -> Unit -) { - when (state) { - is ApiResult.Loading -> { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - is ApiResult.Success -> { - val documents = state.data.results - if (documents.isEmpty()) { - EmptyState( - icon = if (isWarrantyTab) Icons.Default.ReceiptLong else Icons.Default.Description, - message = if (isWarrantyTab) "No warranties found" else "No documents found" - ) - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(documents) { document -> - DocumentCard( - document = document, - isWarrantyCard = isWarrantyTab, - onClick = { document.id?.let { onDocumentClick(it) } } - ) - } - } - } - } - is ApiResult.Error -> { - ErrorState(message = state.message, onRetry = onRetry) - } - is ApiResult.Idle -> {} - } -} - -@Composable -fun DocumentCard(document: Document, isWarrantyCard: Boolean = false, onClick: () -> Unit) { - if (isWarrantyCard) { - // Warranty-specific card layout - val daysUntilExpiration = document.daysUntilExpiration ?: 0 - val statusColor = when { - !document.isActive -> Color.Gray - daysUntilExpiration < 0 -> Color.Red - daysUntilExpiration < 30 -> Color(0xFFF59E0B) - daysUntilExpiration < 90 -> Color(0xFFFBBF24) - else -> Color(0xFF10B981) - } - - Card( - modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - shape = RoundedCornerShape(12.dp) - ) { - Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - document.title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.height(4.dp)) - document.itemName?.let { itemName -> - Text( - itemName, - style = MaterialTheme.typography.bodyMedium, - color = Color.Gray, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - - Box( - modifier = Modifier - .background(statusColor.copy(alpha = 0.2f), RoundedCornerShape(8.dp)) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Text( - when { - !document.isActive -> "Inactive" - daysUntilExpiration < 0 -> "Expired" - daysUntilExpiration < 30 -> "Expiring soon" - else -> "Active" - }, - style = MaterialTheme.typography.labelSmall, - color = statusColor, - fontWeight = FontWeight.Bold - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column { - Text("Provider", style = MaterialTheme.typography.labelSmall, color = Color.Gray) - Text(document.provider ?: "N/A", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) - } - Column(horizontalAlignment = Alignment.End) { - Text("Expires", style = MaterialTheme.typography.labelSmall, color = Color.Gray) - Text(document.endDate ?: "N/A", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium) - } - } - - if (document.isActive && daysUntilExpiration >= 0) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - "$daysUntilExpiration days remaining", - style = MaterialTheme.typography.labelMedium, - color = statusColor - ) - } - - document.category?.let { category -> - Spacer(modifier = Modifier.height(8.dp)) - Box( - modifier = Modifier - .background(Color(0xFFE5E7EB), RoundedCornerShape(6.dp)) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Text( - DocumentCategory.fromValue(category).displayName, - style = MaterialTheme.typography.labelSmall, - color = Color(0xFF374151) - ) - } - } - } - } - } else { - // Regular document card layout - val typeColor = when (document.documentType) { - "warranty" -> Color(0xFF3B82F6) - "manual" -> Color(0xFF8B5CF6) - "receipt" -> Color(0xFF10B981) - "inspection" -> Color(0xFFF59E0B) - else -> Color(0xFF6B7280) - } - - Card( - modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Document icon - Box( - modifier = Modifier - .size(56.dp) - .background(typeColor.copy(alpha = 0.1f), RoundedCornerShape(8.dp)), - contentAlignment = Alignment.Center - ) { - Icon( - when (document.documentType) { - "photo" -> Icons.Default.Image - "warranty", "insurance" -> Icons.Default.VerifiedUser - "manual" -> Icons.Default.MenuBook - "receipt" -> Icons.Default.Receipt - else -> Icons.Default.Description - }, - contentDescription = null, - tint = typeColor, - modifier = Modifier.size(32.dp) - ) - } - - Column(modifier = Modifier.weight(1f)) { - Text( - document.title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.height(4.dp)) - - if (document.description?.isNotBlank() == true) { - Text( - document.description, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .background(typeColor.copy(alpha = 0.2f), RoundedCornerShape(4.dp)) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) { - Text( - DocumentType.fromValue(document.documentType).displayName, - style = MaterialTheme.typography.labelSmall, - color = typeColor - ) - } - - document.fileSize?.let { size -> - Text( - formatFileSize(size), - style = MaterialTheme.typography.labelSmall, - color = Color.Gray - ) - } - } - } - - Icon( - Icons.Default.ChevronRight, - contentDescription = null, - tint = Color.Gray - ) - } - } - } -} - -@Composable -fun EmptyState(icon: androidx.compose.ui.graphics.vector.ImageVector, message: String) { - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon(icon, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.Gray) - Spacer(modifier = Modifier.height(16.dp)) - Text(message, style = MaterialTheme.typography.titleMedium, color = Color.Gray) - } -} - -@Composable -fun ErrorState(message: String, onRetry: () -> Unit) { - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon(Icons.Default.Error, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.Red) - Spacer(modifier = Modifier.height(16.dp)) - Text(message, style = MaterialTheme.typography.bodyLarge, color = Color.Gray) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onRetry) { - Text("Retry") - } - } -} - -fun formatFileSize(bytes: Int): String { - var size = bytes.toDouble() - for (unit in listOf("B", "KB", "MB", "GB")) { - if (size < 1024.0) { - return "${(size * 10).toInt() / 10.0} $unit" - } - size /= 1024.0 - } - return "${(size * 10).toInt() / 10.0} TB" -} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditDocumentScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditDocumentScreen.kt index 85784c7..994ecf1 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditDocumentScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditDocumentScreen.kt @@ -19,6 +19,7 @@ import com.mycrib.shared.network.ApiResult import com.mycrib.platform.ImageData import com.mycrib.platform.rememberImagePicker import com.mycrib.platform.rememberCameraPicker +import com.mycrib.android.ui.components.documents.ErrorState @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/iosApp/iosApp/Documents/Components/DocumentCard.swift b/iosApp/iosApp/Documents/Components/DocumentCard.swift new file mode 100644 index 0000000..ec06d96 --- /dev/null +++ b/iosApp/iosApp/Documents/Components/DocumentCard.swift @@ -0,0 +1,99 @@ +import SwiftUI +import ComposeApp + +struct DocumentCard: View { + let document: Document + + var typeColor: Color { + switch document.documentType { + case "warranty": return .blue + case "manual": return .purple + case "receipt": return AppColors.success + case "inspection": return AppColors.warning + default: return .gray + } + } + + var typeIcon: String { + switch document.documentType { + case "photo": return "photo" + case "warranty", "insurance": return "checkmark.shield" + case "manual": return "book" + case "receipt": return "receipt" + default: return "doc.text" + } + } + + var body: some View { + HStack(spacing: AppSpacing.md) { + // Document Icon + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(typeColor.opacity(0.1)) + .frame(width: 56, height: 56) + + Image(systemName: typeIcon) + .font(.system(size: 24)) + .foregroundColor(typeColor) + } + + VStack(alignment: .leading, spacing: 4) { + Text(document.title) + .font(AppTypography.titleMedium) + .fontWeight(.bold) + .foregroundColor(AppColors.textPrimary) + .lineLimit(1) + + if let description = document.description_, !description.isEmpty { + Text(description) + .font(AppTypography.bodySmall) + .foregroundColor(AppColors.textSecondary) + .lineLimit(2) + } + + HStack(spacing: 8) { + Text(getDocTypeDisplayName(document.documentType)) + .font(AppTypography.labelSmall) + .foregroundColor(typeColor) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(typeColor.opacity(0.2)) + .cornerRadius(4) + + if let fileSize = document.fileSize { + Text(formatFileSize(Int(fileSize))) + .font(AppTypography.labelSmall) + .foregroundColor(AppColors.textSecondary) + } + } + } + + Spacer() + + Image(systemName: "chevron.right") + .foregroundColor(AppColors.textSecondary) + .font(.system(size: 14)) + } + .padding(AppSpacing.md) + .background(AppColors.surface) + .cornerRadius(AppRadius.md) + .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) + } + + private func getDocTypeDisplayName(_ type: String) -> String { + return DocumentType.companion.fromValue(value: type).displayName + } + + private func formatFileSize(_ bytes: Int) -> String { + var size = Double(bytes) + let units = ["B", "KB", "MB", "GB"] + var unitIndex = 0 + + while size >= 1024 && unitIndex < units.count - 1 { + size /= 1024 + unitIndex += 1 + } + + return String(format: "%.1f %@", size, units[unitIndex]) + } +} diff --git a/iosApp/iosApp/Documents/Components/DocumentsTabContent.swift b/iosApp/iosApp/Documents/Components/DocumentsTabContent.swift new file mode 100644 index 0000000..76f4ff0 --- /dev/null +++ b/iosApp/iosApp/Documents/Components/DocumentsTabContent.swift @@ -0,0 +1,50 @@ +import SwiftUI +import ComposeApp + +struct DocumentsTabContent: View { + @ObservedObject var viewModel: DocumentViewModel + let searchText: String + + var filteredDocuments: [Document] { + if searchText.isEmpty { + return viewModel.documents + } + return viewModel.documents.filter { + $0.title.localizedCaseInsensitiveContains(searchText) || + ($0.description_ ?? "").localizedCaseInsensitiveContains(searchText) + } + } + + var body: some View { + if viewModel.isLoading { + Spacer() + ProgressView() + .scaleEffect(1.2) + Spacer() + } else if let error = viewModel.errorMessage { + Spacer() + ErrorView(message: error, retryAction: { viewModel.loadDocuments() }) + Spacer() + } else if filteredDocuments.isEmpty { + Spacer() + EmptyStateView( + icon: "doc", + title: "No documents found", + message: "Add documents related to your residence" + ) + Spacer() + } else { + ScrollView { + LazyVStack(spacing: AppSpacing.sm) { + ForEach(filteredDocuments, id: \.id) { document in + NavigationLink(destination: DocumentDetailView(documentId: document.id?.int32Value ?? 0)) { + DocumentCard(document: document) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(AppSpacing.md) + } + } + } +} diff --git a/iosApp/iosApp/Documents/Components/EmptyStateView.swift b/iosApp/iosApp/Documents/Components/EmptyStateView.swift new file mode 100644 index 0000000..331ec2a --- /dev/null +++ b/iosApp/iosApp/Documents/Components/EmptyStateView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct EmptyStateView: View { + let icon: String + let title: String + let message: String + + var body: some View { + VStack(spacing: AppSpacing.md) { + Image(systemName: icon) + .font(.system(size: 64)) + .foregroundColor(AppColors.textSecondary) + + Text(title) + .font(AppTypography.titleMedium) + .foregroundColor(AppColors.textSecondary) + + Text(message) + .font(AppTypography.bodyMedium) + .foregroundColor(AppColors.textTertiary) + .multilineTextAlignment(.center) + } + .padding(AppSpacing.lg) + } +} diff --git a/iosApp/iosApp/Documents/Components/ImageViewerSheet.swift b/iosApp/iosApp/Documents/Components/ImageViewerSheet.swift new file mode 100644 index 0000000..06db110 --- /dev/null +++ b/iosApp/iosApp/Documents/Components/ImageViewerSheet.swift @@ -0,0 +1,58 @@ +import SwiftUI +import ComposeApp + +struct ImageViewerSheet: View { + let images: [DocumentImage] + @Binding var selectedIndex: Int + let onDismiss: () -> Void + + var body: some View { + NavigationView { + TabView(selection: $selectedIndex) { + ForEach(Array(images.enumerated()), id: \.element.id) { index, image in + ZStack { + Color.black.ignoresSafeArea() + + AsyncImage(url: URL(string: image.imageUrl)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fit) + case .failure: + VStack { + Image(systemName: "photo") + .font(.system(size: 48)) + .foregroundColor(.gray) + Text("Failed to load image") + .foregroundColor(.gray) + } + case .empty: + ProgressView() + .tint(.white) + @unknown default: + EmptyView() + } + } + } + .tag(index) + } + } + .tabViewStyle(.page) + .indexViewStyle(.page(backgroundDisplayMode: .always)) + .navigationTitle("Image \(selectedIndex + 1) of \(images.count)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Done") { + onDismiss() + } + .foregroundColor(.white) + } + } + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(Color.black.opacity(0.8), for: .navigationBar) + .toolbarColorScheme(.dark, for: .navigationBar) + } + } +} diff --git a/iosApp/iosApp/Documents/Components/WarrantiesTabContent.swift b/iosApp/iosApp/Documents/Components/WarrantiesTabContent.swift new file mode 100644 index 0000000..7a1283f --- /dev/null +++ b/iosApp/iosApp/Documents/Components/WarrantiesTabContent.swift @@ -0,0 +1,52 @@ +import SwiftUI +import ComposeApp + +struct WarrantiesTabContent: View { + @ObservedObject var viewModel: DocumentViewModel + let searchText: String + + var filteredWarranties: [Document] { + let warranties = viewModel.documents.filter { $0.documentType == "warranty" } + if searchText.isEmpty { + return warranties + } + return warranties.filter { + $0.title.localizedCaseInsensitiveContains(searchText) || + ($0.itemName ?? "").localizedCaseInsensitiveContains(searchText) || + ($0.provider ?? "").localizedCaseInsensitiveContains(searchText) + } + } + + var body: some View { + if viewModel.isLoading { + Spacer() + ProgressView() + .scaleEffect(1.2) + Spacer() + } else if let error = viewModel.errorMessage { + Spacer() + ErrorView(message: error, retryAction: { viewModel.loadDocuments() }) + Spacer() + } else if filteredWarranties.isEmpty { + Spacer() + EmptyStateView( + icon: "doc.text.viewfinder", + title: "No warranties found", + message: "Add warranties to track coverage periods" + ) + Spacer() + } else { + ScrollView { + LazyVStack(spacing: AppSpacing.sm) { + ForEach(filteredWarranties, id: \.id) { warranty in + NavigationLink(destination: DocumentDetailView(documentId: warranty.id?.int32Value ?? 0)) { + WarrantyCard(document: warranty) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(AppSpacing.md) + } + } + } +} diff --git a/iosApp/iosApp/Documents/Components/WarrantyCard.swift b/iosApp/iosApp/Documents/Components/WarrantyCard.swift new file mode 100644 index 0000000..1e9310f --- /dev/null +++ b/iosApp/iosApp/Documents/Components/WarrantyCard.swift @@ -0,0 +1,107 @@ +import SwiftUI +import ComposeApp + +struct WarrantyCard: View { + let document: Document + + var daysUntilExpiration: Int { + Int(document.daysUntilExpiration ?? 0) + } + + var statusColor: Color { + if !document.isActive { return .gray } + if daysUntilExpiration < 0 { return AppColors.error } + if daysUntilExpiration < 30 { return AppColors.warning } + if daysUntilExpiration < 90 { return .yellow } + return AppColors.success + } + + var statusText: String { + if !document.isActive { return "Inactive" } + if daysUntilExpiration < 0 { return "Expired" } + if daysUntilExpiration < 30 { return "Expiring soon" } + return "Active" + } + + var body: some View { + VStack(alignment: .leading, spacing: AppSpacing.sm) { + // Header + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 4) { + Text(document.title) + .font(AppTypography.titleMedium) + .fontWeight(.bold) + .foregroundColor(AppColors.textPrimary) + + Text(document.itemName ?? "") + .font(AppTypography.bodyMedium) + .foregroundColor(AppColors.textSecondary) + } + + Spacer() + + // Status Badge + Text(statusText) + .font(AppTypography.labelSmall) + .fontWeight(.bold) + .foregroundColor(statusColor) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(statusColor.opacity(0.2)) + .cornerRadius(6) + } + + Divider() + + // Details + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Provider") + .font(AppTypography.labelSmall) + .foregroundColor(AppColors.textSecondary) + Text(document.provider ?? "N/A") + .font(AppTypography.bodyMedium) + .fontWeight(.medium) + .foregroundColor(AppColors.textPrimary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text("Expires") + .font(AppTypography.labelSmall) + .foregroundColor(AppColors.textSecondary) + Text(document.endDate ?? "N/A") + .font(AppTypography.bodyMedium) + .fontWeight(.medium) + .foregroundColor(AppColors.textPrimary) + } + } + + if document.isActive && daysUntilExpiration >= 0 { + Text("\(daysUntilExpiration) days remaining") + .font(AppTypography.labelMedium) + .foregroundColor(statusColor) + } + + // Category Badge + if let category = document.category { + Text(getCategoryDisplayName(category)) + .font(AppTypography.labelSmall) + .foregroundColor(Color(hex: "374151")) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(hex: "E5E7EB")) + .cornerRadius(4) + } + } + .padding(AppSpacing.md) + .background(AppColors.surface) + .cornerRadius(AppRadius.md) + .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) + } + + private func getCategoryDisplayName(_ category: String) -> String { + return DocumentCategory.companion.fromValue(value: category).displayName + } +} diff --git a/iosApp/iosApp/Documents/DocumentDetailView.swift b/iosApp/iosApp/Documents/DocumentDetailView.swift index ad49945..5e364db 100644 --- a/iosApp/iosApp/Documents/DocumentDetailView.swift +++ b/iosApp/iosApp/Documents/DocumentDetailView.swift @@ -435,94 +435,3 @@ struct DocumentDetailView: View { return formatter.string(fromByteCount: Int64(bytes)) } } - -// Helper enums for display names -struct DocumentTypeHelper { - static func displayName(for value: String) -> String { - switch value { - case "warranty": return "Warranty" - case "manual": return "User Manual" - case "receipt": return "Receipt/Invoice" - case "inspection": return "Inspection Report" - case "permit": return "Permit" - case "deed": return "Deed/Title" - case "insurance": return "Insurance" - case "contract": return "Contract" - case "photo": return "Photo" - default: return "Other" - } - } -} - -struct DocumentCategoryHelper { - static func displayName(for value: String) -> String { - switch value { - case "appliance": return "Appliance" - case "hvac": return "HVAC" - case "plumbing": return "Plumbing" - case "electrical": return "Electrical" - case "roofing": return "Roofing" - case "structural": return "Structural" - case "landscaping": return "Landscaping" - case "general": return "General" - default: return "Other" - } - } -} - -// Simple image viewer -struct ImageViewerSheet: View { - let images: [DocumentImage] - @Binding var selectedIndex: Int - let onDismiss: () -> Void - - var body: some View { - NavigationView { - TabView(selection: $selectedIndex) { - ForEach(Array(images.enumerated()), id: \.element.id) { index, image in - ZStack { - Color.black.ignoresSafeArea() - - AsyncImage(url: URL(string: image.imageUrl)) { phase in - switch phase { - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fit) - case .failure: - VStack { - Image(systemName: "photo") - .font(.system(size: 48)) - .foregroundColor(.gray) - Text("Failed to load image") - .foregroundColor(.gray) - } - case .empty: - ProgressView() - .tint(.white) - @unknown default: - EmptyView() - } - } - } - .tag(index) - } - } - .tabViewStyle(.page) - .indexViewStyle(.page(backgroundDisplayMode: .always)) - .navigationTitle("Image \(selectedIndex + 1) of \(images.count)") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Done") { - onDismiss() - } - .foregroundColor(.white) - } - } - .toolbarBackground(.visible, for: .navigationBar) - .toolbarBackground(Color.black.opacity(0.8), for: .navigationBar) - .toolbarColorScheme(.dark, for: .navigationBar) - } - } -} diff --git a/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift b/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift index 666ceb1..c4fdaae 100644 --- a/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift +++ b/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift @@ -201,310 +201,6 @@ struct DocumentsWarrantiesView: View { } } -// MARK: - Warranties Tab -struct WarrantiesTabContent: View { - @ObservedObject var viewModel: DocumentViewModel - let searchText: String - - var filteredWarranties: [Document] { - let warranties = viewModel.documents.filter { $0.documentType == "warranty" } - if searchText.isEmpty { - return warranties - } - return warranties.filter { - $0.title.localizedCaseInsensitiveContains(searchText) || - ($0.itemName ?? "").localizedCaseInsensitiveContains(searchText) || - ($0.provider ?? "").localizedCaseInsensitiveContains(searchText) - } - } - - var body: some View { - if viewModel.isLoading { - Spacer() - ProgressView() - .scaleEffect(1.2) - Spacer() - } else if let error = viewModel.errorMessage { - Spacer() - ErrorView(message: error, retryAction: { viewModel.loadDocuments() }) - Spacer() - } else if filteredWarranties.isEmpty { - Spacer() - EmptyStateView( - icon: "doc.text.viewfinder", - title: "No warranties found", - message: "Add warranties to track coverage periods" - ) - Spacer() - } else { - ScrollView { - LazyVStack(spacing: AppSpacing.sm) { - ForEach(filteredWarranties, id: \.id) { warranty in - NavigationLink(destination: DocumentDetailView(documentId: warranty.id?.int32Value ?? 0)) { - WarrantyCard(document: warranty) - } - .buttonStyle(PlainButtonStyle()) - } - } - .padding(AppSpacing.md) - } - } - } -} - -// MARK: - Documents Tab -struct DocumentsTabContent: View { - @ObservedObject var viewModel: DocumentViewModel - let searchText: String - - var filteredDocuments: [Document] { - if searchText.isEmpty { - return viewModel.documents - } - return viewModel.documents.filter { - $0.title.localizedCaseInsensitiveContains(searchText) || - ($0.description_ ?? "").localizedCaseInsensitiveContains(searchText) - } - } - - var body: some View { - if viewModel.isLoading { - Spacer() - ProgressView() - .scaleEffect(1.2) - Spacer() - } else if let error = viewModel.errorMessage { - Spacer() - ErrorView(message: error, retryAction: { viewModel.loadDocuments() }) - Spacer() - } else if filteredDocuments.isEmpty { - Spacer() - EmptyStateView( - icon: "doc", - title: "No documents found", - message: "Add documents related to your residence" - ) - Spacer() - } else { - ScrollView { - LazyVStack(spacing: AppSpacing.sm) { - ForEach(filteredDocuments, id: \.id) { document in - NavigationLink(destination: DocumentDetailView(documentId: document.id?.int32Value ?? 0)) { - DocumentCard(document: document) - } - .buttonStyle(PlainButtonStyle()) - } - } - .padding(AppSpacing.md) - } - } - } -} - -// MARK: - Warranty Card -struct WarrantyCard: View { - let document: Document - - var daysUntilExpiration: Int { - Int(document.daysUntilExpiration ?? 0) - } - - var statusColor: Color { - if !document.isActive { return .gray } - if daysUntilExpiration < 0 { return AppColors.error } - if daysUntilExpiration < 30 { return AppColors.warning } - if daysUntilExpiration < 90 { return .yellow } - return AppColors.success - } - - var statusText: String { - if !document.isActive { return "Inactive" } - if daysUntilExpiration < 0 { return "Expired" } - if daysUntilExpiration < 30 { return "Expiring soon" } - return "Active" - } - - var body: some View { - VStack(alignment: .leading, spacing: AppSpacing.sm) { - // Header - HStack(alignment: .top) { - VStack(alignment: .leading, spacing: 4) { - Text(document.title) - .font(AppTypography.titleMedium) - .fontWeight(.bold) - .foregroundColor(AppColors.textPrimary) - - Text(document.itemName ?? "") - .font(AppTypography.bodyMedium) - .foregroundColor(AppColors.textSecondary) - } - - Spacer() - - // Status Badge - Text(statusText) - .font(AppTypography.labelSmall) - .fontWeight(.bold) - .foregroundColor(statusColor) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(statusColor.opacity(0.2)) - .cornerRadius(6) - } - - Divider() - - // Details - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Provider") - .font(AppTypography.labelSmall) - .foregroundColor(AppColors.textSecondary) - Text(document.provider ?? "N/A") - .font(AppTypography.bodyMedium) - .fontWeight(.medium) - .foregroundColor(AppColors.textPrimary) - } - - Spacer() - - VStack(alignment: .trailing, spacing: 2) { - Text("Expires") - .font(AppTypography.labelSmall) - .foregroundColor(AppColors.textSecondary) - Text(document.endDate ?? "N/A") - .font(AppTypography.bodyMedium) - .fontWeight(.medium) - .foregroundColor(AppColors.textPrimary) - } - } - - if document.isActive && daysUntilExpiration >= 0 { - Text("\(daysUntilExpiration) days remaining") - .font(AppTypography.labelMedium) - .foregroundColor(statusColor) - } - - // Category Badge - if let category = document.category { - Text(getCategoryDisplayName(category)) - .font(AppTypography.labelSmall) - .foregroundColor(Color(hex: "374151")) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(hex: "E5E7EB")) - .cornerRadius(4) - } - } - .padding(AppSpacing.md) - .background(AppColors.surface) - .cornerRadius(AppRadius.md) - .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) - } - - private func getCategoryDisplayName(_ category: String) -> String { - return DocumentCategory.companion.fromValue(value: category).displayName - } -} - -// MARK: - Document Card -struct DocumentCard: View { - let document: Document - - var typeColor: Color { - switch document.documentType { - case "warranty": return .blue - case "manual": return .purple - case "receipt": return AppColors.success - case "inspection": return AppColors.warning - default: return .gray - } - } - - var typeIcon: String { - switch document.documentType { - case "photo": return "photo" - case "warranty", "insurance": return "checkmark.shield" - case "manual": return "book" - case "receipt": return "receipt" - default: return "doc.text" - } - } - - var body: some View { - HStack(spacing: AppSpacing.md) { - // Document Icon - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(typeColor.opacity(0.1)) - .frame(width: 56, height: 56) - - Image(systemName: typeIcon) - .font(.system(size: 24)) - .foregroundColor(typeColor) - } - - VStack(alignment: .leading, spacing: 4) { - Text(document.title) - .font(AppTypography.titleMedium) - .fontWeight(.bold) - .foregroundColor(AppColors.textPrimary) - .lineLimit(1) - - if let description = document.description_, !description.isEmpty { - Text(description) - .font(AppTypography.bodySmall) - .foregroundColor(AppColors.textSecondary) - .lineLimit(2) - } - - HStack(spacing: 8) { - Text(getDocTypeDisplayName(document.documentType)) - .font(AppTypography.labelSmall) - .foregroundColor(typeColor) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(typeColor.opacity(0.2)) - .cornerRadius(4) - - if let fileSize = document.fileSize { - Text(formatFileSize(Int(fileSize))) - .font(AppTypography.labelSmall) - .foregroundColor(AppColors.textSecondary) - } - } - } - - Spacer() - - Image(systemName: "chevron.right") - .foregroundColor(AppColors.textSecondary) - .font(.system(size: 14)) - } - .padding(AppSpacing.md) - .background(AppColors.surface) - .cornerRadius(AppRadius.md) - .shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1) - } - - private func getDocTypeDisplayName(_ type: String) -> String { - return DocumentType.companion.fromValue(value: type).displayName - } - - private func formatFileSize(_ bytes: Int) -> String { - var size = Double(bytes) - let units = ["B", "KB", "MB", "GB"] - var unitIndex = 0 - - while size >= 1024 && unitIndex < units.count - 1 { - size /= 1024 - unitIndex += 1 - } - - return String(format: "%.1f %@", size, units[unitIndex]) - } -} - // MARK: - Supporting Types extension DocumentCategory: CaseIterable { public static var allCases: [DocumentCategory] { @@ -546,28 +242,3 @@ extension DocumentType: CaseIterable { } } } - -// MARK: - Empty State View -struct EmptyStateView: View { - let icon: String - let title: String - let message: String - - var body: some View { - VStack(spacing: AppSpacing.md) { - Image(systemName: icon) - .font(.system(size: 64)) - .foregroundColor(AppColors.textSecondary) - - Text(title) - .font(AppTypography.titleMedium) - .foregroundColor(AppColors.textSecondary) - - Text(message) - .font(AppTypography.bodyMedium) - .foregroundColor(AppColors.textTertiary) - .multilineTextAlignment(.center) - } - .padding(AppSpacing.lg) - } -} diff --git a/iosApp/iosApp/Documents/Helpers/DocumentHelpers.swift b/iosApp/iosApp/Documents/Helpers/DocumentHelpers.swift new file mode 100644 index 0000000..cef5e86 --- /dev/null +++ b/iosApp/iosApp/Documents/Helpers/DocumentHelpers.swift @@ -0,0 +1,34 @@ +import Foundation + +struct DocumentTypeHelper { + static func displayName(for value: String) -> String { + switch value { + case "warranty": return "Warranty" + case "manual": return "User Manual" + case "receipt": return "Receipt/Invoice" + case "inspection": return "Inspection Report" + case "permit": return "Permit" + case "deed": return "Deed/Title" + case "insurance": return "Insurance" + case "contract": return "Contract" + case "photo": return "Photo" + default: return "Other" + } + } +} + +struct DocumentCategoryHelper { + static func displayName(for value: String) -> String { + switch value { + case "appliance": return "Appliance" + case "hvac": return "HVAC" + case "plumbing": return "Plumbing" + case "electrical": return "Electrical" + case "roofing": return "Roofing" + case "structural": return "Structural" + case "landscaping": return "Landscaping" + case "general": return "General" + default: return "Other" + } + } +}