diff --git a/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt b/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt index 798b771..e1da791 100644 --- a/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt +++ b/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt @@ -3,8 +3,8 @@ package com.sampoom.android import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.usecase.GetStoredUserUseCase +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt index e8f66bc..44eb2fb 100644 --- a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt +++ b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt @@ -1,9 +1,7 @@ package com.sampoom.android.app.navigation import android.annotation.SuppressLint -import androidx.activity.ComponentActivity import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator @@ -25,11 +23,8 @@ 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.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.core.view.WindowInsetsControllerCompat import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -49,18 +44,19 @@ import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.backgroundColor import com.sampoom.android.core.ui.theme.textColor -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.ui.AuthViewModel import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen import com.sampoom.android.feature.cart.ui.CartListScreen import com.sampoom.android.feature.dashboard.ui.DashboardScreen +import com.sampoom.android.feature.dashboard.ui.SettingScreen import com.sampoom.android.feature.order.ui.OrderDetailScreen import com.sampoom.android.feature.order.ui.OrderListScreen import com.sampoom.android.feature.outbound.ui.OutboundListScreen import com.sampoom.android.feature.part.ui.PartListScreen import com.sampoom.android.feature.part.ui.PartScreen -import com.sampoom.android.feature.setting.ui.SettingScreen +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.ui.EmployeeListScreen import kotlinx.coroutines.flow.filterNotNull // Auth Screen @@ -216,6 +212,13 @@ fun AppNavHost( } ) } + composable(ROUTE_EMPLOYEE) { + EmployeeListScreen( + onNavigateBack = { + navController.navigateUp() + } + ) + } } TopSnackBarHost(hostState = snackBarHostState, isError = currentMessage?.isError ?: false) } @@ -238,6 +241,9 @@ fun MainScreen( composable(ROUTE_DASHBOARD) { DashboardScreen( paddingValues = innerPadding, + onEmployeeClick = { + parentNavController.navigate(ROUTE_EMPLOYEE) + }, onSettingClick = { parentNavController.navigate(ROUTE_SETTINGS) }, diff --git a/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt b/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt index 31dff36..d1bf160 100644 --- a/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt +++ b/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt @@ -3,10 +3,10 @@ package com.sampoom.android.core.di import com.google.gson.FieldNamingPolicy import com.google.gson.GsonBuilder import com.sampoom.android.BuildConfig -import com.sampoom.android.core.preferences.AuthPreferences import com.sampoom.android.core.network.TokenAuthenticator import com.sampoom.android.core.network.TokenInterceptor import com.sampoom.android.core.network.TokenRefreshService +import com.sampoom.android.core.preferences.AuthPreferences import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt b/app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt new file mode 100644 index 0000000..1fb518d --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt @@ -0,0 +1,7 @@ +package com.sampoom.android.core.model + +enum class EmployeeStatus { + ACTIVE, // 재직 + LEAVE, // 휴직 + RETIRED // 퇴직 +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt b/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt index d2db843..1cf693d 100644 --- a/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt +++ b/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt @@ -3,7 +3,7 @@ package com.sampoom.android.core.network import com.sampoom.android.core.preferences.AuthPreferences import com.sampoom.android.feature.auth.data.remote.api.AuthApi import com.sampoom.android.feature.auth.data.remote.dto.RefreshRequestDto -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import javax.inject.Inject diff --git a/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt b/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt index 5b3faeb..e6787fe 100644 --- a/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt +++ b/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt @@ -7,11 +7,11 @@ import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.sampoom.android.core.model.UserPosition -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Singleton -import kotlinx.coroutines.flow.first // Per official guidance, DataStore instance should be single and at top-level. private val Context.authDataStore by preferencesDataStore(name = "auth_prefs") diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt b/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt index ce4ed82..976a1aa 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -18,6 +17,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.theme.Main300 import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.theme.White import com.sampoom.android.core.ui.theme.disableColor @@ -79,17 +79,17 @@ fun CommonButton( } } - // Light/secondary (tonal) filled button + // Light/secondary outlined button with semi-transparent background ButtonVariant.Secondary -> { - FilledTonalButton( + OutlinedButton( onClick = onClick, enabled = enabled, shape = shape, modifier = modifier.height(height), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = Main500, - contentColor = White, - disabledContainerColor = disableColor(), + border = BorderStroke(1.dp, Main500), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Main300.copy(alpha = 0.3f), + contentColor = Main500, disabledContentColor = textSecondaryColor() ) ) { diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt b/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt index 33769b5..baad166 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt @@ -9,8 +9,21 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.darkColorScheme +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.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.ImeAction diff --git a/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt b/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt index bc62309..3dfbdac 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt @@ -4,7 +4,6 @@ import androidx.compose.material3.Typography import androidx.compose.ui.text.font.Font 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 com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt b/app/src/main/java/com/sampoom/android/core/util/AuthValidator.kt similarity index 98% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt rename to app/src/main/java/com/sampoom/android/core/util/AuthValidator.kt index b7c4516..e6019f6 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt +++ b/app/src/main/java/com/sampoom/android/core/util/AuthValidator.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.domain +package com.sampoom.android.core.util import com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt b/app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt new file mode 100644 index 0000000..6b41bc1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.core.util + +import com.sampoom.android.core.model.EmployeeStatus + +fun employeeStatusToKorean(status: EmployeeStatus?): String = when (status) { + EmployeeStatus.ACTIVE -> "재직" + EmployeeStatus.LEAVE -> "휴직" + EmployeeStatus.RETIRED -> "퇴직" + else -> "-" +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt index 206707c..6b130cc 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt @@ -1,11 +1,10 @@ package com.sampoom.android.feature.auth.data.mapper import com.sampoom.android.core.model.UserPosition -import com.sampoom.android.feature.auth.data.remote.dto.GetProfileResponseDto import com.sampoom.android.feature.auth.data.remote.dto.GetVendorsResponseDto import com.sampoom.android.feature.auth.data.remote.dto.LoginResponseDto -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.model.Vendor +import com.sampoom.android.feature.user.domain.model.User fun LoginResponseDto.toModel(): User = User( userId = userId, @@ -23,28 +22,6 @@ fun LoginResponseDto.toModel(): User = User( endedAt = null ) -fun GetProfileResponseDto.toModel(): User = User( - userId = userId, - userName = userName, - email = email, - role = role, - accessToken = "", - refreshToken = "", - expiresIn = 0L, - position = position.toUserPosition(), - workspace = workspace, - branch = branch, - agencyId = organizationId, - startedAt = startedAt, - endedAt = endedAt -) - -private fun String.toUserPosition(): UserPosition = try { - UserPosition.valueOf(this.uppercase()) -} catch (_: IllegalArgumentException) { - UserPosition.STAFF -} - fun GetVendorsResponseDto.toModel(): Vendor = Vendor( id = id, vendorCode = vendorCode, diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt index fbbf1fb..d20d25d 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt @@ -2,22 +2,17 @@ package com.sampoom.android.feature.auth.data.remote.api import com.sampoom.android.core.model.ApiResponse import com.sampoom.android.core.model.ApiSuccessResponse +import com.sampoom.android.feature.auth.data.remote.dto.GetVendorsResponseDto import com.sampoom.android.feature.auth.data.remote.dto.LoginRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpResponseDto import com.sampoom.android.feature.auth.data.remote.dto.LoginResponseDto import com.sampoom.android.feature.auth.data.remote.dto.RefreshRequestDto import com.sampoom.android.feature.auth.data.remote.dto.RefreshResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.UpdateProfileRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.UpdateProfileResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.GetProfileResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.GetVendorsResponseDto +import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto +import com.sampoom.android.feature.auth.data.remote.dto.SignUpResponseDto import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Headers -import retrofit2.http.PATCH import retrofit2.http.POST -import retrofit2.http.Query interface AuthApi { @POST("auth/signup") @@ -34,12 +29,6 @@ interface AuthApi { @Headers("X-No-Auth: true") suspend fun login(@Body body: LoginRequestDto): ApiResponse - @GET("user/profile") - suspend fun getProfile(@Query("workspace") workspace: String): ApiResponse - - @PATCH("user/profile") - suspend fun updateProfile(@Body body: UpdateProfileRequestDto): ApiResponse - @GET("site/vendors") suspend fun getVendors(): ApiResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt deleted file mode 100644 index 8db66be..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sampoom.android.feature.auth.data.remote.dto - -data class UpdateProfileRequestDto( - val userName: String, - val position: String, - val workspace: String, - val branch: String -) diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt deleted file mode 100644 index 12e2ab6..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sampoom.android.feature.auth.data.remote.dto - -data class UpdateProfileResponseDto( - val userId: Long, - val userName: String, - val position: String, - val workspace: String, - val branch: String -) diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt index cdc8642..f1e5106 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt @@ -7,10 +7,9 @@ import com.sampoom.android.feature.auth.data.remote.api.AuthApi import com.sampoom.android.feature.auth.data.remote.dto.LoginRequestDto import com.sampoom.android.feature.auth.data.remote.dto.RefreshRequestDto import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.model.VendorList import com.sampoom.android.feature.auth.domain.repository.AuthRepository -import com.sampoom.android.feature.outbound.data.mapper.toModel +import com.sampoom.android.feature.user.domain.model.User import kotlinx.coroutines.delay import javax.inject.Inject @@ -63,29 +62,7 @@ class AuthRepositoryImpl @Inject constructor( val loginUser = loginDto.data.toModel() preferences.saveUser(loginUser) - - val profileUser = retry(times = 5, initialDelay = 300) { - getProfile("AGENCY").getOrThrow() - } - - val user = User( - userId = loginUser.userId, - userName = profileUser.userName, - email = profileUser.email, - role = profileUser.role, - accessToken = loginUser.accessToken, - refreshToken = loginUser.refreshToken, - expiresIn = loginUser.expiresIn, - position = profileUser.position, - workspace = profileUser.workspace, - branch = profileUser.branch, - agencyId = profileUser.agencyId, - startedAt = profileUser.startedAt, - endedAt = profileUser.endedAt - ) - - preferences.saveUser(user) - user + loginUser } } @@ -126,14 +103,6 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun isSignedIn(): Boolean = preferences.hasToken() - override suspend fun getProfile(workspace: String): Result { - return runCatching { - val dto = api.getProfile(workspace) - if (!dto.success) throw Exception(dto.message) - dto.data.toModel() - } - } - override suspend fun getVendorList(): Result { return runCatching { val dto = api.getVendors() diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt index 7d9e833..10e95e1 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt @@ -1,7 +1,7 @@ package com.sampoom.android.feature.auth.domain.repository -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.model.VendorList +import com.sampoom.android.feature.user.domain.model.User interface AuthRepository { suspend fun signUp( @@ -18,6 +18,5 @@ interface AuthRepository { suspend fun refreshToken(): Result suspend fun clearTokens(): Result suspend fun isSignedIn(): Boolean - suspend fun getProfile(workspace: String): Result suspend fun getVendorList(): Result } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt deleted file mode 100644 index 9401624..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.sampoom.android.feature.auth.domain.usecase - -import com.sampoom.android.core.preferences.AuthPreferences -import com.sampoom.android.feature.auth.domain.model.User -import javax.inject.Inject - -class GetStoredUserUseCase @Inject constructor( - private val preferences: AuthPreferences -) { - suspend operator fun invoke(): User? = preferences.getStoredUser() -} - - - diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt index 4c47220..d4a288a 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt @@ -1,7 +1,7 @@ package com.sampoom.android.feature.auth.domain.usecase -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.domain.model.User import javax.inject.Inject class LoginUseCase @Inject constructor( diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt index 9e5ec23..aedc1a9 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt @@ -1,7 +1,7 @@ package com.sampoom.android.feature.auth.domain.usecase -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.domain.model.User import javax.inject.Inject class SignUpUseCase @Inject constructor( diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt index 9e06634..dcb9ef7 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt @@ -5,10 +5,11 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.AuthValidator import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.ValidationResult +import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.auth.domain.usecase.LoginUseCase +import com.sampoom.android.feature.user.domain.usecase.GetProfileUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,6 +21,7 @@ import javax.inject.Inject class LoginViewModel @Inject constructor( private val messageHandler: GlobalMessageHandler, private val singIn: LoginUseCase, + private val getProfile: GetProfileUseCase, private val application: Application ) : ViewModel() { @@ -86,9 +88,21 @@ class LoginViewModel @Inject constructor( _uiState.update { it.copy(loading = true, success = false) } singIn(s.email, s.password) .onSuccess { - _uiState.update { - it.copy(loading = false, success = true) - } + getProfile("AGENCY") + .onSuccess { + _uiState.update { + it.copy(loading = false, success = true) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy(loading = false, success = false) + } + } } .onFailure { throwable -> val backendMessage = throwable.serverMessageOrNull() @@ -98,6 +112,7 @@ class LoginViewModel @Inject constructor( _uiState.update { it.copy(loading = false, success = false) } + return@launch } Log.d(TAG, "submit: ${_uiState.value}") } diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt index 9ead5b9..d7fe3a3 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt index a448e7b..8430b26 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt @@ -6,14 +6,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.R import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.AuthValidator +import com.sampoom.android.core.util.AuthValidator.validateEmail +import com.sampoom.android.core.util.AuthValidator.validatePassword +import com.sampoom.android.core.util.AuthValidator.validatePasswordCheck import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.AuthValidator.validateEmail -import com.sampoom.android.feature.auth.domain.AuthValidator.validatePassword -import com.sampoom.android.feature.auth.domain.AuthValidator.validatePasswordCheck -import com.sampoom.android.feature.auth.domain.ValidationResult +import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.auth.domain.usecase.GetVendorUseCase import com.sampoom.android.feature.auth.domain.usecase.SignUpUseCase +import com.sampoom.android.feature.user.domain.usecase.GetProfileUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,6 +27,7 @@ class SignUpViewModel @Inject constructor( private val messageHandler: GlobalMessageHandler, private val singUp: SignUpUseCase, private val getVendorUseCase: GetVendorUseCase, + private val getProfileUseCase: GetProfileUseCase, private val application: Application ) : ViewModel() { @@ -118,21 +120,21 @@ class SignUpViewModel @Inject constructor( } private fun validateEmail() { - val result = AuthValidator.validateEmail(_state.value.email) + val result = validateEmail(_state.value.email) _state.value = _state.value.copy( emailError = result.toErrorMessage() ) } private fun validatePassword() { - val result = AuthValidator.validatePassword(_state.value.password) + val result = validatePassword(_state.value.password) _state.value = _state.value.copy( passwordError = result.toErrorMessage() ) } private fun validatePasswordCheck() { - val result = AuthValidator.validatePasswordCheck(_state.value.password, _state.value.passwordCheck) + val result = validatePasswordCheck(_state.value.password, _state.value.passwordCheck) _state.value = _state.value.copy( passwordCheckError = result.toErrorMessage() ) @@ -167,9 +169,21 @@ class SignUpViewModel @Inject constructor( position = s.position!!.name ) .onSuccess { - _state.update { - it.copy(loading = false, success = true) - } + getProfileUseCase("AGENCY") + .onSuccess { + _state.update { + it.copy(loading = false, success = true) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _state.update { + it.copy(loading = false, success = false) + } + } } .onFailure { throwable -> val backendMessage = throwable.serverMessageOrNull() diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt index 4971cc0..9245950 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt @@ -47,7 +47,6 @@ import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.sampoom.android.R -import com.sampoom.android.core.model.UserPosition import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.core.ui.theme.FailRed @@ -56,15 +55,16 @@ import com.sampoom.android.core.ui.theme.SuccessGreen import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.dashboard.domain.model.Dashboard import com.sampoom.android.feature.dashboard.domain.model.WeeklySummary import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.ui.OrderItem +import com.sampoom.android.feature.user.domain.model.User @Composable fun DashboardScreen( paddingValues: PaddingValues, + onEmployeeClick: () -> Unit, onSettingClick: () -> Unit, onNavigateOrderDetail: (Order) -> Unit, onNavigationOrder: () -> Unit, @@ -75,20 +75,7 @@ fun DashboardScreen( val user by viewModel.user.collectAsStateWithLifecycle() val pullRefreshState = rememberPullToRefreshState() val orderListPaged = viewModel.orderListPaged.collectAsLazyPagingItems() - val isManager = when (user?.position) { - UserPosition.STAFF, - UserPosition.SENIOR_STAFF, - UserPosition.ASSISTANT_MANAGER, - UserPosition.MANAGER, - UserPosition.DEPUTY_GENERAL_MANAGER, - UserPosition.GENERAL_MANAGER, - UserPosition.DIRECTOR, - UserPosition.VICE_PRESIDENT, - UserPosition.PRESIDENT, - UserPosition.CHAIRMAN -> true - - else -> false - } + val isManager = user?.role == "ADMIN" LaunchedEffect(errorLabel) { viewModel.bindLabel(errorLabel) @@ -99,6 +86,7 @@ fun DashboardScreen( onRefresh = { viewModel.onEvent(DashboardUiEvent.LoadDashboard) orderListPaged.refresh() + viewModel.refreshUser() }, state = pullRefreshState, modifier = Modifier.fillMaxSize(), @@ -135,7 +123,7 @@ fun DashboardScreen( Row { if (isManager) { IconButton( - onClick = { } + onClick = { onEmployeeClick() } ) { Icon( painter = painterResource(R.drawable.employee), @@ -163,7 +151,14 @@ fun DashboardScreen( ) { item { TitleSection(user) } - item { ButtonSection(isManager, uiState.dashboard) } + item { + ButtonSection( + isManager = isManager, + dashboard = uiState.dashboard, + employeeCount = uiState.employeeCount, + onEmployeeClick = { onEmployeeClick() } + ) + } item { OrderListSection( @@ -234,8 +229,10 @@ fun TitleSection( @Composable fun ButtonSection( + onEmployeeClick: () -> Unit, isManager: Boolean, - dashboard: Dashboard? + dashboard: Dashboard?, + employeeCount: Int? ) { Column( modifier = Modifier @@ -252,9 +249,9 @@ fun ButtonSection( ), painter = painterResource(R.drawable.employee), painterDescription = stringResource(R.string.dashboard_employee), - text = 45.toString(), // TODO : API 연동 + text = employeeCount?.toString() ?: stringResource(R.string.common_slash), subText = stringResource(R.string.dashboard_employee), - onClick = { } + onClick = { onEmployeeClick() } ) } @@ -267,17 +264,16 @@ fun ButtonSection( painter = painterResource(R.drawable.car), painterDescription = stringResource(R.string.dashboard_parts_all), text = (dashboard?.totalParts ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_all), - onClick = { } + subText = stringResource(R.string.dashboard_parts_all) ) ButtonCard( modifier = Modifier.weight(1f), painter = painterResource(R.drawable.block), painterDescription = stringResource(R.string.dashboard_parts_out_of_stock), - text = (dashboard?.outOfStockParts ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_out_of_stock), - onClick = { } + text = (dashboard?.outOfStockParts + ?: stringResource(R.string.common_slash)).toString(), + subText = stringResource(R.string.dashboard_parts_out_of_stock) ) } @@ -289,18 +285,18 @@ fun ButtonSection( modifier = Modifier.weight(1f), painter = painterResource(R.drawable.warning), painterDescription = stringResource(R.string.dashboard_parts_low_stock), - text = (dashboard?.lowStockParts ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_low_stock), - onClick = { } + text = (dashboard?.lowStockParts + ?: stringResource(R.string.common_slash)).toString(), + subText = stringResource(R.string.dashboard_parts_low_stock) ) ButtonCard( modifier = Modifier.weight(1f), painter = painterResource(R.drawable.parts), painterDescription = stringResource(R.string.dashboard_parts_on_hand), - text = (dashboard?.totalQuantity ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_on_hand), - onClick = { } + text = (dashboard?.totalQuantity + ?: stringResource(R.string.common_slash)).toString(), + subText = stringResource(R.string.dashboard_parts_on_hand) ) } } @@ -313,7 +309,7 @@ fun ButtonCard( painterDescription: String, text: String, subText: String, - onClick: () -> Unit + onClick: () -> Unit = {} ) { Card( modifier = modifier @@ -484,7 +480,8 @@ fun WeeklySummarySection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = (weeklySummary?.inStockParts ?: stringResource(R.string.common_slash)).toString(), + text = (weeklySummary?.inStockParts + ?: stringResource(R.string.common_slash)).toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = SuccessGreen @@ -501,7 +498,8 @@ fun WeeklySummarySection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = (weeklySummary?.outStockParts ?: stringResource(R.string.common_slash)).toString(), + text = (weeklySummary?.outStockParts + ?: stringResource(R.string.common_slash)).toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = FailRed diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt index 4986b22..cd5733a 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt @@ -12,4 +12,7 @@ data class DashboardUiState( val dashboardError: String? = null, val weeklySummaryLoading: Boolean = false, val weeklySummaryError: String? = null, + val employeeCount: Int? = null, + val employeeCountLoading: Boolean = false, + val employeeCountError: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt index f0e6e4e..4232364 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt @@ -6,14 +6,14 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.usecase.GetStoredUserUseCase import com.sampoom.android.feature.dashboard.domain.usecase.GetDashboardUseCase import com.sampoom.android.feature.dashboard.domain.usecase.WeeklySummaryUseCase import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetEmployeeCountUseCase +import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,7 +27,8 @@ class DashboardViewModel @Inject constructor( private val getOrderListUseCase: GetOrderUseCase, private val getStoredUserUseCase: GetStoredUserUseCase, private val getDashboardUseCase: GetDashboardUseCase, - private val getWeeklySummaryUseCase: WeeklySummaryUseCase + private val getWeeklySummaryUseCase: WeeklySummaryUseCase, + private val getEmployeeCountUseCase: GetEmployeeCountUseCase ): ViewModel() { private companion object { @@ -52,6 +53,7 @@ class DashboardViewModel @Inject constructor( init { loadDashboard() loadWeeklySummary() + loadEmployeeCount() viewModelScope.launch { _user.value = getStoredUserUseCase() } @@ -62,10 +64,12 @@ class DashboardViewModel @Inject constructor( is DashboardUiEvent.LoadDashboard -> { loadDashboard() loadWeeklySummary() + loadEmployeeCount() } is DashboardUiEvent.RetryDashboard -> { loadDashboard() loadWeeklySummary() + loadEmployeeCount() } } } @@ -127,4 +131,39 @@ class DashboardViewModel @Inject constructor( } } } + + private fun loadEmployeeCount() { + viewModelScope.launch { + _uiState.update { it.copy(employeeCountLoading = true, employeeCountError = null) } + + getEmployeeCountUseCase() + .onSuccess { count -> + _uiState.update { + it.copy( + employeeCount = count, + employeeCountLoading = false, + employeeCountError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + weeklySummaryLoading = false, + weeklySummaryError = error + ) + } + } + } + } + + fun refreshUser() { + viewModelScope.launch { + _user.value = getStoredUserUseCase() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt similarity index 84% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingScreen.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt index 46b47e0..f6d3f82 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -16,6 +16,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -23,11 +24,13 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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 @@ -37,13 +40,16 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sampoom.android.R +import com.sampoom.android.core.ui.theme.FailRed import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatDate import com.sampoom.android.core.util.positionToKorean -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.ui.UpdateProfileBottomSheet +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -52,12 +58,15 @@ fun SettingScreen( onNavigateBack: () -> Unit = {}, onLogoutClick: () -> Unit = {} ) { + val coroutineScope = rememberCoroutineScope() val errorLabel = stringResource(R.string.common_error) val nameLabel = stringResource(R.string.signup_title_name) val uiState by viewModel.uiState.collectAsStateWithLifecycle() val user by viewModel.user.collectAsStateWithLifecycle() val pullRefreshState = rememberPullToRefreshState() var showLogoutDialog by remember { mutableStateOf(false) } + var showEditProfileSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(true) LaunchedEffect(errorLabel, nameLabel) { viewModel.bindLabel(errorLabel, nameLabel) @@ -72,7 +81,10 @@ fun SettingScreen( PullToRefreshBox( isRefreshing = false, - onRefresh = { viewModel.onEvent(SettingUiEvent.LoadProfile) }, + onRefresh = { + viewModel.onEvent(SettingUiEvent.LoadProfile) + viewModel.refreshUser() + }, state = pullRefreshState, modifier = Modifier.fillMaxSize(), indicator = { @@ -114,7 +126,7 @@ fun SettingScreen( } item { SettingSection( - onEditProfileClick = { }, + onEditProfileClick = { showEditProfileSheet = true }, onLogoutClick = { showLogoutDialog = true } ) } @@ -122,6 +134,31 @@ fun SettingScreen( } } + if (showEditProfileSheet && user != null) { + ModalBottomSheet( + onDismissRequest = { + coroutineScope.launch { + showEditProfileSheet = false + sheetState.hide() + } + }, + sheetState = sheetState + ) { + UpdateProfileBottomSheet( + user = user!!, + onProfileUpdated = { + viewModel.refreshUser() + }, + onDismiss = { + coroutineScope.launch { + showEditProfileSheet = false + sheetState.hide() + } + } + ) + } + } + if (showLogoutDialog) { AlertDialog( onDismissRequest = { showLogoutDialog = false }, @@ -247,7 +284,7 @@ private fun SettingSection( Text( text = stringResource(R.string.setting_logout), style = MaterialTheme.typography.titleMedium, - color = textColor() + color = FailRed ) Icon( diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiEvent.kt similarity index 62% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiEvent.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiEvent.kt index 4845ecd..6cae162 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiEvent.kt @@ -1,7 +1,6 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui sealed interface SettingUiEvent { object LoadProfile : SettingUiEvent data class NameChanged(val userName: String) : SettingUiEvent - object EditProfile : SettingUiEvent } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiState.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiState.kt similarity index 77% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiState.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiState.kt index caad258..9076f30 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiState.kt @@ -1,6 +1,6 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User data class SettingUiState( val profile: User? = null, diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt similarity index 76% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingViewModel.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt index 2cab7bc..9426de8 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt @@ -1,13 +1,13 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.util.AuthValidator import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.ValidationResult -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.usecase.GetStoredUserUseCase +import com.sampoom.android.core.util.ValidationResult +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -48,12 +48,13 @@ class SettingViewModel @Inject constructor( fun onEvent(event: SettingUiEvent) { when (event) { - is SettingUiEvent.LoadProfile -> {} + is SettingUiEvent.LoadProfile -> { + refreshUser() + } is SettingUiEvent.NameChanged -> { _uiState.value = _uiState.value.copy(userName = event.userName) validateName() } - is SettingUiEvent.EditProfile -> editProfile() } } @@ -72,18 +73,13 @@ class SettingViewModel @Inject constructor( } } - private fun editProfile() = viewModelScope.launch { - validateName() - - if (!_uiState.value.isValid) return@launch - - val s = _uiState.value - _uiState.update { it.copy(loading = true) } - - // TODO : Edit Profile 연동 - } - fun clearSuccess() { _uiState.update { it.copy(profileChangeSuccess = false, logoutSuccess = false) } } + + fun refreshUser() { + viewModelScope.launch { + _user.value = getStoredUserUseCase() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt index 6109771..5141d9f 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.unit.dp import com.sampoom.android.R import com.sampoom.android.core.ui.component.StatusChip import com.sampoom.android.core.ui.theme.backgroundCardColor -import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon @@ -33,7 +32,6 @@ import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.domain.model.OrderPart import com.sampoom.android.feature.order.domain.model.subtotal import com.sampoom.android.feature.order.domain.model.totalCost -import kotlin.collections.forEach @Composable fun OrderDetailContent( diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt index 8614242..e7776d4 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt b/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt index 7aa5276..9b71dd9 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt @@ -10,7 +10,6 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import retrofit2.Retrofit import javax.inject.Singleton -import kotlin.jvm.java @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt index dabc090..a36e706 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.outbound.ui -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,7 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -50,7 +48,6 @@ import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon -import com.sampoom.android.feature.cart.domain.model.subtotal import com.sampoom.android.feature.outbound.domain.model.OutboundPart import com.sampoom.android.feature.outbound.domain.model.subtotal @@ -96,7 +93,9 @@ fun OutboundListScreen( ) } ) { - Column(Modifier.fillMaxSize().padding(paddingValues)) { + Column(Modifier + .fillMaxSize() + .padding(paddingValues)) { Row( modifier = Modifier .fillMaxWidth() @@ -198,7 +197,7 @@ fun OutboundListScreen( .align(Alignment.BottomEnd) .padding(16.dp) .padding(end = 72.dp), - variant = ButtonVariant.Error, + variant = ButtonVariant.Secondary, size = ButtonSize.Large, onClick = { showConfirmDialog = true } ) { Text("${formatWon(uiState.totalCost)} ${stringResource(R.string.outbound_order_parts)}") } diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt index 6eb5820..b64ea15 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt @@ -11,7 +11,6 @@ import com.sampoom.android.feature.outbound.domain.usecase.GetOutboundUseCase import com.sampoom.android.feature.outbound.domain.usecase.ProcessOutboundUseCase import com.sampoom.android.feature.outbound.domain.usecase.UpdateOutboundQuantityUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt index 2afbcfa..9e9e949 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt @@ -203,7 +203,7 @@ fun PartDetailBottomSheet( Row(modifier = Modifier.fillMaxWidth()) { CommonButton( modifier = Modifier.weight(1F), - variant = ButtonVariant.Error, + variant = ButtonVariant.Secondary, size = ButtonSize.Large, leadingIcon = { Icon( diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt index cadb1ae..919e173 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -5,10 +5,12 @@ 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.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card @@ -47,7 +49,6 @@ import com.sampoom.android.R import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.core.ui.theme.backgroundCardColor -import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon @@ -252,10 +253,12 @@ private fun PartListItemCard( ) } + Spacer(Modifier.width(8.dp)) + Icon( painterResource(R.drawable.chevron_right), contentDescription = stringResource(R.string.common_detail), - tint = disableColor() + tint = textSecondaryColor() ) } } diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt index 811fc25..ffa4bcd 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt @@ -8,7 +8,6 @@ import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.feature.part.domain.usecase.GetPartUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt index 2791fed..48fcd67 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt @@ -11,6 +11,7 @@ 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.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -60,7 +61,6 @@ import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.theme.White import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.backgroundColor -import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon @@ -519,7 +519,7 @@ private fun PartItemCard( Icon( painterResource(R.drawable.chevron_right), contentDescription = stringResource(R.string.common_detail), - tint = disableColor() + tint = textSecondaryColor() ) } } @@ -644,10 +644,12 @@ private fun SearchPartItem( ) } + Spacer(Modifier.width(8.dp)) + Icon( painterResource(R.drawable.chevron_right), contentDescription = stringResource(R.string.common_detail), - tint = disableColor() + tint = textSecondaryColor() ) } } diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt index 19d8842..a6a7064 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt @@ -2,7 +2,6 @@ package com.sampoom.android.feature.part.ui import com.sampoom.android.feature.part.domain.model.Category import com.sampoom.android.feature.part.domain.model.Group -import com.sampoom.android.feature.part.domain.model.SearchResult data class PartUiState( // Part diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt index 7ecbd5e..9c45c81 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt @@ -13,7 +13,6 @@ import com.sampoom.android.feature.part.domain.usecase.GetCategoryUseCase import com.sampoom.android.feature.part.domain.usecase.GetGroupUseCase import com.sampoom.android.feature.part.domain.usecase.SearchPartsUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/local/database/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/local/database/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/local/preferences/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/local/preferences/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/mapper/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/mapper/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/remote/api/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/remote/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/remote/dto/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/remote/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/repository/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/di/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/di/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/domain/model/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/domain/model/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/domain/repository/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/domain/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/domain/usecase/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/domain/usecase/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/ui/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/ui/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt new file mode 100644 index 0000000..e6b7df3 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt @@ -0,0 +1,103 @@ +package com.sampoom.android.feature.user.data.mapper + +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto +import com.sampoom.android.feature.user.data.remote.dto.EmployeeDto +import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileResponseDto +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.model.User + +fun GetProfileResponseDto.toModel(): User = User( + userId = userId, + userName = userName, + email = email, + role = role, + accessToken = "", + refreshToken = "", + expiresIn = 0L, + position = position.toUserPosition(), + workspace = workspace, + branch = branch, + agencyId = organizationId, + startedAt = startedAt, + endedAt = endedAt +) + +private fun String.toUserPosition(): UserPosition = try { + UserPosition.valueOf(this.uppercase()) +} catch (_: IllegalArgumentException) { + UserPosition.STAFF +} + +fun UpdateProfileResponseDto.toModel(): User = User( + userId = userId, + userName = userName, + email = "", + role = "", + accessToken = "", + refreshToken = "", + expiresIn = 0L, + position = UserPosition.STAFF, + workspace = "", + branch = "", + agencyId = 0, + startedAt = null, + endedAt = null +) + +fun EditEmployeeResponseDto.toModel(): Employee = Employee( + userId = userId, + email = "", + role = "", + userName = userName, + workspace = workspace, + organizationId = 0, + branch = "", + position = position.toUserPosition(), + status = EmployeeStatus.ACTIVE, + createdAt = null, + startedAt = null, + endedAt = null, + deletedAt = null +) + +fun UpdateEmployeeStatusResponseDto.toModel(): Employee = Employee( + userId = userId, + email = "", + role = "", + userName = userName, + workspace = workspace, + organizationId = 0, + branch = "", + position = UserPosition.STAFF, + status = employeeStatus.toEmployeeStatus(), + createdAt = null, + startedAt = null, + endedAt = null, + deletedAt = null +) + +fun EmployeeDto.toModel(): Employee = Employee( + userId, + email, + role, + userName, + workspace, + organizationId, + branch, + position, + status ?: EmployeeStatus.ACTIVE, + createdAt, + startedAt, + endedAt, + deletedAt +) + +private fun String.toEmployeeStatus(): EmployeeStatus = try { + EmployeeStatus.valueOf(this.uppercase()) +} catch (_: IllegalArgumentException) { + EmployeeStatus.ACTIVE +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt b/app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt new file mode 100644 index 0000000..621f4ba --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt @@ -0,0 +1,47 @@ +package com.sampoom.android.feature.user.data.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.sampoom.android.core.preferences.AuthPreferences +import com.sampoom.android.feature.user.data.mapper.toModel +import com.sampoom.android.feature.user.data.remote.api.UserApi +import com.sampoom.android.feature.user.domain.model.Employee +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class EmployeePagingSource @AssistedInject constructor( + private val api: UserApi, + private val authPreferences: AuthPreferences +) : PagingSource() { + + @AssistedFactory + interface Factory { + fun create(): EmployeePagingSource + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val agencyId = authPreferences.getStoredUser()?.agencyId ?: throw Exception() + val page = params.key ?: 0 + val pageSize = params.loadSize + val response = api.getEmployeeList("AGENCY", agencyId, page, pageSize) + val employee = response.data.users.map { it.toModel() } + val meta = response.data.meta + + LoadResult.Page( + data = employee, + prevKey = if (meta.hasPrevious) page - 1 else null, + nextKey = if (meta.hasNext) page + 1 else null + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt new file mode 100644 index 0000000..a885502 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt @@ -0,0 +1,46 @@ +package com.sampoom.android.feature.user.data.remote.api + +import com.sampoom.android.core.model.ApiResponse +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto +import com.sampoom.android.feature.user.data.remote.dto.EmployeeListDto +import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileResponseDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.Path +import retrofit2.http.Query + +interface UserApi { + @GET("user/profile") + suspend fun getProfile(@Query("workspace") workspace: String): ApiResponse + + @PATCH("user/profile") + suspend fun updateProfile(@Body body: UpdateProfileRequestDto): ApiResponse + + @PATCH("user/profile/{userId}") + suspend fun editEmployee( + @Path("userId") userId: Long, + @Query("workspace") workspace: String, + @Body body: EditEmployeeRequestDto + ): ApiResponse + + @GET("user/info") + suspend fun getEmployeeList( + @Query("workspace") workspace: String, + @Query("organizationId") organizationId: Long, + @Query("page") page: Int = 0, + @Query("size") size: Int = 20 + ): ApiResponse + + @PATCH("user/status/{userId}") + suspend fun updateEmployeeStatus( + @Path("userId") userId: Long, + @Query("workspace") workspace: String, + @Body body: UpdateEmployeeStatusRequestDto + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt new file mode 100644 index 0000000..07b2a36 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class EditEmployeeRequestDto( + val position: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt new file mode 100644 index 0000000..700cdce --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class EditEmployeeResponseDto( + val userId: Long, + val userName: String, + val workspace: String, + val position: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt new file mode 100644 index 0000000..02b7ed1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt @@ -0,0 +1,20 @@ +package com.sampoom.android.feature.user.data.remote.dto + +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.core.model.UserPosition + +data class EmployeeDto( + val userId: Long, + val email: String, + val role: String, + val userName: String, + val workspace: String, + val organizationId: Long, + val branch: String, + val position: UserPosition, + val status: EmployeeStatus?, + val createdAt: String?, + val startedAt: String?, + val endedAt: String?, + val deletedAt: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt new file mode 100644 index 0000000..8ca47a8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt @@ -0,0 +1,15 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class EmployeeListDto( + val users: List, + val meta: EmployeeMetaDto +) + +data class EmployeeMetaDto( + val currentPage: Int, + val totalPages: Int, + val totalElements: Int, + val size: Int, + val hasNext: Boolean, + val hasPrevious: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/GetProfileResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/GetProfileResponseDto.kt similarity index 82% rename from app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/GetProfileResponseDto.kt rename to app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/GetProfileResponseDto.kt index aa46f7d..b54aaef 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/GetProfileResponseDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/GetProfileResponseDto.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.data.remote.dto +package com.sampoom.android.feature.user.data.remote.dto data class GetProfileResponseDto( val userId: Long, @@ -11,4 +11,4 @@ data class GetProfileResponseDto( val organizationId: Long, val startedAt: String, val endedAt: String? -) +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt new file mode 100644 index 0000000..c945218 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateEmployeeStatusRequestDto( + val employeeStatus: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt new file mode 100644 index 0000000..22be469 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateEmployeeStatusResponseDto( + val userId: Long, + val userName: String, + val workspace: String, + val employeeStatus: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt new file mode 100644 index 0000000..d87dc5a --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateProfileRequestDto( + val userName: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt new file mode 100644 index 0000000..ba1b245 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateProfileResponseDto( + val userId: Long, + val userName: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt new file mode 100644 index 0000000..25469c2 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt @@ -0,0 +1,191 @@ +package com.sampoom.android.feature.user.data.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.sampoom.android.core.preferences.AuthPreferences +import com.sampoom.android.core.util.retry +import com.sampoom.android.feature.user.data.mapper.toModel +import com.sampoom.android.feature.user.data.paging.EmployeePagingSource +import com.sampoom.android.feature.user.data.remote.api.UserApi +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val api: UserApi, + private val preferences: AuthPreferences, + private val pagingSourceFactory: EmployeePagingSource.Factory +) : UserRepository { + override suspend fun getStoredUser(): User? { + return preferences.getStoredUser() + } + + override suspend fun getProfile(workspace: String): Result { + return runCatching { + retry(times = 5, initialDelay = 300) { + val dto = api.getProfile(workspace) + if (!dto.success) throw Exception(dto.message) + val profileUser = dto.data.toModel() + val loginUser = preferences.getStoredUser() + + val completeUser = if (loginUser != null) { + User( + userId = profileUser.userId, + userName = profileUser.userName, + email = profileUser.email, + role = profileUser.role, + accessToken = loginUser.accessToken, // 저장된 토큰 + refreshToken = loginUser.refreshToken, // 저장된 토큰 + expiresIn = loginUser.expiresIn, // 저장된 토큰 + position = profileUser.position, + workspace = profileUser.workspace, + branch = profileUser.branch, + agencyId = profileUser.agencyId, + startedAt = profileUser.startedAt, + endedAt = profileUser.endedAt + ) + } else { + throw Exception() + } + + preferences.saveUser(completeUser) + completeUser + } + } + } + + override suspend fun updateProfile(user: User): Result { + return runCatching { + val requestDto = UpdateProfileRequestDto( + userName = user.userName + ) + val dto = api.updateProfile(requestDto) + if (!dto.success) throw Exception(dto.message) + val updatedProfile = dto.data.toModel() + val storedUser = preferences.getStoredUser() + val completeUser = if (storedUser != null) { + User( + userId = updatedProfile.userId, + userName = updatedProfile.userName, + email = user.email, + role = user.role, + accessToken = storedUser.accessToken, + refreshToken = storedUser.refreshToken, + expiresIn = storedUser.expiresIn, + position = user.position, + workspace = user.workspace, + branch = user.branch, + agencyId = user.agencyId, + startedAt = user.startedAt, + endedAt = user.endedAt + ) + } else throw Exception() + + preferences.saveUser(completeUser) + completeUser + } + } + + override fun getEmployeeList(): Flow> { + return Pager( + config = PagingConfig(pageSize = 20), + pagingSourceFactory = { pagingSourceFactory.create() } + ).flow + } + + override suspend fun editEmployee( + employee: Employee, + workspace: String + ): Result { + return runCatching { + val requestDto = EditEmployeeRequestDto( + position = employee.position.name + ) + val dto = api.editEmployee( + userId = employee.userId, + workspace = workspace, + body = requestDto + ) + if (!dto.success) throw Exception(dto.message) + + val updatedEmployee = dto.data.toModel() + val completeEmployee = Employee( + userId = updatedEmployee.userId, + email = employee.email, + role = employee.role, + userName = updatedEmployee.userName.takeIf { it.isNotBlank() } ?: employee.userName, + workspace = updatedEmployee.workspace.takeIf { it.isNotBlank() } ?: employee.workspace, + organizationId = employee.organizationId, + branch = employee.branch, + position = updatedEmployee.position, + status = employee.status, + createdAt = employee.createdAt, + startedAt = employee.startedAt, + endedAt = employee.endedAt, + deletedAt = employee.deletedAt + ) + + completeEmployee + } + } + + override suspend fun updateEmployeeStatus( + employee: Employee, + workspace: String + ): Result { + return runCatching { + val requestDto = UpdateEmployeeStatusRequestDto( + employeeStatus = employee.status.name + ) + val dto = api.updateEmployeeStatus( + userId = employee.userId, + workspace = workspace, + body = requestDto + ) + if (!dto.success) throw Exception(dto.message) + + val updateEmployeeStatus = dto.data.toModel() + val completedEmployeeStatus = Employee( + userId = updateEmployeeStatus.userId, + email = employee.email, + role = employee.role, + userName = updateEmployeeStatus.userName.takeIf { it.isNotBlank() } ?: employee.userName, + workspace = updateEmployeeStatus.workspace.takeIf { it.isNotBlank() } ?: employee.workspace, + organizationId = employee.organizationId, + branch = employee.branch, + position = employee.position, + status = updateEmployeeStatus.status, + createdAt = employee.createdAt, + startedAt = employee.startedAt, + endedAt = employee.endedAt, + deletedAt = employee.deletedAt + ) + + completedEmployeeStatus + } + } + + override suspend fun getEmployeeCount(): Result { + return runCatching { + val user = preferences.getStoredUser() ?: throw Exception() + val workspace = user.workspace + val organizationId = user.agencyId + + val dto = api.getEmployeeList( + workspace = workspace, + organizationId = organizationId, + page = 0, + size = 1 + ) + + if (!dto.success) throw Exception(dto.message) + dto.data.meta.totalElements + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt b/app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt new file mode 100644 index 0000000..3127f0f --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt @@ -0,0 +1,26 @@ +package com.sampoom.android.feature.user.di + +import com.sampoom.android.feature.user.data.remote.api.UserApi +import com.sampoom.android.feature.user.data.repository.UserRepositoryImpl +import com.sampoom.android.feature.user.domain.repository.UserRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class UserBinModule { + @Binds @Singleton + abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository +} + +@Module +@InstallIn(SingletonComponent::class) +object UserProvideModule { + @Provides @Singleton + fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt new file mode 100644 index 0000000..da6bbdb --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt @@ -0,0 +1,20 @@ +package com.sampoom.android.feature.user.domain.model + +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.core.model.UserPosition + +data class Employee( + val userId: Long, + val email: String, + val role: String, + val userName: String, + val workspace: String, + val organizationId: Long, + val branch: String, + val position: UserPosition, + val status: EmployeeStatus, + val createdAt: String?, + val startedAt: String?, + val endedAt: String?, + val deletedAt: String? +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt new file mode 100644 index 0000000..45f8475 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.model + +data class EmployeeList( + val items: List, + val totalCount: Int = items.size, + val isEmpty: Boolean = items.isEmpty() +) { + companion object Companion { + fun empty() = EmployeeList(emptyList()) + } +} diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt similarity index 87% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt rename to app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt index 984500a..00abdf8 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.domain.model +package com.sampoom.android.feature.user.domain.model import com.sampoom.android.core.model.UserPosition @@ -16,4 +16,4 @@ data class User( val agencyId: Long, val startedAt: String?, val endedAt: String? -) +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt new file mode 100644 index 0000000..6c278c0 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt @@ -0,0 +1,16 @@ +package com.sampoom.android.feature.user.domain.repository + +import androidx.paging.PagingData +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.model.User +import kotlinx.coroutines.flow.Flow + +interface UserRepository { + suspend fun getStoredUser(): User? + suspend fun getProfile(workspace: String): Result + suspend fun updateProfile(user: User): Result + fun getEmployeeList(): Flow> + suspend fun editEmployee(employee: Employee, workspace: String): Result + suspend fun updateEmployeeStatus(employee: Employee, workspace: String): Result + suspend fun getEmployeeCount(): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt new file mode 100644 index 0000000..d81d82d --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class EditEmployeeUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(employee: Employee, workspace: String): Result = repository.editEmployee(employee, workspace) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt new file mode 100644 index 0000000..c8bf5ed --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class GetEmployeeCountUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(): Result = repository.getEmployeeCount() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt new file mode 100644 index 0000000..542179e --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt @@ -0,0 +1,13 @@ +package com.sampoom.android.feature.user.domain.usecase + +import androidx.paging.PagingData +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetEmployeeUseCase @Inject constructor( + private val repository: UserRepository +) { + operator fun invoke(): Flow> = repository.getEmployeeList() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt new file mode 100644 index 0000000..3b7e43c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class GetProfileUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(workspace: String): Result = repository.getProfile(workspace) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt new file mode 100644 index 0000000..672956c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class GetStoredUserUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(): User? = repository.getStoredUser() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt new file mode 100644 index 0000000..72b08bc --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class UpdateEmployeeStatusUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(employee: Employee, workspace: String): Result = repository.updateEmployeeStatus(employee, workspace) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt new file mode 100644 index 0000000..e3ed1e1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class UpdateProfileUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(user: User): Result = repository.updateProfile(user) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt new file mode 100644 index 0000000..52d46a1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt @@ -0,0 +1,115 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sampoom.android.R +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.CommonTextField +import com.sampoom.android.core.util.positionToKorean +import com.sampoom.android.feature.user.domain.model.Employee + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditEmployeeBottomSheet( + employee: Employee, + onDismiss: () -> Unit, + onEmployeeUpdated: (Employee) -> Unit = {}, + viewModel: EditEmployeeViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var selectedPosition by rememberSaveable { mutableStateOf(employee.position) } + var positionMenuExpanded by remember { mutableStateOf(false) } + + val errorLabel = stringResource(R.string.common_error) + val editEmployeeLabel = stringResource(R.string.employee_edit_edited) + + LaunchedEffect(employee) { + viewModel.onEvent(EditEmployeeUiEvent.Initialize(employee)) + selectedPosition = employee.position + } + + LaunchedEffect(errorLabel, editEmployeeLabel) { + viewModel.bindLabel(errorLabel, editEmployeeLabel) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + uiState.employee?.let(onEmployeeUpdated) + viewModel.clearStatus() + onDismiss() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ExposedDropdownMenuBox( + expanded = positionMenuExpanded, + onExpandedChange = { positionMenuExpanded = it } + ) { + CommonTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor( + type = ExposedDropdownMenuAnchorType.PrimaryNotEditable, + enabled = true + ), + readOnly = true, + value = positionToKorean(selectedPosition), + onValueChange = {}, + placeholder = stringResource(R.string.signup_placeholder_position), + singleLine = true + ) + ExposedDropdownMenu( + expanded = positionMenuExpanded, + onDismissRequest = { positionMenuExpanded = false } + ) { + UserPosition.entries.forEach { position -> + DropdownMenuItem( + text = { Text(positionToKorean(position)) }, + onClick = { + selectedPosition = position + positionMenuExpanded = false + } + ) + } + } + } + + Spacer(Modifier.height(8.dp)) + + CommonButton( + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && selectedPosition != employee.position, + onClick = { viewModel.onEvent(EditEmployeeUiEvent.EditEmployee(selectedPosition)) } + ) { + Text(stringResource(R.string.common_confirm)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt new file mode 100644 index 0000000..b8cf9e9 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.feature.user.domain.model.Employee + +interface EditEmployeeUiEvent { + data class Initialize(val employee: Employee) : EditEmployeeUiEvent + data class EditEmployee(val position: UserPosition) : EditEmployeeUiEvent + object Dismiss : EditEmployeeUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt new file mode 100644 index 0000000..71a0597 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +data class EditEmployeeUiState( + val employee: Employee? = null, + val isLoading: Boolean = false, + val error: String? = null, + val isSuccess: Boolean = false +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt new file mode 100644 index 0000000..27df54a --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt @@ -0,0 +1,103 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.usecase.EditEmployeeUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class EditEmployeeViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val editEmployeeUseCase: EditEmployeeUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "EditEmployeeViewModel" + } + + private val _uiState = MutableStateFlow(EditEmployeeUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + private var editEmployeeLabel: String = "" + + fun bindLabel(error: String, editEmployee: String) { + errorLabel = error + editEmployeeLabel = editEmployee + } + + fun onEvent(event: EditEmployeeUiEvent) { + when (event) { + is EditEmployeeUiEvent.Initialize -> { + _uiState.update { + it.copy( + employee = event.employee, + isLoading = false, + isSuccess = false + ) + } + } + is EditEmployeeUiEvent.EditEmployee -> { + editEmployee(event.position) + } + is EditEmployeeUiEvent.Dismiss -> { + _uiState.update { + it.copy( + employee = null, + isLoading = false, + error = null + ) + } + } + } + } + + private fun editEmployee(newPosition: UserPosition) { + viewModelScope.launch { + val currentEmployee = _uiState.value.employee ?: run { + messageHandler.showMessage(message = errorLabel, isError = true) + return@launch + } + + val updateEmployee = currentEmployee.copy(position = newPosition) + _uiState.update { it.copy(isLoading = true, error = null) } + + editEmployeeUseCase(updateEmployee, "AGENCY") + .onSuccess { employee -> + _uiState.update { + it.copy( + employee = employee, + isLoading = false, + error = null, + isSuccess = true + ) + } + messageHandler.showMessage(message = editEmployeeLabel, isError = false) + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + isLoading = false, + error = error + ) + } + } + } + } + + fun clearStatus() { + _uiState.update { it.copy(isSuccess = false, error = null) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt new file mode 100644 index 0000000..8b3af46 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.user.ui + +enum class EmployeeBottomSheetType { + STATUS, + EDIT +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt new file mode 100644 index 0000000..cab05cf --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt @@ -0,0 +1,412 @@ +package com.sampoom.android.feature.user.ui + +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.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.ButtonSize +import com.sampoom.android.core.ui.component.ButtonVariant +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.EmptyContent +import com.sampoom.android.core.ui.component.ErrorContent +import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.theme.backgroundCardColor +import com.sampoom.android.core.ui.theme.textColor +import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.core.util.employeeStatusToKorean +import com.sampoom.android.core.util.formatDate +import com.sampoom.android.core.util.positionToKorean +import com.sampoom.android.feature.user.domain.model.Employee + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EmployeeListScreen( + onNavigateBack: () -> Unit = {}, + viewModel: EmployeeListViewModel = hiltViewModel() +) { + val coroutineScope = rememberCoroutineScope() + val errorLabel = stringResource(R.string.common_error) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val employeeListPaged = viewModel.employeeListPaged.collectAsLazyPagingItems() + val pullRefreshState = rememberPullToRefreshState() + val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } + val sheetState = rememberModalBottomSheetState(true) + val selectedEmployee = uiState.selectedEmployee + val bottomSheetType = uiState.bottomSheetType + + LaunchedEffect(errorLabel) { + viewModel.bindLabel(errorLabel) + } + + LaunchedEffect(Unit) { + employeeListPaged.refresh() + } + + LaunchedEffect(selectedEmployee, bottomSheetType) { + if (selectedEmployee != null && bottomSheetType != null && !sheetState.isVisible) { + sheetState.show() + } else if ((selectedEmployee == null || bottomSheetType == null) && sheetState.isVisible) { + sheetState.hide() + } + } + + PullToRefreshBox( + isRefreshing = false, + onRefresh = { employeeListPaged.refresh() }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.employeeLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState + ) + } + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.employee_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + painter = painterResource(R.drawable.ic_arrow_back), + contentDescription = stringResource(R.string.nav_back) + ) + } + } + ) + } + ) { innerPadding -> + when (employeeListPaged.loadState.refresh) { + is LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is LoadState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { employeeListPaged.refresh() }, + modifier = Modifier.height(200.dp) + ) + } + } + + else -> { + if (employeeListPaged.loadState.refresh !is LoadState.Loading && employeeListPaged.itemCount == 0) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + EmptyContent( + message = stringResource(R.string.employee_empty_employee), + modifier = Modifier.height(200.dp) + ) + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + count = employeeListPaged.itemCount, + key = employeeListPaged.itemKey { it.userId } + ) { index -> + val employee = employeeListPaged[index] + if (employee != null) { + EmployeeListItemCard( + employee = employee, + onStatusClick = { + viewModel.onEvent(EmployeeListUiEvent.ShowStatusBottomSheet(employee)) + }, + onEditClick = { + viewModel.onEvent(EmployeeListUiEvent.ShowEditBottomSheet(employee)) + } + ) + } + } + + // 로딩 상태 처리 + item { + when (employeeListPaged.loadState.append) { + is LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is LoadState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.common_error), + color = FailRed + ) + } + } + + else -> {} + } + } + + item { Spacer(Modifier.height(100.dp)) } + } + } + } + } + } + } + + if (selectedEmployee != null && bottomSheetType != null) { + ModalBottomSheet( + onDismissRequest = { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + }, + sheetState = sheetState + ) { + when (bottomSheetType) { + EmployeeBottomSheetType.STATUS -> { + UpdateEmployeeStatusBottomSheet( + employee = selectedEmployee, + onDismiss = { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + }, + onStatusUpdated = { updatedEmployee -> + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + employeeListPaged.refresh() + } + ) + } + EmployeeBottomSheetType.EDIT -> { + EditEmployeeBottomSheet( + employee = selectedEmployee, + onDismiss = { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + }, + onEmployeeUpdated = { updatedEmployee -> + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + employeeListPaged.refresh() + } + ) + } + } + } + } +} + +@Composable +private fun EmployeeListItemCard( + employee: Employee, + onStatusClick: () -> Unit, + onEditClick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = employee.userName, + color = textColor(), + style = MaterialTheme.typography.titleLarge + ) + Text( + text = employeeStatusToKorean(employee.status), + color = textColor(), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Light + ) + } + + Text( + text = positionToKorean(employee.position), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Light + ) + + Spacer(Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_email), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = employee.email, + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_createdAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.createdAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_startedAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.startedAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_endedAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.endedAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_deletedAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.deletedAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Spacer(Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth() + ) { + CommonButton( + modifier = Modifier.weight(1F), + variant = ButtonVariant.Outlined, + size = ButtonSize.Large, + onClick = { onStatusClick() } + ) { + Text(stringResource(R.string.employee_status_edit)) + } + Spacer(Modifier.width(8.dp)) + CommonButton( + modifier = Modifier.weight(1F), + variant = ButtonVariant.Primary, + size = ButtonSize.Large, + onClick = { onEditClick() } + ) { + Text(stringResource(R.string.employee_position_edit)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt new file mode 100644 index 0000000..09e936e --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +interface EmployeeListUiEvent { + object LoadEmployeeList : EmployeeListUiEvent + object RetryEmployeeList : EmployeeListUiEvent + data class ShowEditBottomSheet(val employee: Employee) : EmployeeListUiEvent + data class ShowStatusBottomSheet(val employee: Employee) : EmployeeListUiEvent + object DismissBottomSheet : EmployeeListUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt new file mode 100644 index 0000000..134bcb8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +data class EmployeeListUiState( + val employeeList: List = emptyList(), + val employeeLoading: Boolean = false, + val employeeError: String? = null, + val selectedEmployee: Employee? = null, + val bottomSheetType: EmployeeBottomSheetType? = null +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt new file mode 100644 index 0000000..7747f03 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt @@ -0,0 +1,48 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.usecase.GetEmployeeUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class EmployeeListViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val getEmployeeUseCase: GetEmployeeUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "EmployeeListViewModel" + } + + private val _uiState = MutableStateFlow(EmployeeListUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + + fun bindLabel(error: String) { + errorLabel = error + } + + val employeeListPaged : Flow> = getEmployeeUseCase() + .cachedIn(viewModelScope) + + fun onEvent(event: EmployeeListUiEvent) { + when (event) { + is EmployeeListUiEvent.LoadEmployeeList -> {} + is EmployeeListUiEvent.RetryEmployeeList -> {} + is EmployeeListUiEvent.ShowEditBottomSheet -> _uiState.update { it.copy(selectedEmployee = event.employee, bottomSheetType = EmployeeBottomSheetType.EDIT) } + is EmployeeListUiEvent.ShowStatusBottomSheet -> _uiState.update { it.copy(selectedEmployee = event.employee, bottomSheetType = EmployeeBottomSheetType.STATUS) } + is EmployeeListUiEvent.DismissBottomSheet -> _uiState.update { it.copy(selectedEmployee = null) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt new file mode 100644 index 0000000..0685d3a --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt @@ -0,0 +1,115 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sampoom.android.R +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.CommonTextField +import com.sampoom.android.core.util.employeeStatusToKorean +import com.sampoom.android.feature.user.domain.model.Employee + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateEmployeeStatusBottomSheet( + employee: Employee, + onDismiss: () -> Unit, + onStatusUpdated: (Employee) -> Unit = {}, + viewModel: UpdateEmployeeStatusViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var selectedStatus by rememberSaveable { mutableStateOf(employee.status) } + var employeeStatusMenuExpanded by remember { mutableStateOf(false) } + + val errorLabel = stringResource(R.string.common_error) + val editEmployeeLabel = stringResource(R.string.employee_edit_status_edited) + + LaunchedEffect(employee) { + viewModel.onEvent(UpdateEmployeeStatusUiEvent.Initialize(employee)) + selectedStatus = employee.status + } + + LaunchedEffect(errorLabel, editEmployeeLabel) { + viewModel.bindLabel(errorLabel, editEmployeeLabel) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + uiState.employee?.let(onStatusUpdated) + viewModel.clearStatus() + onDismiss() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ExposedDropdownMenuBox( + expanded = employeeStatusMenuExpanded, + onExpandedChange = { employeeStatusMenuExpanded = it } + ) { + CommonTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor( + type = ExposedDropdownMenuAnchorType.PrimaryNotEditable, + enabled = true + ), + readOnly = true, + value = employeeStatusToKorean(selectedStatus), + onValueChange = {}, + placeholder = stringResource(R.string.employee_placeholder_status_edit), + singleLine = true + ) + ExposedDropdownMenu( + expanded = employeeStatusMenuExpanded, + onDismissRequest = { employeeStatusMenuExpanded = false } + ) { + EmployeeStatus.entries.forEach { status -> + DropdownMenuItem( + text = { Text(employeeStatusToKorean(status)) }, + onClick = { + selectedStatus = status + employeeStatusMenuExpanded = false + } + ) + } + } + } + + Spacer(Modifier.height(8.dp)) + + CommonButton( + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && selectedStatus != employee.status, + onClick = { viewModel.onEvent(UpdateEmployeeStatusUiEvent.EditEmployeeStatus(selectedStatus)) } + ) { + Text(stringResource(R.string.common_confirm)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt new file mode 100644 index 0000000..62fdbbd --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.feature.user.domain.model.Employee + +interface UpdateEmployeeStatusUiEvent { + data class Initialize(val employee: Employee) : UpdateEmployeeStatusUiEvent + data class EditEmployeeStatus(val employeeStatus: EmployeeStatus) : UpdateEmployeeStatusUiEvent + object Dismiss : UpdateEmployeeStatusUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt new file mode 100644 index 0000000..ca7456c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +data class UpdateEmployeeStatusUiState( + val employee: Employee? = null, + val isLoading: Boolean = false, + val error: String? = null, + val isSuccess: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt new file mode 100644 index 0000000..e08ede8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt @@ -0,0 +1,103 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.usecase.UpdateEmployeeStatusUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class UpdateEmployeeStatusViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val updateEmployeeStatusUseCase: UpdateEmployeeStatusUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "UpdateEmployeeStatusViewModel" + } + + private val _uiState = MutableStateFlow(UpdateEmployeeStatusUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + private var editEmployeeLabel: String = "" + + fun bindLabel(error: String, editEmployee: String) { + errorLabel = error + editEmployeeLabel = editEmployee + } + + fun onEvent(event: UpdateEmployeeStatusUiEvent) { + when (event) { + is UpdateEmployeeStatusUiEvent.Initialize -> { + _uiState.update { + it.copy( + employee = event.employee, + isLoading = false, + isSuccess = false + ) + } + } + is UpdateEmployeeStatusUiEvent.EditEmployeeStatus -> { + editEmployeeStatus(event.employeeStatus) + } + is UpdateEmployeeStatusUiEvent.Dismiss -> { + _uiState.update { + it.copy( + employee = null, + isLoading = false, + error = null + ) + } + } + } + } + + private fun editEmployeeStatus(newEmployeeStatus: EmployeeStatus) { + viewModelScope.launch { + val currentEmployee = _uiState.value.employee ?: run { + messageHandler.showMessage(message = errorLabel, isError = true) + return@launch + } + + val updateEmployee = currentEmployee.copy(status = newEmployeeStatus) + _uiState.update { it.copy(isLoading = true, error = null) } + + updateEmployeeStatusUseCase(updateEmployee, "AGENCY") + .onSuccess { employee -> + _uiState.update { + it.copy( + employee = employee, + isLoading = false, + error = null, + isSuccess = true + ) + } + messageHandler.showMessage(message = editEmployeeLabel, isError = false) + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + isLoading = false, + error = error + ) + } + } + } + } + + fun clearStatus() { + _uiState.update { it.copy(isSuccess = false, error = null) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt new file mode 100644 index 0000000..1c836f2 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt @@ -0,0 +1,84 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.CommonTextField +import com.sampoom.android.feature.user.domain.model.User + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateProfileBottomSheet( + user: User, + onDismiss: () -> Unit, + onProfileUpdated: (User) -> Unit = {}, + viewModel: UpdateProfileViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var userName by rememberSaveable { mutableStateOf(user.userName) } + + val errorLabel = stringResource(R.string.common_error) + val updateProfileLabel = stringResource(R.string.setting_edit_profile_edited) + + LaunchedEffect(user) { + viewModel.onEvent(UpdateProfileUiEvent.Initialize(user)) + userName = user.userName + } + + LaunchedEffect(errorLabel, updateProfileLabel) { + viewModel.bindLabel(errorLabel, updateProfileLabel) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + val updatedUser = uiState.user + if (updatedUser != null) onProfileUpdated(updatedUser) + viewModel.clearStatus() + onDismiss() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CommonTextField( + modifier = Modifier.fillMaxWidth(), + value = userName, + onValueChange = { userName = it }, + placeholder = stringResource(R.string.setting_edit_profile_placeholder_username), + singleLine = true + ) + + Spacer(Modifier.height(8.dp)) + + CommonButton( + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && userName.isNotBlank() && userName != user.userName, + onClick = { viewModel.onEvent(UpdateProfileUiEvent.UpdateProfile(userName)) } + ) { + Text(stringResource(R.string.setting_edit_profile)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt new file mode 100644 index 0000000..d58c24e --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt @@ -0,0 +1,9 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.User + +interface UpdateProfileUiEvent { + data class Initialize(val user: User) : UpdateProfileUiEvent + data class UpdateProfile(val userName: String) : UpdateProfileUiEvent + object Dismiss : UpdateProfileUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt new file mode 100644 index 0000000..93aa73f --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.User + +data class UpdateProfileUiState( + val user: User? = null, + val isLoading: Boolean = false, + val error: String? = null, + val isSuccess: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt new file mode 100644 index 0000000..83d91a1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt @@ -0,0 +1,100 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.usecase.UpdateProfileUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class UpdateProfileViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val updateProfileUseCase: UpdateProfileUseCase +) : ViewModel() { + + private companion object Companion { + private const val TAG = "UpdateProfileViewModel" + } + + private val _uiState = MutableStateFlow(UpdateProfileUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + private var updateProfileLabel: String = "" + + fun bindLabel(error: String, updateProfile: String) { + errorLabel = error + updateProfileLabel = updateProfile + } + + fun onEvent(event: UpdateProfileUiEvent) { + when (event) { + is UpdateProfileUiEvent.Initialize -> { + _uiState.update { + it.copy( + user = event.user, + isLoading = false, + isSuccess = false + ) + } + } + is UpdateProfileUiEvent.UpdateProfile -> { + updateProfile(event.userName) + } + is UpdateProfileUiEvent.Dismiss -> { + _uiState.update { + it.copy( + user = null + ) + } + } + } + } + + private fun updateProfile(newUserName: String) { + viewModelScope.launch { + val currentUser = _uiState.value.user ?: run { + messageHandler.showMessage(message = errorLabel, isError = true) + return@launch + } + + val updatedUser = currentUser.copy(userName = newUserName) + _uiState.update { it.copy(isLoading = true, error = null) } + + updateProfileUseCase(updatedUser) + .onSuccess { user -> + _uiState.update { + it.copy( + user = user, + isLoading = false, + error = null, + isSuccess = true + ) + } + messageHandler.showMessage(message = updateProfileLabel, isError = false) + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + isLoading = false, + error = error + ) + } + } + } + } + + fun clearStatus() { + _uiState.update { it.copy(isSuccess = false, error = null) } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1001f02..0e6f7a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,6 +114,20 @@ 입고 처리되었습니다 총 가격 + + 직원관리 + 직원이 없습니다. + 이메일 + 최초생성일 + 근무시작일 + 근무종료일 + 퇴사일 + 재직상태 변경 + 재직상태 선택 + 직급 변경 + 직원 수정이 완료되었습니다. + 직원 상태 수정이 완료되었습니다. + 대기중 주문확인 @@ -127,6 +141,8 @@ 설정 프로필 수정 + 이름 입력 + 프로필 수정이 완료되었습니다. 로그아웃 로그아웃 하시겠습니까?