From a7bc0344cce9c1e4aafd2e327d6ff6142e939404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Fri, 10 Apr 2026 07:30:26 +0700 Subject: [PATCH 01/12] fix(MainActivity) : route of Admin and User when login app --- .../com/example/shoestoreapp/MainActivity.kt | 51 +++---------------- .../features/auth/HomeAdminScreen.kt | 34 ------------- .../features/auth/HomeUserScreen.kt | 34 ------------- 3 files changed, 6 insertions(+), 113 deletions(-) delete mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeAdminScreen.kt delete mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeUserScreen.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt index 2229dcac..73b305b4 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt @@ -21,8 +21,6 @@ import com.example.shoestoreapp.features.auth.presentation.sign_in.LoginScreenCo import com.example.shoestoreapp.features.auth.presentation.sign_up.RegisterScreenContent import com.example.shoestoreapp.features.auth.presentation.welcome.WelcomeScreen import com.example.shoestoreapp.features.auth.presentation.reset_password.create_new_password.CreateNewPasswordScreen -import com.example.shoestoreapp.features.auth.HomeUserScreen -import com.example.shoestoreapp.features.auth.HomeAdminScreen import com.example.shoestoreapp.core.utils.TokenManager import kotlinx.coroutines.launch @@ -49,7 +47,7 @@ fun AppNavHost() { navController = navController, //startDestination = "welcome" //startDestination = "product_list" // ← Test ProductListScreen - startDestination = "admin_product_list" // Test AdminProductListScreen + startDestination = "welcome" // Test AdminProductListScreen ) { // Route 1: Welcome Screen @@ -69,8 +67,8 @@ fun AppNavHost() { // Flattened conditional logic using 'when' statement val destination = when { token.isNullOrEmpty() -> "sign_in" - role?.uppercase() == "ADMIN" -> "home_admin" - else -> "home_user" + role?.uppercase() == "ADMIN" -> "admin_product_list" + else -> "product_list" } // Execute single navigation call @@ -100,12 +98,12 @@ fun AppNavHost() { navController.navigate("forgot_password") }, onNavigateToUserHome = { - navController.navigate("home_user") { + navController.navigate("product_list") { popUpTo("sign_in") { inclusive = true } } }, onNavigateToAdminHome = { - navController.navigate("home_admin") { + navController.navigate("admin_product_list") { popUpTo("sign_in") { inclusive = true } } } @@ -151,29 +149,6 @@ fun AppNavHost() { ) } - // Route 4: User Home - composable("home_user") { - // 1. CREATE A COROUTINE SCOPE TO RUN SUSPEND FUNCTIONS - val coroutineScope = rememberCoroutineScope() - - HomeUserScreen( - onLogoutClick = { - // 2. LAUNCH A BACKGROUND TASK TO CLEAR DATASTORE - coroutineScope.launch { - // Clear Token and Role from the local storage - tokenManager.clearAuthInfo() - - // 3. NAVIGATE BACK TO SIGN IN AND CLEAR ENTIRE BACKSTACK - navController.navigate("sign_in") { - // popUpTo(0) means clearing all previous screens - // so the user cannot press the physical Back button to return to Home - popUpTo(0) { inclusive = true } - } - } - } - ) - } - // Route: Product List Screen composable("product_list") { ProductListScreen( @@ -210,7 +185,7 @@ fun AppNavHost() { } ) } - // Route: Admi Product Screen + // Route: Admin Product Screen composable("admin_product_list") { AdminProductListScreen( viewModel = remember { AdminProductListViewModel() }, @@ -225,19 +200,5 @@ fun AppNavHost() { } ) } - - // Route 5: Admin Home - composable("home_admin") { - val coroutineScope = rememberCoroutineScope() - HomeAdminScreen( onLogoutClick = { - coroutineScope.launch { - tokenManager.clearAuthInfo() - - navController.navigate("sign_in") { - popUpTo(0) { inclusive = true } - } - } - }) - } } } \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeAdminScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeAdminScreen.kt deleted file mode 100644 index 8d715bd0..00000000 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeAdminScreen.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.shoestoreapp.features.auth - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.text.font.FontWeight - -@Composable -fun HomeAdminScreen( - // PASS A CALLBACK FUNCTION TO HANDLE LOGOUT EVENT - onLogoutClick: () -> Unit -) { - // A SIMPLE UI WITH A CENTERED BUTTON - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = "Welcome to Home Admin!") - - Spacer(modifier = Modifier.height(16.dp)) - - // TRIGGER THE CALLBACK WHEN CLICKED - Button(onClick = { onLogoutClick() }) { - Text(text = "Logout") - } - } -} diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeUserScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeUserScreen.kt deleted file mode 100644 index 4faf2288..00000000 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/HomeUserScreen.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.shoestoreapp.features.auth - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.text.font.FontWeight - -@Composable -fun HomeUserScreen( - // PASS A CALLBACK FUNCTION TO HANDLE LOGOUT EVENT - onLogoutClick: () -> Unit -) { - // A SIMPLE UI WITH A CENTERED BUTTON - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = "Welcome to Home User!") - - Spacer(modifier = Modifier.height(16.dp)) - - // TRIGGER THE CALLBACK WHEN CLICKED - Button(onClick = { onLogoutClick() }) { - Text(text = "Logout") - } - } -} From ef7ea6c223b8df7f45ff4b8bc38980d1a4ab176e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Fri, 10 Apr 2026 15:39:19 +0700 Subject: [PATCH 02/12] feat(invoice-core): add shared invoice models and mock workflow data --- .../data/AdminInvoiceMockRepository.kt | 9 + .../admin/invoice/ui/AdminInvoiceScreen.kt | 214 ++++++++++++++++++ .../ui/components/AdminInvoiceFilterChips.kt | 66 ++++++ .../viewmodel/AdminInvoiceViewModel.kt | 54 +++++ .../admin/settings/ui/AdminSettingsScreen.kt | 69 ++++++ .../features/invoice/mock/InvoiceMockData.kt | 118 ++++++++++ .../features/invoice/model/InvoiceModels.kt | 69 ++++++ .../invoice/data/UserInvoiceMockRepository.kt | 11 + .../user/invoice/ui/UserInvoiceScreen.kt | 184 +++++++++++++++ .../invoice/viewmodel/UserInvoiceViewModel.kt | 32 +++ .../user/profile/ui/UserProfileScreen.kt | 69 ++++++ 11 files changed, 895 insertions(+) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt new file mode 100644 index 00000000..8ebd479b --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt @@ -0,0 +1,9 @@ +package com.example.shoestoreapp.features.admin.invoice.data + +import com.example.shoestoreapp.features.invoice.mock.InvoiceMockData +import com.example.shoestoreapp.features.invoice.model.Invoice + +class AdminInvoiceMockRepository { + fun getInvoices(): List = InvoiceMockData.invoices() +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt new file mode 100644 index 00000000..1df32dc2 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt @@ -0,0 +1,214 @@ +package com.example.shoestoreapp.features.admin.invoice.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.unit.dp +import androidx.compose.ui.unit.sp +import com.example.shoestoreapp.features.admin.invoice.ui.components.AdminInvoiceFilterChips +import com.example.shoestoreapp.features.admin.invoice.viewmodel.AdminInvoiceViewModel +import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavBar +import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus + +@Composable +fun AdminInvoiceScreen( + viewModel: AdminInvoiceViewModel = AdminInvoiceViewModel(), + onTabSelected: (AdminBottomNavTab) -> Unit = {} +) { + val selectedStatus by viewModel.selectedStatus.collectAsState() + val invoices by viewModel.visibleInvoices.collectAsState() + + Scaffold( + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .border(1.dp, Color(0xFFE8E8E8)) + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Menu", + tint = Color.Black + ) + Text( + text = "SHOE STORE", + color = Color.Black, + fontWeight = FontWeight.Black, + letterSpacing = 1.6.sp + ) + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = Color(0xFF666666) + ) + } + }, + bottomBar = { + AdminBottomNavBar( + selectedTab = AdminBottomNavTab.ORDERS, + onTabSelected = onTabSelected + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.White) + ) { + Text( + text = "Order Management", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + fontSize = 30.sp, + fontWeight = FontWeight.Black, + color = Color.Black + ) + + AdminInvoiceFilterChips( + selectedStatus = selectedStatus, + onFilterSelected = viewModel::onFilterChange + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(invoices) { invoice -> + InvoiceCard( + invoice = invoice, + onUpdateStatus = { viewModel.cycleStatus(invoice.id) } + ) + } + } + } + } +} + +@Composable +private fun InvoiceCard( + invoice: Invoice, + onUpdateStatus: () -> Unit +) { + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(12.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFE5E5E5)) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Column { + Text( + text = "#${invoice.orderCode}", + fontSize = 11.sp, + color = Color(0xFF9D9D9D), + fontWeight = FontWeight.Bold + ) + Text( + text = invoice.fullName, + fontWeight = FontWeight.ExtraBold, + fontSize = 18.sp, + color = Color.Black + ) + Text( + text = invoice.createdAt, + fontSize = 11.sp, + color = Color(0xFF7B7B7B) + ) + } + + Column(horizontalAlignment = Alignment.End) { + Text( + text = "$${"%.2f".format(invoice.finalPrice)}", + fontWeight = FontWeight.Black, + fontSize = 20.sp, + color = Color.Black + ) + StatusChip(status = invoice.status) + } + } + + Text( + text = "${invoice.invoiceDetails.sumOf { it.quantity }} items - ${invoice.shippingAddress}", + modifier = Modifier.padding(top = 10.dp, bottom = 12.dp), + fontSize = 12.sp, + color = Color(0xFF5E5E5E) + ) + + Button( + onClick = onUpdateStatus, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text(text = "Update Status", fontSize = 11.sp, fontWeight = FontWeight.Bold) + } + } + } +} + +@Composable +private fun StatusChip(status: InvoiceStatus) { + val bg = when (status) { + InvoiceStatus.PENDING -> Color(0xFFF2F2F2) + InvoiceStatus.PAID -> Color(0xFFE8F2FF) + InvoiceStatus.DELIVERING -> Color.Black + InvoiceStatus.CANCELED -> Color(0xFFFFECEB) + } + val fg = when (status) { + InvoiceStatus.PENDING -> Color(0xFF666666) + InvoiceStatus.PAID -> Color(0xFF1F5FAE) + InvoiceStatus.DELIVERING -> Color.White + InvoiceStatus.CANCELED -> Color(0xFFB3261E) + } + + Text( + text = status.name, + modifier = Modifier + .padding(top = 4.dp) + .background(bg, RoundedCornerShape(6.dp)) + .padding(horizontal = 8.dp, vertical = 3.dp), + color = fg, + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt new file mode 100644 index 00000000..1c5072d8 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt @@ -0,0 +1,66 @@ +package com.example.shoestoreapp.features.admin.invoice.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus + +@Composable +fun AdminInvoiceFilterChips( + selectedStatus: InvoiceStatus?, + onFilterSelected: (InvoiceStatus?) -> Unit +) { + val filters = listOf(null, InvoiceStatus.PENDING, InvoiceStatus.PAID, InvoiceStatus.DELIVERING, InvoiceStatus.CANCELED) + + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + filters.forEach { filter -> + val isSelected = selectedStatus == filter + val text = when (filter) { + null -> "ALL" + InvoiceStatus.PENDING -> "PENDING" + InvoiceStatus.PAID -> "PAID" + InvoiceStatus.DELIVERING -> "DELIVERING" + InvoiceStatus.CANCELED -> "CANCELED" + } + + Text( + text = text, + modifier = Modifier + .background( + color = if (isSelected) Color.Black else Color.White, + shape = RoundedCornerShape(20.dp) + ) + .border( + width = if (isSelected) 0.dp else 1.dp, + color = if (isSelected) Color.Black else Color(0xFFE0E0E0), + shape = RoundedCornerShape(20.dp) + ) + .clickable { onFilterSelected(filter) } + .padding(horizontal = 16.dp, vertical = 10.dp), + color = if (isSelected) Color.White else Color(0xFF999999), + fontWeight = FontWeight.Bold, + fontSize = 10.sp, + letterSpacing = 0.8.sp + ) + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt new file mode 100644 index 00000000..4b01c765 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt @@ -0,0 +1,54 @@ +package com.example.shoestoreapp.features.admin.invoice.viewmodel + +import androidx.lifecycle.ViewModel +import com.example.shoestoreapp.features.admin.invoice.data.AdminInvoiceMockRepository +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class AdminInvoiceViewModel( + private val repository: AdminInvoiceMockRepository = AdminInvoiceMockRepository() +) : ViewModel() { + + private val _allInvoices = MutableStateFlow(repository.getInvoices()) + private val _selectedStatus = MutableStateFlow(null) + private val _visibleInvoices = MutableStateFlow(_allInvoices.value) + + val selectedStatus: StateFlow = _selectedStatus.asStateFlow() + val visibleInvoices: StateFlow> = _visibleInvoices.asStateFlow() + + fun onFilterChange(status: InvoiceStatus?) { + _selectedStatus.value = status + applyFilter() + } + + fun cycleStatus(invoiceId: Int) { + _allInvoices.update { invoices -> + invoices.map { invoice -> + if (invoice.id != invoiceId) return@map invoice + + val next = when (invoice.status) { + InvoiceStatus.PENDING -> InvoiceStatus.PAID + InvoiceStatus.PAID -> InvoiceStatus.DELIVERING + InvoiceStatus.DELIVERING -> InvoiceStatus.CANCELED + InvoiceStatus.CANCELED -> InvoiceStatus.PENDING + } + invoice.copy(status = next) + } + } + applyFilter() + } + + private fun applyFilter() { + val status = _selectedStatus.value + _visibleInvoices.value = if (status == null) { + _allInvoices.value + } else { + _allInvoices.value.filter { it.status == status } + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt new file mode 100644 index 00000000..dfe0ac01 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt @@ -0,0 +1,69 @@ +package com.example.shoestoreapp.features.admin.settings.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavBar +import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab + +@Composable +fun AdminSettingsScreen( + onTabSelected: (AdminBottomNavTab) -> Unit = {}, + onLogoutClick: () -> Unit = {} +) { + Scaffold( + bottomBar = { + AdminBottomNavBar( + selectedTab = AdminBottomNavTab.SETTINGS, + onTabSelected = onTabSelected + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(paddingValues) + .padding(horizontal = 20.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Settings", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Text( + text = "Sign out to test login with another account.", + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF666666) + ) + + Button( + onClick = onLogoutClick, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text(text = "Log out") + } + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt new file mode 100644 index 00000000..38cda292 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt @@ -0,0 +1,118 @@ +package com.example.shoestoreapp.features.invoice.mock + +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceDetail +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.model.PaymentMethod + +object InvoiceMockData { + fun invoices(): List { + val now = System.currentTimeMillis() + + return listOf( + Invoice( + id = 1, + publicId = "e7ee9247-6d89-4e2e-9b78-3f89db40a111", + userId = 1, + fullName = "Marcus Alexander", + status = InvoiceStatus.PENDING, + paymentMethod = PaymentMethod.ONLINE, + paymentId = 101, + orderCode = "ORDER-12345", + shippingFee = 5.0, + finalPrice = 284.0, + shippingAddress = "12 Nguyen Hue, District 1, HCM", + phone = "0901234567", + createdAtMillis = now - (45L * 60L * 1000L), + createdAt = "May 24, 2024 - 10:42 AM", + updatedAt = "May 24, 2024 - 10:42 AM", + invoiceDetails = listOf( + InvoiceDetail(11, "detail-11", 1, 301, 1, 120.0), + InvoiceDetail(12, "detail-12", 1, 302, 2, 79.5) + ) + ), + Invoice( + id = 2, + publicId = "a2d2f4d5-f67f-4c63-b9bd-9ac4fef0a222", + userId = 2, + fullName = "Elena Rodriguez", + status = InvoiceStatus.PAID, + paymentMethod = PaymentMethod.ONLINE, + paymentId = 102, + orderCode = "ORDER-12346", + shippingFee = 6.5, + finalPrice = 156.5, + shippingAddress = "88 Le Loi, District 1, HCM", + phone = "0902222333", + createdAtMillis = now - (20L * 60L * 1000L), + createdAt = "May 23, 2024 - 03:15 PM", + updatedAt = "May 23, 2024 - 03:20 PM", + invoiceDetails = listOf( + InvoiceDetail(21, "detail-21", 2, 305, 1, 150.0) + ) + ), + Invoice( + id = 3, + publicId = "7f95f9e7-1bc1-4b17-9e6f-dc35a52b3333", + userId = 1, + fullName = "James Sterling", + status = InvoiceStatus.DELIVERING, + paymentMethod = PaymentMethod.COD, + paymentId = 103, + orderCode = "ORDER-12347", + shippingFee = 10.0, + finalPrice = 420.0, + shippingAddress = "5 Tran Hung Dao, District 5, HCM", + phone = "0918888999", + createdAtMillis = now - (3L * 60L * 60L * 1000L), + createdAt = "May 22, 2024 - 09:00 AM", + updatedAt = "May 22, 2024 - 11:00 AM", + invoiceDetails = listOf( + InvoiceDetail(31, "detail-31", 3, 310, 2, 180.0), + InvoiceDetail(32, "detail-32", 3, 311, 1, 50.0) + ) + ), + Invoice( + id = 4, + publicId = "53b8f841-39fd-4a8f-bcf8-f9c51a1c4444", + userId = 3, + fullName = "Sarah Chen", + status = InvoiceStatus.DELIVERED, + paymentMethod = PaymentMethod.COD, + paymentId = 104, + orderCode = "ORDER-12348", + shippingFee = 0.0, + finalPrice = 89.0, + shippingAddress = "17 Pasteur, District 3, HCM", + phone = "0981111222", + createdAtMillis = now - (8L * 60L * 60L * 1000L), + createdAt = "May 20, 2024 - 11:30 PM", + updatedAt = "May 21, 2024 - 07:20 AM", + invoiceDetails = listOf( + InvoiceDetail(41, "detail-41", 4, 318, 1, 89.0) + ) + ), + Invoice( + id = 5, + publicId = "8e2d4c68-35ce-41f7-bfe3-8842c6305555", + userId = 1, + fullName = "Oliver Brown", + status = InvoiceStatus.CANCELED, + paymentMethod = PaymentMethod.ONLINE, + paymentId = 105, + orderCode = "ORDER-12349", + shippingFee = 0.0, + finalPrice = 132.0, + shippingAddress = "26 Nguyen Trai, District 1, HCM", + phone = "0903444555", + createdAtMillis = now - (26L * 60L * 60L * 1000L), + createdAt = "May 19, 2024 - 08:10 PM", + updatedAt = "May 19, 2024 - 08:45 PM", + invoiceDetails = listOf( + InvoiceDetail(51, "detail-51", 5, 325, 1, 132.0) + ) + ) + ) + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt new file mode 100644 index 00000000..40650e84 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt @@ -0,0 +1,69 @@ +package com.example.shoestoreapp.features.invoice.model + +enum class InvoiceStatus { + PENDING, + PAID, + DELIVERING, + DELIVERED, + CANCELED +} + +enum class PaymentMethod { + ONLINE, + COD +} + +data class InvoiceDetail( + val id: Int, + val publicId: String, + val invoiceId: Int, + val productVariantId: Int, + val quantity: Int, + val unitPrice: Double +) + +data class Invoice( + val id: Int, + val publicId: String, + val userId: Int, + val fullName: String, + val status: InvoiceStatus, + val paymentMethod: PaymentMethod, + val paymentId: Int, + val orderCode: String, + val shippingFee: Double, + val finalPrice: Double, + val shippingAddress: String, + val phone: String, + val createdAtMillis: Long, + val createdAt: String, + val updatedAt: String, + val invoiceDetails: List +) + +private const val ONLINE_PAYMENT_TIMEOUT_MILLIS = 30L * 60L * 1000L + +fun Invoice.shouldAutoCancel(nowMillis: Long = System.currentTimeMillis()): Boolean { + return paymentMethod == PaymentMethod.ONLINE && + status == InvoiceStatus.PENDING && + nowMillis - createdAtMillis >= ONLINE_PAYMENT_TIMEOUT_MILLIS +} + +fun Invoice.nextWorkflowStatus(): InvoiceStatus? { + return when (paymentMethod) { + PaymentMethod.ONLINE -> when (status) { + InvoiceStatus.PENDING -> InvoiceStatus.PAID + InvoiceStatus.PAID -> InvoiceStatus.DELIVERING + InvoiceStatus.DELIVERING -> InvoiceStatus.DELIVERED + InvoiceStatus.DELIVERED, InvoiceStatus.CANCELED -> null + } + + PaymentMethod.COD -> when (status) { + InvoiceStatus.PENDING -> InvoiceStatus.DELIVERING + InvoiceStatus.DELIVERING -> InvoiceStatus.PAID + InvoiceStatus.PAID -> InvoiceStatus.DELIVERED + InvoiceStatus.DELIVERED, InvoiceStatus.CANCELED -> null + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt new file mode 100644 index 00000000..098f0ab0 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt @@ -0,0 +1,11 @@ +package com.example.shoestoreapp.features.user.invoice.data + +import com.example.shoestoreapp.features.invoice.mock.InvoiceMockData +import com.example.shoestoreapp.features.invoice.model.Invoice + +class UserInvoiceMockRepository { + fun getInvoicesByUser(userId: Int): List { + return InvoiceMockData.invoices().filter { it.userId == userId } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt new file mode 100644 index 00000000..53ebd4a3 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt @@ -0,0 +1,184 @@ +package com.example.shoestoreapp.features.user.invoice.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.unit.dp +import androidx.compose.ui.unit.sp +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.user.invoice.viewmodel.UserInvoiceViewModel +import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar +import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab + +@Composable +fun UserInvoiceScreen( + viewModel: UserInvoiceViewModel = UserInvoiceViewModel(), + onTabSelected: (BottomNavTab) -> Unit = {} +) { + val selectedStatus by viewModel.selectedStatus.collectAsState() + val invoices by viewModel.visibleInvoices.collectAsState() + + Scaffold( + bottomBar = { + BottomNavBar( + selectedTab = BottomNavTab.BAG, + onTabSelected = onTabSelected + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.White) + .padding(horizontal = 16.dp) + ) { + Text( + text = "My Orders", + modifier = Modifier.padding(top = 16.dp, bottom = 12.dp), + color = Color.Black, + fontWeight = FontWeight.Black, + fontSize = 28.sp + ) + + UserInvoiceFilterRow( + selectedStatus = selectedStatus, + onFilterSelected = viewModel::onFilterChange + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(invoices) { invoice -> + UserInvoiceCard(invoice = invoice) + } + } + } + } +} + +@Composable +private fun UserInvoiceFilterRow( + selectedStatus: InvoiceStatus?, + onFilterSelected: (InvoiceStatus?) -> Unit +) { + val options = listOf(null, InvoiceStatus.PENDING, InvoiceStatus.PAID, InvoiceStatus.DELIVERING) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + options.forEach { option -> + val selected = selectedStatus == option + val label = option?.name ?: "ALL" + Text( + text = label, + modifier = Modifier + .background(if (selected) Color.Black else Color.White, RoundedCornerShape(18.dp)) + .border(1.dp, if (selected) Color.Black else Color(0xFFE0E0E0), RoundedCornerShape(18.dp)) + .padding(horizontal = 12.dp, vertical = 7.dp) + .then(Modifier), + color = if (selected) Color.White else Color(0xFF888888), + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + options.forEach { option -> + val label = option?.name ?: "ALL" + Text( + text = "", + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 7.dp) + .background(Color.Transparent) + .border(0.dp, Color.Transparent) + ) + } + } +} + +@Composable +private fun UserInvoiceCard(invoice: Invoice) { + Card( + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFE5E5E5)) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Column { + Text( + text = invoice.orderCode, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = Color(0xFF9D9D9D) + ) + Text( + text = invoice.createdAt, + fontSize = 12.sp, + color = Color(0xFF6A6A6A) + ) + } + Text( + text = "$${"%.2f".format(invoice.finalPrice)}", + color = Color.Black, + fontWeight = FontWeight.Black, + fontSize = 18.sp + ) + } + + Text( + text = invoice.shippingAddress, + modifier = Modifier.padding(top = 8.dp), + color = Color(0xFF555555), + fontSize = 12.sp + ) + + Text( + text = "Status: ${invoice.status.name}", + modifier = Modifier.padding(top = 6.dp), + color = when (invoice.status) { + InvoiceStatus.PENDING -> Color(0xFF666666) + InvoiceStatus.PAID -> Color(0xFF1F5FAE) + InvoiceStatus.DELIVERING -> Color.Black + InvoiceStatus.CANCELED -> Color(0xFFB3261E) + }, + fontWeight = FontWeight.Bold, + fontSize = 12.sp + ) + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt new file mode 100644 index 00000000..65f93ad5 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt @@ -0,0 +1,32 @@ +package com.example.shoestoreapp.features.user.invoice.viewmodel + +import androidx.lifecycle.ViewModel +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.user.invoice.data.UserInvoiceMockRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class UserInvoiceViewModel( + private val repository: UserInvoiceMockRepository = UserInvoiceMockRepository(), + userId: Int = 1 +) : ViewModel() { + + private val userInvoices = repository.getInvoicesByUser(userId) + private val _selectedStatus = MutableStateFlow(null) + private val _visibleInvoices = MutableStateFlow(userInvoices) + + val selectedStatus: StateFlow = _selectedStatus.asStateFlow() + val visibleInvoices: StateFlow> = _visibleInvoices.asStateFlow() + + fun onFilterChange(status: InvoiceStatus?) { + _selectedStatus.value = status + _visibleInvoices.value = if (status == null) { + userInvoices + } else { + userInvoices.filter { it.status == status } + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt new file mode 100644 index 00000000..5554bb23 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt @@ -0,0 +1,69 @@ +package com.example.shoestoreapp.features.user.profile.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar +import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab + +@Composable +fun UserProfileScreen( + onTabSelected: (BottomNavTab) -> Unit = {}, + onLogoutClick: () -> Unit = {} +) { + Scaffold( + bottomBar = { + BottomNavBar( + selectedTab = BottomNavTab.PROFILE, + onTabSelected = onTabSelected + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(paddingValues) + .padding(horizontal = 20.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Profile", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Text( + text = "Log out to switch account quickly.", + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF666666) + ) + + Button( + onClick = onLogoutClick, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text(text = "Log out") + } + } + } +} + From 1ea22fc221cd4ce32d289a6d62fd2d1d8216137d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Fri, 10 Apr 2026 15:39:55 +0700 Subject: [PATCH 03/12] feat(invoice-flow): implement admin-user invoice workflow and navigation --- .../com/example/shoestoreapp/MainActivity.kt | 130 +++++++++++++++++- .../admin/invoice/ui/AdminInvoiceScreen.kt | 22 ++- .../ui/components/AdminInvoiceFilterChips.kt | 10 +- .../viewmodel/AdminInvoiceViewModel.kt | 52 +++++-- .../user/invoice/ui/UserInvoiceScreen.kt | 42 +++--- .../ui/product_list/ProductListScreen.kt | 5 +- 6 files changed, 227 insertions(+), 34 deletions(-) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt index 73b305b4..8345ab38 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt @@ -12,9 +12,15 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.example.shoestoreapp.features.user.product.ui.product_detail.ProductDetailScreen import com.example.shoestoreapp.features.user.product.ui.product_list.ProductListScreen +import com.example.shoestoreapp.features.admin.invoice.ui.AdminInvoiceScreen +import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab +import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab import com.example.shoestoreapp.features.user.product.viewmodel.ProductDetailViewModel import com.example.shoestoreapp.features.user.product.viewmodel.ProductListViewModel +import com.example.shoestoreapp.features.user.invoice.ui.UserInvoiceScreen +import com.example.shoestoreapp.features.user.profile.ui.UserProfileScreen import com.example.shoestoreapp.features.admin.product.ui.AdminProductListScreen +import com.example.shoestoreapp.features.admin.settings.ui.AdminSettingsScreen import com.example.shoestoreapp.features.admin.product.viewmodel.AdminProductListViewModel import com.example.shoestoreapp.features.auth.presentation.reset_password.forgot_password.ForgotPasswordScreen import com.example.shoestoreapp.features.auth.presentation.sign_in.LoginScreenContent @@ -61,8 +67,6 @@ fun AppNavHost() { // Guard clause: Early return to prevent deep nesting (SonarCloud Fix) if (token == "LOADING") return@LaunchedEffect - // Wait for 1 second to let users see the Welcome UI properly - kotlinx.coroutines.delay(1000) // Flattened conditional logic using 'when' statement val destination = when { @@ -144,7 +148,9 @@ fun AppNavHost() { navController.navigate("sign_in") }, onNavigateToUserHome = { - navController.navigate("home_user") + navController.navigate("product_list") { + popUpTo("sign_up") { inclusive = true } + } }, ) } @@ -162,6 +168,64 @@ fun AppNavHost() { }, onNavigateToShoppingBag = { println("🔹 Shopping bag clicked") + }, + onBottomTabSelected = { tab -> + when (tab) { + BottomNavTab.PROFILE -> navController.navigate("user_profile") + BottomNavTab.BAG -> navController.navigate("user_invoice_list") + else -> Unit + } + } + ) + } + + // Route: User Invoice Screen + composable("user_invoice_list") { + UserInvoiceScreen( + onTabSelected = { tab -> + when (tab) { + BottomNavTab.HOME, BottomNavTab.SHOP -> navController.navigate("product_list") { + popUpTo("user_invoice_list") { inclusive = true } + } + BottomNavTab.PROFILE -> navController.navigate("user_profile") { + popUpTo("user_invoice_list") { inclusive = true } + } + BottomNavTab.BAG -> Unit + else -> println("🔹 User Tab selected: $tab") + } + } + ) + } + + // Route: User Profile Screen + composable("user_profile") { + val scope = rememberCoroutineScope() + + UserProfileScreen( + onTabSelected = { tab -> + when (tab) { + BottomNavTab.HOME, BottomNavTab.SHOP -> { + navController.navigate("product_list") { + popUpTo("user_profile") { inclusive = true } + } + } + BottomNavTab.BAG -> { + navController.navigate("user_invoice_list") { + popUpTo("user_profile") { inclusive = true } + } + } + BottomNavTab.PROFILE -> Unit + else -> println("🔹 User Tab selected: $tab") + } + }, + onLogoutClick = { + scope.launch { + tokenManager.clearAuthInfo() + navController.navigate("sign_in") { + popUpTo(navController.graph.id) { inclusive = true } + launchSingleTop = true + } + } } ) } @@ -196,7 +260,65 @@ fun AppNavHost() { println("🔹 Add Product clicked") }, onTabSelected = { tab -> - println("🔹 Admin Tab selected: $tab") + when (tab) { + AdminBottomNavTab.ADMIN -> Unit + AdminBottomNavTab.ORDERS -> { + navController.navigate("admin_invoice_list") + } + AdminBottomNavTab.SETTINGS -> { + navController.navigate("admin_settings") + } + else -> { + println("🔹 Admin Tab selected: $tab") + } + } + } + ) + } + + // Route: Admin Invoice Screen + composable("admin_invoice_list") { + AdminInvoiceScreen( + onTabSelected = { tab -> + when (tab) { + AdminBottomNavTab.ADMIN -> navController.navigate("admin_product_list") { + popUpTo("admin_invoice_list") { inclusive = true } + } + AdminBottomNavTab.ORDERS -> Unit + AdminBottomNavTab.SETTINGS -> navController.navigate("admin_settings") { + popUpTo("admin_invoice_list") { inclusive = true } + } + else -> println("🔹 Admin Tab selected: $tab") + } + } + ) + } + + // Route: Admin Settings Screen + composable("admin_settings") { + val scope = rememberCoroutineScope() + + AdminSettingsScreen( + onTabSelected = { tab -> + when (tab) { + AdminBottomNavTab.ADMIN -> navController.navigate("admin_product_list") { + popUpTo("admin_settings") { inclusive = true } + } + AdminBottomNavTab.ORDERS -> navController.navigate("admin_invoice_list") { + popUpTo("admin_settings") { inclusive = true } + } + AdminBottomNavTab.SETTINGS -> Unit + else -> println("🔹 Admin Tab selected: $tab") + } + }, + onLogoutClick = { + scope.launch { + tokenManager.clearAuthInfo() + navController.navigate("sign_in") { + popUpTo(navController.graph.id) { inclusive = true } + launchSingleTop = true + } + } } ) } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt index 1df32dc2..71ea5274 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt @@ -36,6 +36,7 @@ import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottom import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.model.PaymentMethod @Composable fun AdminInvoiceScreen( @@ -107,9 +108,11 @@ fun AdminInvoiceScreen( verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(invoices) { invoice -> + val nextStatus = viewModel.getNextStatus(invoice) InvoiceCard( invoice = invoice, - onUpdateStatus = { viewModel.cycleStatus(invoice.id) } + nextStatus = nextStatus, + onUpdateStatus = { viewModel.advanceStatus(invoice.id) } ) } } @@ -120,6 +123,7 @@ fun AdminInvoiceScreen( @Composable private fun InvoiceCard( invoice: Invoice, + nextStatus: InvoiceStatus?, onUpdateStatus: () -> Unit ) { Card( @@ -151,6 +155,12 @@ private fun InvoiceCard( fontSize = 11.sp, color = Color(0xFF7B7B7B) ) + Text( + text = if (invoice.paymentMethod == PaymentMethod.ONLINE) "ONLINE" else "COD", + fontSize = 11.sp, + color = Color(0xFF5E5E5E), + fontWeight = FontWeight.SemiBold + ) } Column(horizontalAlignment = Alignment.End) { @@ -173,13 +183,19 @@ private fun InvoiceCard( Button( onClick = onUpdateStatus, + enabled = nextStatus != null, shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = Color.Black, contentColor = Color.White ) ) { - Text(text = "Update Status", fontSize = 11.sp, fontWeight = FontWeight.Bold) + val buttonText = if (nextStatus == null) { + "No Action" + } else { + "Mark ${nextStatus.name}" + } + Text(text = buttonText, fontSize = 11.sp, fontWeight = FontWeight.Bold) } } } @@ -191,12 +207,14 @@ private fun StatusChip(status: InvoiceStatus) { InvoiceStatus.PENDING -> Color(0xFFF2F2F2) InvoiceStatus.PAID -> Color(0xFFE8F2FF) InvoiceStatus.DELIVERING -> Color.Black + InvoiceStatus.DELIVERED -> Color(0xFFE9F9ED) InvoiceStatus.CANCELED -> Color(0xFFFFECEB) } val fg = when (status) { InvoiceStatus.PENDING -> Color(0xFF666666) InvoiceStatus.PAID -> Color(0xFF1F5FAE) InvoiceStatus.DELIVERING -> Color.White + InvoiceStatus.DELIVERED -> Color(0xFF1E7D32) InvoiceStatus.CANCELED -> Color(0xFFB3261E) } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt index 1c5072d8..1f3fd295 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt @@ -23,7 +23,14 @@ fun AdminInvoiceFilterChips( selectedStatus: InvoiceStatus?, onFilterSelected: (InvoiceStatus?) -> Unit ) { - val filters = listOf(null, InvoiceStatus.PENDING, InvoiceStatus.PAID, InvoiceStatus.DELIVERING, InvoiceStatus.CANCELED) + val filters = listOf( + null, + InvoiceStatus.PENDING, + InvoiceStatus.PAID, + InvoiceStatus.DELIVERING, + InvoiceStatus.DELIVERED, + InvoiceStatus.CANCELED + ) Row( modifier = Modifier @@ -38,6 +45,7 @@ fun AdminInvoiceFilterChips( InvoiceStatus.PENDING -> "PENDING" InvoiceStatus.PAID -> "PAID" InvoiceStatus.DELIVERING -> "DELIVERING" + InvoiceStatus.DELIVERED -> "DELIVERED" InvoiceStatus.CANCELED -> "CANCELED" } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt index 4b01c765..8d0959c9 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt @@ -4,13 +4,15 @@ import androidx.lifecycle.ViewModel import com.example.shoestoreapp.features.admin.invoice.data.AdminInvoiceMockRepository import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.model.nextWorkflowStatus +import com.example.shoestoreapp.features.invoice.model.shouldAutoCancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update class AdminInvoiceViewModel( - private val repository: AdminInvoiceMockRepository = AdminInvoiceMockRepository() + repository: AdminInvoiceMockRepository = AdminInvoiceMockRepository() ) : ViewModel() { private val _allInvoices = MutableStateFlow(repository.getInvoices()) @@ -20,28 +22,62 @@ class AdminInvoiceViewModel( val selectedStatus: StateFlow = _selectedStatus.asStateFlow() val visibleInvoices: StateFlow> = _visibleInvoices.asStateFlow() + init { + applyTimeoutPolicy() + applyFilter() + } + fun onFilterChange(status: InvoiceStatus?) { + applyTimeoutPolicy() _selectedStatus.value = status applyFilter() } - fun cycleStatus(invoiceId: Int) { + fun getNextStatus(invoice: Invoice): InvoiceStatus? { + return invoice.nextWorkflowStatus() + } + + fun advanceStatus(invoiceId: Int) { + applyTimeoutPolicy() + _allInvoices.update { invoices -> invoices.map { invoice -> if (invoice.id != invoiceId) return@map invoice - val next = when (invoice.status) { - InvoiceStatus.PENDING -> InvoiceStatus.PAID - InvoiceStatus.PAID -> InvoiceStatus.DELIVERING - InvoiceStatus.DELIVERING -> InvoiceStatus.CANCELED - InvoiceStatus.CANCELED -> InvoiceStatus.PENDING + val next = invoice.nextWorkflowStatus() + if (next == null) { + invoice + } else { + invoice.copy( + status = next, + updatedAt = "Updated by admin" + ) } - invoice.copy(status = next) } } applyFilter() } + // Keep old method name to avoid breaking temporary callers. + fun cycleStatus(invoiceId: Int) { + advanceStatus(invoiceId) + } + + private fun applyTimeoutPolicy() { + _allInvoices.update { invoices -> + invoices.map { invoice -> + if (invoice.shouldAutoCancel()) { + invoice.copy( + status = InvoiceStatus.CANCELED, + updatedAt = "Auto-canceled after 30m timeout" + ) + } else { + invoice + } + } + } + } + private fun applyFilter() { val status = _selectedStatus.value _visibleInvoices.value = if (status == null) { diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt index 53ebd4a3..72239220 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt @@ -2,6 +2,8 @@ package com.example.shoestoreapp.features.user.invoice.ui import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -26,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.model.PaymentMethod import com.example.shoestoreapp.features.user.invoice.viewmodel.UserInvoiceViewModel import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab @@ -85,10 +89,19 @@ private fun UserInvoiceFilterRow( selectedStatus: InvoiceStatus?, onFilterSelected: (InvoiceStatus?) -> Unit ) { - val options = listOf(null, InvoiceStatus.PENDING, InvoiceStatus.PAID, InvoiceStatus.DELIVERING) + val options = listOf( + null, + InvoiceStatus.PENDING, + InvoiceStatus.PAID, + InvoiceStatus.DELIVERING, + InvoiceStatus.DELIVERED, + InvoiceStatus.CANCELED + ) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { options.forEach { option -> @@ -99,6 +112,7 @@ private fun UserInvoiceFilterRow( modifier = Modifier .background(if (selected) Color.Black else Color.White, RoundedCornerShape(18.dp)) .border(1.dp, if (selected) Color.Black else Color(0xFFE0E0E0), RoundedCornerShape(18.dp)) + .clickable { onFilterSelected(option) } .padding(horizontal = 12.dp, vertical = 7.dp) .then(Modifier), color = if (selected) Color.White else Color(0xFF888888), @@ -107,22 +121,6 @@ private fun UserInvoiceFilterRow( ) } } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - options.forEach { option -> - val label = option?.name ?: "ALL" - Text( - text = "", - modifier = Modifier - .padding(horizontal = 12.dp, vertical = 7.dp) - .background(Color.Transparent) - .border(0.dp, Color.Transparent) - ) - } - } } @Composable @@ -150,6 +148,12 @@ private fun UserInvoiceCard(invoice: Invoice) { fontSize = 12.sp, color = Color(0xFF6A6A6A) ) + Text( + text = if (invoice.paymentMethod == PaymentMethod.ONLINE) "ONLINE" else "COD", + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + color = Color(0xFF777777) + ) } Text( text = "$${"%.2f".format(invoice.finalPrice)}", @@ -173,6 +177,7 @@ private fun UserInvoiceCard(invoice: Invoice) { InvoiceStatus.PENDING -> Color(0xFF666666) InvoiceStatus.PAID -> Color(0xFF1F5FAE) InvoiceStatus.DELIVERING -> Color.Black + InvoiceStatus.DELIVERED -> Color(0xFF1E7D32) InvoiceStatus.CANCELED -> Color(0xFFB3261E) }, fontWeight = FontWeight.Bold, @@ -182,3 +187,4 @@ private fun UserInvoiceCard(invoice: Invoice) { } } + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/product_list/ProductListScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/product_list/ProductListScreen.kt index 3d1bc7b6..454e8495 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/product_list/ProductListScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/product_list/ProductListScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar +import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab import com.example.shoestoreapp.features.user.product.ui.components.FilterChips import com.example.shoestoreapp.features.user.product.ui.components.ProductCard import com.example.shoestoreapp.features.user.product.ui.components.SearchBar @@ -35,7 +36,8 @@ fun ProductListScreen( viewModel: ProductListViewModel, onNavigateToDetail: (Int) -> Unit = {}, onTopMenuClick: () -> Unit = {}, - onNavigateToShoppingBag: () -> Unit = {} + onNavigateToShoppingBag: () -> Unit = {}, + onBottomTabSelected: (BottomNavTab) -> Unit = {} ) { val productList = viewModel.productList.collectAsState(initial = emptyList()) val selectedFilter = viewModel.selectedFilter.collectAsState() @@ -54,6 +56,7 @@ fun ProductListScreen( selectedTab = selectedBottomTab.value, onTabSelected = { tab -> viewModel.onTabSelected(tab) + onBottomTabSelected(tab) } ) } From f7c7a00fa55df5c92e5fefcc92ba4cd4038cedd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Fri, 10 Apr 2026 15:40:23 +0700 Subject: [PATCH 04/12] feat(auth): adjust auth paths and related screen wiring --- .../features/auth/presentation/components/AuthUiEvent.kt | 2 +- .../auth/presentation/components/BaseAuthViewModel.kt | 5 +---- .../create_new_password/CreateNewPasswordScreen.kt | 2 +- .../create_new_password/CreateNewPasswordViewModel.kt | 2 +- .../reset_password/forgot_password/ForgotPasswordScreen.kt | 2 +- .../forgot_password/ForgotPasswordViewModel.kt | 2 +- .../features/auth/presentation/sign_in/SignInScreen.kt | 2 +- .../features/auth/presentation/sign_in/SignInViewModel.kt | 7 ++----- .../features/auth/presentation/sign_up/SignUpScreen.kt | 2 +- .../features/auth/presentation/sign_up/SignUpViewModel.kt | 4 ++-- 10 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/AuthUiEvent.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/AuthUiEvent.kt index 8f1d7487..34cc0292 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/AuthUiEvent.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/AuthUiEvent.kt @@ -1,4 +1,4 @@ -package com.example.shoestoreapp.features.auth.presentation.common +package com.example.shoestoreapp.features.auth.presentation.components sealed interface AuthUiEvent { object NavigateToUserHome : AuthUiEvent diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/BaseAuthViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/BaseAuthViewModel.kt index 7e284648..b87abd07 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/BaseAuthViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/components/BaseAuthViewModel.kt @@ -1,15 +1,12 @@ -package com.example.shoestoreapp.features.auth.presentation.common +package com.example.shoestoreapp.features.auth.presentation.components import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.shoestoreapp.core.utils.JwtUtils import com.example.shoestoreapp.core.utils.TokenManager import com.example.shoestoreapp.features.auth.domain.repository.AuthRepository -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch abstract class BaseAuthViewModel( diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordScreen.kt index a43b7d50..24efd7bb 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent // IMPORTANT: Using the shared AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent // IMPORTANT: Using the shared AuthUiEvent import com.example.shoestoreapp.features.auth.presentation.components.ResetPasswordContent import com.example.shoestoreapp.features.auth.presentation.components.ResetPasswordTopBar import com.example.shoestoreapp.features.auth.presentation.components.TitleBottom diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordViewModel.kt index 0878e9ad..613dbd66 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/create_new_password/CreateNewPasswordViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope import com.example.shoestoreapp.core.networks.RetrofitInstance import com.example.shoestoreapp.features.auth.data.repository.AuthRepositoryImpl import com.example.shoestoreapp.features.auth.domain.repository.AuthRepository -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent // IMPORTANT: Using the shared AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent // IMPORTANT: Using the shared AuthUiEvent import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordScreen.kt index f1b45435..f547de55 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent import com.example.shoestoreapp.features.auth.presentation.components.ResetPasswordContent import com.example.shoestoreapp.features.auth.presentation.components.ResetPasswordTopBar import com.example.shoestoreapp.features.auth.presentation.components.TitleBottom diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordViewModel.kt index 92df9d6d..374a9ab3 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/reset_password/forgot_password/ForgotPasswordViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.viewModelScope import com.example.shoestoreapp.core.networks.RetrofitInstance import com.example.shoestoreapp.features.auth.data.repository.AuthRepositoryImpl import com.example.shoestoreapp.features.auth.domain.repository.AuthRepository -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent // Dùng hàng chung +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent // Dùng hàng chung import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInScreen.kt index 98d53763..01452783 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInScreen.kt @@ -19,7 +19,7 @@ import com.example.shoestoreapp.core.utils.TokenManager import com.example.shoestoreapp.features.auth.presentation.components.* import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent @Composable fun LoginScreenContent( diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt index 63f6d4eb..8c2097a5 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt @@ -8,12 +8,9 @@ import com.example.shoestoreapp.core.utils.TokenManager import com.example.shoestoreapp.features.auth.data.remote.LoginRequest import com.example.shoestoreapp.features.auth.data.repository.AuthRepositoryImpl import com.example.shoestoreapp.features.auth.domain.repository.AuthRepository -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent -import com.example.shoestoreapp.features.auth.presentation.common.BaseAuthViewModel // Nhớ check lại đường dẫn package chỗ này nhé +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.BaseAuthViewModel // Nhớ check lại đường dẫn package chỗ này nhé import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpScreen.kt index 525f17ae..a2f0d061 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpScreen.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import com.example.shoestoreapp.core.utils.TokenManager -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent import com.example.shoestoreapp.features.auth.presentation.components.* import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpViewModel.kt index b20a8a78..7f3c1cae 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_up/SignUpViewModel.kt @@ -7,8 +7,8 @@ import com.example.shoestoreapp.core.utils.TokenManager import com.example.shoestoreapp.features.auth.data.remote.RegisterRequest import com.example.shoestoreapp.features.auth.data.repository.AuthRepositoryImpl import com.example.shoestoreapp.features.auth.domain.repository.AuthRepository -import com.example.shoestoreapp.features.auth.presentation.common.AuthUiEvent -import com.example.shoestoreapp.features.auth.presentation.common.BaseAuthViewModel +import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent +import com.example.shoestoreapp.features.auth.presentation.components.BaseAuthViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update From b942375091a6316fa2ffd601af330b75b6d433a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Sat, 11 Apr 2026 22:40:00 +0700 Subject: [PATCH 05/12] fix(Logout_Test) : solve Duplicated Lines (%) on New Code 8.6% --- .../ui/components/AdminInvoiceFilterChips.kt | 64 ++------------- .../admin/settings/ui/AdminSettingsScreen.kt | 44 ++--------- .../ui/components/LogoutActionSection.kt | 53 +++++++++++++ .../ui/components/InvoiceStatusFilterChips.kt | 77 +++++++++++++++++++ .../user/invoice/ui/UserInvoiceScreen.kt | 45 +++-------- .../user/profile/ui/UserProfileScreen.kt | 44 ++--------- 6 files changed, 161 insertions(+), 166 deletions(-) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/common/ui/components/LogoutActionSection.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt index 1f3fd295..8fe6f553 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminInvoiceFilterChips.kt @@ -1,74 +1,20 @@ package com.example.shoestoreapp.features.admin.invoice.ui.components -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChips @Composable fun AdminInvoiceFilterChips( selectedStatus: InvoiceStatus?, onFilterSelected: (InvoiceStatus?) -> Unit ) { - val filters = listOf( - null, - InvoiceStatus.PENDING, - InvoiceStatus.PAID, - InvoiceStatus.DELIVERING, - InvoiceStatus.DELIVERED, - InvoiceStatus.CANCELED + InvoiceStatusFilterChips( + selectedStatus = selectedStatus, + onFilterSelected = onFilterSelected, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) - - Row( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - filters.forEach { filter -> - val isSelected = selectedStatus == filter - val text = when (filter) { - null -> "ALL" - InvoiceStatus.PENDING -> "PENDING" - InvoiceStatus.PAID -> "PAID" - InvoiceStatus.DELIVERING -> "DELIVERING" - InvoiceStatus.DELIVERED -> "DELIVERED" - InvoiceStatus.CANCELED -> "CANCELED" - } - - Text( - text = text, - modifier = Modifier - .background( - color = if (isSelected) Color.Black else Color.White, - shape = RoundedCornerShape(20.dp) - ) - .border( - width = if (isSelected) 0.dp else 1.dp, - color = if (isSelected) Color.Black else Color(0xFFE0E0E0), - shape = RoundedCornerShape(20.dp) - ) - .clickable { onFilterSelected(filter) } - .padding(horizontal = 16.dp, vertical = 10.dp), - color = if (isSelected) Color.White else Color(0xFF999999), - fontWeight = FontWeight.Bold, - fontSize = 10.sp, - letterSpacing = 0.8.sp - ) - } - } } - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt index dfe0ac01..f4e6cc14 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/settings/ui/AdminSettingsScreen.kt @@ -1,23 +1,16 @@ package com.example.shoestoreapp.features.admin.settings.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavBar import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab +import com.example.shoestoreapp.features.common.ui.components.LogoutActionSection @Composable fun AdminSettingsScreen( @@ -32,38 +25,15 @@ fun AdminSettingsScreen( ) } ) { paddingValues -> - Column( + LogoutActionSection( + title = "Settings", + description = "Sign out to test login with another account.", + onLogoutClick = onLogoutClick, modifier = Modifier .fillMaxSize() .background(Color.White) .padding(paddingValues) - .padding(horizontal = 20.dp, vertical = 24.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = "Settings", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Text( - text = "Sign out to test login with another account.", - style = MaterialTheme.typography.bodyMedium, - color = Color(0xFF666666) - ) - - Button( - onClick = onLogoutClick, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Black, - contentColor = Color.White - ) - ) { - Text(text = "Log out") - } - } + .padding(horizontal = 20.dp, vertical = 24.dp) + ) } } - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/common/ui/components/LogoutActionSection.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/common/ui/components/LogoutActionSection.kt new file mode 100644 index 00000000..1e27c227 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/common/ui/components/LogoutActionSection.kt @@ -0,0 +1,53 @@ +package com.example.shoestoreapp.features.common.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun LogoutActionSection( + title: String, + description: String, + onLogoutClick: () -> Unit, + modifier: Modifier = Modifier, + buttonText: String = "Log out" +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF666666) + ) + + Button( + onClick = onLogoutClick, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text(text = buttonText) + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt new file mode 100644 index 00000000..445185e4 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt @@ -0,0 +1,77 @@ +package com.example.shoestoreapp.features.invoice.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus + +private val defaultFilters = listOf( + null, + InvoiceStatus.PENDING, + InvoiceStatus.PAID, + InvoiceStatus.DELIVERING, + InvoiceStatus.DELIVERED, + InvoiceStatus.CANCELED +) + +@Composable +fun InvoiceStatusFilterChips( + selectedStatus: InvoiceStatus?, + onFilterSelected: (InvoiceStatus?) -> Unit, + modifier: Modifier = Modifier, + chipCornerRadius: Dp = 20.dp, + chipHorizontalPadding: Dp = 16.dp, + chipVerticalPadding: Dp = 10.dp, + selectedColor: Color = Color.Black, + selectedTextColor: Color = Color.White, + unselectedColor: Color = Color.White, + unselectedTextColor: Color = Color(0xFF999999), + unselectedBorderColor: Color = Color(0xFFE0E0E0), + showSelectedBorder: Boolean = false, + fontSize: TextUnit = 10.sp, + letterSpacing: TextUnit = 0.8.sp +) { + Row( + modifier = modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + defaultFilters.forEach { filter -> + val isSelected = selectedStatus == filter + Text( + text = filter?.name ?: "ALL", + modifier = Modifier + .background( + color = if (isSelected) selectedColor else unselectedColor, + shape = RoundedCornerShape(chipCornerRadius) + ) + .border( + width = if (isSelected && !showSelectedBorder) 0.dp else 1.dp, + color = if (isSelected) selectedColor else unselectedBorderColor, + shape = RoundedCornerShape(chipCornerRadius) + ) + .clickable { onFilterSelected(filter) } + .padding(horizontal = chipHorizontalPadding, vertical = chipVerticalPadding), + color = if (isSelected) selectedTextColor else unselectedTextColor, + fontWeight = FontWeight.Bold, + fontSize = fontSize, + letterSpacing = letterSpacing + ) + } + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt index 72239220..c4733bc8 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt @@ -2,8 +2,6 @@ package com.example.shoestoreapp.features.user.invoice.ui import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -30,6 +27,7 @@ import androidx.compose.ui.unit.sp import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus import com.example.shoestoreapp.features.invoice.model.PaymentMethod +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChips import com.example.shoestoreapp.features.user.invoice.viewmodel.UserInvoiceViewModel import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab @@ -89,38 +87,19 @@ private fun UserInvoiceFilterRow( selectedStatus: InvoiceStatus?, onFilterSelected: (InvoiceStatus?) -> Unit ) { - val options = listOf( - null, - InvoiceStatus.PENDING, - InvoiceStatus.PAID, - InvoiceStatus.DELIVERING, - InvoiceStatus.DELIVERED, - InvoiceStatus.CANCELED - ) - - Row( + InvoiceStatusFilterChips( + selectedStatus = selectedStatus, + onFilterSelected = onFilterSelected, modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - options.forEach { option -> - val selected = selectedStatus == option - val label = option?.name ?: "ALL" - Text( - text = label, - modifier = Modifier - .background(if (selected) Color.Black else Color.White, RoundedCornerShape(18.dp)) - .border(1.dp, if (selected) Color.Black else Color(0xFFE0E0E0), RoundedCornerShape(18.dp)) - .clickable { onFilterSelected(option) } - .padding(horizontal = 12.dp, vertical = 7.dp) - .then(Modifier), - color = if (selected) Color.White else Color(0xFF888888), - fontSize = 10.sp, - fontWeight = FontWeight.Bold - ) - } - } + .padding(bottom = 2.dp), + chipCornerRadius = 18.dp, + chipHorizontalPadding = 12.dp, + chipVerticalPadding = 7.dp, + unselectedTextColor = Color(0xFF888888), + showSelectedBorder = true, + letterSpacing = 0.sp + ) } @Composable diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt index 5554bb23..0d40478e 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt @@ -1,21 +1,14 @@ package com.example.shoestoreapp.features.user.profile.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.example.shoestoreapp.features.common.ui.components.LogoutActionSection import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab @@ -32,38 +25,15 @@ fun UserProfileScreen( ) } ) { paddingValues -> - Column( + LogoutActionSection( + title = "Profile", + description = "Log out to switch account quickly.", + onLogoutClick = onLogoutClick, modifier = Modifier .fillMaxSize() .background(Color.White) .padding(paddingValues) - .padding(horizontal = 20.dp, vertical = 24.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = "Profile", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = Color.Black - ) - - Text( - text = "Log out to switch account quickly.", - style = MaterialTheme.typography.bodyMedium, - color = Color(0xFF666666) - ) - - Button( - onClick = onLogoutClick, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Black, - contentColor = Color.White - ) - ) { - Text(text = "Log out") - } - } + .padding(horizontal = 20.dp, vertical = 24.dp) + ) } } - From dd0f32eb3a0cd6b8a72c17a2ecae7e3aa269a244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Mon, 13 Apr 2026 15:28:20 +0700 Subject: [PATCH 06/12] refactor(invoice): fix sonar cognitive complexity and long parameter list --- .../com/example/shoestoreapp/MainActivity.kt | 475 +++++++++--------- .../ui/components/InvoiceStatusFilterChips.kt | 62 ++- .../user/invoice/ui/UserInvoiceScreen.kt | 22 +- 3 files changed, 284 insertions(+), 275 deletions(-) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt index 8345ab38..04914d5e 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt @@ -7,6 +7,8 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -30,6 +32,24 @@ import com.example.shoestoreapp.features.auth.presentation.reset_password.create import com.example.shoestoreapp.core.utils.TokenManager import kotlinx.coroutines.launch +private object Routes { + const val WELCOME = "welcome" + const val SIGN_IN = "sign_in" + const val SIGN_UP = "sign_up" + const val FORGOT_PASSWORD = "forgot_password" + const val CREATE_NEW_PASSWORD = "create_new_password/{email}/{otp}" + const val PRODUCT_LIST = "product_list" + const val PRODUCT_DETAIL = "product_detail/{productId}" + const val USER_INVOICE_LIST = "user_invoice_list" + const val USER_PROFILE = "user_profile" + const val ADMIN_PRODUCT_LIST = "admin_product_list" + const val ADMIN_INVOICE_LIST = "admin_invoice_list" + const val ADMIN_SETTINGS = "admin_settings" + + fun createNewPassword(email: String, otp: String): String = "create_new_password/$email/$otp" + fun productDetail(productId: Int): String = "product_detail/$productId" +} + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,279 +68,238 @@ fun AppNavHost() { val context = LocalContext.current val tokenManager = remember { TokenManager(context) } - NavHost( navController = navController, - //startDestination = "welcome" - //startDestination = "product_list" // ← Test ProductListScreen - startDestination = "welcome" // Test AdminProductListScreen + startDestination = Routes.WELCOME ) { + authGraph(navController, tokenManager) + userGraph(navController, tokenManager) + adminGraph(navController, tokenManager) + } +} - // Route 1: Welcome Screen - composable("welcome") { - // 1. COLLECT DATA FROM FLOW AS STATE - val token by tokenManager.getToken.collectAsState(initial = "LOADING") - val role by tokenManager.getRole.collectAsState(initial = "") +private fun NavGraphBuilder.authGraph(navController: NavHostController, tokenManager: TokenManager) { + composable(Routes.WELCOME) { + val token by tokenManager.getToken.collectAsState(initial = "LOADING") + val role by tokenManager.getRole.collectAsState(initial = "") - // 2. AUTO-NAVIGATION LOGIC - LaunchedEffect(token, role) { - // Guard clause: Early return to prevent deep nesting (SonarCloud Fix) - if (token == "LOADING") return@LaunchedEffect + LaunchedEffect(token, role) { + if (token == "LOADING") return@LaunchedEffect + val destination = resolveWelcomeDestination(token, role) + navController.navigateAndPopTo(destination, Routes.WELCOME) + } + WelcomeScreen( + onNavigateToSignIn = { + navController.navigateAndPopTo(Routes.SIGN_IN, Routes.WELCOME) + } + ) + } - // Flattened conditional logic using 'when' statement - val destination = when { - token.isNullOrEmpty() -> "sign_in" - role?.uppercase() == "ADMIN" -> "admin_product_list" - else -> "product_list" - } + composable(Routes.SIGN_IN) { + LoginScreenContent( + onNavigateToSignUp = { navController.navigate(Routes.SIGN_UP) }, + onNavigateToForgotPassword = { navController.navigate(Routes.FORGOT_PASSWORD) }, + onNavigateToUserHome = { + navController.navigateAndPopTo(Routes.PRODUCT_LIST, Routes.SIGN_IN) + }, + onNavigateToAdminHome = { + navController.navigateAndPopTo(Routes.ADMIN_PRODUCT_LIST, Routes.SIGN_IN) + } + ) + } - // Execute single navigation call - navController.navigate(destination) { - popUpTo("welcome") { inclusive = true } - } + composable(Routes.FORGOT_PASSWORD) { + ForgotPasswordScreen( + onNavigateCreateNewPassword = { email, otp -> + navController.navigate(Routes.createNewPassword(email, otp)) + }, + onNavigateToSignIn = { navController.navigate(Routes.SIGN_IN) } + ) + } + + composable(Routes.CREATE_NEW_PASSWORD) { backStackEntry -> + val email = backStackEntry.arguments?.getString("email") ?: "" + val otp = backStackEntry.arguments?.getString("otp") ?: "" + + CreateNewPasswordScreen( + email = email, + otp = otp, + onNavigateToSignIn = { navController.navigate(Routes.SIGN_IN) } + ) + } + + composable(Routes.SIGN_UP) { + RegisterScreenContent( + onNavigateToSignIn = { navController.navigate(Routes.SIGN_IN) }, + onNavigateToUserHome = { + navController.navigateAndPopTo(Routes.PRODUCT_LIST, Routes.SIGN_UP) } - // 3. RENDER THE WELCOME UI - WelcomeScreen( - onNavigateToSignIn = { - // Manual fallback just in case the user clicks the button - // before the 1-second delay finishes - navController.navigate("sign_in") { - popUpTo("welcome") { inclusive = true } - } - } - ) - } + ) + } +} - // Route 2: Sign-in Screen - composable("sign_in") { - LoginScreenContent( - onNavigateToSignUp = { - navController.navigate("sign_up") - }, - onNavigateToForgotPassword = { - navController.navigate("forgot_password") - }, - onNavigateToUserHome = { - navController.navigate("product_list") { - popUpTo("sign_in") { inclusive = true } - } - }, - onNavigateToAdminHome = { - navController.navigate("admin_product_list") { - popUpTo("sign_in") { inclusive = true } - } - } - ) - } +private fun NavGraphBuilder.userGraph(navController: NavHostController, tokenManager: TokenManager) { + composable(Routes.PRODUCT_LIST) { + ProductListScreen( + viewModel = remember { ProductListViewModel() }, + onNavigateToDetail = { productId -> + println("onNavigateToDetail called - productId: $productId") + navController.navigate(Routes.productDetail(productId)) + }, + onTopMenuClick = { println("Menu clicked") }, + onNavigateToShoppingBag = { println("Shopping bag clicked") }, + onBottomTabSelected = { tab -> handleUserHomeTabSelection(tab, navController) } + ) + } - // Route 2.1: Forgot Password - composable("forgot_password") { - ForgotPasswordScreen( - // Pass email and OTP to next screen via URL or bundle - onNavigateCreateNewPassword = { email, otp -> - navController.navigate("create_new_password/$email/$otp") - }, - onNavigateToSignIn = { - navController.navigate("sign_in") - }, - ) - } + composable(Routes.USER_INVOICE_LIST) { + UserInvoiceScreen( + onTabSelected = { tab -> handleUserInvoiceTabSelection(tab, navController) } + ) + } + + composable(Routes.USER_PROFILE) { + val scope = rememberCoroutineScope() - // Route 2.2: Create New Password - composable("create_new_password/{email}/{otp}") { - backStackEntry -> - val email = backStackEntry.arguments?.getString("email") ?: "" - val otp = backStackEntry.arguments?.getString("otp") ?: "" - CreateNewPasswordScreen( - email = email, - otp = otp, - onNavigateToSignIn = { - navController.navigate("sign_in") + UserProfileScreen( + onTabSelected = { tab -> handleUserProfileTabSelection(tab, navController) }, + onLogoutClick = { + scope.launch { + tokenManager.clearAuthInfo() + navController.navigateAfterLogout() } - ) - } + } + ) + } - // Route 3: Sign-up - composable("sign_up") { - RegisterScreenContent( - onNavigateToSignIn = { - navController.navigate("sign_in") - }, - onNavigateToUserHome = { - navController.navigate("product_list") { - popUpTo("sign_up") { inclusive = true } - } - }, - ) - } + composable(Routes.PRODUCT_DETAIL) { backStackEntry -> + val productId = backStackEntry.arguments?.getString("productId")?.toInt() ?: 1 - // Route: Product List Screen - composable("product_list") { - ProductListScreen( - viewModel = remember { ProductListViewModel() }, - onNavigateToDetail = { productId -> - println("🟢 onNavigateToDetail called - productId: $productId") - navController.navigate("product_detail/$productId") - }, - onTopMenuClick = { - println("🔹 Menu clicked") - }, - onNavigateToShoppingBag = { - println("🔹 Shopping bag clicked") - }, - onBottomTabSelected = { tab -> - when (tab) { - BottomNavTab.PROFILE -> navController.navigate("user_profile") - BottomNavTab.BAG -> navController.navigate("user_invoice_list") - else -> Unit - } - } - ) - } + ProductDetailScreen( + productId = productId, + viewModel = remember { ProductDetailViewModel() }, + onBackClick = { navController.popBackStack() }, + onNavigateToCart = { println("Navigating to cart") } + ) + } +} - // Route: User Invoice Screen - composable("user_invoice_list") { - UserInvoiceScreen( - onTabSelected = { tab -> - when (tab) { - BottomNavTab.HOME, BottomNavTab.SHOP -> navController.navigate("product_list") { - popUpTo("user_invoice_list") { inclusive = true } - } - BottomNavTab.PROFILE -> navController.navigate("user_profile") { - popUpTo("user_invoice_list") { inclusive = true } - } - BottomNavTab.BAG -> Unit - else -> println("🔹 User Tab selected: $tab") - } - } - ) - } +private fun NavGraphBuilder.adminGraph(navController: NavHostController, tokenManager: TokenManager) { + composable(Routes.ADMIN_PRODUCT_LIST) { + AdminProductListScreen( + viewModel = remember { AdminProductListViewModel() }, + onMenuClick = { println("Admin Menu clicked") }, + onAddProductClick = { println("Add Product clicked") }, + onTabSelected = { tab -> handleAdminProductTabSelection(tab, navController) } + ) + } + + composable(Routes.ADMIN_INVOICE_LIST) { + AdminInvoiceScreen( + onTabSelected = { tab -> handleAdminInvoiceTabSelection(tab, navController) } + ) + } + + composable(Routes.ADMIN_SETTINGS) { + val scope = rememberCoroutineScope() - // Route: User Profile Screen - composable("user_profile") { - val scope = rememberCoroutineScope() - - UserProfileScreen( - onTabSelected = { tab -> - when (tab) { - BottomNavTab.HOME, BottomNavTab.SHOP -> { - navController.navigate("product_list") { - popUpTo("user_profile") { inclusive = true } - } - } - BottomNavTab.BAG -> { - navController.navigate("user_invoice_list") { - popUpTo("user_profile") { inclusive = true } - } - } - BottomNavTab.PROFILE -> Unit - else -> println("🔹 User Tab selected: $tab") - } - }, - onLogoutClick = { - scope.launch { - tokenManager.clearAuthInfo() - navController.navigate("sign_in") { - popUpTo(navController.graph.id) { inclusive = true } - launchSingleTop = true - } - } + AdminSettingsScreen( + onTabSelected = { tab -> handleAdminSettingsTabSelection(tab, navController) }, + onLogoutClick = { + scope.launch { + tokenManager.clearAuthInfo() + navController.navigateAfterLogout() } - ) + } + ) + } +} + +private fun resolveWelcomeDestination(token: String?, role: String?): String = when { + token.isNullOrEmpty() -> Routes.SIGN_IN + role?.uppercase() == "ADMIN" -> Routes.ADMIN_PRODUCT_LIST + else -> Routes.PRODUCT_LIST +} + +private fun NavHostController.navigateAndPopTo(destination: String, popUpRoute: String) { + navigate(destination) { + popUpTo(popUpRoute) { inclusive = true } + } +} + +private fun NavHostController.navigateAfterLogout() { + navigate(Routes.SIGN_IN) { + popUpTo(graph.id) { inclusive = true } + launchSingleTop = true + } +} + +private fun handleUserHomeTabSelection(tab: BottomNavTab, navController: NavHostController) { + when (tab) { + BottomNavTab.PROFILE -> navController.navigate(Routes.USER_PROFILE) + BottomNavTab.BAG -> navController.navigate(Routes.USER_INVOICE_LIST) + else -> Unit + } +} + +private fun handleUserInvoiceTabSelection(tab: BottomNavTab, navController: NavHostController) { + when (tab) { + BottomNavTab.HOME, BottomNavTab.SHOP -> { + navController.navigateAndPopTo(Routes.PRODUCT_LIST, Routes.USER_INVOICE_LIST) + } + BottomNavTab.PROFILE -> { + navController.navigateAndPopTo(Routes.USER_PROFILE, Routes.USER_INVOICE_LIST) } + BottomNavTab.BAG -> Unit + else -> println("User Tab selected: $tab") + } +} - // Route: Product Detail Screen - composable("product_detail/{productId}") { backStackEntry -> - // Lấy productId từ URL - val productId = backStackEntry.arguments?.getString("productId")?.toInt() ?: 1 - - ProductDetailScreen( - productId = productId, - viewModel = remember { ProductDetailViewModel() }, - onBackClick = { - // Click back -> quay lại ProductListScreen - navController.popBackStack() - }, - onNavigateToCart = { - // Click "Add to Cart" -> điều hướng sang Cart screen - println("🔹 Navigating to cart") - // navController.navigate("cart") - } - ) +private fun handleUserProfileTabSelection(tab: BottomNavTab, navController: NavHostController) { + when (tab) { + BottomNavTab.HOME, BottomNavTab.SHOP -> { + navController.navigateAndPopTo(Routes.PRODUCT_LIST, Routes.USER_PROFILE) } - // Route: Admin Product Screen - composable("admin_product_list") { - AdminProductListScreen( - viewModel = remember { AdminProductListViewModel() }, - onMenuClick = { - println("🔹 Admin Menu clicked") - }, - onAddProductClick = { - println("🔹 Add Product clicked") - }, - onTabSelected = { tab -> - when (tab) { - AdminBottomNavTab.ADMIN -> Unit - AdminBottomNavTab.ORDERS -> { - navController.navigate("admin_invoice_list") - } - AdminBottomNavTab.SETTINGS -> { - navController.navigate("admin_settings") - } - else -> { - println("🔹 Admin Tab selected: $tab") - } - } - } - ) + BottomNavTab.BAG -> { + navController.navigateAndPopTo(Routes.USER_INVOICE_LIST, Routes.USER_PROFILE) } + BottomNavTab.PROFILE -> Unit + else -> println("User Tab selected: $tab") + } +} - // Route: Admin Invoice Screen - composable("admin_invoice_list") { - AdminInvoiceScreen( - onTabSelected = { tab -> - when (tab) { - AdminBottomNavTab.ADMIN -> navController.navigate("admin_product_list") { - popUpTo("admin_invoice_list") { inclusive = true } - } - AdminBottomNavTab.ORDERS -> Unit - AdminBottomNavTab.SETTINGS -> navController.navigate("admin_settings") { - popUpTo("admin_invoice_list") { inclusive = true } - } - else -> println("🔹 Admin Tab selected: $tab") - } - } - ) +private fun handleAdminProductTabSelection(tab: AdminBottomNavTab, navController: NavHostController) { + when (tab) { + AdminBottomNavTab.ADMIN -> Unit + AdminBottomNavTab.ORDERS -> navController.navigate(Routes.ADMIN_INVOICE_LIST) + AdminBottomNavTab.SETTINGS -> navController.navigate(Routes.ADMIN_SETTINGS) + else -> println("Admin Tab selected: $tab") + } +} + +private fun handleAdminInvoiceTabSelection(tab: AdminBottomNavTab, navController: NavHostController) { + when (tab) { + AdminBottomNavTab.ADMIN -> { + navController.navigateAndPopTo(Routes.ADMIN_PRODUCT_LIST, Routes.ADMIN_INVOICE_LIST) + } + AdminBottomNavTab.ORDERS -> Unit + AdminBottomNavTab.SETTINGS -> { + navController.navigateAndPopTo(Routes.ADMIN_SETTINGS, Routes.ADMIN_INVOICE_LIST) } + else -> println("Admin Tab selected: $tab") + } +} - // Route: Admin Settings Screen - composable("admin_settings") { - val scope = rememberCoroutineScope() - - AdminSettingsScreen( - onTabSelected = { tab -> - when (tab) { - AdminBottomNavTab.ADMIN -> navController.navigate("admin_product_list") { - popUpTo("admin_settings") { inclusive = true } - } - AdminBottomNavTab.ORDERS -> navController.navigate("admin_invoice_list") { - popUpTo("admin_settings") { inclusive = true } - } - AdminBottomNavTab.SETTINGS -> Unit - else -> println("🔹 Admin Tab selected: $tab") - } - }, - onLogoutClick = { - scope.launch { - tokenManager.clearAuthInfo() - navController.navigate("sign_in") { - popUpTo(navController.graph.id) { inclusive = true } - launchSingleTop = true - } - } - } - ) +private fun handleAdminSettingsTabSelection(tab: AdminBottomNavTab, navController: NavHostController) { + when (tab) { + AdminBottomNavTab.ADMIN -> { + navController.navigateAndPopTo(Routes.ADMIN_PRODUCT_LIST, Routes.ADMIN_SETTINGS) + } + AdminBottomNavTab.ORDERS -> { + navController.navigateAndPopTo(Routes.ADMIN_INVOICE_LIST, Routes.ADMIN_SETTINGS) } + AdminBottomNavTab.SETTINGS -> Unit + else -> println("Admin Tab selected: $tab") } } \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt index 445185e4..a6882c28 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceStatusFilterChips.kt @@ -29,47 +29,67 @@ private val defaultFilters = listOf( InvoiceStatus.CANCELED ) +data class InvoiceStatusFilterChipDimensions( + val chipCornerRadius: Dp = 20.dp, + val chipHorizontalPadding: Dp = 16.dp, + val chipVerticalPadding: Dp = 10.dp +) + +data class InvoiceStatusFilterChipColors( + val selectedColor: Color = Color.Black, + val selectedTextColor: Color = Color.White, + val unselectedColor: Color = Color.White, + val unselectedTextColor: Color = Color(0xFF999999), + val unselectedBorderColor: Color = Color(0xFFE0E0E0) +) + +data class InvoiceStatusFilterChipTypography( + val fontSize: TextUnit = 10.sp, + val letterSpacing: TextUnit = 0.8.sp +) + +data class InvoiceStatusFilterChipStyle( + val dimensions: InvoiceStatusFilterChipDimensions = InvoiceStatusFilterChipDimensions(), + val colors: InvoiceStatusFilterChipColors = InvoiceStatusFilterChipColors(), + val typography: InvoiceStatusFilterChipTypography = InvoiceStatusFilterChipTypography(), + val showSelectedBorder: Boolean = false +) + @Composable fun InvoiceStatusFilterChips( selectedStatus: InvoiceStatus?, onFilterSelected: (InvoiceStatus?) -> Unit, modifier: Modifier = Modifier, - chipCornerRadius: Dp = 20.dp, - chipHorizontalPadding: Dp = 16.dp, - chipVerticalPadding: Dp = 10.dp, - selectedColor: Color = Color.Black, - selectedTextColor: Color = Color.White, - unselectedColor: Color = Color.White, - unselectedTextColor: Color = Color(0xFF999999), - unselectedBorderColor: Color = Color(0xFFE0E0E0), - showSelectedBorder: Boolean = false, - fontSize: TextUnit = 10.sp, - letterSpacing: TextUnit = 0.8.sp + style: InvoiceStatusFilterChipStyle = InvoiceStatusFilterChipStyle(), + filters: List = defaultFilters ) { Row( modifier = modifier.horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - defaultFilters.forEach { filter -> + filters.forEach { filter -> val isSelected = selectedStatus == filter Text( text = filter?.name ?: "ALL", modifier = Modifier .background( - color = if (isSelected) selectedColor else unselectedColor, - shape = RoundedCornerShape(chipCornerRadius) + color = if (isSelected) style.colors.selectedColor else style.colors.unselectedColor, + shape = RoundedCornerShape(style.dimensions.chipCornerRadius) ) .border( - width = if (isSelected && !showSelectedBorder) 0.dp else 1.dp, - color = if (isSelected) selectedColor else unselectedBorderColor, - shape = RoundedCornerShape(chipCornerRadius) + width = if (isSelected && !style.showSelectedBorder) 0.dp else 1.dp, + color = if (isSelected) style.colors.selectedColor else style.colors.unselectedBorderColor, + shape = RoundedCornerShape(style.dimensions.chipCornerRadius) ) .clickable { onFilterSelected(filter) } - .padding(horizontal = chipHorizontalPadding, vertical = chipVerticalPadding), - color = if (isSelected) selectedTextColor else unselectedTextColor, + .padding( + horizontal = style.dimensions.chipHorizontalPadding, + vertical = style.dimensions.chipVerticalPadding + ), + color = if (isSelected) style.colors.selectedTextColor else style.colors.unselectedTextColor, fontWeight = FontWeight.Bold, - fontSize = fontSize, - letterSpacing = letterSpacing + fontSize = style.typography.fontSize, + letterSpacing = style.typography.letterSpacing ) } } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt index c4733bc8..1863dea9 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt @@ -27,6 +27,10 @@ import androidx.compose.ui.unit.sp import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus import com.example.shoestoreapp.features.invoice.model.PaymentMethod +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipColors +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipDimensions +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipStyle +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipTypography import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChips import com.example.shoestoreapp.features.user.invoice.viewmodel.UserInvoiceViewModel import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar @@ -93,12 +97,18 @@ private fun UserInvoiceFilterRow( modifier = Modifier .fillMaxWidth() .padding(bottom = 2.dp), - chipCornerRadius = 18.dp, - chipHorizontalPadding = 12.dp, - chipVerticalPadding = 7.dp, - unselectedTextColor = Color(0xFF888888), - showSelectedBorder = true, - letterSpacing = 0.sp + style = InvoiceStatusFilterChipStyle( + dimensions = InvoiceStatusFilterChipDimensions( + chipCornerRadius = 18.dp, + chipHorizontalPadding = 12.dp, + chipVerticalPadding = 7.dp + ), + colors = InvoiceStatusFilterChipColors( + unselectedTextColor = Color(0xFF888888) + ), + typography = InvoiceStatusFilterChipTypography(letterSpacing = 0.sp), + showSelectedBorder = true + ) ) } From c94b14303f42ed84c8ded948d53d16fd2f1f2d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Sun, 19 Apr 2026 08:08:22 +0700 Subject: [PATCH 07/12] feat(invoice) : create mockdata for admin and user and create screen as well as create viewmodel for features invoice --- .../com/example/shoestoreapp/MainActivity.kt | 9 +- .../admin/invoice/ui/AdminInvoiceScreen.kt | 153 +++---------- .../viewmodel/AdminInvoiceViewModel.kt | 70 +++--- .../data/repository/AuthRepositoryImpl.kt | 3 + .../presentation/sign_in/SignInViewModel.kt | 82 +++---- .../features/invoice/mock/InvoiceMockData.kt | 112 +++------ .../features/invoice/model/InvoiceModels.kt | 68 ++---- .../invoice/ui/components/AdminOrderCard.kt | 212 ++++++++++++++++++ .../invoice/ui/components/StatusBadge.kt | 36 +++ .../invoice/ui/components/UserOrderCard.kt | 135 +++++++++++ .../invoice/data/UserInvoiceMockRepository.kt | 5 +- .../user/invoice/ui/UserInvoiceScreen.kt | 82 +------ .../invoice/viewmodel/UserInvoiceViewModel.kt | 4 +- .../product/ui/components/BottomNavBar.kt | 13 +- .../user/profile/ui/UserProfileScreen.kt | 52 ++++- 15 files changed, 614 insertions(+), 422 deletions(-) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/StatusBadge.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt index 04914d5e..7897ab5a 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/MainActivity.kt @@ -164,6 +164,9 @@ private fun NavGraphBuilder.userGraph(navController: NavHostController, tokenMan UserProfileScreen( onTabSelected = { tab -> handleUserProfileTabSelection(tab, navController) }, + onViewOrdersClick = { + navController.navigate(Routes.USER_INVOICE_LIST) + }, onLogoutClick = { scope.launch { tokenManager.clearAuthInfo() @@ -238,7 +241,7 @@ private fun NavHostController.navigateAfterLogout() { private fun handleUserHomeTabSelection(tab: BottomNavTab, navController: NavHostController) { when (tab) { BottomNavTab.PROFILE -> navController.navigate(Routes.USER_PROFILE) - BottomNavTab.BAG -> navController.navigate(Routes.USER_INVOICE_LIST) + BottomNavTab.BAG -> Unit else -> Unit } } @@ -261,9 +264,7 @@ private fun handleUserProfileTabSelection(tab: BottomNavTab, navController: NavH BottomNavTab.HOME, BottomNavTab.SHOP -> { navController.navigateAndPopTo(Routes.PRODUCT_LIST, Routes.USER_PROFILE) } - BottomNavTab.BAG -> { - navController.navigateAndPopTo(Routes.USER_INVOICE_LIST, Routes.USER_PROFILE) - } + BottomNavTab.BAG -> Unit BottomNavTab.PROFILE -> Unit else -> println("User Tab selected: $tab") } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt index 71ea5274..e10fe70d 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt @@ -10,20 +10,21 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding 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.Menu import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -34,9 +35,9 @@ import com.example.shoestoreapp.features.admin.invoice.ui.components.AdminInvoic import com.example.shoestoreapp.features.admin.invoice.viewmodel.AdminInvoiceViewModel import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavBar import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab -import com.example.shoestoreapp.features.invoice.model.Invoice -import com.example.shoestoreapp.features.invoice.model.InvoiceStatus -import com.example.shoestoreapp.features.invoice.model.PaymentMethod +import com.example.shoestoreapp.features.invoice.model.displayName +import com.example.shoestoreapp.features.invoice.ui.components.AdminOrderCard +import kotlinx.coroutines.launch @Composable fun AdminInvoiceScreen( @@ -45,8 +46,11 @@ fun AdminInvoiceScreen( ) { val selectedStatus by viewModel.selectedStatus.collectAsState() val invoices by viewModel.visibleInvoices.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { Row( modifier = Modifier @@ -108,125 +112,30 @@ fun AdminInvoiceScreen( verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(invoices) { invoice -> - val nextStatus = viewModel.getNextStatus(invoice) - InvoiceCard( + AdminOrderCard( invoice = invoice, - nextStatus = nextStatus, - onUpdateStatus = { viewModel.advanceStatus(invoice.id) } - ) - } - } - } - } -} - -@Composable -private fun InvoiceCard( - invoice: Invoice, - nextStatus: InvoiceStatus?, - onUpdateStatus: () -> Unit -) { - Card( - colors = CardDefaults.cardColors(containerColor = Color.White), - shape = RoundedCornerShape(12.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFE5E5E5)) - ) { - Column(modifier = Modifier.padding(14.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top - ) { - Column { - Text( - text = "#${invoice.orderCode}", - fontSize = 11.sp, - color = Color(0xFF9D9D9D), - fontWeight = FontWeight.Bold - ) - Text( - text = invoice.fullName, - fontWeight = FontWeight.ExtraBold, - fontSize = 18.sp, - color = Color.Black - ) - Text( - text = invoice.createdAt, - fontSize = 11.sp, - color = Color(0xFF7B7B7B) - ) - Text( - text = if (invoice.paymentMethod == PaymentMethod.ONLINE) "ONLINE" else "COD", - fontSize = 11.sp, - color = Color(0xFF5E5E5E), - fontWeight = FontWeight.SemiBold - ) - } + statusOptions = viewModel.getStatusOptions(invoice), + onStatusSelected = { targetStatus -> + viewModel.updateStatus( + orderCode = invoice.orderCode, + targetStatus = targetStatus + ) - Column(horizontalAlignment = Alignment.End) { - Text( - text = "$${"%.2f".format(invoice.finalPrice)}", - fontWeight = FontWeight.Black, - fontSize = 20.sp, - color = Color.Black + scope.launch { + val result = snackbarHostState.showSnackbar( + message = "Status updated to ${targetStatus.displayName()}", + actionLabel = "Undo", + duration = SnackbarDuration.Short + ) + if (result == SnackbarResult.ActionPerformed) { + viewModel.undoLastStatusChange() + } + } + }, + onDetailsClick = {} ) - StatusChip(status = invoice.status) - } - } - - Text( - text = "${invoice.invoiceDetails.sumOf { it.quantity }} items - ${invoice.shippingAddress}", - modifier = Modifier.padding(top = 10.dp, bottom = 12.dp), - fontSize = 12.sp, - color = Color(0xFF5E5E5E) - ) - - Button( - onClick = onUpdateStatus, - enabled = nextStatus != null, - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.Black, - contentColor = Color.White - ) - ) { - val buttonText = if (nextStatus == null) { - "No Action" - } else { - "Mark ${nextStatus.name}" } - Text(text = buttonText, fontSize = 11.sp, fontWeight = FontWeight.Bold) } } } } - -@Composable -private fun StatusChip(status: InvoiceStatus) { - val bg = when (status) { - InvoiceStatus.PENDING -> Color(0xFFF2F2F2) - InvoiceStatus.PAID -> Color(0xFFE8F2FF) - InvoiceStatus.DELIVERING -> Color.Black - InvoiceStatus.DELIVERED -> Color(0xFFE9F9ED) - InvoiceStatus.CANCELED -> Color(0xFFFFECEB) - } - val fg = when (status) { - InvoiceStatus.PENDING -> Color(0xFF666666) - InvoiceStatus.PAID -> Color(0xFF1F5FAE) - InvoiceStatus.DELIVERING -> Color.White - InvoiceStatus.DELIVERED -> Color(0xFF1E7D32) - InvoiceStatus.CANCELED -> Color(0xFFB3261E) - } - - Text( - text = status.name, - modifier = Modifier - .padding(top = 4.dp) - .background(bg, RoundedCornerShape(6.dp)) - .padding(horizontal = 8.dp, vertical = 3.dp), - color = fg, - fontSize = 10.sp, - fontWeight = FontWeight.Bold - ) -} - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt index 8d0959c9..4238e551 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/viewmodel/AdminInvoiceViewModel.kt @@ -5,7 +5,6 @@ import com.example.shoestoreapp.features.admin.invoice.data.AdminInvoiceMockRepo import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus import com.example.shoestoreapp.features.invoice.model.nextWorkflowStatus -import com.example.shoestoreapp.features.invoice.model.shouldAutoCancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,20 +14,24 @@ class AdminInvoiceViewModel( repository: AdminInvoiceMockRepository = AdminInvoiceMockRepository() ) : ViewModel() { + private data class LastStatusChange( + val orderCode: String, + val previousStatus: InvoiceStatus + ) + private val _allInvoices = MutableStateFlow(repository.getInvoices()) private val _selectedStatus = MutableStateFlow(null) private val _visibleInvoices = MutableStateFlow(_allInvoices.value) + private var lastStatusChange: LastStatusChange? = null val selectedStatus: StateFlow = _selectedStatus.asStateFlow() val visibleInvoices: StateFlow> = _visibleInvoices.asStateFlow() init { - applyTimeoutPolicy() applyFilter() } fun onFilterChange(status: InvoiceStatus?) { - applyTimeoutPolicy() _selectedStatus.value = status applyFilter() } @@ -37,45 +40,60 @@ class AdminInvoiceViewModel( return invoice.nextWorkflowStatus() } - fun advanceStatus(invoiceId: Int) { - applyTimeoutPolicy() + fun getStatusOptions(invoice: Invoice): List { + val nextStatus = invoice.nextWorkflowStatus() + val canCancel = invoice.status == InvoiceStatus.PENDING + + return buildList { + if (nextStatus != null) add(nextStatus) + if (canCancel) add(InvoiceStatus.CANCELED) + } + } + + fun advanceStatus(orderCode: String) { + val invoice = _allInvoices.value.firstOrNull { it.orderCode == orderCode } ?: return + val next = invoice.nextWorkflowStatus() ?: return + updateStatus(orderCode = orderCode, targetStatus = next) + } + + fun updateStatus(orderCode: String, targetStatus: InvoiceStatus) { + var previousStatus: InvoiceStatus? = null _allInvoices.update { invoices -> invoices.map { invoice -> - if (invoice.id != invoiceId) return@map invoice + if (invoice.orderCode != orderCode) return@map invoice + if (invoice.status == targetStatus) return@map invoice - val next = invoice.nextWorkflowStatus() - if (next == null) { - invoice - } else { - invoice.copy( - status = next, - updatedAt = "Updated by admin" - ) - } + previousStatus = invoice.status + invoice.copy(status = targetStatus) } } - applyFilter() - } - // Keep old method name to avoid breaking temporary callers. - fun cycleStatus(invoiceId: Int) { - advanceStatus(invoiceId) + if (previousStatus != null) { + lastStatusChange = LastStatusChange(orderCode = orderCode, previousStatus = previousStatus!!) + applyFilter() + } } - private fun applyTimeoutPolicy() { + fun undoLastStatusChange() { + val change = lastStatusChange ?: return + lastStatusChange = null + _allInvoices.update { invoices -> invoices.map { invoice -> - if (invoice.shouldAutoCancel()) { - invoice.copy( - status = InvoiceStatus.CANCELED, - updatedAt = "Auto-canceled after 30m timeout" - ) + if (invoice.orderCode == change.orderCode) { + invoice.copy(status = change.previousStatus) } else { invoice } } } + applyFilter() + } + + // Keep old method name to avoid breaking temporary callers. + fun cycleStatus(orderCode: String) { + advanceStatus(orderCode) } private fun applyFilter() { diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/data/repository/AuthRepositoryImpl.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/data/repository/AuthRepositoryImpl.kt index c58f32e0..82782495 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/data/repository/AuthRepositoryImpl.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/data/repository/AuthRepositoryImpl.kt @@ -10,6 +10,7 @@ import com.example.shoestoreapp.features.auth.data.remote.UpdatePasswordRequest import com.example.shoestoreapp.features.auth.data.remote.VerifyEmailRequest import com.example.shoestoreapp.features.auth.data.remote.VerifyOtpRequest + import kotlinx.coroutines.CancellationException import retrofit2.HttpException import java.io.IOException class AuthRepositoryImpl( @@ -40,6 +41,8 @@ Result.failure(Exception(errorMessage)) } // No internet connection + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Result.failure(Exception(ERROR_OFFLINE)) } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt index 8c2097a5..3593e101 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/auth/presentation/sign_in/SignInViewModel.kt @@ -9,7 +9,7 @@ import com.example.shoestoreapp.features.auth.data.remote.LoginRequest import com.example.shoestoreapp.features.auth.data.repository.AuthRepositoryImpl import com.example.shoestoreapp.features.auth.domain.repository.AuthRepository import com.example.shoestoreapp.features.auth.presentation.components.AuthUiEvent -import com.example.shoestoreapp.features.auth.presentation.components.BaseAuthViewModel // Nhớ check lại đường dẫn package chỗ này nhé +import com.example.shoestoreapp.features.auth.presentation.components.BaseAuthViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update @@ -20,7 +20,7 @@ class SignInViewModel( tokenManager: TokenManager ) : BaseAuthViewModel(repository, tokenManager, SignInState()) { - private val _uiEvent = Channel() + private val _uiEvent = Channel(Channel.BUFFERED) val uiEvent = _uiEvent.receiveAsFlow() override fun updateLoading(isLoading: Boolean) { @@ -93,49 +93,49 @@ class SignInViewModel( viewModelScope.launch { val currentState = _state.value - // 1. Show loading _state.update { it.copy(isLoading = true) } + try { + val request = LoginRequest( + email = currentState.email, + password = currentState.password + ) - // 2. Prepare DTO - val request = LoginRequest( - email = currentState.email, - password = currentState.password - ) - - // 3. Call API via Repository - val result = repository.login(request) - - // 4. Hide loading - _state.update { it.copy(isLoading = false) } - - // 5. Handle Result - result.fold( - onSuccess = { response -> - val token = response.token - val role = JwtUtils.getRoleFromToken(token) - - // Save Token and Role to DataStore - tokenManager.saveAuthInfo(token = token, role = role) - - // Navigate based on Role for standard login - if (role.uppercase() == "ADMIN") { - _uiEvent.send(AuthUiEvent.NavigateToAdminHome) - } else { - _uiEvent.send(AuthUiEvent.NavigateToUserHome) - } - }, - onFailure = { error -> - _state.update { - it.copy( - isLoading = false, - emailError = error.message, - passwordError = error.message, - email = "", - password = "", - ) + val result = repository.login(request) + + result.fold( + onSuccess = { response -> + val token = response.token + val role = JwtUtils.getRoleFromToken(token) + + tokenManager.saveAuthInfo(token = token, role = role) + + if (role.uppercase() == "ADMIN") { + _uiEvent.trySend(AuthUiEvent.NavigateToAdminHome) + } else { + _uiEvent.trySend(AuthUiEvent.NavigateToUserHome) + } + }, + onFailure = { error -> + _state.update { + it.copy( + emailError = error.message, + passwordError = error.message, + email = "", + password = "", + ) + } } + ) + } catch (_: Exception) { + _state.update { + it.copy( + emailError = "Login failed. Please try again.", + passwordError = "Login failed. Please try again." + ) } - ) + } finally { + _state.update { it.copy(isLoading = false) } + } } } } \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt index 38cda292..9003b9a5 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt @@ -1,118 +1,56 @@ package com.example.shoestoreapp.features.invoice.mock import com.example.shoestoreapp.features.invoice.model.Invoice -import com.example.shoestoreapp.features.invoice.model.InvoiceDetail import com.example.shoestoreapp.features.invoice.model.InvoiceStatus -import com.example.shoestoreapp.features.invoice.model.PaymentMethod object InvoiceMockData { fun invoices(): List { - val now = System.currentTimeMillis() - return listOf( Invoice( - id = 1, - publicId = "e7ee9247-6d89-4e2e-9b78-3f89db40a111", - userId = 1, - fullName = "Marcus Alexander", - status = InvoiceStatus.PENDING, - paymentMethod = PaymentMethod.ONLINE, - paymentId = 101, orderCode = "ORDER-12345", - shippingFee = 5.0, - finalPrice = 284.0, - shippingAddress = "12 Nguyen Hue, District 1, HCM", - phone = "0901234567", - createdAtMillis = now - (45L * 60L * 1000L), + userName = "Marcus Alexander", + paymentMethod = "SePay", + status = InvoiceStatus.PENDING, createdAt = "May 24, 2024 - 10:42 AM", - updatedAt = "May 24, 2024 - 10:42 AM", - invoiceDetails = listOf( - InvoiceDetail(11, "detail-11", 1, 301, 1, 120.0), - InvoiceDetail(12, "detail-12", 1, 302, 2, 79.5) - ) + finalPrice = "$284.00", + imageUrl = "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400" ), Invoice( - id = 2, - publicId = "a2d2f4d5-f67f-4c63-b9bd-9ac4fef0a222", - userId = 2, - fullName = "Elena Rodriguez", - status = InvoiceStatus.PAID, - paymentMethod = PaymentMethod.ONLINE, - paymentId = 102, orderCode = "ORDER-12346", - shippingFee = 6.5, - finalPrice = 156.5, - shippingAddress = "88 Le Loi, District 1, HCM", - phone = "0902222333", - createdAtMillis = now - (20L * 60L * 1000L), + userName = "Elena Rodriguez", + paymentMethod = "SePay", + status = InvoiceStatus.PAID, createdAt = "May 23, 2024 - 03:15 PM", - updatedAt = "May 23, 2024 - 03:20 PM", - invoiceDetails = listOf( - InvoiceDetail(21, "detail-21", 2, 305, 1, 150.0) - ) + finalPrice = "$156.50", + imageUrl = "https://images.unsplash.com/photo-1525966222134-fcfa99b8ae77?w=400" ), Invoice( - id = 3, - publicId = "7f95f9e7-1bc1-4b17-9e6f-dc35a52b3333", - userId = 1, - fullName = "James Sterling", - status = InvoiceStatus.DELIVERING, - paymentMethod = PaymentMethod.COD, - paymentId = 103, orderCode = "ORDER-12347", - shippingFee = 10.0, - finalPrice = 420.0, - shippingAddress = "5 Tran Hung Dao, District 5, HCM", - phone = "0918888999", - createdAtMillis = now - (3L * 60L * 60L * 1000L), + userName = "James Sterling", + paymentMethod = "COD", + status = InvoiceStatus.DELIVERING, createdAt = "May 22, 2024 - 09:00 AM", - updatedAt = "May 22, 2024 - 11:00 AM", - invoiceDetails = listOf( - InvoiceDetail(31, "detail-31", 3, 310, 2, 180.0), - InvoiceDetail(32, "detail-32", 3, 311, 1, 50.0) - ) + finalPrice = "$420.00", + imageUrl = "https://images.unsplash.com/photo-1600185365483-26d7a4cc7519?w=400" ), Invoice( - id = 4, - publicId = "53b8f841-39fd-4a8f-bcf8-f9c51a1c4444", - userId = 3, - fullName = "Sarah Chen", - status = InvoiceStatus.DELIVERED, - paymentMethod = PaymentMethod.COD, - paymentId = 104, orderCode = "ORDER-12348", - shippingFee = 0.0, - finalPrice = 89.0, - shippingAddress = "17 Pasteur, District 3, HCM", - phone = "0981111222", - createdAtMillis = now - (8L * 60L * 60L * 1000L), + userName = "Sarah Chen", + paymentMethod = "COD", + status = InvoiceStatus.DELIVERED, createdAt = "May 20, 2024 - 11:30 PM", - updatedAt = "May 21, 2024 - 07:20 AM", - invoiceDetails = listOf( - InvoiceDetail(41, "detail-41", 4, 318, 1, 89.0) - ) + finalPrice = "$89.00", + imageUrl = "https://images.unsplash.com/photo-1460353581641-37baddab0fa2?w=400" ), Invoice( - id = 5, - publicId = "8e2d4c68-35ce-41f7-bfe3-8842c6305555", - userId = 1, - fullName = "Oliver Brown", - status = InvoiceStatus.CANCELED, - paymentMethod = PaymentMethod.ONLINE, - paymentId = 105, orderCode = "ORDER-12349", - shippingFee = 0.0, - finalPrice = 132.0, - shippingAddress = "26 Nguyen Trai, District 1, HCM", - phone = "0903444555", - createdAtMillis = now - (26L * 60L * 60L * 1000L), + userName = "Oliver Brown", + paymentMethod = "SePay", + status = InvoiceStatus.CANCELED, createdAt = "May 19, 2024 - 08:10 PM", - updatedAt = "May 19, 2024 - 08:45 PM", - invoiceDetails = listOf( - InvoiceDetail(51, "detail-51", 5, 325, 1, 132.0) - ) + finalPrice = "$132.00", + imageUrl = "https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=400" ) ) } } - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt index 40650e84..35dd0d61 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt @@ -1,69 +1,49 @@ package com.example.shoestoreapp.features.invoice.model -enum class InvoiceStatus { - PENDING, - PAID, - DELIVERING, - DELIVERED, - CANCELED +enum class InvoiceStatus(val id: Int) { + PENDING(1), + PAID(2), + CANCELED(3), + DELIVERING(4), + DELIVERED(5) } -enum class PaymentMethod { - ONLINE, - COD -} - -data class InvoiceDetail( - val id: Int, - val publicId: String, - val invoiceId: Int, - val productVariantId: Int, - val quantity: Int, - val unitPrice: Double -) - data class Invoice( - val id: Int, - val publicId: String, - val userId: Int, - val fullName: String, - val status: InvoiceStatus, - val paymentMethod: PaymentMethod, - val paymentId: Int, val orderCode: String, - val shippingFee: Double, - val finalPrice: Double, - val shippingAddress: String, - val phone: String, - val createdAtMillis: Long, + val userName: String, + val paymentMethod: String, + val status: InvoiceStatus, val createdAt: String, - val updatedAt: String, - val invoiceDetails: List + val finalPrice: String, + val imageUrl: String ) -private const val ONLINE_PAYMENT_TIMEOUT_MILLIS = 30L * 60L * 1000L - -fun Invoice.shouldAutoCancel(nowMillis: Long = System.currentTimeMillis()): Boolean { - return paymentMethod == PaymentMethod.ONLINE && - status == InvoiceStatus.PENDING && - nowMillis - createdAtMillis >= ONLINE_PAYMENT_TIMEOUT_MILLIS +fun InvoiceStatus.displayName(): String { + return when (this) { + InvoiceStatus.PENDING -> "Pending" + InvoiceStatus.PAID -> "Paid" + InvoiceStatus.CANCELED -> "Canceled" + InvoiceStatus.DELIVERING -> "Delivering" + InvoiceStatus.DELIVERED -> "Delivered" + } } fun Invoice.nextWorkflowStatus(): InvoiceStatus? { - return when (paymentMethod) { - PaymentMethod.ONLINE -> when (status) { + return when (paymentMethod.uppercase()) { + "SEPAY" -> when (status) { InvoiceStatus.PENDING -> InvoiceStatus.PAID InvoiceStatus.PAID -> InvoiceStatus.DELIVERING InvoiceStatus.DELIVERING -> InvoiceStatus.DELIVERED InvoiceStatus.DELIVERED, InvoiceStatus.CANCELED -> null } - PaymentMethod.COD -> when (status) { + "COD" -> when (status) { InvoiceStatus.PENDING -> InvoiceStatus.DELIVERING InvoiceStatus.DELIVERING -> InvoiceStatus.PAID InvoiceStatus.PAID -> InvoiceStatus.DELIVERED InvoiceStatus.DELIVERED, InvoiceStatus.CANCELED -> null } + + else -> null } } - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt new file mode 100644 index 00000000..1f84d410 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt @@ -0,0 +1,212 @@ +package com.example.shoestoreapp.features.invoice.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.model.displayName + +@Composable +fun AdminOrderCard( + invoice: Invoice, + statusOptions: List, + onStatusSelected: (InvoiceStatus) -> Unit, + onDetailsClick: () -> Unit +) { + var isStatusMenuExpanded by remember { mutableStateOf(false) } + + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, Color(0xFFE5E5E5)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Top + ) { + Column( + modifier = Modifier.width(100.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OrderImage(imageUrl = invoice.imageUrl, size = 100.dp) + + OutlinedButton( + onClick = onDetailsClick, + modifier = Modifier + .fillMaxWidth() + .height(38.dp), + border = BorderStroke(1.dp, Color(0xFF1F1F1F)), + shape = RoundedCornerShape(8.dp) + ) { + Text(text = "Details", color = Color.Black) + } + } + + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = invoice.orderCode, + color = Color(0xFF8C8C8C), + fontSize = 11.sp, + letterSpacing = 0.8.sp, + fontWeight = FontWeight.SemiBold + ) + PaymentMethodBadge(paymentMethod = invoice.paymentMethod) + } + + Spacer(modifier = Modifier.height(6.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = invoice.userName, + color = Color.Black, + fontSize = 17.sp, + fontWeight = FontWeight.Bold + ) + StatusBadge(status = invoice.status) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = invoice.createdAt, + color = Color(0xFF6D6D6D), + fontSize = 12.sp + ) + Text( + text = invoice.finalPrice, + color = Color.Black, + fontSize = 20.sp, + fontWeight = FontWeight.ExtraBold + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + Box { + Button( + onClick = { isStatusMenuExpanded = true }, + enabled = statusOptions.isNotEmpty(), + modifier = Modifier + .fillMaxWidth() + .height(42.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(text = "Update Status") + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "Update status", + modifier = Modifier.padding(start = 4.dp) + ) + } + } + + DropdownMenu( + expanded = isStatusMenuExpanded, + onDismissRequest = { isStatusMenuExpanded = false } + ) { + statusOptions.forEach { targetStatus -> + DropdownMenuItem( + text = { Text(text = targetStatus.displayName()) }, + onClick = { + isStatusMenuExpanded = false + onStatusSelected(targetStatus) + } + ) + } + } + } + } + } + } +} + +@Composable +private fun OrderImage(imageUrl: String, size: androidx.compose.ui.unit.Dp) { + Box( + modifier = Modifier + .size(size) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFF0F0F0)) + ) { + AsyncImage( + model = imageUrl, + contentDescription = "Order image", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } +} + +@Composable +private fun PaymentMethodBadge(paymentMethod: String) { + Text( + text = paymentMethod, + modifier = Modifier.padding(horizontal = 2.dp, vertical = 2.dp), + color = Color(0xFF777777), + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/StatusBadge.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/StatusBadge.kt new file mode 100644 index 00000000..f5423e12 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/StatusBadge.kt @@ -0,0 +1,36 @@ +package com.example.shoestoreapp.features.invoice.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus +import com.example.shoestoreapp.features.invoice.model.displayName + +@Composable +fun StatusBadge(status: InvoiceStatus, modifier: Modifier = Modifier) { + val (bg, fg) = when (status) { + InvoiceStatus.PENDING -> Color(0xFFFFF8E1) to Color(0xFF9A6700) + InvoiceStatus.PAID -> Color(0xFFE8F2FF) to Color(0xFF1F5FAE) + InvoiceStatus.CANCELED -> Color(0xFFFFECEB) to Color(0xFFB3261E) + InvoiceStatus.DELIVERING -> Color(0xFFEDEBFF) to Color(0xFF4C2A9B) + InvoiceStatus.DELIVERED -> Color(0xFFEAF7EE) to Color(0xFF1E7D32) + } + + Text( + text = status.displayName(), + modifier = modifier + .background(color = bg, shape = RoundedCornerShape(999.dp)) + .padding(horizontal = 10.dp, vertical = 4.dp), + color = fg, + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt new file mode 100644 index 00000000..5cd872a1 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt @@ -0,0 +1,135 @@ +package com.example.shoestoreapp.features.invoice.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import com.example.shoestoreapp.features.invoice.model.Invoice + +@Composable +fun UserOrderCard( + invoice: Invoice, + onDetailsClick: () -> Unit +) { + Card( + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, Color(0xFFE5E5E5)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + UserOrderImage(imageUrl = invoice.imageUrl) + + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = invoice.orderCode, + color = Color(0xFF757575), + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold + ) + StatusBadge(status = invoice.status) + } + + Spacer(modifier = Modifier.size(4.dp)) + + Text( + text = invoice.paymentMethod, + color = Color(0xFF666666), + fontSize = 12.sp + ) + + Spacer(modifier = Modifier.size(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = invoice.createdAt, + color = Color(0xFF6B6B6B), + fontSize = 12.sp + ) + Text( + text = invoice.finalPrice, + color = Color.Black, + fontSize = 18.sp, + fontWeight = FontWeight.ExtraBold + ) + } + + Button( + onClick = onDetailsClick, + modifier = Modifier.height(34.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp) + ) { + Text( + text = "View Details", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold + ) + } + } + } + } + } +} + +@Composable +private fun UserOrderImage(imageUrl: String) { + Box( + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFF0F0F0)) + ) { + AsyncImage( + model = imageUrl, + contentDescription = "Order image", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt index 098f0ab0..869729ea 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt @@ -4,8 +4,7 @@ import com.example.shoestoreapp.features.invoice.mock.InvoiceMockData import com.example.shoestoreapp.features.invoice.model.Invoice class UserInvoiceMockRepository { - fun getInvoicesByUser(userId: Int): List { - return InvoiceMockData.invoices().filter { it.userId == userId } + fun getInvoicesByUserName(userName: String): List { + return InvoiceMockData.invoices().filter { it.userName.equals(userName, ignoreCase = true) } } } - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt index 1863dea9..1c8e9067 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt @@ -1,37 +1,30 @@ package com.example.shoestoreapp.features.user.invoice.ui import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -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.unit.dp import androidx.compose.ui.unit.sp -import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus -import com.example.shoestoreapp.features.invoice.model.PaymentMethod import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipColors import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipDimensions import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipStyle import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipTypography import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChips +import com.example.shoestoreapp.features.invoice.ui.components.UserOrderCard import com.example.shoestoreapp.features.user.invoice.viewmodel.UserInvoiceViewModel import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab @@ -47,7 +40,7 @@ fun UserInvoiceScreen( Scaffold( bottomBar = { BottomNavBar( - selectedTab = BottomNavTab.BAG, + selectedTab = BottomNavTab.PROFILE, onTabSelected = onTabSelected ) } @@ -79,7 +72,10 @@ fun UserInvoiceScreen( verticalArrangement = Arrangement.spacedBy(10.dp) ) { items(invoices) { invoice -> - UserInvoiceCard(invoice = invoice) + UserOrderCard( + invoice = invoice, + onDetailsClick = {} + ) } } } @@ -111,69 +107,3 @@ private fun UserInvoiceFilterRow( ) ) } - -@Composable -private fun UserInvoiceCard(invoice: Invoice) { - Card( - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFE5E5E5)) - ) { - Column(modifier = Modifier.padding(14.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top - ) { - Column { - Text( - text = invoice.orderCode, - fontWeight = FontWeight.Bold, - fontSize = 12.sp, - color = Color(0xFF9D9D9D) - ) - Text( - text = invoice.createdAt, - fontSize = 12.sp, - color = Color(0xFF6A6A6A) - ) - Text( - text = if (invoice.paymentMethod == PaymentMethod.ONLINE) "ONLINE" else "COD", - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - color = Color(0xFF777777) - ) - } - Text( - text = "$${"%.2f".format(invoice.finalPrice)}", - color = Color.Black, - fontWeight = FontWeight.Black, - fontSize = 18.sp - ) - } - - Text( - text = invoice.shippingAddress, - modifier = Modifier.padding(top = 8.dp), - color = Color(0xFF555555), - fontSize = 12.sp - ) - - Text( - text = "Status: ${invoice.status.name}", - modifier = Modifier.padding(top = 6.dp), - color = when (invoice.status) { - InvoiceStatus.PENDING -> Color(0xFF666666) - InvoiceStatus.PAID -> Color(0xFF1F5FAE) - InvoiceStatus.DELIVERING -> Color.Black - InvoiceStatus.DELIVERED -> Color(0xFF1E7D32) - InvoiceStatus.CANCELED -> Color(0xFFB3261E) - }, - fontWeight = FontWeight.Bold, - fontSize = 12.sp - ) - } - } -} - - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt index 65f93ad5..8da1b001 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/viewmodel/UserInvoiceViewModel.kt @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.asStateFlow class UserInvoiceViewModel( private val repository: UserInvoiceMockRepository = UserInvoiceMockRepository(), - userId: Int = 1 + userName: String = "Marcus Alexander" ) : ViewModel() { - private val userInvoices = repository.getInvoicesByUser(userId) + private val userInvoices = repository.getInvoicesByUserName(userName) private val _selectedStatus = MutableStateFlow(null) private val _visibleInvoices = MutableStateFlow(userInvoices) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/components/BottomNavBar.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/components/BottomNavBar.kt index 3db6a4e8..2020a3a9 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/components/BottomNavBar.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/product/ui/components/BottomNavBar.kt @@ -11,7 +11,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.ShoppingBag import androidx.compose.material.icons.filled.Storefront import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -25,9 +24,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp /** - * BottomNavBar: Thanh điều hướng phía dưới - * @param selectedTab - Tab hiện tại được chọn - * @param onTabSelected - Callback khi chọn tab khác + * Bottom navigation bar. + * @param selectedTab currently selected tab. + * @param onTabSelected callback when a tab is selected. */ @Composable fun BottomNavBar( @@ -63,12 +62,6 @@ fun BottomNavBar( onClick = { onTabSelected(BottomNavTab.FAVORITES) } ) - BottomNavItem( - icon = Icons.Filled.ShoppingBag, - label = "Bag", - isSelected = selectedTab == BottomNavTab.BAG, - onClick = { onTabSelected(BottomNavTab.BAG) } - ) BottomNavItem( icon = Icons.Filled.Person, diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt index 0d40478e..5d35467e 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/profile/ui/UserProfileScreen.kt @@ -1,20 +1,27 @@ package com.example.shoestoreapp.features.user.profile.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.example.shoestoreapp.features.common.ui.components.LogoutActionSection import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab @Composable fun UserProfileScreen( onTabSelected: (BottomNavTab) -> Unit = {}, + onViewOrdersClick: () -> Unit = {}, onLogoutClick: () -> Unit = {} ) { Scaffold( @@ -25,15 +32,46 @@ fun UserProfileScreen( ) } ) { paddingValues -> - LogoutActionSection( - title = "Profile", - description = "Log out to switch account quickly.", - onLogoutClick = onLogoutClick, + Column( modifier = Modifier .fillMaxSize() .background(Color.White) .padding(paddingValues) - .padding(horizontal = 20.dp, vertical = 24.dp) - ) + .padding(horizontal = 20.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Text( + text = "Profile", + color = Color.Black, + fontWeight = FontWeight.Bold + ) + + Text( + text = "Manage your account and orders.", + color = Color(0xFF666666) + ) + + Button( + onClick = onViewOrdersClick, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text(text = "View Orders") + } + + Button( + onClick = onLogoutClick, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ) + ) { + Text(text = "Log out") + } + } } } From 70beb1d2ece17429059194c5f359c3a9272bac46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Wed, 22 Apr 2026 21:33:23 +0700 Subject: [PATCH 08/12] feat(invoice) : create dto , api and mapper data --- .../data/AdminInvoiceMockRepository.kt | 2 +- .../invoice/data/AdminInvoiceRepository.kt | 3 ++ .../admin/invoice/ui/AdminInvoiceScreen.kt | 2 +- .../invoice/ui/components/AdminOrderCard.kt | 28 +++++++++++++------ .../invoice/{mock => data}/InvoiceMockData.kt | 7 +---- .../invoice/data/mapper/InvoiceMapper.kt | 23 +++++++++++++++ .../features/invoice/data/remote/Data.kt | 19 +++++++++++++ .../invoice/data/remote/InvoiceApi.kt | 27 ++++++++++++++++++ .../invoice/data/remote/InvoiceDetailsDto.kt | 11 ++++++++ .../invoice/data/remote/InvoiceListDto.kt | 21 ++++++++++++++ .../features/invoice/data/remote/Item.kt | 27 ++++++++++++++++++ .../data/remote/UpdateStatusRequestDto.kt | 9 ++++++ .../data/remote/UpdateStatusResponseDto.kt | 10 +++++++ .../features/invoice/model/InvoiceModels.kt | 23 +++++++++------ .../invoice/data/UserInvoiceMockRepository.kt | 2 +- .../invoice/data/UserInvoiceRepository.kt | 4 +++ .../user/invoice/ui/UserInvoiceScreen.kt | 2 +- .../invoice/ui/components/UserOrderCard.kt | 25 +++++++++++------ 18 files changed, 209 insertions(+), 36 deletions(-) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceRepository.kt rename Frontend/app/src/main/java/com/example/shoestoreapp/features/{ => admin}/invoice/ui/components/AdminOrderCard.kt (88%) rename Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/{mock => data}/InvoiceMockData.kt (76%) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusRequestDto.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusResponseDto.kt create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceRepository.kt rename Frontend/app/src/main/java/com/example/shoestoreapp/features/{ => user}/invoice/ui/components/UserOrderCard.kt (83%) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt index 8ebd479b..d74555bd 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceMockRepository.kt @@ -1,6 +1,6 @@ package com.example.shoestoreapp.features.admin.invoice.data -import com.example.shoestoreapp.features.invoice.mock.InvoiceMockData +import com.example.shoestoreapp.features.invoice.data.InvoiceMockData import com.example.shoestoreapp.features.invoice.model.Invoice class AdminInvoiceMockRepository { diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceRepository.kt new file mode 100644 index 00000000..558eb978 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/data/AdminInvoiceRepository.kt @@ -0,0 +1,3 @@ +package com.example.shoestoreapp.features.admin.invoice.data + +class AdminInvoiceRepository diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt index e10fe70d..205fffcd 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/AdminInvoiceScreen.kt @@ -36,7 +36,7 @@ import com.example.shoestoreapp.features.admin.invoice.viewmodel.AdminInvoiceVie import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavBar import com.example.shoestoreapp.features.admin.product.ui.components.AdminBottomNavTab import com.example.shoestoreapp.features.invoice.model.displayName -import com.example.shoestoreapp.features.invoice.ui.components.AdminOrderCard +import com.example.shoestoreapp.features.admin.invoice.ui.components.AdminOrderCard import kotlinx.coroutines.launch @Composable diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt similarity index 88% rename from Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt rename to Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt index 1f84d410..fe6cafc3 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/AdminOrderCard.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt @@ -1,4 +1,4 @@ -package com.example.shoestoreapp.features.invoice.ui.components +package com.example.shoestoreapp.features.admin.invoice.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -36,12 +36,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus import com.example.shoestoreapp.features.invoice.model.displayName +import com.example.shoestoreapp.features.invoice.ui.components.StatusBadge @Composable fun AdminOrderCard( @@ -52,6 +54,10 @@ fun AdminOrderCard( ) { var isStatusMenuExpanded by remember { mutableStateOf(false) } + val paymentMethodText = invoice.paymentMethod?.trim().orEmpty().ifEmpty { "-" } + val createdAtText = invoice.createdAt?.trim().orEmpty().ifEmpty { "-" } + val finalPriceText = invoice.finalPrice?.trim().orEmpty().ifEmpty { "-" } + Card( colors = CardDefaults.cardColors(containerColor = Color.White), shape = RoundedCornerShape(8.dp), @@ -68,8 +74,6 @@ fun AdminOrderCard( modifier = Modifier.width(100.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - OrderImage(imageUrl = invoice.imageUrl, size = 100.dp) - OutlinedButton( onClick = onDetailsClick, modifier = Modifier @@ -95,7 +99,7 @@ fun AdminOrderCard( letterSpacing = 0.8.sp, fontWeight = FontWeight.SemiBold ) - PaymentMethodBadge(paymentMethod = invoice.paymentMethod) + PaymentMethodBadge(paymentMethod = paymentMethodText) } Spacer(modifier = Modifier.height(6.dp)) @@ -111,7 +115,14 @@ fun AdminOrderCard( fontSize = 17.sp, fontWeight = FontWeight.Bold ) - StatusBadge(status = invoice.status) + invoice.status?.let { status -> + StatusBadge(status = status) + } ?: Text( + text = "Unknown", + color = Color(0xFF8C8C8C), + fontSize = 12.sp, + fontWeight = FontWeight.Medium + ) } Spacer(modifier = Modifier.height(8.dp)) @@ -122,12 +133,12 @@ fun AdminOrderCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = invoice.createdAt, + text = createdAtText, color = Color(0xFF6D6D6D), fontSize = 12.sp ) Text( - text = invoice.finalPrice, + text = finalPriceText, color = Color.Black, fontSize = 20.sp, fontWeight = FontWeight.ExtraBold @@ -183,7 +194,7 @@ fun AdminOrderCard( } @Composable -private fun OrderImage(imageUrl: String, size: androidx.compose.ui.unit.Dp) { +private fun OrderImage(imageUrl: String, size: Dp) { Box( modifier = Modifier .size(size) @@ -209,4 +220,3 @@ private fun PaymentMethodBadge(paymentMethod: String) { fontWeight = FontWeight.Bold ) } - diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/InvoiceMockData.kt similarity index 76% rename from Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt rename to Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/InvoiceMockData.kt index 9003b9a5..a023c4fd 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/mock/InvoiceMockData.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/InvoiceMockData.kt @@ -1,4 +1,4 @@ -package com.example.shoestoreapp.features.invoice.mock +package com.example.shoestoreapp.features.invoice.data import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus @@ -13,7 +13,6 @@ object InvoiceMockData { status = InvoiceStatus.PENDING, createdAt = "May 24, 2024 - 10:42 AM", finalPrice = "$284.00", - imageUrl = "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400" ), Invoice( orderCode = "ORDER-12346", @@ -22,7 +21,6 @@ object InvoiceMockData { status = InvoiceStatus.PAID, createdAt = "May 23, 2024 - 03:15 PM", finalPrice = "$156.50", - imageUrl = "https://images.unsplash.com/photo-1525966222134-fcfa99b8ae77?w=400" ), Invoice( orderCode = "ORDER-12347", @@ -31,7 +29,6 @@ object InvoiceMockData { status = InvoiceStatus.DELIVERING, createdAt = "May 22, 2024 - 09:00 AM", finalPrice = "$420.00", - imageUrl = "https://images.unsplash.com/photo-1600185365483-26d7a4cc7519?w=400" ), Invoice( orderCode = "ORDER-12348", @@ -40,7 +37,6 @@ object InvoiceMockData { status = InvoiceStatus.DELIVERED, createdAt = "May 20, 2024 - 11:30 PM", finalPrice = "$89.00", - imageUrl = "https://images.unsplash.com/photo-1460353581641-37baddab0fa2?w=400" ), Invoice( orderCode = "ORDER-12349", @@ -49,7 +45,6 @@ object InvoiceMockData { status = InvoiceStatus.CANCELED, createdAt = "May 19, 2024 - 08:10 PM", finalPrice = "$132.00", - imageUrl = "https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?w=400" ) ) } diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt new file mode 100644 index 00000000..d4d5b6fd --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt @@ -0,0 +1,23 @@ +package com.example.shoestoreapp.features.invoice.data.mapper + +import com.example.shoestoreapp.features.invoice.data.remote.Item +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus + +fun Item.toDomain() : Invoice { + return Invoice( + orderCode = this.orderCode ?: "", + userName = this.username ?: "Empty", + paymentMethod = this.paymentName, + status = when (this.status){ + 1 -> InvoiceStatus.PENDING + 2 -> InvoiceStatus.PAID + 3 -> InvoiceStatus.CANCELED + 4 -> InvoiceStatus.DELIVERING + 5 -> InvoiceStatus.DELIVERED + else -> InvoiceStatus.PENDING + }, + createdAt = this.dateCreated, + finalPrice = this.finalPrice + ) +} \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt new file mode 100644 index 00000000..fbc19036 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt @@ -0,0 +1,19 @@ +package com.example.shoestoreapp.features.invoice.data.remote + + +import com.google.gson.annotations.SerializedName + +data class Data( + @SerializedName("color") + val color: String?, + @SerializedName("imageUrl") + val imageUrl: String?, + @SerializedName("productName") + val productName: String?, + @SerializedName("quantity") + val quantity: Int?, + @SerializedName("size") + val size: Int?, + @SerializedName("unitPrice") + val unitPrice: Int? +) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt new file mode 100644 index 00000000..07dd5ee2 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt @@ -0,0 +1,27 @@ +package com.example.shoestoreapp.features.invoice.data.remote + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface InvoiceApi { + @GET ("api/invoice/admin/get-all") + suspend fun getallInvoices( + @Query("PageNumber") pageNumber: Int = 1, + @Query("PageSize") pageSize: Int = 10 + ): Response + + @GET ("api/invoice/user/{invoiceGuid}/details") + suspend fun getInvoiceDetails( + @Path ("invoiceGuid") invoiceGuid: String, + ): Response + + @PUT("api/invoice/user/{invoiceGuid}/status") + suspend fun updateInvoiceStatus( + @Path ("invoiceGuid") invoiceGuid: String, + @Body request: UpdateStatusRequestDto + ): Response +} \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt new file mode 100644 index 00000000..c176c561 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt @@ -0,0 +1,11 @@ +package com.example.shoestoreapp.features.invoice.data.remote + + +import com.google.gson.annotations.SerializedName + +data class InvoiceDetailsDto( + @SerializedName("data") + val `data`: List?, + @SerializedName("message") + val message: String? +) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt new file mode 100644 index 00000000..73f14039 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt @@ -0,0 +1,21 @@ +package com.example.shoestoreapp.features.invoice.data.remote + + +import com.google.gson.annotations.SerializedName + +data class InvoiceListDto( + @SerializedName("hasNext") + val hasNext: Boolean?, + @SerializedName("hasPrevious") + val hasPrevious: Boolean?, + @SerializedName("items") + val item: List?, + @SerializedName("pageNumber") + val pageNumber: String?, + @SerializedName("pageSize") + val pageSize: String?, + @SerializedName("totalCount") + val totalCount: String?, + @SerializedName("totalPages") + val totalPages: String? +) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt new file mode 100644 index 00000000..3e8bf76f --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt @@ -0,0 +1,27 @@ +package com.example.shoestoreapp.features.invoice.data.remote + + +import com.google.gson.annotations.SerializedName + +data class Item( + @SerializedName("address") + val address: String?, + @SerializedName("dateCreated") + val dateCreated: String?, + @SerializedName("finalPrice") + val finalPrice: String?, + @SerializedName("orderCode") + val orderCode: String?, + @SerializedName("paymentName") + val paymentName: String?, + @SerializedName("phone") + val phone: String?, + @SerializedName("publicId") + val publicId: String?, + @SerializedName("status") + val status: Int?, + @SerializedName("updateCreated") + val updateCreated: Any?, + @SerializedName("username") + val username: String? +) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusRequestDto.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusRequestDto.kt new file mode 100644 index 00000000..d6f44198 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusRequestDto.kt @@ -0,0 +1,9 @@ +package com.example.shoestoreapp.features.invoice.data.remote + + +import com.google.gson.annotations.SerializedName + +data class UpdateStatusRequestDto( + @SerializedName("status") + val status: Int? +) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusResponseDto.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusResponseDto.kt new file mode 100644 index 00000000..2f4e24dd --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/UpdateStatusResponseDto.kt @@ -0,0 +1,10 @@ +package com.example.shoestoreapp.features.invoice.data.remote + +import com.google.gson.annotations.SerializedName + +data class UpdateStatusResponseDto ( + @SerializedName("invoiceCode") + val status: String, + @SerializedName ("invoiceStatus") + val invoiceStatus: Int +) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt index 35dd0d61..53420476 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt @@ -1,6 +1,6 @@ package com.example.shoestoreapp.features.invoice.model -enum class InvoiceStatus(val id: Int) { +enum class InvoiceStatus(val id: Int?) { PENDING(1), PAID(2), CANCELED(3), @@ -11,11 +11,10 @@ enum class InvoiceStatus(val id: Int) { data class Invoice( val orderCode: String, val userName: String, - val paymentMethod: String, - val status: InvoiceStatus, - val createdAt: String, - val finalPrice: String, - val imageUrl: String + val paymentMethod: String?, + val status: InvoiceStatus?, + val createdAt: String?, + val finalPrice: String?, ) fun InvoiceStatus.displayName(): String { @@ -29,15 +28,16 @@ fun InvoiceStatus.displayName(): String { } fun Invoice.nextWorkflowStatus(): InvoiceStatus? { - return when (paymentMethod.uppercase()) { - "SEPAY" -> when (status) { + val currentStatus = status ?: return null + return when (paymentMethod?.trim()?.uppercase()) { + "SEPAY" -> when (currentStatus) { InvoiceStatus.PENDING -> InvoiceStatus.PAID InvoiceStatus.PAID -> InvoiceStatus.DELIVERING InvoiceStatus.DELIVERING -> InvoiceStatus.DELIVERED InvoiceStatus.DELIVERED, InvoiceStatus.CANCELED -> null } - "COD" -> when (status) { + "COD" -> when (currentStatus) { InvoiceStatus.PENDING -> InvoiceStatus.DELIVERING InvoiceStatus.DELIVERING -> InvoiceStatus.PAID InvoiceStatus.PAID -> InvoiceStatus.DELIVERED @@ -47,3 +47,8 @@ fun Invoice.nextWorkflowStatus(): InvoiceStatus? { else -> null } } + +fun Int?.toInvoiceStatusOrNull(): InvoiceStatus? { + val rawValue = this ?: return null + return InvoiceStatus.entries.firstOrNull { it.id == rawValue } +} diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt index 869729ea..cb32abd8 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceMockRepository.kt @@ -1,6 +1,6 @@ package com.example.shoestoreapp.features.user.invoice.data -import com.example.shoestoreapp.features.invoice.mock.InvoiceMockData +import com.example.shoestoreapp.features.invoice.data.InvoiceMockData import com.example.shoestoreapp.features.invoice.model.Invoice class UserInvoiceMockRepository { diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceRepository.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceRepository.kt new file mode 100644 index 00000000..5483d832 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/data/UserInvoiceRepository.kt @@ -0,0 +1,4 @@ +package com.example.shoestoreapp.features.user.invoice.data + +class UserInvoiceRepository { +} \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt index 1c8e9067..dcd4fbed 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/UserInvoiceScreen.kt @@ -24,7 +24,7 @@ import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilt import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipStyle import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChipTypography import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusFilterChips -import com.example.shoestoreapp.features.invoice.ui.components.UserOrderCard +import com.example.shoestoreapp.features.user.invoice.ui.components.UserOrderCard import com.example.shoestoreapp.features.user.invoice.viewmodel.UserInvoiceViewModel import com.example.shoestoreapp.features.user.product.ui.components.BottomNavBar import com.example.shoestoreapp.features.user.product.ui.components.BottomNavTab diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt similarity index 83% rename from Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt rename to Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt index 5cd872a1..75ebd667 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/UserOrderCard.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt @@ -1,4 +1,4 @@ -package com.example.shoestoreapp.features.invoice.ui.components +package com.example.shoestoreapp.features.user.invoice.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -30,12 +30,17 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.ui.components.StatusBadge @Composable fun UserOrderCard( invoice: Invoice, onDetailsClick: () -> Unit ) { + val paymentMethodText = invoice.paymentMethod?.trim().orEmpty().ifEmpty { "-" } + val createdAtText = invoice.createdAt?.trim().orEmpty().ifEmpty { "-" } + val finalPriceText = invoice.finalPrice?.trim().orEmpty().ifEmpty { "-" } + Card( colors = CardDefaults.cardColors(containerColor = Color.White), shape = RoundedCornerShape(8.dp), @@ -48,8 +53,6 @@ fun UserOrderCard( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { - UserOrderImage(imageUrl = invoice.imageUrl) - Column(modifier = Modifier.weight(1f)) { Row( modifier = Modifier.fillMaxWidth(), @@ -62,13 +65,20 @@ fun UserOrderCard( fontSize = 12.sp, fontWeight = FontWeight.SemiBold ) - StatusBadge(status = invoice.status) + invoice.status?.let { status -> + StatusBadge(status = status) + } ?: Text( + text = "Unknown", + color = Color(0xFF8C8C8C), + fontSize = 11.sp, + fontWeight = FontWeight.Medium + ) } Spacer(modifier = Modifier.size(4.dp)) Text( - text = invoice.paymentMethod, + text = paymentMethodText, color = Color(0xFF666666), fontSize = 12.sp ) @@ -82,12 +92,12 @@ fun UserOrderCard( ) { Column { Text( - text = invoice.createdAt, + text = createdAtText, color = Color(0xFF6B6B6B), fontSize = 12.sp ) Text( - text = invoice.finalPrice, + text = finalPriceText, color = Color.Black, fontSize = 18.sp, fontWeight = FontWeight.ExtraBold @@ -132,4 +142,3 @@ private fun UserOrderImage(imageUrl: String) { ) } } - From bf89c381ca395ba7689d7250b687a9c9e829af59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Wed, 22 Apr 2026 21:45:19 +0700 Subject: [PATCH 09/12] fix : duplicated code in AdminOrderCard and UserOrderCard to reduce percent duplication to below 3% --- .../invoice/ui/components/AdminOrderCard.kt | 23 ------------------- .../invoice/ui/components/UserOrderCard.kt | 22 ------------------ 2 files changed, 45 deletions(-) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt index fe6cafc3..72c69a78 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt @@ -1,17 +1,14 @@ package com.example.shoestoreapp.features.admin.invoice.ui.components import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -32,14 +29,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus import com.example.shoestoreapp.features.invoice.model.displayName @@ -193,22 +186,6 @@ fun AdminOrderCard( } } -@Composable -private fun OrderImage(imageUrl: String, size: Dp) { - Box( - modifier = Modifier - .size(size) - .clip(RoundedCornerShape(8.dp)) - .background(Color(0xFFF0F0F0)) - ) { - AsyncImage( - model = imageUrl, - contentDescription = "Order image", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } -} @Composable private fun PaymentMethodBadge(paymentMethod: String) { diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt index 75ebd667..d1e18bc2 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt @@ -1,14 +1,11 @@ package com.example.shoestoreapp.features.user.invoice.ui.components import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -22,13 +19,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.ui.components.StatusBadge @@ -126,19 +120,3 @@ fun UserOrderCard( } } -@Composable -private fun UserOrderImage(imageUrl: String) { - Box( - modifier = Modifier - .size(80.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color(0xFFF0F0F0)) - ) { - AsyncImage( - model = imageUrl, - contentDescription = "Order image", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } -} From d49a2f951ef23f74e53bb63b636c8e2feb1511d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Wed, 22 Apr 2026 21:57:40 +0700 Subject: [PATCH 10/12] fix : duplicated code in AdminOrderCard and UserOrderCard to reduce percent duplication to below 3% --- .../invoice/ui/components/AdminOrderCard.kt | 27 +++------- .../ui/components/InvoiceCardCommon.kt | 50 +++++++++++++++++++ .../invoice/ui/components/UserOrderCard.kt | 28 +++-------- 3 files changed, 66 insertions(+), 39 deletions(-) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceCardCommon.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt index 72c69a78..af9607fd 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/admin/invoice/ui/components/AdminOrderCard.kt @@ -15,8 +15,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -36,7 +34,9 @@ import androidx.compose.ui.unit.sp import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus import com.example.shoestoreapp.features.invoice.model.displayName -import com.example.shoestoreapp.features.invoice.ui.components.StatusBadge +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceCardContainer +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusOrUnknown +import com.example.shoestoreapp.features.invoice.ui.components.invoiceTextOrDash @Composable fun AdminOrderCard( @@ -47,15 +47,11 @@ fun AdminOrderCard( ) { var isStatusMenuExpanded by remember { mutableStateOf(false) } - val paymentMethodText = invoice.paymentMethod?.trim().orEmpty().ifEmpty { "-" } - val createdAtText = invoice.createdAt?.trim().orEmpty().ifEmpty { "-" } - val finalPriceText = invoice.finalPrice?.trim().orEmpty().ifEmpty { "-" } + val paymentMethodText = invoiceTextOrDash(invoice.paymentMethod) + val createdAtText = invoiceTextOrDash(invoice.createdAt) + val finalPriceText = invoiceTextOrDash(invoice.finalPrice) - Card( - colors = CardDefaults.cardColors(containerColor = Color.White), - shape = RoundedCornerShape(8.dp), - border = BorderStroke(1.dp, Color(0xFFE5E5E5)) - ) { + InvoiceCardContainer { Row( modifier = Modifier .fillMaxWidth() @@ -108,14 +104,7 @@ fun AdminOrderCard( fontSize = 17.sp, fontWeight = FontWeight.Bold ) - invoice.status?.let { status -> - StatusBadge(status = status) - } ?: Text( - text = "Unknown", - color = Color(0xFF8C8C8C), - fontSize = 12.sp, - fontWeight = FontWeight.Medium - ) + InvoiceStatusOrUnknown(status = invoice.status) } Spacer(modifier = Modifier.height(8.dp)) diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceCardCommon.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceCardCommon.kt new file mode 100644 index 00000000..0aed0f61 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/ui/components/InvoiceCardCommon.kt @@ -0,0 +1,50 @@ +package com.example.shoestoreapp.features.invoice.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.shoestoreapp.features.invoice.model.InvoiceStatus + +@Composable +fun InvoiceCardContainer( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = Color.White), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(1.dp, Color(0xFFE5E5E5)), + content = content + ) +} + +fun invoiceTextOrDash(value: String?): String { + return value?.trim().orEmpty().ifEmpty { "-" } +} + +@Composable +fun InvoiceStatusOrUnknown( + status: InvoiceStatus?, + unknownFontSize: TextUnit = 12.sp +) { + status?.let { + StatusBadge(status = it) + } ?: Text( + text = "Unknown", + color = Color(0xFF8C8C8C), + fontSize = unknownFontSize, + fontWeight = FontWeight.Medium + ) +} + diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt index d1e18bc2..a70f1395 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/user/invoice/ui/components/UserOrderCard.kt @@ -1,6 +1,5 @@ package com.example.shoestoreapp.features.user.invoice.ui.components -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -13,8 +12,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -24,22 +21,20 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.shoestoreapp.features.invoice.model.Invoice -import com.example.shoestoreapp.features.invoice.ui.components.StatusBadge +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceCardContainer +import com.example.shoestoreapp.features.invoice.ui.components.InvoiceStatusOrUnknown +import com.example.shoestoreapp.features.invoice.ui.components.invoiceTextOrDash @Composable fun UserOrderCard( invoice: Invoice, onDetailsClick: () -> Unit ) { - val paymentMethodText = invoice.paymentMethod?.trim().orEmpty().ifEmpty { "-" } - val createdAtText = invoice.createdAt?.trim().orEmpty().ifEmpty { "-" } - val finalPriceText = invoice.finalPrice?.trim().orEmpty().ifEmpty { "-" } + val paymentMethodText = invoiceTextOrDash(invoice.paymentMethod) + val createdAtText = invoiceTextOrDash(invoice.createdAt) + val finalPriceText = invoiceTextOrDash(invoice.finalPrice) - Card( - colors = CardDefaults.cardColors(containerColor = Color.White), - shape = RoundedCornerShape(8.dp), - border = BorderStroke(1.dp, Color(0xFFE5E5E5)) - ) { + InvoiceCardContainer { Row( modifier = Modifier .fillMaxWidth() @@ -59,14 +54,7 @@ fun UserOrderCard( fontSize = 12.sp, fontWeight = FontWeight.SemiBold ) - invoice.status?.let { status -> - StatusBadge(status = status) - } ?: Text( - text = "Unknown", - color = Color(0xFF8C8C8C), - fontSize = 11.sp, - fontWeight = FontWeight.Medium - ) + InvoiceStatusOrUnknown(status = invoice.status, unknownFontSize = 11.sp) } Spacer(modifier = Modifier.size(4.dp)) From b94598fef8d78dd2ee285a2a71ceef0c67c4f318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Thu, 23 Apr 2026 09:48:06 +0700 Subject: [PATCH 11/12] feat(Invoice) : impliment repository of invoice and fix some dto to match --- .../invoice/data/mapper/InvoiceMapper.kt | 17 +++++++-- .../data/remote/{Data.kt => DetailDto.kt} | 2 +- .../invoice/data/remote/InvoiceApi.kt | 2 +- .../invoice/data/remote/InvoiceDetailsDto.kt | 2 +- .../invoice/data/remote/InvoiceListDto.kt | 2 +- .../data/remote/{Item.kt => ItemDto.kt} | 2 +- .../repositories/InvoiceRepositoryImpl.kt | 36 +++++++++++++++++++ .../features/invoice/model/InvoiceModels.kt | 9 +++++ 8 files changed, 65 insertions(+), 7 deletions(-) rename Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/{Data.kt => DetailDto.kt} (95%) rename Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/{Item.kt => ItemDto.kt} (97%) create mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/repositories/InvoiceRepositoryImpl.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt index d4d5b6fd..7fabfb4b 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/mapper/InvoiceMapper.kt @@ -1,10 +1,12 @@ package com.example.shoestoreapp.features.invoice.data.mapper -import com.example.shoestoreapp.features.invoice.data.remote.Item +import com.example.shoestoreapp.features.invoice.data.remote.DetailDto +import com.example.shoestoreapp.features.invoice.data.remote.ItemDto +import com.example.shoestoreapp.features.invoice.model.Detail import com.example.shoestoreapp.features.invoice.model.Invoice import com.example.shoestoreapp.features.invoice.model.InvoiceStatus -fun Item.toDomain() : Invoice { +fun ItemDto.toDomain() : Invoice { return Invoice( orderCode = this.orderCode ?: "", userName = this.username ?: "Empty", @@ -20,4 +22,15 @@ fun Item.toDomain() : Invoice { createdAt = this.dateCreated, finalPrice = this.finalPrice ) +} + +fun DetailDto.toDomain() : Detail { + return Detail( + color = this.color ?: "", + imageUrl = this.imageUrl ?: "", + productName = this.productName ?: "", + quantity = this.quantity ?: 0, + size = this.size ?: 0, + unitPrice = this.unitPrice ?: 0 + ) } \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/DetailDto.kt similarity index 95% rename from Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt rename to Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/DetailDto.kt index fbc19036..29d570c8 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Data.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/DetailDto.kt @@ -3,7 +3,7 @@ package com.example.shoestoreapp.features.invoice.data.remote import com.google.gson.annotations.SerializedName -data class Data( +data class DetailDto( @SerializedName("color") val color: String?, @SerializedName("imageUrl") diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt index 07dd5ee2..8be71f4a 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceApi.kt @@ -9,7 +9,7 @@ import retrofit2.http.Query interface InvoiceApi { @GET ("api/invoice/admin/get-all") - suspend fun getallInvoices( + suspend fun getAllInvoices( @Query("PageNumber") pageNumber: Int = 1, @Query("PageSize") pageSize: Int = 10 ): Response diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt index c176c561..14ba91c7 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceDetailsDto.kt @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName data class InvoiceDetailsDto( @SerializedName("data") - val `data`: List?, + val detailDto: List?, @SerializedName("message") val message: String? ) \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt index 73f14039..00d892b4 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/InvoiceListDto.kt @@ -9,7 +9,7 @@ data class InvoiceListDto( @SerializedName("hasPrevious") val hasPrevious: Boolean?, @SerializedName("items") - val item: List?, + val itemDto: List?, @SerializedName("pageNumber") val pageNumber: String?, @SerializedName("pageSize") diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/ItemDto.kt similarity index 97% rename from Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt rename to Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/ItemDto.kt index 3e8bf76f..5bb4f6e8 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/ItemDto.kt @@ -3,7 +3,7 @@ package com.example.shoestoreapp.features.invoice.data.remote import com.google.gson.annotations.SerializedName -data class Item( +data class ItemDto( @SerializedName("address") val address: String?, @SerializedName("dateCreated") diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/repositories/InvoiceRepositoryImpl.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/repositories/InvoiceRepositoryImpl.kt new file mode 100644 index 00000000..4a44e5f2 --- /dev/null +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/repositories/InvoiceRepositoryImpl.kt @@ -0,0 +1,36 @@ +package com.example.shoestoreapp.features.invoice.data.repositories + +import com.example.shoestoreapp.features.invoice.data.remote.InvoiceApi +import com.example.shoestoreapp.features.invoice.data.mapper.toDomain +import com.example.shoestoreapp.features.invoice.data.remote.UpdateStatusRequestDto +import com.example.shoestoreapp.features.invoice.model.Invoice +import com.example.shoestoreapp.features.invoice.model.Detail +class InvoiceRepositoryImpl (private val api : InvoiceApi) { + suspend fun getAllInvoices(): Result> = runCatching { + val response = api.getAllInvoices(pageNumber = 1, pageSize = 10) + if (response.isSuccessful) { + val dtos = response.body()?.itemDto ?: emptyList() + dtos.map { it.toDomain() } + } else { + throw Exception("Lỗi API: ${response.code()} - ${response.message()}") + } + } + + suspend fun updateInvoiceStatus(invoiceGuid: String, status: Int): Result = runCatching { + val request = UpdateStatusRequestDto(status = status) + val response = api.updateInvoiceStatus(invoiceGuid, request) + if (!response.isSuccessful) { + throw Exception("Lỗi update : ${response.code()} - ${response.message()}") + } + } + + suspend fun getInvoiceDetails(invoiceGuid: String): Result> = runCatching { + val response = api.getInvoiceDetails(invoiceGuid) + if (response.isSuccessful) { + val dtos = response.body()?.detailDto ?: emptyList() + dtos.map { it.toDomain() } + } else { + throw Exception("Lỗi : ${response.code()} - ${response.message()}") + } + } +} \ No newline at end of file diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt index 53420476..a2a48583 100644 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt +++ b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/model/InvoiceModels.kt @@ -16,6 +16,15 @@ data class Invoice( val createdAt: String?, val finalPrice: String?, ) +// Data Details +data class Detail ( + val color: String, + val imageUrl: String, + val productName: String, + val quantity: Int, + val size: Int, + val unitPrice: Int +) fun InvoiceStatus.displayName(): String { return when (this) { From 5cf329086c2ecf7bf0f42462d856cf56f350e7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20L=C3=AA=20Ho=C3=A0ng=20H=E1=BA=A3o?= Date: Thu, 23 Apr 2026 12:10:22 +0700 Subject: [PATCH 12/12] fix : duplicated itemdto --- .../features/invoice/data/remote/Item.kt | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt diff --git a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt b/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt deleted file mode 100644 index 3e8bf76f..00000000 --- a/Frontend/app/src/main/java/com/example/shoestoreapp/features/invoice/data/remote/Item.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.shoestoreapp.features.invoice.data.remote - - -import com.google.gson.annotations.SerializedName - -data class Item( - @SerializedName("address") - val address: String?, - @SerializedName("dateCreated") - val dateCreated: String?, - @SerializedName("finalPrice") - val finalPrice: String?, - @SerializedName("orderCode") - val orderCode: String?, - @SerializedName("paymentName") - val paymentName: String?, - @SerializedName("phone") - val phone: String?, - @SerializedName("publicId") - val publicId: String?, - @SerializedName("status") - val status: Int?, - @SerializedName("updateCreated") - val updateCreated: Any?, - @SerializedName("username") - val username: String? -) \ No newline at end of file