diff --git a/app/src/main/java/com/example/belindas_closet/data/Datasource.kt b/app/src/main/java/com/example/belindas_closet/data/Datasource.kt index bd0d652e..5156a434 100644 --- a/app/src/main/java/com/example/belindas_closet/data/Datasource.kt +++ b/app/src/main/java/com/example/belindas_closet/data/Datasource.kt @@ -3,10 +3,10 @@ package com.example.belindas_closet.data import com.example.belindas_closet.R import com.example.belindas_closet.model.Product import com.example.belindas_closet.model.ProductGender -import com.example.belindas_closet.model.ProductSizes import com.example.belindas_closet.model.ProductSizePantsInseam import com.example.belindas_closet.model.ProductSizePantsWaist import com.example.belindas_closet.model.ProductSizeShoes +import com.example.belindas_closet.model.ProductSizes import com.example.belindas_closet.model.ProductType class Datasource { @@ -140,7 +140,7 @@ class Datasource { ProductSizePantsInseam.XS, "This is a handbag", R.drawable.product12.toString(), - "13" + "655b46baa82ef869be176174" ) ) productList.add( diff --git a/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt b/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt index ada21881..04af3c94 100644 --- a/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt +++ b/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt @@ -4,6 +4,7 @@ object HttpRoutes { private const val BASE_URL = "http://10.0.2.2:3000/api" const val PRODUCTS = "$BASE_URL/products" const val PRODUCT = "$BASE_URL/products/{id}" + const val ARCHIVE = "$BASE_URL/products/archive" const val LOGIN = "$BASE_URL/auth/login" const val FORGOT_PASSWORD = "$BASE_URL/auth/forgot-password" const val SIGNUP = "$BASE_URL/auth/signup" diff --git a/app/src/main/java/com/example/belindas_closet/data/network/auth/ArchiveService.kt b/app/src/main/java/com/example/belindas_closet/data/network/auth/ArchiveService.kt new file mode 100644 index 00000000..8f9d42bf --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/auth/ArchiveService.kt @@ -0,0 +1,42 @@ +package com.example.belindas_closet.data.network.auth + +import com.example.belindas_closet.MainActivity +import com.example.belindas_closet.data.network.dto.auth_dto.ArchiveRequest +import com.example.belindas_closet.data.network.dto.auth_dto.ArchiveResponse +import io.ktor.client.HttpClient +import io.ktor.client.engine.android.Android +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.DEFAULT +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +interface ArchiveService { + suspend fun archive(archiveRequest: ArchiveRequest) : ArchiveResponse? + + companion object { + fun create() : ArchiveService { + return ArchiveServiceImpl( + client = HttpClient(Android) { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } + install (Logging) { + level = LogLevel.ALL + logger = Logger.DEFAULT + } + }, + getToken = suspend { + MainActivity.getPref().getString("token", "") ?: "" + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/data/network/auth/ArchiveServiceImpl.kt b/app/src/main/java/com/example/belindas_closet/data/network/auth/ArchiveServiceImpl.kt new file mode 100644 index 00000000..8e1852a2 --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/auth/ArchiveServiceImpl.kt @@ -0,0 +1,48 @@ +package com.example.belindas_closet.data.network.auth + +import com.example.belindas_closet.data.network.HttpRoutes +import com.example.belindas_closet.data.network.dto.auth_dto.ArchiveRequest +import com.example.belindas_closet.data.network.dto.auth_dto.ArchiveResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.RedirectResponseException +import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.request.header +import io.ktor.client.request.put +import io.ktor.client.request.url +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.util.InternalAPI +import kotlinx.serialization.json.Json + +class ArchiveServiceImpl ( + private val client: HttpClient, + private val getToken: suspend () -> String +) : ArchiveService { + @OptIn(InternalAPI::class) + override suspend fun archive(archiveRequest: ArchiveRequest): ArchiveResponse? { + return try { + val token = getToken() + val response = client.put { + url("${HttpRoutes.ARCHIVE}/${archiveRequest.id}") + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + header(HttpHeaders.Authorization, "Bearer $token") + body = Json.encodeToString(ArchiveRequest.serializer(), archiveRequest) + } + response.body() + } catch (e: RedirectResponseException) { + println("Error: ${e.response.status.description}") + null + } catch (e: ClientRequestException) { + println("Error: ${e.response.status.description}") + null + } catch (e: ServerResponseException) { + println("Error: ${e.response.status.description}") + null + } catch (e: Exception) { + println("Error: ${e.message}") + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/data/network/auth/LoginService.kt b/app/src/main/java/com/example/belindas_closet/data/network/auth/LoginService.kt index 68edb892..da1faefc 100644 --- a/app/src/main/java/com/example/belindas_closet/data/network/auth/LoginService.kt +++ b/app/src/main/java/com/example/belindas_closet/data/network/auth/LoginService.kt @@ -13,6 +13,7 @@ import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json interface LoginService { + suspend fun login(loginRequest: LoginRequest) : LoginResponse? companion object { diff --git a/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/ArchiveRequest.kt b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/ArchiveRequest.kt new file mode 100644 index 00000000..41311d4c --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/ArchiveRequest.kt @@ -0,0 +1,13 @@ +package com.example.belindas_closet.data.network.dto.auth_dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ArchiveRequest( + @SerialName("id") + val id: String, + + @SerialName("role") + val role: Role +) diff --git a/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/ArchiveResponse.kt b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/ArchiveResponse.kt new file mode 100644 index 00000000..7ca25197 --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/ArchiveResponse.kt @@ -0,0 +1,10 @@ +package com.example.belindas_closet.data.network.dto.auth_dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ArchiveResponse( + @SerialName("isSold") + val isSold: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt b/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt index 25ad9bf1..67667174 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt @@ -1,5 +1,8 @@ package com.example.belindas_closet.screen +import android.content.Context +import android.net.http.HttpException +import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -34,25 +37,27 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.navigation.NavController import com.example.belindas_closet.MainActivity import com.example.belindas_closet.R import com.example.belindas_closet.Routes import com.example.belindas_closet.data.Datasource +import com.example.belindas_closet.data.network.auth.ArchiveService +import com.example.belindas_closet.data.network.dto.auth_dto.ArchiveRequest +import com.example.belindas_closet.data.network.dto.auth_dto.Role import com.example.belindas_closet.model.Product import com.example.belindas_closet.model.ProductSizes +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -99,6 +104,8 @@ fun UpdateIndividualProductCard(product: Product, navController: NavController) var isArchive by remember { mutableStateOf(false) } var isSave by remember { mutableStateOf(false) } var isCancel by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val current = LocalContext.current Card( modifier = Modifier @@ -210,8 +217,9 @@ fun UpdateIndividualProductCard(product: Product, navController: NavController) editor.putStringSet("hidden", hidden) editor.apply() navController.navigate(Routes.ProductDetail.route) - // TODO: Add the product to "sold" collection in database - // Remove the product from product page + coroutineScope.launch { + archive(product.id, navController, current) + } isArchive = false }, onDismiss = { isArchive = false @@ -335,7 +343,7 @@ fun ConfirmationArchiveDialogIndividual( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text(stringResource(R.string.update_confirm_confirm_text)) + Text(stringResource(R.string.update_archive_confirm_text)) Spacer(modifier = Modifier.padding(8.dp)) Row( modifier = Modifier @@ -447,4 +455,27 @@ fun ConfirmCancelDialogIndividual( } } } +} + +suspend fun archive(productId: String, navController: NavController, current: Context) { + return try { + val archiveRequest = ArchiveRequest( + id = productId, + role = Role.ADMIN + ) + val archiveResponse = ArchiveService.create().archive(archiveRequest) + if (productId.count() != 24) { + Toast.makeText(current, R.string.archive_invalid_id, Toast.LENGTH_SHORT).show() + } else if (archiveResponse != null) { + MainActivity.getPref().edit().putBoolean("isSold", archiveResponse.isSold).apply() + navController.navigate(Routes.ProductDetail.route) + Toast.makeText(current, R.string.archive_successful_toast, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(current, R.string.archive_failed_toast, Toast.LENGTH_SHORT).show() + } + } catch (e: HttpException) { + e.printStackTrace() + println("Error: ${e.message}") + Toast.makeText(current, "Archive failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/screen/Login.kt b/app/src/main/java/com/example/belindas_closet/screen/Login.kt index b6adc3b2..ed3fbd0d 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/Login.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/Login.kt @@ -301,11 +301,12 @@ fun NSCMascot() { } suspend fun loginWithValidCredentials(email: String, password: String, navController: NavHostController, current: Context) { - // login with valid credentials try { val loginRequest = LoginRequest(email, password) val loginResponse = LoginService.create().login(loginRequest) if (loginResponse != null) { + val token = loginResponse.token + saveToken(token) MainActivity.getPref().edit().putString("token", loginResponse.token).apply() navController.navigate(Routes.AdminView.route) Toast.makeText( @@ -313,6 +314,7 @@ suspend fun loginWithValidCredentials(email: String, password: String, navContro "Welcome ${getName(loginResponse.token)} to Belinda's Closet!", Toast.LENGTH_SHORT ).show() + loginResponse.token } else { Toast.makeText( current, @@ -341,3 +343,7 @@ fun getName(token: String): String? { null } } + +fun saveToken(token: String) { + MainActivity.getPref().edit().putString("token", token).apply() +} diff --git a/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt b/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt index 33749bd3..09eeb3e9 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt @@ -1,23 +1,14 @@ package com.example.belindas_closet.screen -import android.app.Activity import android.content.res.Resources -import android.util.DisplayMetrics -import android.view.Display -import android.view.Window -import android.view.WindowManager -import android.view.WindowMetrics import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -34,29 +25,22 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar 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.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.example.belindas_closet.MainActivity import com.example.belindas_closet.R import com.example.belindas_closet.Routes -import com.example.belindas_closet.model.Product import com.example.belindas_closet.data.Datasource -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.material3.rememberDrawerState -import androidx.compose.ui.text.style.TextOverflow +import com.example.belindas_closet.model.Product @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d0c105dd..98e1889d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,10 +41,13 @@ Are you sure you want to delete this product? - Are you sure you want to archive this product? + Are you sure you want to archive this product? Are you sure you want to save this product? Are you sure you want to cancel this edit? Update Product + Archival successful! + Invalid ID! + Archival failed, please try again! Forgot Password?