From 6a29fcde8c413b8692f7886140ef123e4dfdef5b Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Tue, 14 Oct 2025 09:15:34 +0300 Subject: [PATCH 01/12] feat: implement Koin modules for dependency injection and add user preferences management --- .gitignore | 4 + composeApp/build.gradle.kts | 9 + .../src/androidMain/AndroidManifest.xml | 1 + .../kotlin/org/example/project/MyApp.kt | 10 +- .../datasource/DataStoreLocalDataSourceImp.kt | 99 +++++++++ .../org/example/project/di/AndroidModule.kt | 8 + .../kotlin/org/example/project/App.kt | 4 +- .../example/project/data/dto/CraftsmanDto.kt | 80 +++++++ .../datasource/StorageLocalDataSource.kt | 15 ++ .../data/local/datasource/UserPreferences.kt | 7 + .../local/datasource/UserPreferencesImpl.kt | 25 +++ .../project/data/mapper/CraftsmanMapper.kt | 68 ++++++ .../project/data/mapper/ExceptionMapper.kt | 47 ++++ .../datasource/CraftsmanRemoteDataSource.kt | 41 ++++ .../CraftsmanRemoteDataSourceImpl.kt | 121 ++++++++++ .../data/remote/network/APIConstant.kt | 41 ++++ .../data/remote/network/ApiCallWrapper.kt | 58 +++++ .../data/remote/network/HttpClientFactory.kt | 42 ++++ .../repository/CraftsmanRepositoryImpl.kt | 112 ++++++++++ .../org/example/project/di/CraftoModule.kt | 16 +- .../org/example/project/di/DataModule.kt | 18 ++ .../org/example/project/di/NetworkModule.kt | 77 ++++--- .../org/example/project/di/domainModule.kt | 16 ++ .../kotlin/org/example/project/di/initKoin.kt | 6 +- .../project/domain/entity/Craftsman.kt | 41 ++++ .../domain/exception/DomainExceptions.kt | 12 + .../example/project/domain/model/WorkImage.kt | 24 ++ .../domain/repository/CraftsmanRepository.kt | 33 +++ .../CreateCraftsmanProfileUseCase.kt | 53 +++++ .../DeleteCraftsmanAccountUseCase.kt | 27 +++ .../craftsman/GetCraftsmanProfileUseCase.kt | 14 ++ .../craftsman/GetCraftsmanStatusUseCase.kt | 21 ++ .../usecase/craftsman/UploadIdCardsUseCase.kt | 77 +++++++ .../craftsman/UploadWorkPortfolioUseCase.kt | 53 +++++ .../setupScreens/IntegrationTestScreen.kt | 209 ++++++++++++++++++ .../project/CraftsmanRepositoryTest.kt | 116 ++++++++++ .../org/example/project/MainViewController.kt | 5 +- .../datasource/StorageLocalDataSourceImpl.kt | 74 +++++++ .../org/example/project/di/IosModule.kt | 9 + gradle/libs.versions.toml | 33 +-- 40 files changed, 1668 insertions(+), 58 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt create mode 100644 composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferences.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/model/WorkImage.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt create mode 100644 composeApp/src/commonTest/kotlin/org/example/project/CraftsmanRepositoryTest.kt create mode 100644 composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt create mode 100644 composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt diff --git a/.gitignore b/.gitignore index e37364f..225937e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ captures **/xcshareddata/WorkspaceSettings.xcsettings # Project exclude paths + +### Google Service ### +**/google-service.json +google-service.json diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index bf0c799..20f5532 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -65,6 +65,7 @@ kotlin { api(libs.koin.annotations) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.annotations) implementation(libs.ktor.client.core) implementation(libs.ktor.serialization.kotlinx.json) @@ -73,10 +74,18 @@ kotlin { implementation(libs.bundles.coil) + implementation(libs.kotlinx.datetime) + + implementation(libs.androidx.datastore.preferences) + } commonTest.dependencies { implementation(libs.kotlin.test) + + implementation(libs.kotlinx.coroutines.test) + implementation(libs.ktor.client.mock) + implementation(libs.koin.test) } iosMain.dependencies { diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index f5bc803..5bf9760 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -9,6 +9,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:usesCleartextTraffic="true" android:theme="@android:style/Theme.Material.Light.NoActionBar"> by preferencesDataStore( + name = "crafto_preferences" +) + +@Single +class StorageLocalDataSourceImpl( + private val context: Context +) : StorageLocalDataSource { + + private val dataStore = context.dataStore + + override suspend fun saveString(key: String, value: String) { + val preferencesKey = stringPreferencesKey(key) + dataStore.edit { preferences -> + preferences[preferencesKey] = value + } + } + + override suspend fun getString(key: String): String? { + val preferencesKey = stringPreferencesKey(key) + return dataStore.data.map { preferences -> + preferences[preferencesKey] + }.first() + } + + override suspend fun saveStringSet(key: String, values: Set) { + val preferencesKey = stringSetPreferencesKey(key) + dataStore.edit { preferences -> + preferences[preferencesKey] = values + } + } + + override suspend fun getStringSet(key: String): Set? { + val preferencesKey = stringSetPreferencesKey(key) + return dataStore.data.map { preferences -> + preferences[preferencesKey] + }.first() + } + + override suspend fun saveLong(key: String, value: Long) { + val preferencesKey = longPreferencesKey(key) + dataStore.edit { preferences -> + preferences[preferencesKey] = value + } + } + + override suspend fun getLong(key: String): Long? { + val preferencesKey = longPreferencesKey(key) + return dataStore.data.map { preferences -> + preferences[preferencesKey] + }.first() + } + + override suspend fun saveBoolean(key: String, value: Boolean) { + val preferencesKey = booleanPreferencesKey(key) + dataStore.edit { preferences -> + preferences[preferencesKey] = value + } + } + + override suspend fun getBoolean(key: String): Boolean? { + val preferencesKey = booleanPreferencesKey(key) + return dataStore.data.map { preferences -> + preferences[preferencesKey] + }.first() + } + + override suspend fun remove(key: String) { + dataStore.edit { preferences -> + preferences.remove(stringPreferencesKey(key)) + preferences.remove(intPreferencesKey(key)) + preferences.remove(longPreferencesKey(key)) + preferences.remove(booleanPreferencesKey(key)) + preferences.remove(stringSetPreferencesKey(key)) + } + } + + override suspend fun removeAll() { + dataStore.edit { preferences -> + preferences.clear() + } + } + + override suspend fun getAllKeys(): Set { + return dataStore.data.map { preferences -> + preferences.asMap().keys.map { it.name }.toSet() + }.first() + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt new file mode 100644 index 0000000..d25e681 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt @@ -0,0 +1,8 @@ +package org.example.project.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.example.project.data.datasource.local") +class AndroidModule \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index 1236764..975916b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -3,12 +3,14 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.onboarding.OnboardingScreen +import org.example.project.presentation.screens.setupScreens.TestScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @Preview fun App() { AppTheme { - OnboardingScreen() + //OnboardingScreen() + TestScreen() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt new file mode 100644 index 0000000..f96efc1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt @@ -0,0 +1,80 @@ +package org.example.project.data.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateCraftsmanRequest( + val personalInfo: PersonalInfoDto, + val categories: List +) + +@Serializable +data class CraftsmanSetupResponseDto( + val craftsmanId: String, + val status: String, + val message: String +) + +@Serializable +data class IdCardUploadResponseDto( + val craftsmanId: String, + val idCardFrontUrl: String, + val idCardBackUrl: String, + val message: String, + val idVerificationStatus: String +) + +@Serializable +data class WorkPortfolioResponseDto( + val craftsmanId: String, + val workImageUrls: List, + val message: String, + val totalImages: Int +) + +@Serializable +data class CraftsmanProfileResponseDto( + val craftsmanId: String, + val personalInfo: PersonalInfoDto, + val categories: List, + val status: String, + val verificationInfo: VerificationInfoDto, + val createdAt: String +) + +@Serializable +data class CraftsmanStatusResponseDto( + val craftsmanId: String, + val status: String, + val verificationStatus: String, + val message: String +) + +@Serializable +data class DeleteAccountResponseDto( + val success: Boolean, + val message: String +) + +@Serializable +data class PersonalInfoDto( + val firstName: String, + val lastName: String, + val phoneNumber: String, + val address: String +) + +@Serializable +data class VerificationInfoDto( + val status: String, + val idCardFrontUrl: String?, + val idCardBackUrl: String?, + val workPortfolioUrls: List +) + +@Serializable +data class ErrorResponseDto( + val code: String, + val message: String, + val timestamp: String +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt new file mode 100644 index 0000000..d2ef580 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt @@ -0,0 +1,15 @@ +package org.example.project.data.local.datasource + +interface StorageLocalDataSource { + suspend fun saveString(key: String, value: String) + suspend fun getString(key: String): String? + suspend fun saveStringSet(key: String, values: Set) + suspend fun getStringSet(key: String): Set? + suspend fun saveLong(key: String, value: Long) + suspend fun getLong(key: String): Long? + suspend fun saveBoolean(key: String, value: Boolean) + suspend fun getBoolean(key: String): Boolean? + suspend fun remove(key: String) + suspend fun removeAll() + suspend fun getAllKeys(): Set +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferences.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferences.kt new file mode 100644 index 0000000..c4227af --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferences.kt @@ -0,0 +1,7 @@ +package org.example.project.data.local.datasource + +interface UserPreferences { + suspend fun getUserId(): String? + suspend fun setUserId(userId: String) + suspend fun clearUserId() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt new file mode 100644 index 0000000..829a740 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt @@ -0,0 +1,25 @@ +package org.example.project.data.local.datasource + +import org.koin.core.annotation.Single + +@Single +class UserPreferencesImpl( + private val storage: StorageLocalDataSource +) : UserPreferences { + + companion object { + private const val KEY_USER_ID = "user_id" + } + + override suspend fun getUserId(): String? { + return storage.getString(KEY_USER_ID) + } + + override suspend fun setUserId(userId: String) { + storage.saveString(KEY_USER_ID, userId) + } + + override suspend fun clearUserId() { + storage.remove(KEY_USER_ID) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt new file mode 100644 index 0000000..ab7633d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt @@ -0,0 +1,68 @@ +package org.example.project.data.mapper + +import org.example.project.data.dto.CraftsmanProfileResponseDto +import org.example.project.data.dto.PersonalInfoDto +import org.example.project.data.dto.VerificationInfoDto +import org.example.project.domain.entity.Craftsman +import org.example.project.domain.entity.CraftsmanStatus +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.entity.VerificationDocuments +import org.example.project.domain.entity.VerificationStatus +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +fun CraftsmanProfileResponseDto.toDomain(): Craftsman { + return Craftsman( + craftsmanId = craftsmanId, + personalInfo = personalInfo.toDomain(), + categories = categories, + status = CraftsmanStatus.valueOf(status), + verificationStatus = VerificationStatus.valueOf(verificationInfo.status), + verification = verificationInfo.toVerificationDocuments(), + createdAt = Instant.parse(createdAt) + ) +} + +fun VerificationInfoDto.toVerificationDocuments(): VerificationDocuments { + return VerificationDocuments( + idCardFrontUrl = idCardFrontUrl, + idCardBackUrl = idCardBackUrl, + workPortfolioUrls = workPortfolioUrls + ) +} + +fun PersonalInfoDto.toDomain(): PersonalInfo { + return PersonalInfo( + firstName = firstName, + lastName = lastName, + phoneNumber = phoneNumber, + address = address + ) +} + +fun PersonalInfo.toDto(): PersonalInfoDto { + return PersonalInfoDto( + firstName = firstName, + lastName = lastName, + phoneNumber = phoneNumber, + address = address + ) +} + +fun String.toCraftsmanStatus(): CraftsmanStatus { + return try { + CraftsmanStatus.valueOf(this) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid craftsman status: $this") + } +} + +fun String.toVerificationStatus(): VerificationStatus { + return try { + VerificationStatus.valueOf(this) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid verification status: $this") + } +} + diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt new file mode 100644 index 0000000..8ae0a0d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt @@ -0,0 +1,47 @@ +package org.example.project.data.mapper + +import org.example.project.data.dto.ErrorResponseDto +import org.example.project.domain.exception.* + +fun Exception.toDomainException(): CraftoException { + return when (this) { + is CraftoException -> this + is IllegalArgumentException -> ValidationException(message ?: "Invalid input") + is NoSuchElementException -> NotFoundException(message ?: "Resource not found") + is IllegalStateException -> this.toIllegalStateError() + else -> NetworkException(message ?: "Network error occurred") + } +} + +private fun IllegalStateException.toIllegalStateError(): CraftoException { + return if (isAuthenticationError()) { + UnauthorizedException(message?: "User not authenticated") + } else { + UnknownException(message ?: "Invalid state") + } +} + +private fun IllegalStateException.isAuthenticationError(): Boolean { + return message?.contains("auth", ignoreCase = true) == true +} + +fun ErrorResponseDto.toDomainException(): CraftoException { + return when (code) { + "CRAFTSMAN_EXISTS" -> AlreadyExistsException("Craftsman profile already exists") + "INVALID_INPUT" -> ValidationException(message) + "NOT_FOUND" -> NotFoundException(message) + "FORBIDDEN" -> ForbiddenException(message) + "UNAUTHORIZED" -> UnauthorizedException(message) + else -> UnknownException(message) + } +} + +suspend fun safeApiCall( + apiCall: suspend () -> T +): Result { + return try { + Result.success(apiCall()) + } catch (e: Exception) { + Result.failure(e.toDomainException() as Exception) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt new file mode 100644 index 0000000..4e3dfa1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt @@ -0,0 +1,41 @@ +package org.example.project.data.remote.datasource + +import org.example.project.data.dto.CraftsmanProfileResponseDto +import org.example.project.data.dto.CraftsmanSetupResponseDto +import org.example.project.data.dto.CraftsmanStatusResponseDto +import org.example.project.data.dto.CreateCraftsmanRequest +import org.example.project.data.dto.DeleteAccountResponseDto +import org.example.project.data.dto.IdCardUploadResponseDto +import org.example.project.data.dto.WorkPortfolioResponseDto +import org.example.project.domain.model.WorkImage + +interface CraftsmanRemoteDataSource { + suspend fun createCraftsmanProfile( + userId: String, + request: CreateCraftsmanRequest + ): CraftsmanSetupResponseDto + + suspend fun uploadIdCards( + userId: String, + craftsmanId: String, + idCardFront: ByteArray, + idCardFrontFileName: String, + idCardBack: ByteArray, + idCardBackFileName: String + ): IdCardUploadResponseDto + + suspend fun uploadWorkPortfolio( + userId: String, + craftsmanId: String, + workImages: List + ): WorkPortfolioResponseDto + + suspend fun getCraftsmanProfile(userId: String): CraftsmanProfileResponseDto + + suspend fun getCraftsmanStatus(craftsmanId: String): CraftsmanStatusResponseDto + + suspend fun deleteCraftsmanAccount( + userId: String, + craftsmanId: String + ): DeleteAccountResponseDto +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt new file mode 100644 index 0000000..9ac0169 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -0,0 +1,121 @@ +package org.example.project.data.remote.datasource + +import io.ktor.client.HttpClient +import io.ktor.client.request.delete +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.client.request.get +import io.ktor.client.request.header +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.contentType +import org.example.project.data.dto.CraftsmanProfileResponseDto +import org.example.project.data.dto.CraftsmanSetupResponseDto +import org.example.project.data.dto.CraftsmanStatusResponseDto +import org.example.project.data.dto.CreateCraftsmanRequest +import org.example.project.data.dto.DeleteAccountResponseDto +import org.example.project.data.dto.IdCardUploadResponseDto +import org.example.project.data.dto.WorkPortfolioResponseDto +import org.example.project.data.remote.network.ApiConstants +import org.example.project.data.remote.network.ApiConstants.BASE_URL +import org.example.project.data.remote.network.ApiConstants.Headers.USER_ID +import org.example.project.data.remote.network.wrapApiCall +import org.example.project.domain.model.WorkImage +import org.koin.core.annotation.Single + +@Single +class CraftsmanRemoteDataSourceImpl( + private val httpClient: HttpClient, +) : CraftsmanRemoteDataSource { + private val baseUrl: String = BASE_URL + override suspend fun createCraftsmanProfile( + userId: String, + request: CreateCraftsmanRequest + ): CraftsmanSetupResponseDto { + return wrapApiCall { + httpClient.post(baseUrl + ApiConstants.Endpoints.CRAFTSMAN_SETUP) { + header(ApiConstants.Headers.USER_ID, userId) + contentType(ContentType.Application.Json) + setBody(request) + } + } + } + + override suspend fun uploadIdCards( + userId: String, + craftsmanId: String, + idCardFront: ByteArray, + idCardFrontFileName: String, + idCardBack: ByteArray, + idCardBackFileName: String + ): IdCardUploadResponseDto { + return wrapApiCall { + httpClient.submitFormWithBinaryData( + url = baseUrl + ApiConstants.Endpoints.craftsmanIdCards(craftsmanId), + formData = formData { + append("idCardFront", idCardFront, Headers.build { + append(HttpHeaders.ContentType, "image/*") + append(HttpHeaders.ContentDisposition, "filename=\"$idCardFrontFileName\"") + }) + append("idCardBack", idCardBack, Headers.build { + append(HttpHeaders.ContentType, "image/*") + append(HttpHeaders.ContentDisposition, "filename=\"$idCardBackFileName\"") + }) + } + ) { + header(USER_ID, userId) + } + } + } + + override suspend fun uploadWorkPortfolio( + userId: String, + craftsmanId: String, + workImages: List + ): WorkPortfolioResponseDto { + return wrapApiCall { + httpClient.submitFormWithBinaryData( + url = baseUrl + ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId), + formData = formData { + workImages.forEach { image -> + append("workImages", image.data, Headers.build { + append(HttpHeaders.ContentType, "image/*") + append(HttpHeaders.ContentDisposition, "filename=\"${image.fileName}\"") + }) + } + } + ) { + header(USER_ID, userId) + } + } + } + + override suspend fun getCraftsmanProfile(userId: String): CraftsmanProfileResponseDto { + return wrapApiCall { + httpClient.get(baseUrl + ApiConstants.Endpoints.CRAFTSMAN_PROFILE) { + header(ApiConstants.Headers.USER_ID, userId) + } + } + } + + override suspend fun getCraftsmanStatus(craftsmanId: String): CraftsmanStatusResponseDto { + return wrapApiCall { + httpClient.get(baseUrl + ApiConstants.Endpoints.craftsmanStatus(craftsmanId)) + } + } + + override suspend fun deleteCraftsmanAccount( + userId: String, + craftsmanId: String + ): DeleteAccountResponseDto { + return wrapApiCall { + httpClient.delete(baseUrl + ApiConstants.Endpoints.deleteCraftsman(craftsmanId)) { + header(USER_ID, userId) + } + } + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt new file mode 100644 index 0000000..fbc3527 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt @@ -0,0 +1,41 @@ +package org.example.project.data.remote.network + +object ApiConstants { + const val BASE_URL = "http://192.168.1.52:8085" + + // Headers + object Headers { + const val USER_ID = "userId" + const val AUTH_TOKEN = "Authorization" + const val CONTENT_TYPE = "Content-Type" + const val ACCEPT = "Accept" + } + + // Timeouts (in milliseconds) + object Timeouts { + const val REQUEST = 30_000L // 30 seconds + const val CONNECT = 10_000L // 10 seconds + const val SOCKET = 30_000L // 30 seconds + } + + // File Upload + object FileUpload { + const val MAX_FILE_SIZE = 4 * 1024 * 1024 // 4MB + val ALLOWED_IMAGE_TYPES = listOf("jpg", "jpeg", "png") + const val MAX_PORTFOLIO_IMAGES = 4 + } + + // API Endpoints + object Endpoints { + // Craftsman endpoints + const val CRAFTSMAN_SETUP = "/craftsman/setup" + const val CRAFTSMAN_PROFILE = "/craftsman/profile" + + fun craftsmanIdCards(craftsmanId: String) = "/craftsman/$craftsmanId/verify/id-cards" + fun craftsmanWorkPortfolio(craftsmanId: String) = "/craftsman/$craftsmanId/verify/work-portfolio" + fun craftsmanStatus(craftsmanId: String) = "/craftsman/$craftsmanId/status" + fun deleteCraftsman(craftsmanId: String) = "/craftsman/$craftsmanId" + + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt new file mode 100644 index 0000000..8fc146a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt @@ -0,0 +1,58 @@ +package org.example.project.data.remote.network + +import io.ktor.client.call.body +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode +import org.example.project.data.dto.ErrorResponseDto +import org.example.project.domain.exception.AlreadyExistsException +import org.example.project.domain.exception.ApiException +import org.example.project.domain.exception.CraftoException +import org.example.project.domain.exception.ForbiddenException +import org.example.project.domain.exception.NetworkException +import org.example.project.domain.exception.NotFoundException +import org.example.project.domain.exception.UnauthorizedException + +suspend inline fun wrapApiCall( + apiCall: suspend () -> HttpResponse +): T { + try { + val response = apiCall() + + return when (response.status) { + HttpStatusCode.OK, HttpStatusCode.Created -> { + response.body() + } + HttpStatusCode.Unauthorized -> { + throw UnauthorizedException("Authentication required") + } + HttpStatusCode.Forbidden -> { + throw ForbiddenException("Access denied") + } + HttpStatusCode.NotFound -> { + throw NotFoundException("Resource not found") + } + HttpStatusCode.Conflict -> { + val error = try { + response.body() + } catch (e: Exception) { + null + } + throw AlreadyExistsException(error?.message ?: "Resource already exists") + } + else -> { + val error = try { + response.body() + } catch (e: Exception) { + null + } + throw ApiException(error?.message ?: "API error: ${response.status.value}") + } + } + } catch (e: CraftoException) { + throw e + } catch (e: io.ktor.client.network.sockets.SocketTimeoutException) { + throw NetworkException("Connection timeout") + } catch (e: Exception) { + throw NetworkException("Network error: ${e.message}") + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt new file mode 100644 index 0000000..09e9dc8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt @@ -0,0 +1,42 @@ +package org.example.project.data.remote.network + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.DEFAULT +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.header +import io.ktor.http.ContentType +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +fun createHttpClient(): HttpClient { + return HttpClient { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + encodeDefaults = true + }) + } + + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.ALL + } + + install(HttpTimeout) { + requestTimeoutMillis = ApiConstants.Timeouts.REQUEST + connectTimeoutMillis = ApiConstants.Timeouts.CONNECT + socketTimeoutMillis = ApiConstants.Timeouts.SOCKET + } + + defaultRequest { + header(ApiConstants.Headers.ACCEPT, ContentType.Application.Json) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt new file mode 100644 index 0000000..bdb86eb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt @@ -0,0 +1,112 @@ +package org.example.project.data.repository + +import org.example.project.data.dto.CreateCraftsmanRequest +import org.example.project.data.local.datasource.UserPreferences +import org.example.project.data.mapper.toDomain +import org.example.project.data.mapper.toDto +import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource +import org.example.project.domain.entity.Craftsman +import org.example.project.domain.entity.CraftsmanStatus +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.entity.VerificationDocuments +import org.example.project.domain.exception.ApiException +import org.example.project.domain.exception.UnauthorizedException +import org.example.project.domain.model.WorkImage +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Single + +@Single +class CraftsmanRepositoryImpl ( + private val remoteDataSource: CraftsmanRemoteDataSource, + private val userPreferences: UserPreferences +) : CraftsmanRepository { + override suspend fun createCraftsmanProfile( + personalInfo: PersonalInfo, + categories: List + ): String { + val userId = userPreferences.getUserId() + ?: throw UnauthorizedException("User must be logged in to create craftsman profile") + + val request = CreateCraftsmanRequest( + personalInfo = personalInfo.toDto(), + categories = categories + ) + + val response = remoteDataSource.createCraftsmanProfile(userId, request) + + return response.craftsmanId + } + + override suspend fun uploadIdCards( + craftsmanId: String, + idCardFront: ByteArray, + idCardFrontFileName: String, + idCardBack: ByteArray, + idCardBackFileName: String + ): VerificationDocuments { + val userId = userPreferences.getUserId() + ?: throw UnauthorizedException("User must be logged in to upload documents") + + val response = remoteDataSource.uploadIdCards( + userId = userId, + craftsmanId = craftsmanId, + idCardFront = idCardFront, + idCardFrontFileName = idCardFrontFileName, + idCardBack = idCardBack, + idCardBackFileName = idCardBackFileName + ) + + return VerificationDocuments( + idCardFrontUrl = response.idCardFrontUrl, + idCardBackUrl = response.idCardBackUrl, + workPortfolioUrls = emptyList() + ) + } + + override suspend fun uploadWorkPortfolio( + craftsmanId: String, + workImages: List + ): List { + val userId = userPreferences.getUserId() + ?: throw UnauthorizedException() + + val response = remoteDataSource.uploadWorkPortfolio( + userId, + craftsmanId, + workImages + ) + + return response.workImageUrls + } + + override suspend fun getCraftsmanProfile(): Craftsman { + val userId = userPreferences.getUserId() + ?: throw UnauthorizedException() + + val response = remoteDataSource.getCraftsmanProfile(userId) + return response.toDomain() + } + + override suspend fun getCraftsmanStatus(craftsmanId: String): CraftsmanStatus { + val response = remoteDataSource.getCraftsmanStatus(craftsmanId) + return try { + CraftsmanStatus.valueOf(response.status) + } catch (e: IllegalArgumentException) { + throw ApiException("Invalid craftsman status: ${response.status}") + } + } + + override suspend fun deleteCraftsmanAccount(craftsmanId: String) { + + val userId = userPreferences.getUserId() + ?: throw UnauthorizedException() + + val response = remoteDataSource.deleteCraftsmanAccount(userId, craftsmanId) + + if (!response.success) { + throw ApiException(response.message) + } + userPreferences.clearUserId() + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt index 032127c..874eb67 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt @@ -1,8 +1,8 @@ -package org.example.project.di - -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Module - -@Module -@ComponentScan("org.example.project") -class CraftoModule \ No newline at end of file +//package org.example.project.di +// +//import org.koin.core.annotation.ComponentScan +//import org.koin.core.annotation.Module +// +//@Module +//@ComponentScan("org.example.project") +//class CraftoModule \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt new file mode 100644 index 0000000..75794f3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -0,0 +1,18 @@ +package org.example.project.di + +import io.ktor.client.HttpClient +import org.example.project.data.local.datasource.StorageLocalDataSource +import org.example.project.data.local.datasource.UserPreferences +import org.example.project.data.local.datasource.UserPreferencesImpl +import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource +import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl +import org.example.project.data.remote.network.ApiConstants +import org.example.project.data.repository.CraftsmanRepositoryImpl +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +@ComponentScan("org.example.project.data") +class DataModule \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt index 110d1ba..e5dcdda 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt @@ -9,45 +9,56 @@ import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import org.example.project.data.remote.network.createHttpClient +import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module import org.koin.core.annotation.Single -//192.168.1.15 +////192.168.1.15 +//@Module +//class NetworkModule { +// @Single +// fun provideHttpClient(): HttpClient { +// return HttpClient(engine = getHttpEngine()) { +// defaultRequest { +// url("http://10.0.2.2:8085/") +// } +// +// install(Logging) { +// level = LogLevel.ALL +// logger = object : Logger { +// override fun log(message: String) { +// println(message) +// } +// } +// } +// +// install(HttpTimeout) { +// connectTimeoutMillis = TIME_OUT_INTERVAL_MILLI +// requestTimeoutMillis = TIME_OUT_INTERVAL_MILLI +// } +// +// install(ContentNegotiation) { +// json( +// Json { +// isLenient = true +// ignoreUnknownKeys = true +// } +// ) +// } +// } +// } +// +// private companion object { +// const val TIME_OUT_INTERVAL_MILLI = 10_000L +// } +//} + @Module +@ComponentScan("org.example.project.data.network") class NetworkModule { @Single fun provideHttpClient(): HttpClient { - return HttpClient(engine = getHttpEngine()) { - defaultRequest { - url("http://10.0.2.2:8085/") - } - - install(Logging) { - level = LogLevel.ALL - logger = object : Logger { - override fun log(message: String) { - println(message) - } - } - } - - install(HttpTimeout) { - connectTimeoutMillis = TIME_OUT_INTERVAL_MILLI - requestTimeoutMillis = TIME_OUT_INTERVAL_MILLI - } - - install(ContentNegotiation) { - json( - Json { - isLenient = true - ignoreUnknownKeys = true - } - ) - } - } - } - - private companion object { - const val TIME_OUT_INTERVAL_MILLI = 10_000L + return createHttpClient() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt new file mode 100644 index 0000000..a1ebe8b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt @@ -0,0 +1,16 @@ +package org.example.project.di + +import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase +import org.example.project.domain.usecase.craftsman.DeleteCraftsmanAccountUseCase +import org.example.project.domain.usecase.craftsman.GetCraftsmanProfileUseCase +import org.example.project.domain.usecase.craftsman.GetCraftsmanStatusUseCase +import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase +import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.example.project.domain.usecase") +class DomainModule \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt index a4aba07..b36339f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt @@ -7,6 +7,10 @@ import org.koin.ksp.generated.module fun initKoin(config: KoinAppDeclaration? = null) { startKoin { config?.invoke(this) - modules(CraftoModule().module, NetworkModule().module) + modules( + NetworkModule().module, + DataModule().module, + DomainModule().module, + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt new file mode 100644 index 0000000..d078a38 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt @@ -0,0 +1,41 @@ +package org.example.project.domain.entity + +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +data class Craftsman @OptIn(ExperimentalTime::class) constructor( + val craftsmanId: String, + val personalInfo: PersonalInfo, + val categories: List, + val status: CraftsmanStatus, + val verificationStatus: VerificationStatus, + val verification: VerificationDocuments, + val createdAt: Instant, +) + +data class PersonalInfo( + val firstName: String, + val lastName: String, + val phoneNumber: String, + val address: String +) + +data class VerificationDocuments( + val idCardFrontUrl: String? = null, + val idCardBackUrl: String? = null, + val workPortfolioUrls: List = emptyList() +) + +enum class CraftsmanStatus { + PENDING_VERIFICATION, + ACTIVE, + SUSPENDED, + REJECTED +} + +enum class VerificationStatus { + NOT_SUBMITTED, + PENDING, + VERIFIED, + REJECTED +} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt new file mode 100644 index 0000000..769c148 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt @@ -0,0 +1,12 @@ +package org.example.project.domain.exception + +sealed class CraftoException(message: String?) : Exception(message) + +class UnauthorizedException(message: String? = "User not authenticated") : CraftoException(message) +class ForbiddenException(message: String? = "Access denied") : CraftoException(message) +class NotFoundException(message: String?) : CraftoException(message) +class AlreadyExistsException(message: String?) : CraftoException(message) +class ValidationException(message: String?) : CraftoException(message) +class NetworkException(message: String? = "Network error occurred") : CraftoException(message) +class ApiException(message: String?) : CraftoException(message) +class UnknownException(message: String? = "Unknown error occurred") : CraftoException(message) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/model/WorkImage.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/model/WorkImage.kt new file mode 100644 index 0000000..bf23f83 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/model/WorkImage.kt @@ -0,0 +1,24 @@ +package org.example.project.domain.model + +data class WorkImage( + val data: ByteArray, + val fileName: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as WorkImage + + if (!data.contentEquals(other.data)) return false + if (fileName != other.fileName) return false + + return true + } + + override fun hashCode(): Int { + var result = data.contentHashCode() + result = 31 * result + fileName.hashCode() + return result + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt new file mode 100644 index 0000000..986c870 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt @@ -0,0 +1,33 @@ +package org.example.project.domain.repository + +import org.example.project.domain.entity.Craftsman +import org.example.project.domain.entity.CraftsmanStatus +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.entity.VerificationDocuments +import org.example.project.domain.model.WorkImage + +interface CraftsmanRepository { + suspend fun createCraftsmanProfile( + personalInfo: PersonalInfo, + categories: List + ): String + + suspend fun uploadIdCards( + craftsmanId: String, + idCardFront: ByteArray, + idCardFrontFileName: String, + idCardBack: ByteArray, + idCardBackFileName: String + ): VerificationDocuments + + suspend fun uploadWorkPortfolio( + craftsmanId: String, + workImages: List + ): List + + suspend fun getCraftsmanProfile(): Craftsman + + suspend fun getCraftsmanStatus(craftsmanId: String): CraftsmanStatus + + suspend fun deleteCraftsmanAccount(craftsmanId: String) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt new file mode 100644 index 0000000..be3c6c9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt @@ -0,0 +1,53 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Factory + +@Factory +class CreateCraftsmanProfileUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke( + personalInfo: PersonalInfo, + categories: List + ): String { + // Business validation - throw ValidationException for any invalid input + + // Validate categories + if (categories.isEmpty()) { + throw ValidationException("Please select at least one service category") + } + + // Validate personal info + if (personalInfo.firstName.isBlank()) { + throw ValidationException("First name is required") + } + + if (personalInfo.lastName.isBlank()) { + throw ValidationException("Last name is required") + } + + if (personalInfo.phoneNumber.isBlank()) { + throw ValidationException("Phone number is required") + } + + if (!isValidPhoneNumber(personalInfo.phoneNumber)) { + throw ValidationException("Please enter a valid phone number") + } + + if (personalInfo.address.isBlank()) { + throw ValidationException("Address is required") + } + + // All validation passed - call repository + // No try-catch needed - let exceptions propagate to ViewModel + return repository.createCraftsmanProfile(personalInfo, categories) + } + + private fun isValidPhoneNumber(phone: String): Boolean { + // Basic phone validation - accepts international format + return phone.matches(Regex("^\\+?[1-9]\\d{1,14}$")) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt new file mode 100644 index 0000000..c87b221 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt @@ -0,0 +1,27 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Factory + +@Factory +class DeleteCraftsmanAccountUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke( + craftsmanId: String, + confirmDelete: Boolean = false + ) { + // Business validation + if (craftsmanId.isBlank()) { + throw ValidationException("Craftsman ID is required") + } + + if (!confirmDelete) { + throw ValidationException("Please confirm account deletion") + } + + // Direct repository call + repository.deleteCraftsmanAccount(craftsmanId) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt new file mode 100644 index 0000000..b7433fd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt @@ -0,0 +1,14 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.domain.entity.Craftsman +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Factory + +@Factory +class GetCraftsmanProfileUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke(): Craftsman { + return repository.getCraftsmanProfile() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt new file mode 100644 index 0000000..16d0913 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt @@ -0,0 +1,21 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.domain.entity.CraftsmanStatus +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Factory + +@Factory +class GetCraftsmanStatusUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke(craftsmanId: String): CraftsmanStatus { + // Business validation + if (craftsmanId.isBlank()) { + throw ValidationException("Craftsman ID is required") + } + + // Direct repository call + return repository.getCraftsmanStatus(craftsmanId) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt new file mode 100644 index 0000000..81b4494 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt @@ -0,0 +1,77 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.data.remote.network.ApiConstants +import org.example.project.domain.entity.VerificationDocuments +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Factory + +@Factory +class UploadIdCardsUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke( + craftsmanId: String, + idCardFront: ByteArray, + idCardFrontFileName: String, + idCardBack: ByteArray, + idCardBackFileName: String + ): VerificationDocuments { + // Validate craftsman ID + if (craftsmanId.isBlank()) { + throw ValidationException("Craftsman ID is required") + } + + // Validate file sizes + if (idCardFront.isEmpty()) { + throw ValidationException("Please select front ID card image") + } + + if (idCardBack.isEmpty()) { + throw ValidationException("Please select back ID card image") + } + + if (idCardFront.size > ApiConstants.FileUpload.MAX_FILE_SIZE) { + throw ValidationException("Front ID card image size must be less than 4MB") + } + + if (idCardBack.size > ApiConstants.FileUpload.MAX_FILE_SIZE) { + throw ValidationException("Back ID card image size must be less than 4MB") + } + + // Validate file names (must have extensions) + if (!idCardFrontFileName.contains(".")) { + throw ValidationException("Invalid front ID card file name") + } + + if (!idCardBackFileName.contains(".")) { + throw ValidationException("Invalid back ID card file name") + } + + // Validate file types + val frontExtension = idCardFrontFileName.substringAfterLast('.', "").lowercase() + val backExtension = idCardBackFileName.substringAfterLast('.', "").lowercase() + + if (frontExtension !in ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + throw ValidationException( + "Front ID card must be one of: ${ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + ) + } + + if (backExtension !in ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + throw ValidationException( + "Back ID card must be one of: ${ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + ) + } + + // All validation passed - call repository + // No error handling - let exceptions propagate + return repository.uploadIdCards( + craftsmanId = craftsmanId, + idCardFront = idCardFront, + idCardFrontFileName = idCardFrontFileName, + idCardBack = idCardBack, + idCardBackFileName = idCardBackFileName + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt new file mode 100644 index 0000000..59716f4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt @@ -0,0 +1,53 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.data.remote.network.ApiConstants +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.model.WorkImage +import org.example.project.domain.repository.CraftsmanRepository +import org.koin.core.annotation.Factory + +@Factory +class UploadWorkPortfolioUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke( + craftsmanId: String, + workImages: List + ): List { + // Business validation + if (craftsmanId.isBlank()) { + throw ValidationException("Craftsman ID is required") + } + + if (workImages.isEmpty()) { + throw ValidationException("Please select at least one work image") + } + + if (workImages.size > ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { + throw ValidationException( + "You can upload maximum ${ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" + ) + } + + // Validate each image + workImages.forEachIndexed { index, image -> + if (image.data.isEmpty()) { + throw ValidationException("Image ${index + 1} is empty") + } + + if (image.data.size > ApiConstants.FileUpload.MAX_FILE_SIZE) { + throw ValidationException("Image ${index + 1} size must be less than 4MB") + } + + val extension = image.fileName.substringAfterLast('.', "").lowercase() + if (extension !in ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + throw ValidationException( + "Image ${index + 1} must be JPEG or PNG" + ) + } + } + + // Direct repository call + return repository.uploadWorkPortfolio(craftsmanId, workImages) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt new file mode 100644 index 0000000..7a8b7b0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt @@ -0,0 +1,209 @@ +package org.example.project.presentation.screens.setupScreens + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.example.project.data.local.datasource.UserPreferences +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.exception.* +import org.example.project.domain.model.WorkImage +import org.example.project.domain.usecase.* +import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase +import org.example.project.domain.usecase.craftsman.GetCraftsmanProfileUseCase +import org.example.project.domain.usecase.craftsman.GetCraftsmanStatusUseCase +import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase +import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase +import org.koin.compose.koinInject +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class) +@Composable +fun TestScreen() { + val scope = rememberCoroutineScope() + var result by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var isLoggedIn by remember { mutableStateOf(false) } + var currentUserId by remember { mutableStateOf(null) } + + // Get dependencies from Koin + val userPreferences: UserPreferences = koinInject() + val createCraftsmanUseCase: CreateCraftsmanProfileUseCase = koinInject() + + // Check login status on composition + LaunchedEffect(Unit) { + currentUserId = userPreferences.getUserId() + isLoggedIn = currentUserId != null + } + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "API Test Screen", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + + // Login Status Card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (isLoggedIn) Color(0xFF4CAF50).copy(alpha = 0.1f) + else Color(0xFFF44336).copy(alpha = 0.1f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Login Status:", + style = MaterialTheme.typography.titleMedium + ) + Text( + if (isLoggedIn) "Logged In" else "Not Logged In", + color = if (isLoggedIn) Color(0xFF4CAF50) else Color(0xFFF44336), + fontWeight = FontWeight.Bold + ) + } + + if (isLoggedIn) { + Text( + "User ID: $currentUserId", + style = MaterialTheme.typography.bodyMedium + ) + } + + Button( + onClick = { + scope.launch { + if (isLoggedIn) { + // Logout + userPreferences.clearUserId() + isLoggedIn = false + currentUserId = null + result = "Logged out successfully" + } else { + // Simulate login + val testUserId = "test-user-${Clock.System.now()}" + userPreferences.setUserId(testUserId) + currentUserId = testUserId + isLoggedIn = true + result = "Logged in with ID: $testUserId" + } + } + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = if (isLoggedIn) Color(0xFFF44336) else Color(0xFF4CAF50) + ) + ) { + Text(if (isLoggedIn) "Logout" else "Simulate Login") + } + } + } + + Divider() + + // Test Actions + Text( + "Test Actions", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Button( + onClick = { + scope.launch { + isLoading = true + try { + val personalInfo = PersonalInfo( + firstName = "Test", + lastName = "User ${Clock.System.now()}", + phoneNumber = "+1234567890", + address = "123 Test Street" + ) + + val craftsmanId = createCraftsmanUseCase( + personalInfo, + listOf("plumbing", "electrical") + ) + + result = "Success! Craftsman ID: $craftsmanId" + } catch (e: Exception) { + result = "Error: ${e.message}" + } finally { + isLoading = false + } + } + }, + enabled = !isLoading && isLoggedIn, + modifier = Modifier.fillMaxWidth() + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Test Create Craftsman Profile") + } + } + + if (!isLoggedIn) { + Text( + "Please login first to test API calls", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + + // Result Display + if (result.isNotEmpty()) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (result.startsWith("Success") || result.contains("Logged")) + Color(0xFF4CAF50).copy(alpha = 0.1f) + else Color(0xFFF44336).copy(alpha = 0.1f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + "Result:", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Text( + text = result, + style = MaterialTheme.typography.bodyMedium, + color = if (result.startsWith("Success") || result.contains("Logged")) + Color(0xFF4CAF50) + else Color(0xFFF44336) + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/org/example/project/CraftsmanRepositoryTest.kt b/composeApp/src/commonTest/kotlin/org/example/project/CraftsmanRepositoryTest.kt new file mode 100644 index 0000000..719abaf --- /dev/null +++ b/composeApp/src/commonTest/kotlin/org/example/project/CraftsmanRepositoryTest.kt @@ -0,0 +1,116 @@ +package org.example.project + +import kotlinx.coroutines.test.runTest +import org.example.project.domain.entity.Craftsman +import org.example.project.domain.entity.CraftsmanStatus +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.entity.VerificationDocuments +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.model.WorkImage +import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + + +class CreateCraftsmanProfileUseCaseTest { + + private lateinit var useCase: CreateCraftsmanProfileUseCase + private lateinit var mockRepository: TestCraftsmanRepository + + @BeforeTest + fun setup() { + mockRepository = TestCraftsmanRepository() + useCase = CreateCraftsmanProfileUseCase(mockRepository) + } + + @Test + fun `invoke succeeds with valid input`() = runTest { + // Given + val personalInfo = PersonalInfo( + firstName = "John", + lastName = "Doe", + phoneNumber = "+1234567890", + address = "123 Test St" + ) + + // When + val result = useCase(personalInfo, listOf("plumbing")) + + // Then + assertEquals("craftsman123", result) + } + + @Test + fun `invoke throws ValidationException for empty categories`() = runTest { + // Given + val personalInfo = PersonalInfo( + firstName = "John", + lastName = "Doe", + phoneNumber = "+1234567890", + address = "123 Test St" + ) + + // When/Then + val exception = assertFailsWith { + useCase(personalInfo, emptyList()) + } + assertEquals("Please select at least one service category", exception.message) + } + + @Test + fun `invoke throws ValidationException for invalid phone`() = runTest { + // Given + val personalInfo = PersonalInfo( + firstName = "John", + lastName = "Doe", + phoneNumber = "invalid", + address = "123 Test St" + ) + + // When/Then + assertFailsWith { + useCase(personalInfo, listOf("plumbing")) + } + } +} + +class TestCraftsmanRepository : CraftsmanRepository { + override suspend fun createCraftsmanProfile( + personalInfo: PersonalInfo, + categories: List + ): String { + return "craftsman123" + } + + override suspend fun uploadIdCards( + craftsmanId: String, + idCardFront: ByteArray, + idCardFrontFileName: String, + idCardBack: ByteArray, + idCardBackFileName: String + ): VerificationDocuments { + TODO("Not yet implemented") + } + + override suspend fun uploadWorkPortfolio( + craftsmanId: String, + workImages: List + ): List { + TODO("Not yet implemented") + } + + override suspend fun getCraftsmanProfile(): Craftsman { + TODO("Not yet implemented") + } + + override suspend fun getCraftsmanStatus(craftsmanId: String): CraftsmanStatus { + TODO("Not yet implemented") + } + + override suspend fun deleteCraftsmanAccount(craftsmanId: String) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt b/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt index f581b42..c88cfae 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt @@ -1,10 +1,13 @@ package org.example.project import androidx.compose.ui.window.ComposeUIViewController +import org.example.project.di.IosModule import org.example.project.di.initKoin fun MainViewController() = ComposeUIViewController( configure = { - initKoin() + initKoin { + modules(IosModule().module) + } } ) { App() } \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt b/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt new file mode 100644 index 0000000..de98c11 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt @@ -0,0 +1,74 @@ +package org.example.project.data.local.datasource + +import org.koin.core.annotation.Single +import platform.Foundation.NSUserDefaults + +@Single +class StorageLocalDataSourceImpl : StorageLocalDataSource { + + private val userDefaults = NSUserDefaults.standardUserDefaults + + override suspend fun saveString(key: String, value: String) { + userDefaults.setObject(value, forKey = key) + } + + override suspend fun getString(key: String): String? { + return userDefaults.stringForKey(key) + } + + override suspend fun saveStringSet(key: String, values: Set) { + val list = values.toList() + userDefaults.setObject(list, forKey = key) + } + + override suspend fun getStringSet(key: String): Set? { + val array = userDefaults.objectForKey(key) as? List<*> + return array?.mapNotNull { it as? String }?.toSet() + } + + override suspend fun saveLong(key: String, value: Long) { + // NSUserDefaults uses NSInteger (Long on 64-bit platforms) + userDefaults.setInteger(value, forKey = key) + } + + override suspend fun getLong(key: String): Long? { + return if (userDefaults.objectForKey(key) != null) { + userDefaults.integerForKey(key) + } else { + null + } + } + + override suspend fun saveBoolean(key: String, value: Boolean) { + userDefaults.setBool(value, forKey = key) + } + + override suspend fun getBoolean(key: String): Boolean? { + return if (userDefaults.objectForKey(key) != null) { + userDefaults.boolForKey(key) + } else { + null + } + } + + override suspend fun remove(key: String) { + userDefaults.removeObjectForKey(key) + } + + override suspend fun removeAll() { + // Get the app's bundle identifier to only remove app-specific keys + val dictionary = userDefaults.dictionaryRepresentation() + dictionary.keys.forEach { key -> + (key as? String)?.let { + userDefaults.removeObjectForKey(it) + } + } + userDefaults.synchronize() + } + + override suspend fun getAllKeys(): Set { + return userDefaults.dictionaryRepresentation().keys + .mapNotNull { it as? String } + .toSet() + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt new file mode 100644 index 0000000..2463ad9 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt @@ -0,0 +1,9 @@ +package org.example.project.di + + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.example.project.data.datasource.local") +class IosModule \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46e4f10..531f7d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -adaptive = "1.2.0-alpha06" +adaptive = "1.2.0-alpha07" agp = "8.13.0" android-compileSdk = "36" android-minSdk = "24" @@ -8,34 +8,38 @@ androidx-activity = "1.11.0" androidx-appcompat = "1.7.1" androidx-core = "1.17.0" androidx-espresso = "3.7.0" -androidx-lifecycle = "2.9.3" +androidx-lifecycle = "2.9.4" androidx-testExt = "1.3.0" coilComposeVersion = "3.3.0" -composeMultiplatform = "1.8.2" +composeMultiplatform = "1.9.0" +datastorePreferences = "1.1.7" junit = "4.13.2" +koinAnnotations = "2.2.0" +koinTest = "4.1.1" kotlin = "2.2.20" koin = "4.1.1" coil = "3.3.0" koinComposeMultiplatform = "4.1.1" +kotlinxCoroutinesTest = "1.10.2" +kotlinxDatetime = "0.7.1" ksp = "2.2.10-2.0.2" -koin-annotations = "2.1.0" -ktor = "3.2.3" -kotlinx-serialization = "1.5.1" +ktor = "3.3.1" +kotlinx-serialization = "1.9.0" - - -google-services = "4.4.3" +google-services = "4.4.4" #newer version of firebase-bom causes gradle errors firebase-bom = "33.16.0" firebaseCrashlytics = "3.0.6" -ktorClientCio = "3.3.0" +ktorClientCio = "3.3.1" +ktorClientMock = "3.3.1" [libraries] adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "adaptive" } adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" } adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" } -coil3-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilComposeVersion" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +koin-test = { module = "io.insert-koin:koin-test", version.ref = "koinTest" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -52,16 +56,19 @@ firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics- #koin koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } -koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotations" } -koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" } +koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koinAnnotations" } +koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotations" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } # ktor +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCio" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientMock" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } From 44891f0cfdae0878fd9fb11daffe26db93c874a7 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Wed, 15 Oct 2025 17:10:31 +0300 Subject: [PATCH 02/12] feat: refactor Koin modules to use koin dsl and improve dependency injection for craftsman setup --- build.gradle.kts | 1 + composeApp/build.gradle.kts | 57 +++++++++++++---- .../kotlin/org/example/project/MyApp.kt | 4 +- .../datasource/DataStoreLocalDataSourceImp.kt | 5 +- .../org/example/project/di/AndroidModule.kt | 14 +++-- .../local/datasource/UserPreferencesImpl.kt | 2 - .../CraftsmanRemoteDataSourceImpl.kt | 15 ++--- .../data/remote/network/HttpClientFactory.kt | 6 +- .../repository/CraftsmanRepositoryImpl.kt | 1 - .../org/example/project/di/CraftoModule.kt | 16 ++--- .../org/example/project/di/DataModule.kt | 21 ++++--- .../org/example/project/di/NetworkModule.kt | 63 ++----------------- .../org/example/project/di/domainModule.kt | 16 ++--- .../kotlin/org/example/project/di/initKoin.kt | 9 +-- .../domain/usecase/GetCategoriesUseCase.kt | 5 +- .../CreateCraftsmanProfileUseCase.kt | 3 +- .../DeleteCraftsmanAccountUseCase.kt | 3 +- .../craftsman/GetCraftsmanProfileUseCase.kt | 3 +- .../craftsman/GetCraftsmanStatusUseCase.kt | 3 +- .../usecase/craftsman/UploadIdCardsUseCase.kt | 4 +- .../craftsman/UploadWorkPortfolioUseCase.kt | 3 +- .../project/presentation/model/ImageData.kt | 20 ++++++ .../presentation/model/PersonalInfoUiModel.kt | 9 +++ .../AccountSetupCategoryScreen.kt | 2 +- .../viewmodel/base/BaseScreenState.kt | 6 ++ .../viewmodel/base/BaseViewModel.kt | 12 +++- .../viewmodel/base/ErrorUiState.kt | 14 ++++- .../craftsmansetup/CraftsmanSetupEffect.kt | 5 ++ .../CraftsmanSetupInteractionListener.kt | 26 ++++++++ .../craftsmansetup/CraftsmanSetupUiState.kt | 32 ++++++++++ .../viewmodel/mapper/Exception.kt | 32 ++++++++++ .../viewmodel/mapper/PersonalInfo.kt | 13 ++++ .../org/example/project/MainViewController.kt | 4 +- .../org/example/project/di/IosModule.kt | 13 ++-- .../example/project/di/getHttpEngine.ios.kt | 6 +- gradle/libs.versions.toml | 9 +-- 36 files changed, 302 insertions(+), 155 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/model/PersonalInfoUiModel.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt diff --git a/build.gradle.kts b/build.gradle.kts index bf7b850..137e3a3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,5 @@ plugins { alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.firebase.crashlytics) apply false + alias(libs.plugins.ksp) apply false } \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 20f5532..10ea153 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -34,6 +34,28 @@ kotlin { } sourceSets { + +// val commonMain by getting { +// kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") +// } +// +// // Android +// val androidMain by getting { +// kotlin.srcDir("build/generated/ksp/android/androidDebug/kotlin") +// kotlin.srcDir("build/generated/ksp/android/androidRelease/kotlin") +// } +// +// // iOS +// val iosX64Main by getting { +// kotlin.srcDir("build/generated/ksp/iosX64/iosX64Main/kotlin") +// } +// val iosArm64Main by getting { +// kotlin.srcDir("build/generated/ksp/iosArm64/iosArm64Main/kotlin") +// } +// val iosSimulatorArm64Main by getting { +// kotlin.srcDir("build/generated/ksp/iosSimulatorArm64/iosSimulatorArm64Main/kotlin") +// } + androidMain.dependencies { implementation(compose.preview) implementation(libs.androidx.activity.compose) @@ -60,12 +82,13 @@ kotlin { implementation(libs.adaptive) implementation(libs.adaptive.layout) implementation(libs.adaptive.navigation) + //koin - api(libs.koin.core) - api(libs.koin.annotations) + implementation(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) - implementation(libs.koin.annotations) + implementation(libs.koin.compose.viewmodel.navigation) + api(libs.koin.annotations) implementation(libs.ktor.client.core) implementation(libs.ktor.serialization.kotlinx.json) @@ -92,22 +115,22 @@ kotlin { implementation(libs.ktor.client.darwin) } } - - sourceSets.named("commonMain").configure { - kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") - } - } ksp { + //arg("KOIN_USE_COMPOSE_VIEWMODEL","true") arg("KOIN_CONFIG_CHECK","true") } -project.tasks.withType(KotlinCompilationTask::class.java).configureEach { - if (name != "kspCommonMainKotlinMetadata") { - dependsOn("kspCommonMainKotlinMetadata") - } -} +//tasks.withType>().configureEach { +// if (name != "kspCommonMainKotlinMetadata") { +// dependsOn("kspCommonMainKotlinMetadata") +// } +//} +// +//kotlin.sourceSets.getByName("commonMain") { +// kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") +//} android { @@ -141,5 +164,13 @@ android { dependencies { debugImplementation(compose.uiTooling) add("kspCommonMainMetadata", libs.koin.ksp.compiler) + + //Android + add("kspAndroid", libs.koin.ksp.compiler) + + // iOS (all targets you use) + add("kspIosX64", libs.koin.ksp.compiler) + add("kspIosArm64", libs.koin.ksp.compiler) + add("kspIosSimulatorArm64", libs.koin.ksp.compiler) } diff --git a/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt b/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt index c2c9f3a..5776dcc 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/MyApp.kt @@ -1,7 +1,7 @@ package org.example.project import android.app.Application -import org.example.project.di.AndroidModule +import org.example.project.di.androidModule import org.example.project.di.initKoin import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -13,7 +13,7 @@ class MyApp : Application() { initKoin { androidContext(this@MyApp) androidLogger() - modules(AndroidModule().module) + modules(androidModule) } } } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt b/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt index b07a918..58fcb76 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt @@ -6,14 +6,13 @@ import androidx.datastore.preferences.core.* import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import org.koin.core.annotation.Single + private val Context.dataStore: DataStore by preferencesDataStore( name = "crafto_preferences" ) -@Single -class StorageLocalDataSourceImpl( +class DataStoreLocalDataSourceImp( private val context: Context ) : StorageLocalDataSource { diff --git a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt index d25e681..b261258 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt @@ -1,8 +1,12 @@ package org.example.project.di -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Module +import org.example.project.data.local.datasource.StorageLocalDataSource +import org.example.project.data.local.datasource.DataStoreLocalDataSourceImp +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module -@Module -@ComponentScan("org.example.project.data.datasource.local") -class AndroidModule \ No newline at end of file +val androidModule = module { + single { + DataStoreLocalDataSourceImp(androidContext()) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt index 829a740..0b1895d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt @@ -1,8 +1,6 @@ package org.example.project.data.local.datasource -import org.koin.core.annotation.Single -@Single class UserPreferencesImpl( private val storage: StorageLocalDataSource ) : UserPreferences { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt index 9ac0169..f9c58c6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -20,23 +20,20 @@ import org.example.project.data.dto.DeleteAccountResponseDto import org.example.project.data.dto.IdCardUploadResponseDto import org.example.project.data.dto.WorkPortfolioResponseDto import org.example.project.data.remote.network.ApiConstants -import org.example.project.data.remote.network.ApiConstants.BASE_URL import org.example.project.data.remote.network.ApiConstants.Headers.USER_ID import org.example.project.data.remote.network.wrapApiCall import org.example.project.domain.model.WorkImage import org.koin.core.annotation.Single -@Single class CraftsmanRemoteDataSourceImpl( private val httpClient: HttpClient, ) : CraftsmanRemoteDataSource { - private val baseUrl: String = BASE_URL override suspend fun createCraftsmanProfile( userId: String, request: CreateCraftsmanRequest ): CraftsmanSetupResponseDto { return wrapApiCall { - httpClient.post(baseUrl + ApiConstants.Endpoints.CRAFTSMAN_SETUP) { + httpClient.post(ApiConstants.Endpoints.CRAFTSMAN_SETUP) { header(ApiConstants.Headers.USER_ID, userId) contentType(ContentType.Application.Json) setBody(request) @@ -54,7 +51,7 @@ class CraftsmanRemoteDataSourceImpl( ): IdCardUploadResponseDto { return wrapApiCall { httpClient.submitFormWithBinaryData( - url = baseUrl + ApiConstants.Endpoints.craftsmanIdCards(craftsmanId), + url = ApiConstants.Endpoints.craftsmanIdCards(craftsmanId), formData = formData { append("idCardFront", idCardFront, Headers.build { append(HttpHeaders.ContentType, "image/*") @@ -78,7 +75,7 @@ class CraftsmanRemoteDataSourceImpl( ): WorkPortfolioResponseDto { return wrapApiCall { httpClient.submitFormWithBinaryData( - url = baseUrl + ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId), + url = ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId), formData = formData { workImages.forEach { image -> append("workImages", image.data, Headers.build { @@ -95,7 +92,7 @@ class CraftsmanRemoteDataSourceImpl( override suspend fun getCraftsmanProfile(userId: String): CraftsmanProfileResponseDto { return wrapApiCall { - httpClient.get(baseUrl + ApiConstants.Endpoints.CRAFTSMAN_PROFILE) { + httpClient.get(ApiConstants.Endpoints.CRAFTSMAN_PROFILE) { header(ApiConstants.Headers.USER_ID, userId) } } @@ -103,7 +100,7 @@ class CraftsmanRemoteDataSourceImpl( override suspend fun getCraftsmanStatus(craftsmanId: String): CraftsmanStatusResponseDto { return wrapApiCall { - httpClient.get(baseUrl + ApiConstants.Endpoints.craftsmanStatus(craftsmanId)) + httpClient.get(ApiConstants.Endpoints.craftsmanStatus(craftsmanId)) } } @@ -112,7 +109,7 @@ class CraftsmanRemoteDataSourceImpl( craftsmanId: String ): DeleteAccountResponseDto { return wrapApiCall { - httpClient.delete(baseUrl + ApiConstants.Endpoints.deleteCraftsman(craftsmanId)) { + httpClient.delete(ApiConstants.Endpoints.deleteCraftsman(craftsmanId)) { header(USER_ID, userId) } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt index 09e9dc8..cc2285d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/HttpClientFactory.kt @@ -1,6 +1,7 @@ package org.example.project.data.remote.network import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest @@ -13,8 +14,8 @@ import io.ktor.http.ContentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -fun createHttpClient(): HttpClient { - return HttpClient { +fun createHttpClient(engine: HttpClientEngine): HttpClient { + return HttpClient(engine) { install(ContentNegotiation) { json(Json { prettyPrint = true @@ -36,6 +37,7 @@ fun createHttpClient(): HttpClient { } defaultRequest { + url(ApiConstants.BASE_URL) header(ApiConstants.Headers.ACCEPT, ContentType.Application.Json) } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt index bdb86eb..adf3daa 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt @@ -15,7 +15,6 @@ import org.example.project.domain.model.WorkImage import org.example.project.domain.repository.CraftsmanRepository import org.koin.core.annotation.Single -@Single class CraftsmanRepositoryImpl ( private val remoteDataSource: CraftsmanRemoteDataSource, private val userPreferences: UserPreferences diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt index 874eb67..032127c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt @@ -1,8 +1,8 @@ -//package org.example.project.di -// -//import org.koin.core.annotation.ComponentScan -//import org.koin.core.annotation.Module -// -//@Module -//@ComponentScan("org.example.project") -//class CraftoModule \ No newline at end of file +package org.example.project.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("org.example.project") +class CraftoModule \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index 75794f3..17942d2 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -1,18 +1,21 @@ package org.example.project.di -import io.ktor.client.HttpClient -import org.example.project.data.local.datasource.StorageLocalDataSource + import org.example.project.data.local.datasource.UserPreferences import org.example.project.data.local.datasource.UserPreferencesImpl import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl -import org.example.project.data.remote.network.ApiConstants import org.example.project.data.repository.CraftsmanRepositoryImpl import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Module -import org.koin.core.annotation.Single +import org.koin.dsl.module -@Module -@ComponentScan("org.example.project.data") -class DataModule \ No newline at end of file +val dataModule = module { + single { UserPreferencesImpl(get()) } + single { CraftsmanRemoteDataSourceImpl(get()) } + single { + CraftsmanRepositoryImpl( + remoteDataSource = get(), + userPreferences = get() + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt index e5dcdda..d8c8cce 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/NetworkModule.kt @@ -1,64 +1,9 @@ package org.example.project.di -import io.ktor.client.HttpClient -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.defaultRequest -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.json.Json import org.example.project.data.remote.network.createHttpClient -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Module -import org.koin.core.annotation.Single +import org.koin.dsl.module -////192.168.1.15 -//@Module -//class NetworkModule { -// @Single -// fun provideHttpClient(): HttpClient { -// return HttpClient(engine = getHttpEngine()) { -// defaultRequest { -// url("http://10.0.2.2:8085/") -// } -// -// install(Logging) { -// level = LogLevel.ALL -// logger = object : Logger { -// override fun log(message: String) { -// println(message) -// } -// } -// } -// -// install(HttpTimeout) { -// connectTimeoutMillis = TIME_OUT_INTERVAL_MILLI -// requestTimeoutMillis = TIME_OUT_INTERVAL_MILLI -// } -// -// install(ContentNegotiation) { -// json( -// Json { -// isLenient = true -// ignoreUnknownKeys = true -// } -// ) -// } -// } -// } -// -// private companion object { -// const val TIME_OUT_INTERVAL_MILLI = 10_000L -// } -//} - -@Module -@ComponentScan("org.example.project.data.network") -class NetworkModule { - @Single - fun provideHttpClient(): HttpClient { - return createHttpClient() - } +val networkModule = module { + single { getHttpEngine() } + single { createHttpClient(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt index a1ebe8b..b21d57f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt @@ -1,16 +1,18 @@ package org.example.project.di -import org.example.project.domain.repository.CraftsmanRepository import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase import org.example.project.domain.usecase.craftsman.DeleteCraftsmanAccountUseCase import org.example.project.domain.usecase.craftsman.GetCraftsmanProfileUseCase import org.example.project.domain.usecase.craftsman.GetCraftsmanStatusUseCase import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Factory -import org.koin.core.annotation.Module +import org.koin.dsl.module -@Module -@ComponentScan("org.example.project.domain.usecase") -class DomainModule \ No newline at end of file +val domainModule = module { + factory { CreateCraftsmanProfileUseCase(get()) } + factory { UploadIdCardsUseCase(get()) } + factory { UploadWorkPortfolioUseCase(get()) } + factory { GetCraftsmanProfileUseCase(get()) } + factory { GetCraftsmanStatusUseCase(get()) } + factory { DeleteCraftsmanAccountUseCase(get()) } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt index b36339f..e988bf3 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt @@ -2,15 +2,16 @@ package org.example.project.di import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration -import org.koin.ksp.generated.module + fun initKoin(config: KoinAppDeclaration? = null) { startKoin { config?.invoke(this) modules( - NetworkModule().module, - DataModule().module, - DomainModule().module, + //CraftoModule().module, + networkModule, + dataModule, + domainModule ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt index 1c5536f..b060556 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt @@ -2,10 +2,11 @@ package org.example.project.domain.usecase import org.example.project.domain.entity.Category import org.example.project.domain.repository.CategoryRepository +import org.koin.core.annotation.Factory import org.koin.core.annotation.Provided -import org.koin.core.annotation.Single -@Single + +@Factory class GetCategoriesUseCase( @Provided val repository: CategoryRepository) { suspend operator fun invoke(): List { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt index be3c6c9..fd8a72a 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt @@ -3,9 +3,8 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.entity.PersonalInfo import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Factory -@Factory + class CreateCraftsmanProfileUseCase( private val repository: CraftsmanRepository ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt index c87b221..4aeea2a 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt @@ -2,9 +2,8 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Factory -@Factory + class DeleteCraftsmanAccountUseCase( private val repository: CraftsmanRepository ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt index b7433fd..f409389 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanProfileUseCase.kt @@ -2,9 +2,8 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.entity.Craftsman import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Factory -@Factory + class GetCraftsmanProfileUseCase( private val repository: CraftsmanRepository ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt index 16d0913..f3365e0 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt @@ -3,9 +3,8 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.entity.CraftsmanStatus import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Factory -@Factory + class GetCraftsmanStatusUseCase( private val repository: CraftsmanRepository ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt index 81b4494..f7639be 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt @@ -4,9 +4,9 @@ import org.example.project.data.remote.network.ApiConstants import org.example.project.domain.entity.VerificationDocuments import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Factory -@Factory + + class UploadIdCardsUseCase( private val repository: CraftsmanRepository ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt index 59716f4..73861e0 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt @@ -4,9 +4,8 @@ import org.example.project.data.remote.network.ApiConstants import org.example.project.domain.exception.ValidationException import org.example.project.domain.model.WorkImage import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Factory -@Factory + class UploadWorkPortfolioUseCase( private val repository: CraftsmanRepository ) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt new file mode 100644 index 0000000..5a501a0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt @@ -0,0 +1,20 @@ +package org.example.project.presentation.model + +data class ImageData( + val uri: String, // For displaying in UI + val fileName: String, // Original file name + val byteArray: ByteArray // Actual data to upload +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as ImageData + return uri == other.uri && fileName == other.fileName + } + + override fun hashCode(): Int { + var result = uri.hashCode() + result = 31 * result + fileName.hashCode() + return result + } +} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/PersonalInfoUiModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/PersonalInfoUiModel.kt new file mode 100644 index 0000000..bbfd55d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/PersonalInfoUiModel.kt @@ -0,0 +1,9 @@ +package org.example.project.presentation.model + +data class PersonalInfoUiModel( + val firstName: String, + val lastName: String, + val phoneNumber: String, + val address: String + +) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt index 6f1c4b3..9c9d111 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.ui.screens.setupScreens +package org.example.project.presentation.screens.setupScreens import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt new file mode 100644 index 0000000..0923509 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt @@ -0,0 +1,6 @@ +package org.example.project.presentation.viewmodel.base + +interface BaseScreenState { + val isLoading: Boolean + val error: ErrorUiState? +} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt index 14bd982..614233e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.example.project.presentation.viewmodel.mapper.toErrorUiState abstract class BaseViewModel( initialState: SCREEN_STATE, @@ -21,24 +22,29 @@ abstract class BaseViewModel( protected val _effect = MutableSharedFlow() val effect = _effect.asSharedFlow() + protected val _isLoading = MutableStateFlow(false) + val isLoading = _isLoading.asStateFlow() + protected fun tryToCall( call: suspend () -> T, onSuccess: (T) -> Unit, onError: (error: ErrorUiState) -> Unit, + showLoading: Boolean = false, dispatcher: CoroutineDispatcher = Dispatchers.IO, ) { viewModelScope.launch(dispatcher) { + if (showLoading) _isLoading.value = true try { val result = call() onSuccess(result) } catch (e: Exception) { - onError(ErrorUiState(e.message ?: "Unknown error")) + onError(e.toErrorUiState()) + } finally { + if (showLoading) _isLoading.value = false } } } - - protected fun updateState(updater: (SCREEN_STATE) -> SCREEN_STATE) = _state.update(updater) protected fun sendNewEffect(newEffect: SCREEN_EFFECT) { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt index 0dad128..3486336 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt @@ -2,4 +2,16 @@ package org.example.project.presentation.viewmodel.base data class ErrorUiState ( val message: String = "", -) + val errorType: ErrorType = ErrorType.UNKNOWN, +){ + enum class ErrorType { + NETWORK, + AUTHENTICATION, + VALIDATION, + SERVER, + UNKNOWN + } +} + + + diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt new file mode 100644 index 0000000..7cf5b47 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt @@ -0,0 +1,5 @@ +package org.example.project.presentation.viewmodel.craftsmansetup + +sealed interface CraftsmanRegistrationEffect { + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt new file mode 100644 index 0000000..64e12c5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -0,0 +1,26 @@ +package org.example.project.presentation.viewmodel.craftsmansetup + +import org.example.project.presentation.model.ImageData +import org.example.project.presentation.model.PersonalInfoUiModel + +interface CraftsmanSetupInteractionListener { + fun onUserTypeSelected(userType: UserType) + + // Service Selection + fun onServiceToggled(service: String) + fun onServicesNextClicked(personalInfo: PersonalInfoUiModel) + + // Identity Verification + fun onIdCardSelected(isFront: Boolean, imageData: ImageData) + fun onUploadIdCards() + fun onSkipIdentityVerification() + + // Portfolio + fun onPortfolioImagesAdded(images: List) + fun onPortfolioImageRemoved(index: Int) + fun onWorkDescriptionChanged(description: String) + fun onUploadPortfolio() + + // Common + fun onBackPressed() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt new file mode 100644 index 0000000..20ec59c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt @@ -0,0 +1,32 @@ +package org.example.project.presentation.viewmodel.craftsmansetup + +import org.example.project.presentation.model.ImageData +import org.example.project.presentation.model.PersonalInfoUiModel +import org.example.project.presentation.viewmodel.base.BaseScreenState +import org.example.project.presentation.viewmodel.base.ErrorUiState + +data class CraftsmanSetupUiState( + override val isLoading: Boolean = false, + override val error: ErrorUiState? = null, + val currentStep: RegistrationStep = RegistrationStep.USER_TYPE, + val userType: UserType = UserType.CRAFTSMAN, + val selectedServices: Set = emptySet(), + val personalInfo: PersonalInfoUiModel, + val idCardFront: ImageData? = null, + val idCardBack: ImageData? = null, + val portfolioImages: List = emptyList(), + val workDescription: String = "", + + ): BaseScreenState + +enum class RegistrationStep { + USER_TYPE, // Choose Customer/Craftsman + SERVICE_SELECTION, // What services do you offer + IDENTITY_VERIFICATION,// Upload ID (Optional) + PORTFOLIO_UPLOAD, // Show your work +} + +enum class UserType { + CUSTOMER, + CRAFTSMAN +} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt new file mode 100644 index 0000000..ac3a7b5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt @@ -0,0 +1,32 @@ +package org.example.project.presentation.viewmodel.mapper + +import org.example.project.domain.exception.ForbiddenException +import org.example.project.domain.exception.NetworkException +import org.example.project.domain.exception.UnauthorizedException +import org.example.project.domain.exception.ValidationException +import org.example.project.presentation.viewmodel.base.ErrorUiState + +fun Throwable.toErrorUiState(): ErrorUiState { + return when (this) { + is NetworkException -> ErrorUiState( + message = message ?: "Please check your internet connection", + errorType = ErrorUiState.ErrorType.NETWORK, + ) + is UnauthorizedException -> ErrorUiState( + message = message ?: "Please login to continue", + errorType = ErrorUiState.ErrorType.AUTHENTICATION, + ) + is ValidationException -> ErrorUiState( + message = message ?: "Please check your input", + errorType = ErrorUiState.ErrorType.VALIDATION, + ) + is ForbiddenException -> ErrorUiState( + message = message ?: "You don't have permission", + errorType = ErrorUiState.ErrorType.AUTHENTICATION, + ) + else -> ErrorUiState( + message = message ?: "Something went wrong", + errorType = ErrorUiState.ErrorType.UNKNOWN, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt new file mode 100644 index 0000000..bc0c9f3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt @@ -0,0 +1,13 @@ +package org.example.project.presentation.viewmodel.mapper + +import org.example.project.domain.entity.PersonalInfo +import org.example.project.presentation.model.PersonalInfoUiModel + +fun PersonalInfo.toUiModel(): PersonalInfoUiModel { + return PersonalInfoUiModel( + firstName = firstName, + lastName = lastName, + phoneNumber = phoneNumber, + address = address + ) +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt b/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt index c88cfae..680f866 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/MainViewController.kt @@ -1,13 +1,13 @@ package org.example.project import androidx.compose.ui.window.ComposeUIViewController -import org.example.project.di.IosModule import org.example.project.di.initKoin +import org.example.project.di.iosModule fun MainViewController() = ComposeUIViewController( configure = { initKoin { - modules(IosModule().module) + modules(iosModule) } } ) { App() } \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt index 2463ad9..264c6ff 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt @@ -1,9 +1,12 @@ package org.example.project.di -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Module +import org.example.project.data.local.datasource.StorageLocalDataSource +import org.example.project.data.local.datasource.StorageLocalDataSourceImpl +import org.koin.dsl.module -@Module -@ComponentScan("org.example.project.data.datasource.local") -class IosModule \ No newline at end of file +val iosModule = module { + single { + StorageLocalDataSourceImpl() + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt b/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt index fed8979..666e2ea 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/di/getHttpEngine.ios.kt @@ -3,4 +3,8 @@ package org.example.project.di import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.darwin.Darwin -actual fun getHttpEngine(): HttpClientEngine = Darwin.create() \ No newline at end of file +actual fun getHttpEngine(): HttpClientEngine = Darwin.create{ + configureRequest { + setAllowsCellularAccess(true) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 531f7d1..ecdc057 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,16 +14,16 @@ coilComposeVersion = "3.3.0" composeMultiplatform = "1.9.0" datastorePreferences = "1.1.7" junit = "4.13.2" -koinAnnotations = "2.2.0" -koinTest = "4.1.1" kotlin = "2.2.20" +ksp = "2.2.20-2.0.4" +koinTest = "4.1.1" koin = "4.1.1" +koinAnnotations = "2.2.0" +ktor = "3.3.1" coil = "3.3.0" koinComposeMultiplatform = "4.1.1" kotlinxCoroutinesTest = "1.10.2" kotlinxDatetime = "0.7.1" -ksp = "2.2.10-2.0.2" -ktor = "3.3.1" kotlinx-serialization = "1.9.0" google-services = "4.4.4" @@ -60,6 +60,7 @@ koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotations" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } +koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" } # ktor kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } From 58bd815bd3963e8386c8ae8d79c0f8120d1ac8eb Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Mon, 20 Oct 2025 12:33:24 +0300 Subject: [PATCH 03/12] feat: implement craftsman setup UI and integrate category selection with improved state management --- composeApp/build.gradle.kts | 2 + .../src/androidMain/AndroidManifest.xml | 7 + .../datasource/DataStoreLocalDataSourceImp.kt | 1 + .../org/example/project/di/AndroidModule.kt | 2 +- .../org/example/project/util/AppLogger.kt | 21 ++ .../drawable/selection_craftsman.png | Bin 0 -> 17448 bytes .../drawable/selection_customer.png | Bin 0 -> 14235 bytes .../kotlin/org/example/project/App.kt | 5 +- .../kotlin/org/example/project/Greeting.kt | 9 - .../local}/StorageLocalDataSource.kt | 2 +- .../local}/UserPreferences.kt | 2 +- .../datasource/remote/CategoryDataSource.kt | 7 + .../remote}/CraftsmanRemoteDataSource.kt | 16 +- .../datasource/CategoryDataSourceImpl.kt | 10 + .../local/datasource/UserPreferencesImpl.kt | 3 + .../project/data/mapper/CraftsmanMapper.kt | 13 +- .../project/data/mapper/ExceptionMapper.kt | 2 +- .../project/data/memory/categorySeed.kt | 20 ++ .../dataSource/CategoryDataSourceImpl.kt | 17 - .../data/memory/dataSource/categoryList.kt | 86 ----- .../CraftsmanRemoteDataSourceImpl.kt | 16 +- .../data/{ => remote}/dto/CraftsmanDto.kt | 9 +- .../data/remote/network/ApiCallWrapper.kt | 2 +- .../data/repository/CategoryRepositoryImpl.kt | 16 +- .../repository/CraftsmanRepositoryImpl.kt | 20 +- .../dataSource/CategoryDataSource.kt | 7 - .../dataSource/memory/dto/CategoryEntity.kt | 10 - .../data/repository/mapper/Category.kt | 22 -- .../org/example/project/di/DataModule.kt | 14 +- .../di/{domainModule.kt => DomainModule.kt} | 2 + .../example/project/di/PresentationModule.kt | 16 + .../kotlin/org/example/project/di/initKoin.kt | 3 +- .../example/project/domain/entity/Category.kt | 5 +- .../domain/usecase/GetCategoriesUseCase.kt | 3 +- .../designsystem/components/PrimaryButton.kt | 2 +- .../components/ProgressIndicator.kt | 16 +- .../designsystem/components/SelectionCard.kt | 18 +- .../designsystem/components/chip.kt | 10 +- .../project/presentation/model/CategoryUi.kt | 10 + .../project/presentation/model/ImageData.kt | 6 +- .../AccountSetupCategoryScreen.kt | 75 ---- .../setupScreens/IntegrationTestScreen.kt | 13 +- .../component/CategoryActionBox.kt | 92 ----- .../AccountSetupTopBar.kt | 17 +- .../composable/CategoryActionBox.kt | 92 +++++ .../SetupScreenScaffold.kt | 123 ++++--- .../TitleDescriptionBox.kt | 3 +- .../page/IdentityVerificationPage.kt | 105 ++++++ .../composable/page/PersonalInfoPage.kt | 63 ++++ .../composable/page/PortfolioUploadPage.kt | 214 ++++++++++++ .../composable/page/ServiceSelectionPage.kt | 43 +++ .../composable/page/UserTypeSelectionPage.kt | 45 +++ .../craftsmansetup/CraftsmanSetupScreen.kt | 230 ++++++++++++ .../usersetup/AccountSetupCategoryScreen.kt | 74 ++++ .../accountSetup/AccountSetupViewModel.kt | 90 ++--- .../viewmodel/base/ErrorUiState.kt | 5 +- .../craftsmansetup/CraftsmanSetupEffect.kt | 2 +- .../CraftsmanSetupInteractionListener.kt | 7 +- .../craftsmansetup/CraftsmanSetupUiState.kt | 77 +++- .../craftsmansetup/CraftsmanSetupViewModel.kt | 329 ++++++++++++++++++ .../viewmodel/mapper/CraftsmanMapper.kt | 30 ++ .../viewmodel/mapper/Exception.kt | 30 +- .../viewmodel/mapper/PersonalInfo.kt | 13 - .../org/example/project/util/AppLogger.kt | 7 + .../datasource/StorageLocalDataSourceImpl.kt | 2 +- .../org/example/project/di/IosModule.kt | 2 +- .../org/example/project/util/AppLogger.kt | 22 ++ gradle/libs.versions.toml | 2 + 68 files changed, 1699 insertions(+), 540 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/org/example/project/util/AppLogger.kt create mode 100644 composeApp/src/commonMain/composeResources/drawable/selection_craftsman.png create mode 100644 composeApp/src/commonMain/composeResources/drawable/selection_customer.png delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/Greeting.kt rename composeApp/src/commonMain/kotlin/org/example/project/data/{local/datasource => datasource/local}/StorageLocalDataSource.kt (91%) rename composeApp/src/commonMain/kotlin/org/example/project/data/{local/datasource => datasource/local}/UserPreferences.kt (73%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CategoryDataSource.kt rename composeApp/src/commonMain/kotlin/org/example/project/data/{remote/datasource => datasource/remote}/CraftsmanRemoteDataSource.kt (63%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/memory/categorySeed.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/CategoryDataSourceImpl.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/categoryList.kt rename composeApp/src/commonMain/kotlin/org/example/project/data/{ => remote}/dto/CraftsmanDto.kt (92%) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/CategoryDataSource.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/memory/dto/CategoryEntity.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Category.kt rename composeApp/src/commonMain/kotlin/org/example/project/di/{domainModule.kt => DomainModule.kt} (89%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/model/CategoryUi.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/{component => composable}/AccountSetupTopBar.kt (84%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/{component => composable}/SetupScreenScaffold.kt (67%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/{component => composable}/TitleDescriptionBox.kt (94%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/util/AppLogger.kt create mode 100644 composeApp/src/iosMain/kotlin/org/example/project/util/AppLogger.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 10ea153..e69e0f7 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -101,6 +101,8 @@ kotlin { implementation(libs.androidx.datastore.preferences) + implementation(libs.calf.file.picker) + } commonTest.dependencies { diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 5bf9760..315d338 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -2,6 +2,13 @@ + + + + + + by preferencesDataStore( diff --git a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt index b261258..87d472a 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt @@ -1,6 +1,6 @@ package org.example.project.di -import org.example.project.data.local.datasource.StorageLocalDataSource +import org.example.project.data.datasource.local.StorageLocalDataSource import org.example.project.data.local.datasource.DataStoreLocalDataSourceImp import org.koin.android.ext.koin.androidContext import org.koin.dsl.module diff --git a/composeApp/src/androidMain/kotlin/org/example/project/util/AppLogger.kt b/composeApp/src/androidMain/kotlin/org/example/project/util/AppLogger.kt new file mode 100644 index 0000000..70b72c3 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/org/example/project/util/AppLogger.kt @@ -0,0 +1,21 @@ +package org.example.project.util + +import android.util.Log + +actual object AppLogger { + actual fun e(tag: String, message: String, throwable: Throwable?) { + if (throwable != null) { + Log.e(tag, message, throwable) + } else { + Log.e(tag, message) + } + } + + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/selection_craftsman.png b/composeApp/src/commonMain/composeResources/drawable/selection_craftsman.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd6739b872bedb6eec9f7a16ca2ad4c23fc3878 GIT binary patch literal 17448 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 literal 0 HcmV?d00001 diff --git a/composeApp/src/commonMain/composeResources/drawable/selection_customer.png b/composeApp/src/commonMain/composeResources/drawable/selection_customer.png new file mode 100644 index 0000000000000000000000000000000000000000..8b439317f2116c8ea3f38aa0e7c3219e83c463d2 GIT binary patch literal 14235 zcmV;MH)P0(P)TI`Q0-0_5RZH@?HoKLN6gekgCW^je-RgSJ1Vrx+rTy6am`;AK4XbxPpZxDyTGJ zRiq^$B!CI&2v4S-~XKRyYs@|MZ_g5Z^j&$OnY;u{LVe)`#tCU01n6jIUon* zfE+Zk52>TR6!r!4(+gUNSm|wr+C-1!U(iOK@ zLNpJJ6u~N5yu^OsgYO4;@geZ(wg-`D9Fh%Un1PFmjfEUyTDW&J3BG7$Oh#-K#^QG%~LI%F{TnFN{ z^&%RH?0on;x4hxD+ivQEzcah4mzJ$j%@#4bYfmDM+urdr3 z?cKY_OLK#O7G4U{0WVOCj|+~v5wKN2o#2J1KO)QFd7eZUB1j=nIi&A-s2mcl}9lrQiryVx${PO!km5K(HDZD6a_7qo4=*Agj4s=5oQfNY=QUEQubE}tywy=BYsaN<9I^oBH(kDS6rx@y-z8p>&gJrOW$#XPl2T8P%MF#!s8f2&_zWUd>p>u z!5zmnw{6`#tH>B)!j&U0?3rjb$WI7bKBrp0EXk zA~>z&{KE%)+yzWI=@poYP~0G=);^3o0M!XCMt}wbtw$Jp(0kzf$%7XyeD18HdX9lA zj3+8h%f(H1{^FGWoYiJ%WVIab!rCg|svf zUlQahFpdB{0*;0Tvc44}Egc~0TOb*af~iwP;*v)28?y6|9H!FpvrC#xXOF(>^Iux} zjq9#i4OJ!;XCa>LA9<=+GM0iCW)f)XT}%;erv$_OLy+J00@O_Gl}&yQnxYy+G%2F- zsPr)fAykqUmr_g{Ut<;XVtDsn80;H_!%ti+wfZ)gFj{1~sYesDd!QCyj4lN{(h9a3 z40M4&J(|(pw(6qOj#~y*CKaa;y>?LO?VC)HTX3xcW&$PAyLqcjY&?d>oPa8migFpJo_cCb*G0m$iqg)d#lY|=*iZag zF0FS(ZTAdWa%)9EDjq0J4=7;3JCc-v6v(lJW7#l^Q7@=w`Xrn=9sCaG7W}18|TW0d^oVzGB6S_{xhl4eOtWh1-4$hb-=a654VXQAVAD3bY8cWEe$C-^F;svL(h9kWHl_Gch5| zh=4;*S`0nM9Eq@n8ODhkg(8@`;t@QL<5MFN5k|ZY7M*dL=>5?{GWG4x!Ppz$2M71e zhZ0&D`n$2QF-k!XD8%a}jk;cRNGpRv1FVr1SJ#sOjc;RA$2_>TH2aq zvaw#Y)HQ(n)SYsD<}I*f$)VCI6-Dc;u4aJ8#-WO&q7-7)s#TV>?M&0)mb!U!I-yW< zMQUV3&RV=c3_SZhx&cZjUD49n3Nshag;sk8L~4?9#=;)aJhM}}xq{&Ik&sJ0%pNoe zkw_HQ|8|WW8%g8y4m7qk%Y%;U7KJRj8Zbor%oZr5O44kfExI=U2DaBX$cC0ygv#}* zgRmmG2W>M;!(WPB;=4z3FuH?oF_1?N%|-Hwt5W zhTso(-UEl6dV+`L4?SxH#+$|11vPU|$ z4Y5Q_?A*8$YHH%p+)yj_jHh5u&%xq>dw=@Hfq{VoF5`6+W7U>*A3S++*Sj@416DpK zo94_0n4ARZ2Dr{9KIXuqU%dt9A9IN8e)9>UzO_Xfu}dLQ(;$6}B?OaOXcjOI2tsbd zD;DA8cfJ)5k7=e`g5HfU!u@yMFSC;w8I8xa&bc!s!5*5Sfv*`*gAS+UXx=l=JTn1R zCY6+SGWDF^_Sh;hJaGJ`|9%`s_VkKVKlUM6k7q4q7MxX35ljpLrf3ub3?w0}iBQ;f zh)5EjfoLU>TU<32BAvvDQhLf(AsL0qu{5TeHbapcRy`-vV>uD8NkXKd5oR1e=Q}4~ zbj_7eWffJG#ASG?p&c(Iy!^4rIz$@@ipB-3U$js#gh+ffC_K?9TEZKj6eT3RdiM@X zEo$Qy$mrFS^!p~DsAj(z%?;Gun z@`mg0l9ylk6&TvvPi&X^J2Qf)b&RlRu=8n202itk)VDOi)i>TP&$;MR7%ZyFDyaxA zm5sOooOJ$jXzi>8-6@KpfnEMlht0X|igS-9aH?u*Q!7Rx-gd$A_sD^5($A-$Zf=j9 zchnKGe#R`Jn5mGMWa>fPcCtlL%w|M;bB)-(VGE|EIy0D{JfOOZ|I z1zeQb0L>JlyILKP8Q6{8U7 z5jt@3MYO3y7>%7mi`SqNvFS_~Q&wBDwP!04NknCPOHz&w4$2cwKS6j%g9~EHHA6_< zK*cb?M2Ne9V3mh)LNXDB?#_A{MRMz~!xu684eX1`r8b0L8N$xx@TCSArYW7A2lEzq z9Z*G5F$%GB^@6`+)vz-xrJZPmn+nn=OK@Hml~GK?Rn6h@FUq5uW)aXKz$AT`pg zsq-6}8@;PL|}@7MNNifP56W=U671B z0mYiiau6hm1cf1-(LIpf`m7k+@C@V!`vsuuh$NC^Z^Cj1I*h8GqVoKJ8agPVK-1#q zvtY*K60;G1=B!rz_p3L(5%#qWR)tiOW^=wj^84>y)pqEduls$2ZeSG+W9A_Os46O4 z6IJ=dpaiX~P>d&`ks5LyNMPuiaI%xKm`Vw^RKVrdk?V-z(REdQhe4`iJKU$?5|c)8 z2?4s%xYQR2g|1~-2Ew&nKYPplT_UGoVM zrL&lW0|;Y4s;vxEWfWfP5kqW!ZnHf2 zSCv$3#qE%r$U@XZo&woN#a;ohyHJI((wa=co3FeS`ZjKcXMght znL=ftlix6fSiEE*9PyzGWomSisB>=TTlHng?Cy+3_Knr3uY2IqV-BiW*x9l#TK5Rh8*81AzV!jI<}-( zf$j&~!$GdXA!U$?2@OPJQ=NEm!!CII{>Q!J4_{QAaEq+91 zC|rRtE8y~c50%BW5%F|<=$*)2bIw=pyAEDIVP&wlKl!Ygw~w}wn2WWyipPKQ%k!4s zblb1+IqjkAKl6q&&O7rS!wpIpFIl&I^ZRFg`WFxM!hTYUYE=mFuCq>Tz4NEnjo7>T z!OLf)of<`^G6QxlFI}rBT+5XXaueJijQWE)DAd#^w;%HHA1s8|M_3u0-K+lyeItho z%OV#iFp2Vvz&FNU-1_O6pSkfXGQC;4Yc~)CtJzjbCgV{@oP9|Q_LEeLLa@&z1atB1 z$n5i$&s%!NMeqEcy=ORrNPmws`#~yMA=q~=T{mRzVKd4*{wqK`I>0In6!YW$pZ@YWVC13vbqua}$0baZ4*REKhf8mzT=Iw2} zG{dpA()hR-e{mxOb6cTd{(NbUWJCmc6jh!fWPWT^c|Erfp0uZ7FPrr^iqV ziYHq*qRMzPW< zuttVu-Q4-Y$fw|ful=C;H=8z1!hVoS;WGHg-?i?$C(l3X)W^NCExtcFL^YCJ^T3@q z)VUqdP=|7fj9@;4P9u$A=x8FWuLDJVAqf@{E^3k^Y!?r2#z7MpUV0Jm808G-P)W&S z3pGs_6S)%2wmc#8+&6-|?^$*5l{fx;EBy6tgmrbt9sf3S!G&+xYd!mdXV^vE8Xbe- z7oUTg(@uh>8FL72<*y=KUzmq426xMLiD9omrI4|@dLax$p1x9 zgg@Bk?|$Q`1?Qf<*IoO(C-NDiI6NW-Hmrk&bI+8`v*v+nU#BmI3W-XJ<_U~NJ;DJC zWUW7ge-!hhb>j^RoSd^Ps+=8MDa%ri@Pch+N7SfYH)~@l6&)&H^nK!!St` z`9Sk7T6X-WvwhEfb#;ICHG^lNw$QFulG(y5I;gkr7%lNnP(+ z*7Jf3PCfJU&+LBj-*ya4@)-dCOPS6=rAo(Se_)TU_f1_hsJsmVFYMscvrIa`_z`QV}O3h~6pSFDKN^zAPf-L=p8 zUNNU4Wqe8%)pyhl;T15e+C41v`evf- zp=gzYhL~22UU+34yat)BF$DdvUS@nXT88HXjGIUqDCSq$1O{BbS3)cyPu77JScoZK zAyk-YMKNWx5eIf@tPl+;<)Nf%CM6yRLy15)_*kF@nbK_@W=gnrq-2Ri%oNc`jY#T^ zqRwj(4NjYA7@Gm+iwWV!0*+2%QBxGt6NmiQt(RZ)N?+(^nlv-i=YVTz#>fy1=lY#l z|8N1s%qZD5sfD${0e&u@CsrUBN!bv-Nq~<~EhvNlJbhjhElM!IUAmK_Xg7t3L@-^> zMst8jpI%37d&Y;1o;^sOD0+z{RuPDL@dfL*E{mdL5shWV{`NgE8vc zq*PNqBh;BjMKh6z^w&Nq#v(DP)369!2x&$^S6#mXFxNM0>u63PE{rER%+sm9+ZFma zkE}uW5(OW1F_SC#7r*(KpRIU2XRKVgl0L!9VHEmhAOBj@Df8MsIWX9N_cb?vX0ugr zXvTzUAfg**}orao`e=<;`wr(E?uDkNkM^GXD+@kn*J@QE5E`(A(_`wf0iu!uc zOOwDO?Py#gmHIoVAQFusS{>kdnKU)eMCFY^abT-Vb}!;oScHeB?gFExPGp+3Z`WLQ z)jz?02mz*>%b+jh-*e};*E@(c;ICrvm-l@JX@atK@N5M`7gKn(n3%Fr0uUs#w29{34BT>~gbS00wT?*l4!UbP$X!~DIe zHZTxVup23oP%X|c?%r*NuYJu~#rfz$3JLjIYSitk(GMAR-pH_R2k&_*T~*+fgt9V0IjR?$nll6{b0G_%hR%h{ zMEh?r(rNPm5^5M?w}h5xU@+yq`_hws4=UUEvr5s_)Hsz;GRhvd6Z61sjaO8>q@z?d zk+H>~eI8)nOWC^xkVwR<7MPN)^XJ_WSUH~(N`b@_D+6T!Jh$56A{XDaAh0dz*##bC z9{yPDSEY`NgO&l|JBbzo%4UPd#qfl5ZsOtJntyo)tTZ+0ygX(|P1J|5(S!JLr4#%f z$Z}|&pkb*#*J3(caYe^OJXGJlW3=|cAHL%wukV z`+j*B4oXCjDmTzl6>K%9L^B8-p%wsMGB5DF)0e&B27n*m`ifsPR5)I9!c*vN=ro{w zGtg8=JADRHUYD{AJ^XXvnVJyJJ)mG10ki!621xul3(olWY;B-cUz zpirwLgwxc8rUsKyW}_lLwjF{jjV%kpFFI(A2oi{ZMsI-zjSvug+*teM zb2q)xm#kq65tQAOI?L(k(I~i?w2;WVa0?`82&wYe&O_K{E+*e)_pJr3I3W?`TbNZtX5>LrW#Vktqt z30cU2JKQgfWFw`UM0o>ms-mDwDO6&>_sBk|U6?}45H<>n5cUk40~z^N`M>h&zFz_# zGZKQp`S=k>SObV&r(MxHqZ77|c&p$QY8{*7?Im_XVJ;FPtr+?sMtVdy$Xt=j`y>xh z#So=A0&Sg`VUVTJ!q7y;6(WMV&_9uT2KJLoD}^}eq{A8^CVDg~LI~7QFv9SIq6H?N zHHkzEjIZ7dL8}S1t#vXpG$vZRvog|f9Qys7lo?DRx2}g`?_Ov+t_zpM1}#pL%5wPQ zVIUqe*%c_VOILmV5T>FMLN5YhDp058)XIjPy*h$n9@qf}vn5p41X9sM1%$#KsB7(z zkvR>*H=@Bg%R2AI7d612!36GKx2|YUrUzZeZAZdP4{emP0_1<{YE^KjK8`R|7|hud z|0W>@mm44PX0^8&SA6A@{|Wo4cxO6s8RCaG{>z1Pn(HqVjuUoW(*&`I)M7QlsPBMS zYZo+hEry8EB&|$0WG&RmFM8fiq5 zYTt^Ii4PtDhIy&7`u)hz-{Bia1lKlt@Qb7!2( z2(8MuUczeb`>XFWoZK) zeKk*^r8m(88*QZ$vP>pPv9Ft)9}dEP$ReM=pD z>(<-rX3gw0cJ10F7Ibx+Tet7fbtJUxsIlw1JMJ&{io9n0Vd}SqX(Z10+HLyyWH@CN z4%O7qlx=y_hZ~^6WLm`#Xb?;$uxOw?*B)>Swuzzf11_e43m;)M+N#K?}K*_{M8 zn-*R^3-;KUEENmr_XBW7T{P=$(y>Z{GJ`<(q;8m^JiAT{fhx6|8ybip2q}gz#v}p^ zQ{Pe{4JW_#jZjJzz(Z3WHzG8QECjGLnv%=MtV>2~RvdCjWaXw!`wcYuTgbFhh`tv$ zZJF26IgjCBRUjh5h%c;snW}(cgsu$zssa)Zv`7-nMnjr)O(MYPzxL1sriP|;9Y+*X zV=(mK8cGaVc^DMpNRG$LF=VJQYKhUl@1UPX5GZM6qaz^uP}M2o0k1 zkR$K~hA57WNR5?3FJt1WLNAc|u2hPC-vbKqzm-w}Zu5QnjS7YS2`mM}M>r zMO2)Xf}|P55y(x5NJ2rpo*MpPr^kVMFsw`(L{#TgmFRGjrq1{}qmt?!WSDNy%qnCw z&pr`T2#0p?6BNKv**jqTFf%s;1J(q84ywCMv~>vDk3&*phf)OPUOdn5I_V^Ponk*L zMy68?p|H2H)rIVi-l%CJUd#KkVtim|%5H~MQDF!uc@UjczyfW<$gTf0ER0(8Y)3f7 zg0zbTDz_K8-hOaLELqoqfnaT&h{obzm{eA!EA$!6N>?@qm{FABTcO~6l?nK0UYyBc5#%Pp%jdu=mB7sxpoAENl`la) zYd~FlC#ZRJ3mxLx}&t=L?f#(D(3@pzAeI+tdQlcw8FMC?*9uS|Ech zgpRR=hKdwfex%frbr9t9!XMa#N;blUxIVfgPe)7xKL>3CX_qXeQIzV2TQrgD@z9tY zvT%VkPkt{|?d%t@=x-_0v4l=u`<<2j#=>)2hYR{Cb+hI?oYaj}A`yk!xG9pgaY#*M zJR7+V7cmHrDyKXunU4UT%^;&mnjj}>!HSuTO!f9cetbd(c0i@;L`F$2%ncFiDcYQq z4ULpY1o<47(P%Rdma)aBK=RPzWxSzTnqCp8;9W--MhvU3Ud9D)T+sJ^9(TKJ7XUy(2hWBJISn`io-v<>c(^(;2 zR*sn0(D3Y|H>7s$8o;&6OR^BcGr`cJxU!gX?h%MX4uJ2G!GJ zfB}ebX{Q#3vNfinY5SE$Jw4_x|M1LVmwfh{FG2+?14+eM2%wFzyhpt3u6`F|fC!*!zJmeE*P@t5(ru+x^BVyk1g?Q|Pq% zhC^4@wRV0)=7(ey3TW~J5z|XDMr)C8MIEu2!Q>2t>e_H&saaRk`7tV|SBRFBQyRgH zCuIy2F!qg!oP$9tpo>80+p#Q3N>C_R!g8q*Si-{YgJA9Uy&pLI9iO~xQ-6P0n^*Z# ziIZ&o`Z?XpMiR?mXZ{!{jOIXh(@0iWnr3I@J6aHbK)5>4d}19z7g{(RKP)aF^fN>< znCT3aLSYhUP%sUJ`bHU_$ce(<9gvF~AnOt$h#Im6`HCzgMXt5$kFirOJRFp9p-gSR zm9V|uQjw#LXU$)HGHL)M<3c23wHRC`I2%ZNQj82fD_aU_VM~?uG0Lc^%@2YQ=qyjQ zg?&wvP!1)g3h8QVFndXYo=!o2%OKQxwz#3@D4DFSgT+m)o#98Rco*@fQjrP~Z)!L! zm-9fK+k*Ghq=>%}jr?zB9t-iwA&BPF(nWJG5n-&c3y}0mq8?p96xAYbXb?ljAwhU2 z-lmwsAXlL)BHRu%5nznLb&fT)5XUzt(R6$#2DK52CC&O3D^}<$SFWrY%TShzRES6{ zHdE&Ej1UG=c>uZ~3js;#ftzT6w3!rk+$Q=Pik6M&q7QjmRIbBx8FuzC7%=eF^??h9 z2!x4hMZ3E<>K@dg1!_eF>d;*1_jLG~*A5-zK4{-A2qJ@nHtqA2gDR7XREUPg`nvJ4 zyt?y}Bu~{cbJdGf1r>O*vPCg9P5F?|~;}lZL5*}lOInMyo zSiB$vp2R4j5jBNI7ix&3yZBhsjQSQhpb)RW9DVfBiIM{m=EZqLvD!QbAnb9Y<|@^1 zK~OX|E_fO-6;k+Aa)%4*b3=us84q~T6{|*LX{e?FUuJSIX#g?PhejGdP%d}nxg>(? z`z9wxX)6A1s4}Uv4OC#!k*7CVw!JTu(^NwfcU@BZBdAT;Bv+{i0tsp!PU_+U+nz5h z2YC*psXeNxLFLd8tU``ML=!dAjK?7skBdk=K?E35X~a;3wq_L6l*6;8#>U}Y z@4aFMRH0OyLUb*f-&(Tv#d2!%Jk^O8Ol_KiuE7iV%%=Wn&+~*$gcmA9uH*_gmV9Jr zBIi-7pom0C5t#jq(&8g^9f=~`jeJK0A6Lp?N@Q5cgm8P0ZW{3XhK&b96-vb^ME~B2 zIn?UNUD%)+E7bnGaw>!Lxn&=C;HOV~G);BDI0BUd^oDll<}1!QyQ#V1ms&K2`hW_6 zi%x_WDucj_)Qt#IASOfIs1O)Bn7nzbuBrM?s((OeYc^D&RGdOAd*kv&=@i9sQyhd+ z2Q~+R+Bi+BK1t74+o*Ydphi2O?;qK_yO&;c|C3LyNuX<>`U={m7cWS9>7)d#3S zx=$`bSCv(&DHz|_Mc(A(Gu}KGs!%FUAyVTLi^B0cs*696VR37@fnHT*hl(M*Kz086 z6i8rj>G9BU;yg5(jBH5CEQk=Jf*h%-*8S13 z(N3sBsW^o=`~0)#J4i)`H7!$v8v-8lKoyPrFIUm~T>L_}LsMczMRwbZYv}g6-~4V< zJXynTfEJsSLZ@pkZy;sRNI|%Ck2nyG(M|m!;wqVsf%HOOzYwUjiFb zL=mXodO^eLxZKsFSuYT}4SW(Q?d+*1P$Mpk-|@9Ex2`b|tPEc8 zDoj_&I%wfcLej;SwvISdp;VkgAOqP{9!W{duI7L!JOaY0D!|isR9PEYhjQb62?NJD zeFJ;Shw^+khsq#%93U~BTq1!kA)I~5<(fj5VRtrJ4$f^!FfNH8CSpRBNyRBdV^d=? z2^ddtlAZ!FW;RFHb4fQS14D5GPvkq&C0j+inOGDIk+im1cl7m6v=P!2KJyQ}Y|$Fqv@wSn~bcpBI6tD}jRi~}eu z1C(2hHFcBzKAo9phbohbQiu~zJ-02FE0*ty&>{#mH9$d@wJ$whYB06ErZ&lRNO{lBaNHP_ z%T0N7a=8JWqX+ebg|zz2g-fcA&!{McaA(e(4+_LVa$NSC7?`I?rF9w*JKEZ0G$uMRJv)i!3F8p6LYzZh83SCfTQc((_P2}fG zU6sa_5lKcMg0PY;1f_`G2we!>;=#exlhHrexBE{HiR6Z!4d+vaaRUbuA{;Oz6eF%w zPHIzF7~7**MDKP;(_{8z#B-D*&?7a(0<+_gdL88RG|SXL>gYEIy#jqKF{MPRI~`?i4&R@rV~&eLZGHpOzm5N zpJ8l#yz1cVic*NlVyS~yPKJZ+!$hN;D)L?!iqhbk$*}96M}XufhhKIgTytD*q|Gxx z&6?oHUJ^8f-TpFsGrp(Xhw9`wU?E;l!Em5K%@5`HVah@TurJdn&x_zy4b;g@Ky)^f z-tuzCeER9N>&&RhtF3nK(o|-lp62S^sR861V(1W#B7?fdh6q%dREk3I`qVbm#mW&K z8wyr|K$IOkTL#{bOL5=Cc2ZDDWpahV{tYj8U~6<|pBYbvfoV8hw!A?Pk0)e2f(fv) z?U+JE!HhyUdy7_$?AS;)#`|f88HSpIU#^;x`-*MD?2%juJs-iddUr%V z`*Oz}9N#?{qq$U24pYl+Ba9rTQc?o1ga|^y%yoU}@7t?gf9sDv1Xa?;M^Y&YAus>X z`|il5Q@rMu?LiTd!jkPm$#MDG#tRJ;qxwnD3uyCI<}#>SDq|Pn*m&}+mpkyGi%;sA zm>A^-UKayaPxWhnhswrGDNhqtWxF!0k)X1>n)+I4qW4&^_^^M4Dw2vR!yhX+9fqdn zIhe#a`i5|D5xu|%4;RwK$ML=(g4!VJJ7OpZnixtifNXuo;V*aGiG1<6`eYnDtxSRe zRnlZ>rqyup*V`E)YqD1BQ;ZAroE=Z-dzNflJnG(^ZqZ!hY5s?=O zD42N}3?U)8i*s-O*PX9G{gfl;nghVA6}ixYi(YL6k-U zLP;8uM$5Q(mTSHXMLqiL(5@Yy{_!`z@Y_FgTymTKN3Q+mCl=0b{#>HAsUcuD5%8eW zz%GGiXGM*jf~YkqBVIuyyc`^H%(1Rg7E3tBPwlu+lf4Zgo9beKG{6B=B>wVzgAB>kWZwkU$+eCws z3o-l>1(z~~L1sFr5hZ-QB^iUy|Ma)9haP%}Dr~A|EI=hGg!t8eKXFA(*OJ?vlFbX{ zVwy?)ng%LHb8Z*R$ZwHGDGga01bP*884`Xz-g5lczIgSk*@DQ(ZhFGiQ#&v)H8Ia{ z1-Oh$)w1|GK0}5J#HiyLk(*}%D{I=T2_}kAqk@yU@UQh*6DOra0sjq%DdBjP} zZr{GW-^Zj=r-?B{b7Hs;0j8D%$pvz=u>c@93U%Hthz2>i7aGAftuNy1-}IN>;G6e8 zIQPNyOqlK35W|ckr`JI?QVUix2@$Udam*@Ap8`#^9QdpGF`*&u(e}gp9=-eB=bw2A zREg`TgrJh5#^9en@u`6=Tle}zCMdd__E_V6@FaC-f#-;rm*;kh@#I|DTXQgI^{udE z$&xv*=1s;nZ<$e>FkqslOAgm|gB@>?kysRJH3y=Idl(Uu04ZrkOyGXdXiONI2x=M{ z-}arK-1}+x3!ArAicEKfAXDs46r2q&Zs|+tI=8b$!?_?w^@OX+uIFyWgKdDN_K)8W!o5+jM6#a=+8gs&>QZ& z_rdd_3S_z~g#UE!z^xkxbBF3050(U3-iK2%v`m|`a*9d2-B4UX-QMA@%p5{G&Z~3n z9qpY7n>J(TU48*nn-;TNk~YpmUBqj;5P-$Ko`Mv^Gz@KcXu#{7+kNllmtRi(o)w*f zPG^M>U%mDFGm^EH?89N;Nf1~>d;@_}K5v{(Eqs!zt}{hPzM41d z8yXs=X)+#HV9D~1J0YhA7h1%x!Q{d0z9n)T>wL&Cn zTB7-4N!q0nxdQ@+6g7?61#M!2@c|M|jsuu3v-*^ozYLSoEO(=zj!JHK-E zHSj9G=M%rY?~sPJPQ!~N{Yg83biN>w%_Q7Gsx6ezC2_3cG2cPfsD$rd#7n+t%K~k* z;(B#Z!7{CJi2i}T-aymakvGr+%fSrO;k~t7o3=T!g^g#Wjkocd2F3vGiDBYmC2E`X zzFphDbi)TP{sO49)qNFj7hL<1w{Kf<_0J8AoX5PTp{`NyTCm_Ds9>4S5wVuHChvLU#g~1iRLIk$*>IS-K{Ko| z6EwKCk*cGSz{0cG6UoLFF*!E&&|?qWarC{ned9Oq*UzDro1S{g{q>#Sx;atP@Wia1 zLr;mtVomX=R&EI76qL$qc|r*v9of`~A=+oo(T4Wy{MhF|`rcnb1x`MR7UIcR7kG4rlIfOisgNGsv32LO$%y~s`@VPcBRh8Nplu3XD{4438(_tX z6-~Lsyt9vAdhGdV2M(V&o(~w=#SQ|TNk#KRS(%U x9i?2|dO!}y0XZNC +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt similarity index 63% rename from composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt index 4e3dfa1..5518ec9 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt @@ -1,12 +1,12 @@ -package org.example.project.data.remote.datasource +package org.example.project.data.datasource.remote -import org.example.project.data.dto.CraftsmanProfileResponseDto -import org.example.project.data.dto.CraftsmanSetupResponseDto -import org.example.project.data.dto.CraftsmanStatusResponseDto -import org.example.project.data.dto.CreateCraftsmanRequest -import org.example.project.data.dto.DeleteAccountResponseDto -import org.example.project.data.dto.IdCardUploadResponseDto -import org.example.project.data.dto.WorkPortfolioResponseDto +import org.example.project.data.remote.dto.CraftsmanProfileResponseDto +import org.example.project.data.remote.dto.CraftsmanSetupResponseDto +import org.example.project.data.remote.dto.CraftsmanStatusResponseDto +import org.example.project.data.remote.dto.CreateCraftsmanRequest +import org.example.project.data.remote.dto.DeleteAccountResponseDto +import org.example.project.data.remote.dto.IdCardUploadResponseDto +import org.example.project.data.remote.dto.WorkPortfolioResponseDto import org.example.project.domain.model.WorkImage interface CraftsmanRemoteDataSource { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt new file mode 100644 index 0000000..2ce6452 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt @@ -0,0 +1,10 @@ +package org.example.project.data.local.datasource + +import org.example.project.data.datasource.remote.CategoryDataSource +import org.example.project.data.remote.dto.CategoryDto + +class CategoryMemoryDataSource( + private val seed: List +) : CategoryDataSource { + override suspend fun getCategories(): List = seed +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt index 0b1895d..adfe47c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt @@ -1,5 +1,8 @@ package org.example.project.data.local.datasource +import org.example.project.data.datasource.local.StorageLocalDataSource +import org.example.project.data.datasource.local.UserPreferences + class UserPreferencesImpl( private val storage: StorageLocalDataSource diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt index ab7633d..08071e5 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt @@ -1,8 +1,10 @@ package org.example.project.data.mapper -import org.example.project.data.dto.CraftsmanProfileResponseDto -import org.example.project.data.dto.PersonalInfoDto -import org.example.project.data.dto.VerificationInfoDto +import org.example.project.data.remote.dto.CategoryDto +import org.example.project.data.remote.dto.CraftsmanProfileResponseDto +import org.example.project.data.remote.dto.PersonalInfoDto +import org.example.project.data.remote.dto.VerificationInfoDto +import org.example.project.domain.entity.Category import org.example.project.domain.entity.Craftsman import org.example.project.domain.entity.CraftsmanStatus import org.example.project.domain.entity.PersonalInfo @@ -66,3 +68,8 @@ fun String.toVerificationStatus(): VerificationStatus { } } +fun CategoryDto.toDomain(): Category = Category( + id = id, + title = title, + colorHex = colorHex +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt index 8ae0a0d..48cddd6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/ExceptionMapper.kt @@ -1,6 +1,6 @@ package org.example.project.data.mapper -import org.example.project.data.dto.ErrorResponseDto +import org.example.project.data.remote.dto.ErrorResponseDto import org.example.project.domain.exception.* fun Exception.toDomainException(): CraftoException { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/memory/categorySeed.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/memory/categorySeed.kt new file mode 100644 index 0000000..89cca62 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/memory/categorySeed.kt @@ -0,0 +1,20 @@ +package org.example.project.data.memory + +import org.example.project.data.remote.dto.CategoryDto + + +val categorySeed = listOf( + CategoryDto(1, "Plumbing", 0xFF9B59B6), + CategoryDto(2, "Electrical", 0xFF1ABC9C), + CategoryDto(3, "Cleaning", 0xFF3498DB), + CategoryDto(4, "AC Repair", 0xFFF39C12), + CategoryDto(5, "Furniture", 0xFFD35400), + CategoryDto(6, "Painting", 0xFF34495E), + CategoryDto(7, "Carpentry", 0xFFE67E22), + CategoryDto(8, "Roofing", 0xFF7F8C8D), + CategoryDto(9, "Landscaping", 0xFF2ECC71), + CategoryDto(10, "Pest Control", 0xFFC0392B), + CategoryDto(11, "Appliance Repair", 0xFF00BCD4), + CategoryDto(12, "Pool Maintenance", 0xFF8E44AD), + CategoryDto(13, "HVAC Maintenance", 0xFF27AE60), +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/CategoryDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/CategoryDataSourceImpl.kt deleted file mode 100644 index e3b4fc0..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/CategoryDataSourceImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.example.project.data.memory.dataSource - -import org.example.project.data.repository.dataSource.CategoryDataSource -import org.example.project.data.repository.dataSource.memory.dto.CategoryEntity -import org.example.project.data.repository.mapper.toCategoryEntity -import org.example.project.domain.entity.Category -import org.koin.core.annotation.Provided -import org.koin.core.annotation.Single - -@Single(binds = [CategoryDataSource::class]) -class CategoryDataSourceImpl( - @Provided val categoryList: List -) : CategoryDataSource { - override suspend fun getCategories(): List { - return categoryList.map { it.toCategoryEntity() } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/categoryList.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/categoryList.kt deleted file mode 100644 index a49a25b..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/memory/dataSource/categoryList.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.example.project.data.memory.dataSource - -import androidx.compose.ui.graphics.Color -import org.example.project.domain.entity.Category - - -val categoryList = listOf( - Category( - id = 1, - title = "Plumbing", - isSelected = false, - color = Color(0xFF9B59B6), - ), - Category( - id = 2, - title = "Electrical", - isSelected = false, - color = Color(0xFF1ABC9C), - ), - Category( - id = 3, - title = "Cleaning", - isSelected = false, - color = Color(0xFF3498DB), - ), - Category( - id = 4, - title = "AC Repair", - isSelected = false, - color = Color(0xFFF39C12), - ), - Category( - id = 5, - title = "Furniture", - isSelected = false, - color = Color(0xFFD35400), - ), - Category( - id = 6, - title = "Painting", - isSelected = false, - color = Color(0xFF34495E), - ), - Category( - id = 7, - title = "Carpentry", - isSelected = false, - color = Color(0xFFE67E22), - ), - Category( - id = 8, - title = "Roofing", - isSelected = false, - color = Color(0xFF7F8C8D), - ), - Category( - id = 9, - title = "Landscaping", - isSelected = false, - color = Color(0xFF2ECC71), - ), - Category( - id = 10, - title = "Pest Control", - isSelected = false, - color = Color(0xFFC0392B), - ), - Category( - id = 11, - title = "Appliance Repair", - isSelected = false, - color = Color(0xFF00BCD4), - ), - Category( - id = 12, - title = "Pool Maintenance", - isSelected = false, - color = Color(0xFF8E44AD), - ), - Category( - id = 13, - title = "HVAC Maintenance", - isSelected = false, - color = Color(0xFF27AE60), - ), -) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt index f9c58c6..3e186e2 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -12,18 +12,18 @@ import io.ktor.http.ContentType import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.contentType -import org.example.project.data.dto.CraftsmanProfileResponseDto -import org.example.project.data.dto.CraftsmanSetupResponseDto -import org.example.project.data.dto.CraftsmanStatusResponseDto -import org.example.project.data.dto.CreateCraftsmanRequest -import org.example.project.data.dto.DeleteAccountResponseDto -import org.example.project.data.dto.IdCardUploadResponseDto -import org.example.project.data.dto.WorkPortfolioResponseDto +import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource +import org.example.project.data.remote.dto.CraftsmanProfileResponseDto +import org.example.project.data.remote.dto.CraftsmanSetupResponseDto +import org.example.project.data.remote.dto.CraftsmanStatusResponseDto +import org.example.project.data.remote.dto.CreateCraftsmanRequest +import org.example.project.data.remote.dto.DeleteAccountResponseDto +import org.example.project.data.remote.dto.IdCardUploadResponseDto +import org.example.project.data.remote.dto.WorkPortfolioResponseDto import org.example.project.data.remote.network.ApiConstants import org.example.project.data.remote.network.ApiConstants.Headers.USER_ID import org.example.project.data.remote.network.wrapApiCall import org.example.project.domain.model.WorkImage -import org.koin.core.annotation.Single class CraftsmanRemoteDataSourceImpl( private val httpClient: HttpClient, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt index f96efc1..f9ebc30 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/CraftsmanDto.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt @@ -1,4 +1,4 @@ -package org.example.project.data.dto +package org.example.project.data.remote.dto import kotlinx.serialization.Serializable @@ -77,4 +77,11 @@ data class ErrorResponseDto( val code: String, val message: String, val timestamp: String +) + + +data class CategoryDto( + val id: Int, + val title: String, + val colorHex: Long ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt index 8fc146a..52e3109 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt @@ -3,7 +3,7 @@ package org.example.project.data.remote.network import io.ktor.client.call.body import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpStatusCode -import org.example.project.data.dto.ErrorResponseDto +import org.example.project.data.remote.dto.ErrorResponseDto import org.example.project.domain.exception.AlreadyExistsException import org.example.project.domain.exception.ApiException import org.example.project.domain.exception.CraftoException diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt index 4ed14b3..c6b3b76 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt @@ -1,17 +1,15 @@ package org.example.project.data.repository -import org.example.project.data.repository.dataSource.CategoryDataSource -import org.example.project.data.repository.mapper.toCategoryDomain + +import org.example.project.data.datasource.remote.CategoryDataSource +import org.example.project.data.mapper.toDomain import org.example.project.domain.entity.Category import org.example.project.domain.repository.CategoryRepository -import org.koin.core.annotation.Provided -import org.koin.core.annotation.Single -@Single(binds = [CategoryRepository::class]) + class CategoryRepositoryImpl( - @Provided val dataSource: CategoryDataSource + private val dataSource: CategoryDataSource ) : CategoryRepository { - override suspend fun getCategories(): List { - return dataSource.getCategories().map { it.toCategoryDomain() } - } + override suspend fun getCategories(): List = + dataSource.getCategories().map { it.toDomain() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt index adf3daa..9f894ca 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt @@ -1,10 +1,10 @@ package org.example.project.data.repository -import org.example.project.data.dto.CreateCraftsmanRequest -import org.example.project.data.local.datasource.UserPreferences +import org.example.project.data.remote.dto.CreateCraftsmanRequest +import org.example.project.data.datasource.local.UserPreferences import org.example.project.data.mapper.toDomain import org.example.project.data.mapper.toDto -import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource +import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource import org.example.project.domain.entity.Craftsman import org.example.project.domain.entity.CraftsmanStatus import org.example.project.domain.entity.PersonalInfo @@ -13,18 +13,26 @@ import org.example.project.domain.exception.ApiException import org.example.project.domain.exception.UnauthorizedException import org.example.project.domain.model.WorkImage import org.example.project.domain.repository.CraftsmanRepository -import org.koin.core.annotation.Single +import kotlin.time.Clock +import kotlin.time.ExperimentalTime class CraftsmanRepositoryImpl ( private val remoteDataSource: CraftsmanRemoteDataSource, private val userPreferences: UserPreferences ) : CraftsmanRepository { + @OptIn(ExperimentalTime::class) override suspend fun createCraftsmanProfile( personalInfo: PersonalInfo, categories: List ): String { - val userId = userPreferences.getUserId() - ?: throw UnauthorizedException("User must be logged in to create craftsman profile") + var userId = userPreferences.getUserId() + //?: throw UnauthorizedException("Session must be created to create craftsman profile") + + if (userId.isNullOrBlank()) { + userId = "temp-user-${Clock.System.now()}" + println("⚠️ Using temporary userId: $userId (remove after OTP integration)") + userPreferences.setUserId(userId) + } val request = CreateCraftsmanRequest( personalInfo = personalInfo.toDto(), diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/CategoryDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/CategoryDataSource.kt deleted file mode 100644 index 566e52f..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/CategoryDataSource.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.example.project.data.repository.dataSource - -import org.example.project.data.repository.dataSource.memory.dto.CategoryEntity - -interface CategoryDataSource { - suspend fun getCategories(): List -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/memory/dto/CategoryEntity.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/memory/dto/CategoryEntity.kt deleted file mode 100644 index 340c029..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/dataSource/memory/dto/CategoryEntity.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.example.project.data.repository.dataSource.memory.dto - -import androidx.compose.ui.graphics.Color - -data class CategoryEntity( - val id: Int, - val title: String, - val isSelected: Boolean, - val color: Color, -) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Category.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Category.kt deleted file mode 100644 index a72d3f3..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/mapper/Category.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.example.project.data.repository.mapper - -import org.example.project.data.repository.dataSource.memory.dto.CategoryEntity -import org.example.project.domain.entity.Category - -fun Category.toCategoryEntity(): CategoryEntity { - return CategoryEntity( - id = id, - title = title, - isSelected = isSelected, - color = color, - ) -} - -fun CategoryEntity.toCategoryDomain(): Category { - return Category( - id = id, - title = title, - isSelected = isSelected, - color = color, - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index 17942d2..8d9f015 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -1,11 +1,16 @@ package org.example.project.di -import org.example.project.data.local.datasource.UserPreferences +import org.example.project.data.datasource.local.UserPreferences +import org.example.project.data.datasource.remote.CategoryDataSource +import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource +import org.example.project.data.local.datasource.CategoryMemoryDataSource import org.example.project.data.local.datasource.UserPreferencesImpl -import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource +import org.example.project.data.memory.categorySeed import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl +import org.example.project.data.repository.CategoryRepositoryImpl import org.example.project.data.repository.CraftsmanRepositoryImpl +import org.example.project.domain.repository.CategoryRepository import org.example.project.domain.repository.CraftsmanRepository import org.koin.dsl.module @@ -18,4 +23,9 @@ val dataModule = module { userPreferences = get() ) } + single { categorySeed } + single { CategoryMemoryDataSource(get()) } + single { CategoryRepositoryImpl(get()) } + + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt similarity index 89% rename from composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt rename to composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt index b21d57f..0311452 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/domainModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt @@ -1,5 +1,6 @@ package org.example.project.di +import org.example.project.domain.usecase.GetCategoriesUseCase import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase import org.example.project.domain.usecase.craftsman.DeleteCraftsmanAccountUseCase import org.example.project.domain.usecase.craftsman.GetCraftsmanProfileUseCase @@ -15,4 +16,5 @@ val domainModule = module { factory { GetCraftsmanProfileUseCase(get()) } factory { GetCraftsmanStatusUseCase(get()) } factory { DeleteCraftsmanAccountUseCase(get()) } + factory { GetCategoriesUseCase(get())} } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt new file mode 100644 index 0000000..e1790d7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt @@ -0,0 +1,16 @@ +package org.example.project.di + +import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val presentationModule = module { + viewModel { + CraftsmanSetupViewModel( + createCraftsmanUseCase = get(), + uploadIdCardsUseCase = get(), + uploadWorkPortfolioUseCase = get(), + getCategoriesUseCase = get(), + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt index e988bf3..e38cf21 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/initKoin.kt @@ -11,7 +11,8 @@ fun initKoin(config: KoinAppDeclaration? = null) { //CraftoModule().module, networkModule, dataModule, - domainModule + domainModule, + presentationModule ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Category.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Category.kt index dbb4dde..a9c3f10 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Category.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Category.kt @@ -1,10 +1,7 @@ package org.example.project.domain.entity -import androidx.compose.ui.graphics.Color - data class Category( val id: Int, val title: String, - val isSelected: Boolean, - val color: Color, + val colorHex: Long, ) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt index b060556..634a537 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/GetCategoriesUseCase.kt @@ -6,9 +6,8 @@ import org.koin.core.annotation.Factory import org.koin.core.annotation.Provided -@Factory class GetCategoriesUseCase( - @Provided val repository: CategoryRepository) { + val repository: CategoryRepository) { suspend operator fun invoke(): List { return repository.getCategories() } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/PrimaryButton.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/PrimaryButton.kt index 194dd81..3e8f47d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/PrimaryButton.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/PrimaryButton.kt @@ -20,7 +20,7 @@ fun PrimaryButton( modifier = modifier, text = text, buttonState =buttonState, - enabled =enabled, + enabled =enabled && buttonState != ButtonState.LOADING, colors = ButtonDefaults.buttonColors( containerColor = AppTheme.craftoColors.button.primary, contentColor = AppTheme.craftoColors.button.onPrimary, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/ProgressIndicator.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/ProgressIndicator.kt index c548bdf..05e6b0d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/ProgressIndicator.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/ProgressIndicator.kt @@ -1,5 +1,7 @@ package org.example.project.presentation.designsystem.components +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -7,6 +9,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -18,12 +21,19 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun ProgressIndicator( currentPage: Int, - totalPage:Int, + totalPage: Int, + animationDuration: Int = 300, modifier: Modifier = Modifier, progressColor: Color = AppTheme.craftoColors.brand.primary, trackColor: Color = AppTheme.craftoColors.background.card, ) { + val animatedProgress by animateFloatAsState( + targetValue = (currentPage.toFloat() / totalPage.toFloat()).coerceIn(0f, 1f), + animationSpec = tween(durationMillis = animationDuration), + label = "progress_animation" + ) + Box( modifier = modifier .fillMaxWidth() @@ -36,7 +46,7 @@ fun ProgressIndicator( ) { Box( modifier = Modifier - .fillMaxWidth((currentPage.toFloat() / totalPage.toFloat()).coerceIn(0f,1f)) + .fillMaxWidth(animatedProgress) .height(8.dp) .clip(RoundedCornerShape(AppTheme.craftoRadius.full)) .background( @@ -44,9 +54,7 @@ fun ProgressIndicator( RoundedCornerShape(AppTheme.craftoRadius.full) ) ) - } - } @Preview diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/SelectionCard.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/SelectionCard.kt index a1c88ac..810555c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/SelectionCard.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/SelectionCard.kt @@ -3,6 +3,8 @@ package org.example.project.presentation.designsystem.components import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.Image import androidx.compose.foundation.border +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -14,9 +16,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.selection_card_img +import crafto.composeapp.generated.resources.selection_craftsman import org.example.project.presentation.designsystem.textstyle.AppTheme import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview @@ -42,7 +46,10 @@ fun SelectionCard( Card( onClick = onCardClick, - modifier.border(1.dp, strokeColor, RoundedCornerShape(AppTheme.craftoRadius.xl)), + modifier=modifier + .fillMaxSize() + .border(1.dp, strokeColor, RoundedCornerShape(AppTheme.craftoRadius.xl)) + , shape = RoundedCornerShape(AppTheme.craftoRadius.xl), colors = CardDefaults.cardColors( background @@ -50,8 +57,13 @@ fun SelectionCard( ) { Image( painter = img, + alignment = Alignment.Center, contentDescription = null, - modifier = Modifier.padding(12.dp) + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(12.dp), ) Text( @@ -75,7 +87,7 @@ fun SelectionCard( @Composable private fun SelectionCardPreview() { SelectionCard( - img = painterResource(Res.drawable.selection_card_img), + img = painterResource(Res.drawable.selection_craftsman), title = "Title", caption = "Caption", isSelected = true, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/chip.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/chip.kt index 541169a..9957dc3 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/chip.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/designsystem/components/chip.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import org.example.project.presentation.designsystem.colors.Background import org.example.project.presentation.designsystem.textstyle.AppTheme import org.jetbrains.compose.ui.tooling.preview.Preview @@ -26,7 +27,10 @@ fun Chip( modifier: Modifier = Modifier, text: String, isSelected: Boolean, + isBordered: Boolean = false, textColor: Color, + selectedBackgroundColor: Color = AppTheme.craftoColors.brand.tertiary, + unselectedBackgroundColor: Color = AppTheme.craftoColors.shade.quinary, borderColor: Color = AppTheme.craftoColors.brand.secondary, onChipSelected: (String) -> Unit = {}, ) { @@ -35,13 +39,13 @@ fun Chip( .clip(RoundedCornerShape(AppTheme.craftoRadius.full)) .background( if (isSelected) - AppTheme.craftoColors.brand.tertiary + selectedBackgroundColor else - AppTheme.craftoColors.shade.quinary, + unselectedBackgroundColor, shape = RoundedCornerShape(AppTheme.craftoRadius.full) ) .then( - if (isSelected) modifier.border( + if (isSelected && isBordered) modifier.border( 1.dp, borderColor, RoundedCornerShape( AppTheme.craftoRadius.full diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/CategoryUi.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/CategoryUi.kt new file mode 100644 index 0000000..c2d4e04 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/CategoryUi.kt @@ -0,0 +1,10 @@ +package org.example.project.presentation.model + +import androidx.compose.ui.graphics.Color + +data class CategoryUi( + val id: Int, + val title: String, + val color: Color, + val isSelected: Boolean = false +) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt index 5a501a0..150ccb9 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/model/ImageData.kt @@ -1,9 +1,9 @@ package org.example.project.presentation.model data class ImageData( - val uri: String, // For displaying in UI - val fileName: String, // Original file name - val byteArray: ByteArray // Actual data to upload + val uri: String, + val fileName: String, + val byteArray: ByteArray ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt deleted file mode 100644 index 9c9d111..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.example.project.presentation.screens.setupScreens - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import crafto.composeapp.generated.resources.Res -import crafto.composeapp.generated.resources.account_setup_craftsman_category_description -import crafto.composeapp.generated.resources.account_setup_craftsman_category_title -import crafto.composeapp.generated.resources.account_setup_customer_category_description -import crafto.composeapp.generated.resources.account_setup_customer_category_title -import org.example.project.presentation.ui.screens.setupScreens.component.CategoryActionBox -import org.example.project.presentation.ui.screens.setupScreens.component.SetupScreenScaffold -import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState -import org.example.project.presentation.viewmodel.accountSetup.AccountSetupViewModel -import org.jetbrains.compose.resources.stringResource -import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI - -@OptIn(KoinExperimentalAPI::class) -@Composable -fun AccountSetupCategoryScreen( - viewModel: AccountSetupViewModel = koinViewModel(), -) { - val state by viewModel.state.collectAsState() - AccountSetupCategoryContent( - state = state, - isCustomer = true, - currentPageNumber = 2, - onBackButtonClick = {}, - onNextButtonClick = {}, - onChipSelected = viewModel::onCategorySelected - ) -} - -@Composable -fun AccountSetupCategoryContent( - modifier: Modifier = Modifier, - state: AccountSetupState, - isCustomer: Boolean, - currentPageNumber: Int, - onBackButtonClick: () -> Unit, - onNextButtonClick: () -> Unit, - onChipSelected: (id: Int) -> Unit, -) { - SetupScreenScaffold( - modifier = modifier, - currentPageNumber = currentPageNumber, - title = selectCustomerOrCraftsmanText(isCustomer).first, - description = selectCustomerOrCraftsmanText(isCustomer).second, - onBackButtonClick = onBackButtonClick, - onNextButtonClick = onNextButtonClick, - ) { - CategoryActionBox( - state = state, - onChipSelected = onChipSelected, - ) - } - -} - -@Composable -private fun selectCustomerOrCraftsmanText(isCustomer: Boolean): Pair { - return if (isCustomer) { - stringResource(Res.string.account_setup_customer_category_title) to - stringResource(Res.string.account_setup_customer_category_description) - } else { - stringResource(Res.string.account_setup_craftsman_category_title) to - stringResource(Res.string.account_setup_craftsman_category_description) - } -} - - - - diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt index 7a8b7b0..897685d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt @@ -1,28 +1,17 @@ package org.example.project.presentation.screens.setupScreens import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.example.project.data.local.datasource.UserPreferences +import org.example.project.data.datasource.local.UserPreferences import org.example.project.domain.entity.PersonalInfo -import org.example.project.domain.exception.* -import org.example.project.domain.model.WorkImage -import org.example.project.domain.usecase.* import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase -import org.example.project.domain.usecase.craftsman.GetCraftsmanProfileUseCase -import org.example.project.domain.usecase.craftsman.GetCraftsmanStatusUseCase -import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase -import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase import org.koin.compose.koinInject import kotlin.time.Clock import kotlin.time.ExperimentalTime diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt deleted file mode 100644 index c89dbab..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.example.project.presentation.ui.screens.setupScreens.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import org.example.project.data.memory.dataSource.categoryList -import org.example.project.presentation.designsystem.components.Chip -import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.util.extension.toAnimatedColor -import org.example.project.presentation.viewmodel.accountSetup.AccountSetupCategoryState -import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState -import org.jetbrains.compose.ui.tooling.preview.Preview - -@Composable -fun CategoryActionBox( - modifier: Modifier = Modifier, - state: AccountSetupState, - onChipSelected: (id: Int) -> Unit, -) { - Box(modifier = modifier.fillMaxWidth()) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - - state.categoryState.categories.forEach { category -> - val isSelected = category.isSelected - Chip( - text = category.title, - isSelected = isSelected, - onChipSelected = { onChipSelected(category.id) }, - modifier = Modifier.background( - color = category.color.toAnimatedColor( - isSelected, - AppTheme.craftoColors.background.card - ), - shape = RoundedCornerShape(AppTheme.craftoRadius.full) - ), - textColor = AppTheme.craftoColors.background.card - .toAnimatedColor( - isSelected, - AppTheme.craftoColors.shade.secondary - ), - borderColor = category.color.toAnimatedColor( - condition = isSelected, - falseConditionColor = Color.Transparent, - duration = 100 - - ) - ) - } - } - } -} - -@Preview -@Composable -fun CategoryActionBoxLightPreview() { - AppTheme { - CategoryActionBox( - state = AccountSetupState( - categoryState = AccountSetupCategoryState( - categories = categoryList - ) - ), - onChipSelected = {} - ) - - } -} - -@Preview -@Composable -fun CategoryActionBoxDarkPreview() { - AppTheme(isDarkTheme = true) { - CategoryActionBox( - state = AccountSetupState( - categoryState = AccountSetupCategoryState( - categories = categoryList - ) - ), - onChipSelected = {} - ) - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/AccountSetupTopBar.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/AccountSetupTopBar.kt similarity index 84% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/AccountSetupTopBar.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/AccountSetupTopBar.kt index 8470ec9..90592b1 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/AccountSetupTopBar.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/AccountSetupTopBar.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.ui.screens.setupScreens.component +package org.example.project.presentation.screens.setupScreens.composable import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -27,6 +28,9 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun AccountSetupTopBar( modifier: Modifier = Modifier, currentPage: Int, + totalPages: Int=4, + animatedDuration: Int = 300, + showBackButton: Boolean = true, onBackButtonClick: () -> Unit ) { @@ -36,10 +40,15 @@ fun AccountSetupTopBar( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - BackButton(onBackButtonClick = onBackButtonClick) + if (showBackButton) { + BackButton(onBackButtonClick = onBackButtonClick) + } else { + Spacer(modifier = Modifier.size(48.dp)) + } ProgressIndicator( currentPage = currentPage, - totalPage = 4, + totalPage = totalPages, + animationDuration = animatedDuration, modifier = Modifier.fillMaxWidth(0.75f), ) } @@ -90,7 +99,7 @@ fun AccountSetupTopBarDarkPreview() { AppTheme(isDarkTheme = true) { AccountSetupTopBar( onBackButtonClick = {}, - currentPage = 2 + currentPage = 3 ) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt new file mode 100644 index 0000000..439c83d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt @@ -0,0 +1,92 @@ +//package org.example.project.presentation.screens.setupScreens.component +// +//import androidx.compose.foundation.background +//import androidx.compose.foundation.layout.Arrangement +//import androidx.compose.foundation.layout.Box +//import androidx.compose.foundation.layout.FlowRow +//import androidx.compose.foundation.layout.fillMaxWidth +//import androidx.compose.foundation.shape.RoundedCornerShape +//import androidx.compose.runtime.Composable +//import androidx.compose.ui.Modifier +//import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.unit.dp +//import org.example.project.data.memory.categorySeed +//import org.example.project.presentation.designsystem.components.Chip +//import org.example.project.presentation.designsystem.textstyle.AppTheme +//import org.example.project.presentation.util.extension.toAnimatedColor +//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupCategoryState +//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState +//import org.jetbrains.compose.ui.tooling.preview.Preview +// +//@Composable +//fun CategoryActionBox( +// modifier: Modifier = Modifier, +// state: AccountSetupState, +// onChipSelected: (id: Int) -> Unit, +//) { +// Box(modifier = modifier.fillMaxWidth()) { +// FlowRow( +// horizontalArrangement = Arrangement.spacedBy(12.dp), +// verticalArrangement = Arrangement.spacedBy(12.dp) +// ) { +// +// state.categoryState.categories.forEach { category -> +// val isSelected = category.isSelected +// Chip( +// text = category.title, +// isSelected = isSelected, +// onChipSelected = { onChipSelected(category.id) }, +// modifier = Modifier.background( +// color = category.colorHex.toAnimatedColor( +// isSelected, +// AppTheme.craftoColors.background.card +// ), +// shape = RoundedCornerShape(AppTheme.craftoRadius.full) +// ), +// textColor = AppTheme.craftoColors.background.card +// .toAnimatedColor( +// isSelected, +// AppTheme.craftoColors.shade.secondary +// ), +// borderColor = category.colorHex.toAnimatedColor( +// condition = isSelected, +// falseConditionColor = Color.Transparent, +// duration = 100 +// +// ) +// ) +// } +// } +// } +//} +// +//@Preview +//@Composable +//fun CategoryActionBoxLightPreview() { +// AppTheme { +// CategoryActionBox( +// state = AccountSetupState( +// categoryState = AccountSetupCategoryState( +// categories = categorySeed +// ) +// ), +// onChipSelected = {} +// ) +// +// } +//} +// +//@Preview +//@Composable +//fun CategoryActionBoxDarkPreview() { +// AppTheme(isDarkTheme = true) { +// CategoryActionBox( +// state = AccountSetupState( +// categoryState = AccountSetupCategoryState( +// categories = categorySeed +// ) +// ), +// onChipSelected = {} +// ) +// } +//} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/SetupScreenScaffold.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt similarity index 67% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/SetupScreenScaffold.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt index af31586..be8a17d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/SetupScreenScaffold.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.ui.screens.setupScreens.component +package org.example.project.presentation.screens.setupScreens.composable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -20,7 +20,6 @@ import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.account_setup_craftsman_category_description import crafto.composeapp.generated.resources.account_setup_craftsman_category_title import crafto.composeapp.generated.resources.next -import org.example.project.data.memory.dataSource.categoryList import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton import org.example.project.presentation.designsystem.textstyle.AppTheme @@ -34,6 +33,10 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun SetupScreenScaffold( modifier: Modifier = Modifier, currentPageNumber: Int, + totalPages: Int = 4, + nextButtonText: String = stringResource(Res.string.next), + nextButtonEnabled: Boolean = true, + nextButtonState: ButtonState = ButtonState.Enable, title: String, description: String, onBackButtonClick: () -> Unit, @@ -51,10 +54,14 @@ fun SetupScreenScaffold( modifier = modifier, currentPageNumber = currentPageNumber, title = title, + totalPages = totalPages, + nextButtonText = nextButtonText, + nextButtonEnabled = nextButtonEnabled, + nextButtonState = nextButtonState, description = description, onBackButtonClick = onBackButtonClick, onNextButtonClick = onNextButtonClick, - content = content + content = content, ) } @@ -63,6 +70,10 @@ fun SetupScreenScaffold( LandscapeLayout( modifier = modifier, currentPageNumber = currentPageNumber, + totalPages = totalPages, + nextButtonText = nextButtonText, + nextButtonEnabled = nextButtonEnabled, + nextButtonState = nextButtonState, title = title, description = description, onBackButtonClick = onBackButtonClick, @@ -80,6 +91,10 @@ private fun PortraitLayout( modifier: Modifier = Modifier, currentPageNumber: Int, title: String, + totalPages: Int, + nextButtonText: String, + nextButtonEnabled: Boolean, + nextButtonState: ButtonState, description: String, onBackButtonClick: () -> Unit, onNextButtonClick: () -> Unit, @@ -94,21 +109,32 @@ private fun PortraitLayout( verticalArrangement = Arrangement.spacedBy(32.dp), ) { - AccountSetupTopBar(onBackButtonClick = onBackButtonClick, currentPage = currentPageNumber) + AccountSetupTopBar( + onBackButtonClick = onBackButtonClick, + currentPage = currentPageNumber, + totalPages = totalPages ) + TitleDescriptionBox( modifier = Modifier.weight(1f), title = title, description = description, ) - content() + + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + content() + } + PrimaryButton( - text = stringResource(Res.string.next), - enabled = true, - buttonState = ButtonState.Enable, + text = nextButtonText, + enabled = nextButtonEnabled, + buttonState = nextButtonState, modifier = Modifier.fillMaxWidth(), onClick = onNextButtonClick ) - } } @@ -116,6 +142,10 @@ private fun PortraitLayout( private fun LandscapeLayout( modifier: Modifier = Modifier, currentPageNumber: Int, + totalPages: Int, + nextButtonText: String, + nextButtonEnabled: Boolean, + nextButtonState: ButtonState, title: String, description: String, onBackButtonClick: () -> Unit, @@ -131,7 +161,10 @@ private fun LandscapeLayout( verticalArrangement = Arrangement.spacedBy(32.dp), ) { - AccountSetupTopBar(onBackButtonClick = onBackButtonClick, currentPage = currentPageNumber) + AccountSetupTopBar( + onBackButtonClick = onBackButtonClick, + currentPage = currentPageNumber, + totalPages = totalPages) Row( modifier = Modifier .fillMaxWidth() @@ -149,9 +182,9 @@ private fun LandscapeLayout( } PrimaryButton( - text = stringResource(Res.string.next), - enabled = true, - buttonState = ButtonState.Enable, + text = nextButtonText, + enabled = nextButtonEnabled, + buttonState = nextButtonState, modifier = Modifier .fillMaxWidth(0.5f) .align(Alignment.End), @@ -161,35 +194,35 @@ private fun LandscapeLayout( } } -@Preview -@Composable -fun SetupScreenScaffoldLightPreview() { - SetupScreenScaffold( - currentPageNumber = 2, - title = stringResource(Res.string.account_setup_craftsman_category_title), - description = stringResource(Res.string.account_setup_craftsman_category_description), - onBackButtonClick = {}, - onNextButtonClick = {}, - - ) - { - CategoryActionBox( - state = AccountSetupState( - categoryState = AccountSetupCategoryState( - categories = categoryList - ) - ), - onChipSelected = {} - ) - } - - -} - -@Preview -@Composable -fun SetupScreenScaffoldDarkPreview() { - AppTheme(isDarkTheme = true) { - SetupScreenScaffoldLightPreview() - } -} \ No newline at end of file +//@Preview +//@Composable +//fun SetupScreenScaffoldLightPreview() { +// SetupScreenScaffold( +// currentPageNumber = 2, +// title = stringResource(Res.string.account_setup_craftsman_category_title), +// description = stringResource(Res.string.account_setup_craftsman_category_description), +// onBackButtonClick = {}, +// onNextButtonClick = {}, +// +// ) +// { +// CategoryActionBox( +// state = AccountSetupState( +// categoryState = AccountSetupCategoryState( +// categories = categorySeed +// ) +// ), +// onChipSelected = {} +// ) +// } +// +// +//} + +//@Preview +//@Composable +//fun SetupScreenScaffoldDarkPreview() { +// AppTheme(isDarkTheme = true) { +// SetupScreenScaffoldLightPreview() +// } +//} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/TitleDescriptionBox.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/TitleDescriptionBox.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/TitleDescriptionBox.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/TitleDescriptionBox.kt index 1e179c8..ecbd456 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/TitleDescriptionBox.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/TitleDescriptionBox.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.ui.screens.setupScreens.component +package org.example.project.presentation.screens.setupScreens.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -27,7 +27,6 @@ fun TitleDescriptionBox( title = title, description = description ) - } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt new file mode 100644 index 0000000..cc2864e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt @@ -0,0 +1,105 @@ +package org.example.project.presentation.screens.setupScreens.composable.page + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.camera +import crafto.composeapp.generated.resources.plus +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.model.ImageData +import org.jetbrains.compose.resources.painterResource + +@Composable +fun IdentityVerificationPage( + idCardFront: ImageData?, + idCardBack: ImageData?, + onIdCardSelected: (isFront: Boolean, imageData: ImageData) -> Unit, + onUploadClick: () -> Unit, + onSkip: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + UploadBox( + title = "Upload Front of National ID", + image = idCardFront, + onSelect = { img -> onIdCardSelected(true, img) } + ) + + UploadBox( + title = "Upload Back of National ID", + image = idCardBack, + onSelect = { img -> onIdCardSelected(false, img) } + ) + + Spacer(modifier = Modifier.weight(1f)) + + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = onSkip + ) { Text("I'll Verify Later") } + } +} + +@Composable +private fun UploadBox( + title: String, + image: ImageData?, + onSelect: (ImageData) -> Unit +) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(title, style = AppTheme.textStyle.body.smallMedium) + Box( + modifier = Modifier + .fillMaxWidth() + .height(150.dp) + .background( + color = AppTheme.craftoColors.background.card, + RoundedCornerShape(12.dp) + ) + .clickable { + // TODO: implement image picker + }, + contentAlignment = Alignment.Center + ) { + if (image == null) + Icon(painterResource(Res.drawable.camera), contentDescription = "upload image") + else { + AsyncImage( + model = image.uri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(12.dp)) + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt new file mode 100644 index 0000000..91a50c1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt @@ -0,0 +1,63 @@ +package org.example.project.presentation.screens.setupScreens.composable.page + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import org.example.project.presentation.designsystem.components.TextField +import org.example.project.presentation.model.PersonalInfoUiModel + +@Composable +fun PersonalInfoPage( + personalInfo: PersonalInfoUiModel, + onPersonalInfoChanged: (PersonalInfoUiModel) -> Unit, + isLoading: Boolean +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + TextField( + labelText = "First Name", + text = personalInfo.firstName, + onTextChange = { onPersonalInfoChanged(personalInfo.copy(firstName = it)) }, + enabledState = !isLoading, + inputKeyboard = KeyboardOptions(imeAction = ImeAction.Next) + ) + TextField( + labelText = "Last Name", + text = personalInfo.lastName, + onTextChange = { onPersonalInfoChanged(personalInfo.copy(lastName = it)) }, + enabledState = !isLoading, + inputKeyboard = KeyboardOptions(imeAction = ImeAction.Next) + ) + TextField( + text = personalInfo.phoneNumber, + onTextChange = { onPersonalInfoChanged(personalInfo.copy(phoneNumber = it)) }, + labelText = "Phone Number", + enabledState = !isLoading, + inputKeyboard = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Phone) + ) + TextField( + text = personalInfo.address, + onTextChange = { onPersonalInfoChanged(personalInfo.copy(address = it)) }, + labelText = "Address", + maxLines = 3, + enabledState = !isLoading, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt new file mode 100644 index 0000000..d2cd3d4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt @@ -0,0 +1,214 @@ +package org.example.project.presentation.screens.setupScreens.composable.page + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.mohamedrejeb.calf.core.LocalPlatformContext +import com.mohamedrejeb.calf.io.getName +import com.mohamedrejeb.calf.io.readByteArray +import com.mohamedrejeb.calf.picker.FilePickerFileType +import com.mohamedrejeb.calf.picker.FilePickerSelectionMode +import com.mohamedrejeb.calf.picker.rememberFilePickerLauncher +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.camera +import crafto.composeapp.generated.resources.plus +import crafto.composeapp.generated.resources.x +import kotlinx.coroutines.launch +import org.example.project.presentation.designsystem.components.TextField +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.model.ImageData +import org.jetbrains.compose.resources.painterResource +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalTime::class) +@Composable +fun PortfolioUploadPage( + images: List, + canAddMore: Boolean, + onAddPhotosClicked: (List) -> Unit, + onImageRemoved: (Int) -> Unit, + workDescription: String, + onDescriptionChanged: (String) -> Unit +) { + val scope = rememberCoroutineScope() + val context = LocalPlatformContext.current + + val imagePicker = rememberFilePickerLauncher( + type = FilePickerFileType.Image, + selectionMode = FilePickerSelectionMode.Multiple, + ) { files -> + scope.launch { + val imageDataList = files.take(4 - images.size).mapNotNull { file -> + try { + val fileName = file.getName(context) ?: "image_${Clock.System.now()}.jpg" + val byteArray = file.readByteArray(context) + + ImageData( + uri = fileName, + byteArray = byteArray, + fileName = fileName + ) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + if (imageDataList.isNotEmpty()) { + onAddPhotosClicked(imageDataList) + } + } + } + + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + // Either show the empty placeholder or thumbnails + add button + if (images.isEmpty()) { + EmptyPortfolioBox( + onAddPhotosClicked={ imagePicker.launch() } + ) + } else { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + images.forEachIndexed { index, image -> + PortfolioThumbnail(image) { + onImageRemoved(index) + } + } + if (canAddMore) { + AddPhotoBox( + onAddPhotosClicked= { imagePicker.launch() } + ) + } + } + } + + TextField( + labelText = "Describe Your Work (Optional)", + text = workDescription, + onTextChange = onDescriptionChanged, + hint = "You can mention your years of experience, tools you use, or types of jobs you usually handle.", + modifier = Modifier.fillMaxWidth(), + maxLines = 4 + ) + } +} + +// Shown when no images yet +@Composable +private fun EmptyPortfolioBox(onAddPhotosClicked: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2f) + .background( + AppTheme.craftoColors.background.card, + RoundedCornerShape(12.dp) + ) + .clickable { onAddPhotosClicked() }, + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = painterResource(Res.drawable.camera), + contentDescription = "Add Photos", + tint = AppTheme.craftoColors.shade.secondary, + modifier = Modifier.size(36.dp) + ) + Spacer(Modifier.height(8.dp)) + Text( + "Tap to add photos", + style = AppTheme.textStyle.body.smallMedium, + color = AppTheme.craftoColors.shade.secondary + ) + } + } +} + +// Thumbnail with remove button +@Composable +private fun PortfolioThumbnail( + imageData: ImageData, + onRemove: () -> Unit +) { + Box( + modifier = Modifier + .size(100.dp) + .clip(RoundedCornerShape(12.dp)), + contentAlignment = Alignment.TopEnd + ) { + AsyncImage( + model = imageData.byteArray, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + IconButton( + onClick = onRemove, + modifier = Modifier + .size(24.dp) + .background( + AppTheme.craftoColors.background.card.copy(alpha = 0.6f), + RoundedCornerShape(8.dp) + ) + .padding(4.dp) + ) { + Icon( + painter = painterResource(Res.drawable.x), + contentDescription = "Remove", + tint = AppTheme.craftoColors.shade.primary + ) + } + } +} + +// Add (+) box shown beside thumbnails +@Composable +private fun AddPhotoBox(onAddPhotosClicked: () -> Unit) { + Box( + modifier = Modifier + .size(100.dp) + .background( + AppTheme.craftoColors.background.card, + RoundedCornerShape(12.dp) + ) + .clickable { onAddPhotosClicked() }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(Res.drawable.plus), + contentDescription = "Add", + tint = AppTheme.craftoColors.shade.secondary, + modifier = Modifier.size(21.dp) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt new file mode 100644 index 0000000..f6bd32f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt @@ -0,0 +1,43 @@ +package org.example.project.presentation.screens.setupScreens.composable.page + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.example.project.presentation.designsystem.components.Chip +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.model.CategoryUi + +@Composable +fun ServiceSelectionPage( + availableCategories: List, + selectedServiceIds: Set, + onCategoryToggled: (Int) -> Unit +) { + FlowRow( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + availableCategories.forEach { category -> + Chip( + isSelected = category.id in selectedServiceIds, + onChipSelected = { onCategoryToggled(category.id) }, + textColor= if (category.id in selectedServiceIds) + AppTheme.craftoColors.background.card + else + AppTheme.craftoColors.shade.secondary, + text = category.title, + selectedBackgroundColor = category.color, + unselectedBackgroundColor = AppTheme.craftoColors.background.card, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt new file mode 100644 index 0000000..e85c021 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt @@ -0,0 +1,45 @@ +package org.example.project.presentation.screens.setupScreens.composable.page + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.selection_craftsman +import crafto.composeapp.generated.resources.selection_customer +import org.example.project.presentation.designsystem.components.SelectionCard +import org.example.project.presentation.viewmodel.craftsmansetup.UserType +import org.jetbrains.compose.resources.painterResource + +@Composable +fun UserTypeSelectionPage( + selectedType: UserType?, + onTypeSelected: (UserType) -> Unit +) { + Row( + modifier = Modifier + .fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + SelectionCard( + img = painterResource(Res.drawable.selection_customer), + title = "Customer", + caption = "I need help with a\nservice", + isSelected = selectedType == UserType.CUSTOMER, + onCardClick = { onTypeSelected(UserType.CUSTOMER) }, + modifier = Modifier.weight(1f) + ) + SelectionCard( + img = painterResource(Res.drawable.selection_craftsman), + title = "Craftsman", + caption = "I offer services", + isSelected = selectedType == UserType.CRAFTSMAN, + onCardClick = { onTypeSelected(UserType.CRAFTSMAN) }, + modifier = Modifier.weight(1f) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt new file mode 100644 index 0000000..099b172 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt @@ -0,0 +1,230 @@ +package org.example.project.presentation.screens.setupScreens.craftsmansetup + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.example.project.presentation.designsystem.components.ButtonState +import org.example.project.presentation.designsystem.components.TextButton +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.screens.setupScreens.composable.SetupScreenScaffold +import org.example.project.presentation.screens.setupScreens.composable.page.PortfolioUploadPage +import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanRegistrationEffect +import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupUiState +import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupViewModel +import org.example.project.presentation.viewmodel.craftsmansetup.RegistrationStep +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CraftsmanSetupScreen( + viewModel: CraftsmanSetupViewModel= koinViewModel(), + onComplete: () -> Unit={}, + onClose: () -> Unit={} +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val pagerState = rememberPagerState( + initialPage = state.currentPageIndex, + pageCount = { state.totalPages } + ) + + LaunchedEffect(pagerState.currentPage) { + viewModel.onPageChanged(pagerState.currentPage) + } + + LaunchedEffect(state.currentPageIndex) { + pagerState.animateScrollToPage(state.currentPageIndex) + } + + + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + CraftsmanRegistrationEffect.RegistrationComplete -> onComplete() + } + } + } + + AnimatedVisibility( + visible = true, + enter = EnterTransition.None, + exit = ExitTransition.None + ) { + CraftsmanSetupContent( + state = state, + viewModel = viewModel, + pagerState = pagerState, + onClose = onClose + ) + } + + AnimatedVisibility( + visible = state.error != null, + enter = slideInVertically { it } + fadeIn(), + exit = slideOutVertically { it } + fadeOut() + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter + ) { + state.error?.let { error -> + ErrorSnackbar( + error = error, + onDismiss = viewModel::clearError + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CraftsmanSetupContent( + state: CraftsmanSetupUiState, + viewModel: CraftsmanSetupViewModel, + pagerState: PagerState, + onClose: () -> Unit +) { + SetupScreenScaffold( + currentPageNumber = state.currentPageIndex + 1, + totalPages = state.totalPages, + nextButtonText = state.nextButtonText, + nextButtonEnabled = state.canNavigateNext && !state.isLoading, + nextButtonState = if (state.isLoading) ButtonState.LOADING else ButtonState.Enable, + onBackButtonClick = { + if (state.currentPageIndex == 0) onClose() + else viewModel.navigateBack() + }, + onNextButtonClick = { + when (state.currentStep) { + RegistrationStep.IDENTITY_VERIFICATION -> { + if (state.idCardFront != null && state.idCardBack != null) { + viewModel.onUploadIdCards() + } else { + viewModel.onSkipIdentityVerification() + } + } + else -> viewModel.navigateNext() + } + }, + title = when (state.currentStep) { + RegistrationStep.USER_TYPE -> { "How would you like to use Crafto?" } + RegistrationStep.SERVICE_SELECTION -> {"What services do you offer?"} + RegistrationStep.PERSONAL_INFO -> {"Let’s personalize your profile"} + RegistrationStep.PORTFOLIO_UPLOAD -> {"Show Us Your Work"} + RegistrationStep.IDENTITY_VERIFICATION -> {"Verify Your Identity\n(Optional)"} + }, + description =when (state.currentStep) { + RegistrationStep.USER_TYPE -> { "You can switch roles anytime from your profile." } + RegistrationStep.SERVICE_SELECTION -> {"Choose your specialties to get relevant job requests. You can change this later."} + RegistrationStep.PERSONAL_INFO -> {"We’ll use this to personalize your experience. You can add a profile photo too, or skip for now."} + RegistrationStep.PORTFOLIO_UPLOAD -> {"Add photos or a video of your past work. This helps build trust with customers."} + RegistrationStep.IDENTITY_VERIFICATION -> {"Uploading your ID helps build trust with customers. Verified craftsmen get more jobs and a special badge on their profile."} + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(AppTheme.craftoColors.background.screen) + ) { + HorizontalPager( + state = pagerState, + userScrollEnabled = state.isSwipeEnabled && state.canNavigateNext + ) { page -> +// when (RegistrationStep.fromIndex(page)) { +// RegistrationStep.USER_TYPE -> { +// UserTypeSelectionPage( +// selectedType = state.userType, +// onTypeSelected = viewModel::onUserTypeSelected, +// ) +// } +// RegistrationStep.SERVICE_SELECTION -> { +// ServiceSelectionPage( +// availableCategories = state.availableCategories, +// onCategoryToggled = viewModel::onCategoryToggled, +// selectedServiceIds = state.selectedCategoryIds, +// ) +// } +// +// RegistrationStep.PERSONAL_INFO -> { +// PersonalInfoPage( +// personalInfo = state.personalInfo, +// onPersonalInfoChanged = viewModel::onPersonalInfoChanged, +// isLoading = state.isLoading, +// ) +// } + + //RegistrationStep.PORTFOLIO_UPLOAD -> { + PortfolioUploadPage( + images = state.portfolioImages, + workDescription = state.workDescription, + canAddMore = state.canAddMoreImages, + onAddPhotosClicked = viewModel::onPortfolioImagesAdded, + onImageRemoved = viewModel::onPortfolioImageRemoved, + onDescriptionChanged = viewModel::onWorkDescriptionChanged, + ) + //} + + //RegistrationStep.IDENTITY_VERIFICATION -> { +// IdentityVerificationPage( +// idCardFront = state.idCardFront, +// idCardBack = state.idCardBack, +// onIdCardSelected = viewModel::onIdCardSelected, +// onUploadClick = viewModel::onUploadIdCards, +// onSkip = {}, +// ) + //} + //} + } + } + } +} + +@Composable +fun ErrorSnackbar( + error: ErrorUiState, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + Snackbar( + modifier = modifier.padding(16.dp), + shape = RoundedCornerShape( 8.dp), + containerColor = AppTheme.craftoColors.additional.primaryRed.copy(alpha = 0.95f), + contentColor = AppTheme.craftoColors.button.onPrimary, + action = { + TextButton( + onClick = onDismiss, text = "Dismiss", + enabled = true, buttonState = ButtonState.Enable + ) + } + ) { + Text( + text = error.message, + style = AppTheme.textStyle.body.medium, + color = AppTheme.craftoColors.button.onPrimary + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt new file mode 100644 index 0000000..acb81d5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt @@ -0,0 +1,74 @@ +//package org.example.project.presentation.screens.setupScreens.usersetup +// +//import androidx.compose.runtime.Composable +//import androidx.compose.runtime.collectAsState +//import androidx.compose.runtime.getValue +//import androidx.compose.ui.Modifier +//import crafto.composeapp.generated.resources.Res +//import crafto.composeapp.generated.resources.account_setup_craftsman_category_description +//import crafto.composeapp.generated.resources.account_setup_craftsman_category_title +//import crafto.composeapp.generated.resources.account_setup_customer_category_description +//import crafto.composeapp.generated.resources.account_setup_customer_category_title +//import org.example.project.presentation.screens.setupScreens.component.CategoryActionBox +//import org.example.project.presentation.screens.setupScreens.component.SetupScreenScaffold +//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState +//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupViewModel +//import org.jetbrains.compose.resources.stringResource +//import org.koin.compose.viewmodel.koinViewModel +//import org.koin.core.annotation.KoinExperimentalAPI +// +//@Composable +//fun AccountSetupCategoryScreen( +// viewModel: AccountSetupViewModel, +//) { +// val state by viewModel.state.collectAsState() +// AccountSetupCategoryContent( +// state = state, +// isCustomer = true, +// currentPageNumber = 2, +// onBackButtonClick = {}, +// onNextButtonClick = {}, +// onChipSelected = viewModel::onCategorySelected +// ) +//} +// +//@Composable +//fun AccountSetupCategoryContent( +// modifier: Modifier = Modifier, +// state: AccountSetupState, +// isCustomer: Boolean, +// currentPageNumber: Int, +// onBackButtonClick: () -> Unit, +// onNextButtonClick: () -> Unit, +// onChipSelected: (id: Int) -> Unit, +//) { +// SetupScreenScaffold( +// modifier = modifier, +// currentPageNumber = currentPageNumber, +// title = selectCustomerOrCraftsmanText(isCustomer).first, +// description = selectCustomerOrCraftsmanText(isCustomer).second, +// onBackButtonClick = onBackButtonClick, +// onNextButtonClick = onNextButtonClick, +// ) { +// CategoryActionBox( +// state = state, +// onChipSelected = onChipSelected, +// ) +// } +// +//} +// +//@Composable +//private fun selectCustomerOrCraftsmanText(isCustomer: Boolean): Pair { +// return if (isCustomer) { +// stringResource(Res.string.account_setup_customer_category_title) to +// stringResource(Res.string.account_setup_customer_category_description) +// } else { +// stringResource(Res.string.account_setup_craftsman_category_title) to +// stringResource(Res.string.account_setup_craftsman_category_description) +// } +//} +// +// +// +// diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt index 02e120b..0b7cab7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt @@ -1,45 +1,45 @@ -package org.example.project.presentation.viewmodel.accountSetup - -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch -import org.example.project.domain.usecase.GetCategoriesUseCase -import org.example.project.presentation.viewmodel.base.BaseViewModel -import org.koin.android.annotation.KoinViewModel -import org.koin.core.annotation.Provided - -@KoinViewModel -class AccountSetupViewModel( - @Provided private val getCategoriesUseCase : GetCategoriesUseCase -) : BaseViewModel(AccountSetupState()), - AccountSetupInterActionListener { - - init { - fetchCategories() - } - - private fun fetchCategories() { - viewModelScope.launch { - val categories = getCategoriesUseCase.invoke() - updateState { - it.copy( - categoryState = it.categoryState.copy(categories = categories) - ) - } - } - } - - override fun onCategorySelected(id: Int) { - updateState { - val categories = it.categoryState.categories.map { category -> - if (category.id == id) { - category.copy(isSelected = !category.isSelected) - } else { - category - } - } - it.copy( - categoryState = it.categoryState.copy(categories = categories) - ) - } - } -} \ No newline at end of file +//package org.example.project.presentation.viewmodel.accountSetup +// +//import androidx.lifecycle.viewModelScope +//import kotlinx.coroutines.launch +//import org.example.project.domain.usecase.GetCategoriesUseCase +//import org.example.project.presentation.viewmodel.base.BaseViewModel +//import org.koin.android.annotation.KoinViewModel +//import org.koin.core.annotation.Provided +// +//@KoinViewModel +//class AccountSetupViewModel( +// @Provided private val getCategoriesUseCase : GetCategoriesUseCase +//) : BaseViewModel(AccountSetupState()), +// AccountSetupInterActionListener { +// +// init { +// fetchCategories() +// } +// +// private fun fetchCategories() { +// viewModelScope.launch { +// val categories = getCategoriesUseCase.invoke() +// updateState { +// it.copy( +// categoryState = it.categoryState.copy(categories = categories) +// ) +// } +// } +// } +// +// override fun onCategorySelected(id: Int) { +// updateState { +// val categories = it.categoryState.categories.map { category -> +// if (category.id == id) { +// category.copy(isSelected = !category.isSelected) +// } else { +// category +// } +// } +// it.copy( +// categoryState = it.categoryState.copy(categories = categories) +// ) +// } +// } +//} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt index 3486336..7e2cbd1 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt @@ -11,7 +11,4 @@ data class ErrorUiState ( SERVER, UNKNOWN } -} - - - +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt index 7cf5b47..0c770e8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt @@ -1,5 +1,5 @@ package org.example.project.presentation.viewmodel.craftsmansetup sealed interface CraftsmanRegistrationEffect { - + data object RegistrationComplete : CraftsmanRegistrationEffect } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt index 64e12c5..e8b6151 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -7,8 +7,8 @@ interface CraftsmanSetupInteractionListener { fun onUserTypeSelected(userType: UserType) // Service Selection - fun onServiceToggled(service: String) - fun onServicesNextClicked(personalInfo: PersonalInfoUiModel) + fun onCategoryToggled(categoryId: Int) + fun onPersonalInfoChanged(personalInfo: PersonalInfoUiModel) // Identity Verification fun onIdCardSelected(isFront: Boolean, imageData: ImageData) @@ -20,7 +20,4 @@ interface CraftsmanSetupInteractionListener { fun onPortfolioImageRemoved(index: Int) fun onWorkDescriptionChanged(description: String) fun onUploadPortfolio() - - // Common - fun onBackPressed() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt index 20ec59c..233d56d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt @@ -1,5 +1,7 @@ package org.example.project.presentation.viewmodel.craftsmansetup +import org.example.project.domain.entity.VerificationDocuments +import org.example.project.presentation.model.CategoryUi import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel import org.example.project.presentation.viewmodel.base.BaseScreenState @@ -8,25 +10,74 @@ import org.example.project.presentation.viewmodel.base.ErrorUiState data class CraftsmanSetupUiState( override val isLoading: Boolean = false, override val error: ErrorUiState? = null, - val currentStep: RegistrationStep = RegistrationStep.USER_TYPE, - val userType: UserType = UserType.CRAFTSMAN, - val selectedServices: Set = emptySet(), - val personalInfo: PersonalInfoUiModel, - val idCardFront: ImageData? = null, - val idCardBack: ImageData? = null, + + // Pager state + val currentPageIndex: Int = 0, + val totalPages: Int = 5, + val canNavigateNext: Boolean = false, + val canNavigateBack: Boolean = false, + val isSwipeEnabled: Boolean = true, + + val isUploadingProfile: Boolean = false, + val isUploadingPortfolio: Boolean = false, + val isUploadingIdCards: Boolean = false, + val uploadedPortfolioUrls: List = emptyList(), + val verificationDocuments: VerificationDocuments? = null, + + // User type selection + val userType: UserType? = null, + + // Service selection + val availableCategories: List = emptyList(), + val selectedCategoryIds: Set = emptySet(), + + // Personal info + val personalInfo: PersonalInfoUiModel = PersonalInfoUiModel("", "", "", ""), + + // Portfolio val portfolioImages: List = emptyList(), val workDescription: String = "", + val canAddMoreImages: Boolean = true, - ): BaseScreenState + // Identity verification + val idCardFront: ImageData? = null, + val idCardBack: ImageData? = null, + + // Flow state + val craftsmanId: String? = null, + val isProfileCreated: Boolean = false, +): BaseScreenState { + + val currentStep: RegistrationStep + get() = RegistrationStep.fromIndex(currentPageIndex) + + val progress: Float + get() = (currentPageIndex + 1) / totalPages.toFloat() -enum class RegistrationStep { - USER_TYPE, // Choose Customer/Craftsman - SERVICE_SELECTION, // What services do you offer - IDENTITY_VERIFICATION,// Upload ID (Optional) - PORTFOLIO_UPLOAD, // Show your work + val nextButtonText: String + get() = when (currentStep) { + RegistrationStep.USER_TYPE -> "Next" + RegistrationStep.SERVICE_SELECTION -> "Next" + RegistrationStep.PERSONAL_INFO -> if (isLoading) "Creating Profile..." else "Next" + RegistrationStep.PORTFOLIO_UPLOAD -> "Next" + RegistrationStep.IDENTITY_VERIFICATION -> "See Nearby Requests" + } +} + +enum class RegistrationStep(val index: Int) { + USER_TYPE(0), // Choose Customer/Craftsman + SERVICE_SELECTION(1), // What services do you offer + PERSONAL_INFO(2), // Collect personal information + PORTFOLIO_UPLOAD(3), // Show your work + IDENTITY_VERIFICATION(4); // Upload ID (Optional) + + companion object { + fun fromIndex(index: Int): RegistrationStep = + entries.firstOrNull { it.index == index } ?: USER_TYPE + } } enum class UserType { CUSTOMER, CRAFTSMAN -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt new file mode 100644 index 0000000..049cd92 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt @@ -0,0 +1,329 @@ +package org.example.project.presentation.viewmodel.craftsmansetup + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.example.project.util.AppLogger +import org.example.project.domain.usecase.GetCategoriesUseCase +import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase +import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase +import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase +import org.example.project.presentation.model.ImageData +import org.example.project.presentation.model.PersonalInfoUiModel +import org.example.project.presentation.viewmodel.base.BaseViewModel +import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.presentation.viewmodel.mapper.toDomain +import org.example.project.presentation.viewmodel.mapper.toUi +import org.example.project.presentation.viewmodel.mapper.toWorkImages + +class CraftsmanSetupViewModel( + private val createCraftsmanUseCase: CreateCraftsmanProfileUseCase, + private val uploadIdCardsUseCase: UploadIdCardsUseCase, + private val uploadWorkPortfolioUseCase: UploadWorkPortfolioUseCase, + private val getCategoriesUseCase: GetCategoriesUseCase +) : BaseViewModel( + CraftsmanSetupUiState() +), CraftsmanSetupInteractionListener { + override fun onUserTypeSelected(userType: UserType) { + when (userType) { + UserType.CRAFTSMAN -> { + updateState { + it.copy( + userType = userType, + canNavigateNext = true + ) + } + } + UserType.CUSTOMER -> { + TODO("Implement Customer setup flow – redirect to CustomerSetupScreen when ready") + } + } + } + + init { + validateCurrentPage() + fetchCategories() + + viewModelScope.launch { + isLoading.collect { loading -> + updateState { it.copy(isLoading = loading) } + } + } + } + + override fun onCategoryToggled(categoryId: Int) { + updateState { state -> + val newSelection = if (categoryId in state.selectedCategoryIds) { + state.selectedCategoryIds - categoryId + } else { + state.selectedCategoryIds + categoryId + } + state.copy( + selectedCategoryIds = newSelection, + canNavigateNext = newSelection.isNotEmpty() + ) + } + } + + override fun onPersonalInfoChanged(personalInfo: PersonalInfoUiModel) { + updateState { + it.copy( + personalInfo = personalInfo, + canNavigateNext = validatePersonalInfo(personalInfo) + ) + } + } + + override fun onIdCardSelected( + isFront: Boolean, + imageData: ImageData + ) { + updateState { state -> + if (isFront) { + state.copy( + idCardFront = imageData, + canNavigateNext = state.idCardBack != null + ) + } else { + state.copy( + idCardBack = imageData, + canNavigateNext = state.idCardFront != null + ) + } + } + } + + override fun onUploadIdCards() { + val craftsmanId = state.value.craftsmanId ?: return + val frontCard = state.value.idCardFront ?: return + val backCard = state.value.idCardBack ?: return + + updateState { it.copy(isSwipeEnabled = false) } + + tryToCall( + call = { + uploadIdCardsUseCase( + craftsmanId = craftsmanId, + idCardFront = frontCard.byteArray, + idCardFrontFileName = frontCard.fileName, + idCardBack = backCard.byteArray, + idCardBackFileName = backCard.fileName + ) + }, + onSuccess = { verificationDocs -> + updateState { + it.copy(isSwipeEnabled = true) + } + sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) + }, + onError = { error -> + updateState { + it.copy( + error = error, + isSwipeEnabled = true + ) + } + }, + showLoading = true + ) + } + + override fun onSkipIdentityVerification() { + sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) + } + + override fun onPortfolioImagesAdded(images: List) { + updateState { state -> + val currentImages = state.portfolioImages + val totalImages = currentImages + images + val limitedImages = totalImages.take(4) + + state.copy( + portfolioImages = limitedImages, + canAddMoreImages = limitedImages.size < 4, + canNavigateNext = limitedImages.isNotEmpty() + ) + } + } + + override fun onPortfolioImageRemoved(index: Int) { + updateState { state -> + val newImages = state.portfolioImages.toMutableList().apply { removeAt(index) } + state.copy( + portfolioImages = newImages, + canAddMoreImages = true, + canNavigateNext = newImages.isNotEmpty() + ) + } + } + + override fun onWorkDescriptionChanged(description: String) { + updateState { state -> + state.copy(workDescription = description) + } + } + + override fun onUploadPortfolio() { + val craftsmanId = state.value.craftsmanId + if (craftsmanId == null) { + updateState { it.copy(error = ErrorUiState("Profile not created yet")) } + return + } + + updateState { it.copy(isSwipeEnabled = false) } + + tryToCall( + call = { + uploadWorkPortfolioUseCase( + craftsmanId = craftsmanId, + workImages = state.value.portfolioImages.toWorkImages() + ) + }, + onSuccess = { uploadedUrls -> + updateState { + it.copy( + isSwipeEnabled = true, + // Store uploaded URLs if needed + ) + } + // Navigate to ID verification + navigateNext() + }, + onError = { error -> + updateState { + it.copy( + error = error, + isSwipeEnabled = true + ) + } + }, + showLoading = true + ) + } + + fun clearError() { + updateState { it.copy(error = null) } + } + + fun navigateNext() { + val currentIndex = state.value.currentPageIndex + when (state.value.currentStep) { + RegistrationStep.PERSONAL_INFO -> { + if (!state.value.isProfileCreated) { + AppLogger.d("CraftsmanSetupViewModel", "Creating profile") + createCraftsmanProfile() + return + } + } + RegistrationStep.PORTFOLIO_UPLOAD -> { + if (state.value.portfolioImages.isNotEmpty()) { + onUploadPortfolio() + return // Don't navigate yet, wait for success + } + } + else -> { + // Normal navigation for other steps + } + } + if (currentIndex < state.value.totalPages - 1 && state.value.canNavigateNext) { + updateState { it.copy(currentPageIndex = currentIndex + 1) } + } + } + + fun navigateBack() { + val currentIndex = state.value.currentPageIndex + if (currentIndex > 0) { + // Prevent going back after profile creation + val targetIndex = if (state.value.isProfileCreated && currentIndex == 3) { + currentIndex // Stay on current page + } else { + currentIndex - 1 + } + updateState { it.copy(currentPageIndex = targetIndex) } + } + } + + fun onPageChanged(pageIndex: Int) { + updateState { + it.copy( + currentPageIndex = pageIndex, + canNavigateBack = pageIndex > 0, + ) + } + validateCurrentPage() + } + + private fun fetchCategories() { + tryToCall( + call = { getCategoriesUseCase() }, + onSuccess = { categories -> + val categoryUiList = categories.map { it.toUi() } + updateState { it.copy(availableCategories = categoryUiList) } + }, + onError = { error -> + updateState { it.copy(error = error) } + }, + showLoading = true + ) + } + + private fun validateCurrentPage() { + val canProceed = when (state.value.currentStep) { + RegistrationStep.USER_TYPE -> state.value.userType != null + RegistrationStep.SERVICE_SELECTION -> state.value.selectedCategoryIds.isNotEmpty() + RegistrationStep.PERSONAL_INFO -> validatePersonalInfo(state.value.personalInfo) + RegistrationStep.PORTFOLIO_UPLOAD -> state.value.portfolioImages.isNotEmpty() + RegistrationStep.IDENTITY_VERIFICATION -> true // Optional step + } + + updateState { it.copy(canNavigateNext = canProceed) } + } + + private fun createCraftsmanProfile() { + val selectedCategoryTitles = state.value.availableCategories + .filter { it.id in state.value.selectedCategoryIds } + .map { it.title } + updateState { it.copy(isSwipeEnabled = false) } + + tryToCall( + call = { + AppLogger.d("CraftsmanSetupViewModel", "call createCraftsmanUseCase") + createCraftsmanUseCase( + personalInfo = state.value.personalInfo.toDomain(), + categories = selectedCategoryTitles + ) + }, + onSuccess = { craftsmanId -> + AppLogger.d("CraftsmanSetupViewModel", "call onSuccess") + updateState { + it.copy( + craftsmanId = craftsmanId, + isProfileCreated = true, + isSwipeEnabled = true + ) + } + // Auto navigate to portfolio after successful creation + navigateNext() + }, + onError = { error -> + AppLogger.d("CraftsmanSetupViewModel", error.message) + updateState { + it.copy( + error = error, + isSwipeEnabled = true + ) + } + }, + showLoading = true + ) + } +} + + +private fun validatePersonalInfo(info: PersonalInfoUiModel): Boolean { + return info.firstName.length>=3 && + info.lastName.length>=3 && + info.phoneNumber.length>=10 && + info.phoneNumber.matches(Regex("^\\+?[1-9]\\d{1,14}$")) && + info.address.isNotBlank() +} + diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt new file mode 100644 index 0000000..0bc674e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt @@ -0,0 +1,30 @@ +package org.example.project.presentation.viewmodel.mapper + +import androidx.compose.ui.graphics.Color +import org.example.project.domain.entity.Category +import org.example.project.domain.entity.PersonalInfo +import org.example.project.domain.model.WorkImage +import org.example.project.presentation.model.CategoryUi +import org.example.project.presentation.model.ImageData +import org.example.project.presentation.model.PersonalInfoUiModel + +fun PersonalInfoUiModel.toDomain(): PersonalInfo { + return PersonalInfo( + firstName = firstName, + lastName = lastName, + phoneNumber = phoneNumber, + address = address + ) +} + +fun List.toWorkImages(): List { + return map { imageData -> + WorkImage( + data = imageData.byteArray, + fileName = imageData.fileName + ) + } +} + +fun Category.toUi(): CategoryUi = + CategoryUi(id, title, Color(colorHex)) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt index ac3a7b5..5dbf89f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt @@ -1,5 +1,7 @@ package org.example.project.presentation.viewmodel.mapper +import io.ktor.client.network.sockets.SocketTimeoutException +import io.ktor.client.plugins.HttpRequestTimeoutException import org.example.project.domain.exception.ForbiddenException import org.example.project.domain.exception.NetworkException import org.example.project.domain.exception.UnauthorizedException @@ -8,25 +10,31 @@ import org.example.project.presentation.viewmodel.base.ErrorUiState fun Throwable.toErrorUiState(): ErrorUiState { return when (this) { - is NetworkException -> ErrorUiState( - message = message ?: "Please check your internet connection", - errorType = ErrorUiState.ErrorType.NETWORK, + is NetworkException, + is SocketTimeoutException, + is HttpRequestTimeoutException -> ErrorUiState( + message = "Please check your internet connection and try again.", + errorType = ErrorUiState.ErrorType.NETWORK ) + is UnauthorizedException -> ErrorUiState( - message = message ?: "Please login to continue", - errorType = ErrorUiState.ErrorType.AUTHENTICATION, + message = "Please login to continue.", + errorType = ErrorUiState.ErrorType.AUTHENTICATION ) + is ValidationException -> ErrorUiState( - message = message ?: "Please check your input", - errorType = ErrorUiState.ErrorType.VALIDATION, + message = message ?: "Please check your input.", + errorType = ErrorUiState.ErrorType.VALIDATION ) + is ForbiddenException -> ErrorUiState( - message = message ?: "You don't have permission", - errorType = ErrorUiState.ErrorType.AUTHENTICATION, + message = "You don't have permission to perform this action.", + errorType = ErrorUiState.ErrorType.AUTHENTICATION ) + else -> ErrorUiState( - message = message ?: "Something went wrong", - errorType = ErrorUiState.ErrorType.UNKNOWN, + message = "Something went wrong. Please try again later.", + errorType = ErrorUiState.ErrorType.UNKNOWN ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt deleted file mode 100644 index bc0c9f3..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/PersonalInfo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.example.project.presentation.viewmodel.mapper - -import org.example.project.domain.entity.PersonalInfo -import org.example.project.presentation.model.PersonalInfoUiModel - -fun PersonalInfo.toUiModel(): PersonalInfoUiModel { - return PersonalInfoUiModel( - firstName = firstName, - lastName = lastName, - phoneNumber = phoneNumber, - address = address - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/util/AppLogger.kt b/composeApp/src/commonMain/kotlin/org/example/project/util/AppLogger.kt new file mode 100644 index 0000000..2fb4db0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/util/AppLogger.kt @@ -0,0 +1,7 @@ +package org.example.project.util + +expect object AppLogger { + fun e(tag: String, message: String, throwable: Throwable? = null) + fun d(tag: String, message: String) + fun i(tag: String, message: String) +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt b/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt index de98c11..280c005 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt @@ -1,9 +1,9 @@ package org.example.project.data.local.datasource +import org.example.project.data.datasource.local.StorageLocalDataSource import org.koin.core.annotation.Single import platform.Foundation.NSUserDefaults -@Single class StorageLocalDataSourceImpl : StorageLocalDataSource { private val userDefaults = NSUserDefaults.standardUserDefaults diff --git a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt index 264c6ff..a65d3d3 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt @@ -1,7 +1,7 @@ package org.example.project.di -import org.example.project.data.local.datasource.StorageLocalDataSource +import org.example.project.data.datasource.local.StorageLocalDataSource import org.example.project.data.local.datasource.StorageLocalDataSourceImpl import org.koin.dsl.module diff --git a/composeApp/src/iosMain/kotlin/org/example/project/util/AppLogger.kt b/composeApp/src/iosMain/kotlin/org/example/project/util/AppLogger.kt new file mode 100644 index 0000000..e25bea0 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/org/example/project/util/AppLogger.kt @@ -0,0 +1,22 @@ +package org.example.project.util + +import platform.Foundation.NSLog + +actual object AppLogger { + actual fun e(tag: String, message: String, throwable: Throwable?) { + + if (throwable != null) { + NSLog("ERROR: [$tag] $message. Throwable: $throwable CAUSE ${throwable.cause}") + } else { + NSLog("ERROR: [$tag] $message") + } + } + + actual fun d(tag: String, message: String) { + NSLog("DEBUG: [$tag] $message") + } + + actual fun i(tag: String, message: String) { + NSLog("INFO: [$tag] $message") + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecdc057..eee6826 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ androidx-core = "1.17.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.9.4" androidx-testExt = "1.3.0" +calfFilePicker = "0.8.0" coilComposeVersion = "3.3.0" composeMultiplatform = "1.9.0" datastorePreferences = "1.1.7" @@ -39,6 +40,7 @@ adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", versi adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "adaptive" } adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptive" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +calf-file-picker = { module = "com.mohamedrejeb.calf:calf-file-picker", version.ref = "calfFilePicker" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koinTest" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } From 65a93ea44d10e31fb9276995e4509c4df3e56178 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Mon, 20 Oct 2025 16:58:39 +0300 Subject: [PATCH 04/12] refactor onboarding API integration and refactor file upload constants --- .../project/data/mapper/OnboardingMapper.kt | 2 +- .../data/{ => remote}/dto/OnboardingDto.kt | 4 +- .../data/remote/network/APIConstant.kt | 8 +- .../repository/OnboardingRepositoryImp.kt | 8 +- .../project/data/utils/NetworkConstants.kt | 5 - .../example/project/data/utils/safeApiCall.kt | 184 +++++++++--------- .../usecase/craftsman/UploadIdCardsUseCase.kt | 13 +- .../craftsman/UploadWorkPortfolioUseCase.kt | 9 +- .../org/example/project/util/AppConstant.kt | 10 + 9 files changed, 120 insertions(+), 123 deletions(-) rename composeApp/src/commonMain/kotlin/org/example/project/data/{ => remote}/dto/OnboardingDto.kt (88%) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt index ce1da95..1a087bb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/OnboardingMapper.kt @@ -1,6 +1,6 @@ package org.example.project.data.mapper -import org.example.project.data.dto.OnboardingDto +import org.example.project.data.remote.dto.OnboardingDto import org.example.project.domain.entity.OnboardingItem fun OnboardingDto.toEntity() : OnboardingItem = diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnboardingDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/OnboardingDto.kt similarity index 88% rename from composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnboardingDto.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/OnboardingDto.kt index 3b355fa..c39a314 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/dto/OnboardingDto.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/OnboardingDto.kt @@ -1,4 +1,4 @@ -package org.example.project.data.dto +package org.example.project.data.remote.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -13,4 +13,4 @@ data class OnboardingDto( val imageUrl : String, @SerialName("description") val description : String -) +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt index fbc3527..7083722 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt @@ -18,18 +18,12 @@ object ApiConstants { const val SOCKET = 30_000L // 30 seconds } - // File Upload - object FileUpload { - const val MAX_FILE_SIZE = 4 * 1024 * 1024 // 4MB - val ALLOWED_IMAGE_TYPES = listOf("jpg", "jpeg", "png") - const val MAX_PORTFOLIO_IMAGES = 4 - } - // API Endpoints object Endpoints { // Craftsman endpoints const val CRAFTSMAN_SETUP = "/craftsman/setup" const val CRAFTSMAN_PROFILE = "/craftsman/profile" + const val ONBOARDING_END_POINT = "/onboarding" fun craftsmanIdCards(craftsmanId: String) = "/craftsman/$craftsmanId/verify/id-cards" fun craftsmanWorkPortfolio(craftsmanId: String) = "/craftsman/$craftsmanId/verify/work-portfolio" diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt index 84365bf..b4ec49b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/OnboardingRepositoryImp.kt @@ -2,10 +2,10 @@ package org.example.project.data.repository import io.ktor.client.HttpClient import io.ktor.client.request.get -import org.example.project.data.dto.OnboardingDto +import org.example.project.data.remote.dto.OnboardingDto import org.example.project.data.mapper.toEntity -import org.example.project.data.utils.NetworkConstants.ONBOARDING_END_POINT -import org.example.project.data.utils.safeApiCall +import org.example.project.data.remote.network.ApiConstants.Endpoints.ONBOARDING_END_POINT +import org.example.project.data.remote.network.wrapApiCall import org.example.project.domain.entity.OnboardingItem import org.example.project.domain.repository.OnboardingRepository import org.koin.core.annotation.Provided @@ -18,7 +18,7 @@ class OnboardingRepositoryImp( ) : OnboardingRepository { override suspend fun getOnboardingData(): List { - return safeApiCall> { + return wrapApiCall> { httpClient.get(ONBOARDING_END_POINT) }.map { it.toEntity() } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt deleted file mode 100644 index 9c7d8c5..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/NetworkConstants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.project.data.utils - -object NetworkConstants { - const val ONBOARDING_END_POINT = "/onboarding" -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt index f0a034f..767e1ce 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt @@ -1,92 +1,92 @@ -package org.example.project.data.utils - -import io.ktor.client.call.body -import io.ktor.client.statement.HttpResponse -import io.ktor.http.HttpStatusCode -import io.ktor.util.network.UnresolvedAddressException -import kotlinx.io.IOException - -internal suspend inline fun safeApiCall( - execute: suspend () -> HttpResponse -): T { - val result = try { - execute() - } catch (exception: IOException) { - logError(SAFE_API_CALL_TAG, "IOException", exception.message.toString()) - - } catch (exception: UnresolvedAddressException) { - logError(SAFE_API_CALL_TAG, "UnresolvedAddressException", exception.message.toString()) - } catch (exception: Exception) { - logError(SAFE_API_CALL_TAG, "Unknown exception", exception.message.toString()) - } - - return handleResponseStatusCode(result as HttpResponse) -} - -private suspend inline fun handleResponseStatusCode(result: HttpResponse): T { - return when (result.status.value) { - in 200..299 -> { - result.body() - } - - in 400..499 -> { - when (result.status) { - HttpStatusCode.Unauthorized -> { - logError( - HANDLE_ERROR_STATUS_TAG, - "Unauthorized", - "Not authorized to do this action" - ) - throw Exception() - } - - HttpStatusCode.NotFound -> { - logError( - HANDLE_ERROR_STATUS_TAG, - "Not found", - "the resource you requested could not be found" - ) - throw Exception() - } - - else -> { - logError( - HANDLE_ERROR_STATUS_TAG, - "Unknown 400s status code ${result.status.value}", - "An error with status code ${result.status.value} happened" - ) - throw Exception() - } - } - } - - in 500..599 -> { - logError( - HANDLE_ERROR_STATUS_TAG, - "Server error", - "An error occurred on the server side" - ) - throw Exception() - } - - else -> { - logError( - HANDLE_ERROR_STATUS_TAG, - "Unknown status code ${result.status.value}", - "An error with status code ${result.status.value} happened" - ) - throw Exception() - } - } -} - -private fun logError( - tag: String, - type: String, - message: String -) { - println("$tag----------- : $type $message") -} - -private const val SAFE_API_CALL_TAG = "safeApiCall" -private const val HANDLE_ERROR_STATUS_TAG = "handleErrorStatus" \ No newline at end of file +//package org.example.project.data.utils +// +//import io.ktor.client.call.body +//import io.ktor.client.statement.HttpResponse +//import io.ktor.http.HttpStatusCode +//import io.ktor.util.network.UnresolvedAddressException +//import kotlinx.io.IOException +// +//internal suspend inline fun safeApiCall( +// execute: suspend () -> HttpResponse +//): T { +// val result = try { +// execute() +// } catch (exception: IOException) { +// logError(SAFE_API_CALL_TAG, "IOException", exception.message.toString()) +// +// } catch (exception: UnresolvedAddressException) { +// logError(SAFE_API_CALL_TAG, "UnresolvedAddressException", exception.message.toString()) +// } catch (exception: Exception) { +// logError(SAFE_API_CALL_TAG, "Unknown exception", exception.message.toString()) +// } +// +// return handleResponseStatusCode(result as HttpResponse) +//} +// +//private suspend inline fun handleResponseStatusCode(result: HttpResponse): T { +// return when (result.status.value) { +// in 200..299 -> { +// result.body() +// } +// +// in 400..499 -> { +// when (result.status) { +// HttpStatusCode.Unauthorized -> { +// logError( +// HANDLE_ERROR_STATUS_TAG, +// "Unauthorized", +// "Not authorized to do this action" +// ) +// throw Exception() +// } +// +// HttpStatusCode.NotFound -> { +// logError( +// HANDLE_ERROR_STATUS_TAG, +// "Not found", +// "the resource you requested could not be found" +// ) +// throw Exception() +// } +// +// else -> { +// logError( +// HANDLE_ERROR_STATUS_TAG, +// "Unknown 400s status code ${result.status.value}", +// "An error with status code ${result.status.value} happened" +// ) +// throw Exception() +// } +// } +// } +// +// in 500..599 -> { +// logError( +// HANDLE_ERROR_STATUS_TAG, +// "Server error", +// "An error occurred on the server side" +// ) +// throw Exception() +// } +// +// else -> { +// logError( +// HANDLE_ERROR_STATUS_TAG, +// "Unknown status code ${result.status.value}", +// "An error with status code ${result.status.value} happened" +// ) +// throw Exception() +// } +// } +//} +// +//private fun logError( +// tag: String, +// type: String, +// message: String +//) { +// println("$tag----------- : $type $message") +//} +// +//private const val SAFE_API_CALL_TAG = "safeApiCall" +//private const val HANDLE_ERROR_STATUS_TAG = "handleErrorStatus" \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt index f7639be..2599388 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt @@ -1,6 +1,5 @@ package org.example.project.domain.usecase.craftsman -import org.example.project.data.remote.network.ApiConstants import org.example.project.domain.entity.VerificationDocuments import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository @@ -31,11 +30,11 @@ class UploadIdCardsUseCase( throw ValidationException("Please select back ID card image") } - if (idCardFront.size > ApiConstants.FileUpload.MAX_FILE_SIZE) { + if (idCardFront.size > org.example.project.util.ApiConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Front ID card image size must be less than 4MB") } - if (idCardBack.size > ApiConstants.FileUpload.MAX_FILE_SIZE) { + if (idCardBack.size > org.example.project.util.ApiConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Back ID card image size must be less than 4MB") } @@ -52,15 +51,15 @@ class UploadIdCardsUseCase( val frontExtension = idCardFrontFileName.substringAfterLast('.', "").lowercase() val backExtension = idCardBackFileName.substringAfterLast('.', "").lowercase() - if (frontExtension !in ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (frontExtension !in org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( - "Front ID card must be one of: ${ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + "Front ID card must be one of: ${org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" ) } - if (backExtension !in ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (backExtension !in org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( - "Back ID card must be one of: ${ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + "Back ID card must be one of: ${org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" ) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt index 73861e0..6db213f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt @@ -1,6 +1,5 @@ package org.example.project.domain.usecase.craftsman -import org.example.project.data.remote.network.ApiConstants import org.example.project.domain.exception.ValidationException import org.example.project.domain.model.WorkImage import org.example.project.domain.repository.CraftsmanRepository @@ -22,9 +21,9 @@ class UploadWorkPortfolioUseCase( throw ValidationException("Please select at least one work image") } - if (workImages.size > ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { + if (workImages.size > org.example.project.util.ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { throw ValidationException( - "You can upload maximum ${ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" + "You can upload maximum ${org.example.project.util.ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" ) } @@ -34,12 +33,12 @@ class UploadWorkPortfolioUseCase( throw ValidationException("Image ${index + 1} is empty") } - if (image.data.size > ApiConstants.FileUpload.MAX_FILE_SIZE) { + if (image.data.size > org.example.project.util.ApiConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Image ${index + 1} size must be less than 4MB") } val extension = image.fileName.substringAfterLast('.', "").lowercase() - if (extension !in ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (extension !in org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( "Image ${index + 1} must be JPEG or PNG" ) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt new file mode 100644 index 0000000..5ddbd70 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt @@ -0,0 +1,10 @@ +package org.example.project.util + +object ApiConstants { + // File Upload + object FileUpload { + const val MAX_FILE_SIZE = 4 * 1024 * 1024 // 4MB + val ALLOWED_IMAGE_TYPES = listOf("jpg", "jpeg", "png") + const val MAX_PORTFOLIO_IMAGES = 4 + } +} \ No newline at end of file From cb04323a27b873b17ba5ef11b618ef765a957c48 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Tue, 21 Oct 2025 21:29:12 +0300 Subject: [PATCH 05/12] feat: update API constants, enhance image upload validation, and implement image picker utility --- .../org/example/project/MainActivity.kt | 1 - .../kotlin/org/example/project/App.kt | 2 - .../CraftsmanRemoteDataSourceImpl.kt | 56 ++++- .../data/remote/network/APIConstant.kt | 2 +- .../example/project/data/utils/safeApiCall.kt | 92 -------- .../CreateCraftsmanProfileUseCase.kt | 7 - .../DeleteCraftsmanAccountUseCase.kt | 2 - .../craftsman/GetCraftsmanStatusUseCase.kt | 2 - .../usecase/craftsman/UploadIdCardsUseCase.kt | 18 +- .../craftsman/UploadWorkPortfolioUseCase.kt | 11 +- .../setupScreens/IntegrationTestScreen.kt | 198 ------------------ .../page/IdentityVerificationPage.kt | 25 ++- .../composable/page/PortfolioUploadPage.kt | 59 +++--- .../craftsmansetup/CraftsmanSetupScreen.kt | 73 ++++--- .../usersetup/AccountSetupCategoryScreen.kt | 74 ------- .../presentation/util/ImagePickerUtils.kt | 144 +++++++++++++ .../CraftsmanSetupInteractionListener.kt | 1 + .../craftsmansetup/CraftsmanSetupUiState.kt | 10 +- .../craftsmansetup/CraftsmanSetupViewModel.kt | 163 ++++++++++---- .../viewmodel/mapper/Exception.kt | 13 +- .../org/example/project/util/AppConstant.kt | 9 +- 21 files changed, 445 insertions(+), 517 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt diff --git a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt index ad137b4..b77e0b0 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt @@ -11,7 +11,6 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - //Firebase.initialize(this) setContent { App() } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index f85229e..a473b68 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -2,7 +2,6 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.screens.setupScreens.TestScreen import org.example.project.presentation.screens.setupScreens.craftsmansetup.CraftsmanSetupScreen import org.jetbrains.compose.ui.tooling.preview.Preview @@ -12,6 +11,5 @@ fun App() { AppTheme { //OnboardingScreen() CraftsmanSetupScreen() - //TestScreen() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt index 3e186e2..f210ba6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -24,10 +24,12 @@ import org.example.project.data.remote.network.ApiConstants import org.example.project.data.remote.network.ApiConstants.Headers.USER_ID import org.example.project.data.remote.network.wrapApiCall import org.example.project.domain.model.WorkImage +import org.example.project.util.AppLogger class CraftsmanRemoteDataSourceImpl( private val httpClient: HttpClient, ) : CraftsmanRemoteDataSource { + override suspend fun createCraftsmanProfile( userId: String, request: CreateCraftsmanRequest @@ -49,21 +51,35 @@ class CraftsmanRemoteDataSourceImpl( idCardBack: ByteArray, idCardBackFileName: String ): IdCardUploadResponseDto { + AppLogger.d("API", "=== Starting ID Cards Upload ===") + AppLogger.d("API", "UserId: $userId") + AppLogger.d("API", "CraftsmanId: $craftsmanId") + AppLogger.d("API", "Front: $idCardFrontFileName (${idCardFront.size} bytes)") + AppLogger.d("API", "Back: $idCardBackFileName (${idCardBack.size} bytes)") + return wrapApiCall { httpClient.submitFormWithBinaryData( url = ApiConstants.Endpoints.craftsmanIdCards(craftsmanId), formData = formData { + val frontMimeType = getMimeType(idCardFrontFileName) + AppLogger.d("API", "Front MIME type: $frontMimeType") + append("idCardFront", idCardFront, Headers.build { - append(HttpHeaders.ContentType, "image/*") + append(HttpHeaders.ContentType, frontMimeType) append(HttpHeaders.ContentDisposition, "filename=\"$idCardFrontFileName\"") }) + + val backMimeType = getMimeType(idCardBackFileName) + AppLogger.d("API", "Back MIME type: $backMimeType") + append("idCardBack", idCardBack, Headers.build { - append(HttpHeaders.ContentType, "image/*") + append(HttpHeaders.ContentType, backMimeType) append(HttpHeaders.ContentDisposition, "filename=\"$idCardBackFileName\"") }) } ) { header(USER_ID, userId) + AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanIdCards(craftsmanId)}") } } } @@ -73,19 +89,36 @@ class CraftsmanRemoteDataSourceImpl( craftsmanId: String, workImages: List ): WorkPortfolioResponseDto { + AppLogger.d("API", "=== Starting Portfolio Upload ===") + AppLogger.d("API", "UserId: $userId") + AppLogger.d("API", "CraftsmanId: $craftsmanId") + AppLogger.d("API", "Number of images: ${workImages.size}") + + workImages.forEachIndexed { index, image -> + AppLogger.d("API", "Image $index: ${image.fileName}, ${image.data.size} bytes") + } + return wrapApiCall { httpClient.submitFormWithBinaryData( url = ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId), formData = formData { - workImages.forEach { image -> - append("workImages", image.data, Headers.build { - append(HttpHeaders.ContentType, "image/*") - append(HttpHeaders.ContentDisposition, "filename=\"${image.fileName}\"") - }) + workImages.forEachIndexed { index, image -> + val mimeType = getMimeType(image.fileName) + AppLogger.d("API", "Appending image $index: ${image.fileName} ($mimeType)") + + append( + key = "workImages", + value = image.data, + headers = Headers.build { + append(HttpHeaders.ContentType, mimeType) + append(HttpHeaders.ContentDisposition, "filename=\"${image.fileName}\"") + } + ) } } ) { - header(USER_ID, userId) + header(ApiConstants.Headers.USER_ID, userId) + AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId)}") } } } @@ -115,4 +148,11 @@ class CraftsmanRemoteDataSourceImpl( } } + private fun getMimeType(fileName: String): String { + return when (fileName.substringAfterLast('.', "").lowercase()) { + "png" -> "image/png" + "jpg", "jpeg" -> "image/jpeg" + else -> "image/jpeg" // Default to JPEG + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt index 7083722..04293d1 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt @@ -1,7 +1,7 @@ package org.example.project.data.remote.network object ApiConstants { - const val BASE_URL = "http://192.168.1.52:8085" + const val BASE_URL = "http://192.168.1.53:8085" // Headers object Headers { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt deleted file mode 100644 index 767e1ce..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/utils/safeApiCall.kt +++ /dev/null @@ -1,92 +0,0 @@ -//package org.example.project.data.utils -// -//import io.ktor.client.call.body -//import io.ktor.client.statement.HttpResponse -//import io.ktor.http.HttpStatusCode -//import io.ktor.util.network.UnresolvedAddressException -//import kotlinx.io.IOException -// -//internal suspend inline fun safeApiCall( -// execute: suspend () -> HttpResponse -//): T { -// val result = try { -// execute() -// } catch (exception: IOException) { -// logError(SAFE_API_CALL_TAG, "IOException", exception.message.toString()) -// -// } catch (exception: UnresolvedAddressException) { -// logError(SAFE_API_CALL_TAG, "UnresolvedAddressException", exception.message.toString()) -// } catch (exception: Exception) { -// logError(SAFE_API_CALL_TAG, "Unknown exception", exception.message.toString()) -// } -// -// return handleResponseStatusCode(result as HttpResponse) -//} -// -//private suspend inline fun handleResponseStatusCode(result: HttpResponse): T { -// return when (result.status.value) { -// in 200..299 -> { -// result.body() -// } -// -// in 400..499 -> { -// when (result.status) { -// HttpStatusCode.Unauthorized -> { -// logError( -// HANDLE_ERROR_STATUS_TAG, -// "Unauthorized", -// "Not authorized to do this action" -// ) -// throw Exception() -// } -// -// HttpStatusCode.NotFound -> { -// logError( -// HANDLE_ERROR_STATUS_TAG, -// "Not found", -// "the resource you requested could not be found" -// ) -// throw Exception() -// } -// -// else -> { -// logError( -// HANDLE_ERROR_STATUS_TAG, -// "Unknown 400s status code ${result.status.value}", -// "An error with status code ${result.status.value} happened" -// ) -// throw Exception() -// } -// } -// } -// -// in 500..599 -> { -// logError( -// HANDLE_ERROR_STATUS_TAG, -// "Server error", -// "An error occurred on the server side" -// ) -// throw Exception() -// } -// -// else -> { -// logError( -// HANDLE_ERROR_STATUS_TAG, -// "Unknown status code ${result.status.value}", -// "An error with status code ${result.status.value} happened" -// ) -// throw Exception() -// } -// } -//} -// -//private fun logError( -// tag: String, -// type: String, -// message: String -//) { -// println("$tag----------- : $type $message") -//} -// -//private const val SAFE_API_CALL_TAG = "safeApiCall" -//private const val HANDLE_ERROR_STATUS_TAG = "handleErrorStatus" \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt index fd8a72a..b50796d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt @@ -12,14 +12,10 @@ class CreateCraftsmanProfileUseCase( personalInfo: PersonalInfo, categories: List ): String { - // Business validation - throw ValidationException for any invalid input - - // Validate categories if (categories.isEmpty()) { throw ValidationException("Please select at least one service category") } - // Validate personal info if (personalInfo.firstName.isBlank()) { throw ValidationException("First name is required") } @@ -40,13 +36,10 @@ class CreateCraftsmanProfileUseCase( throw ValidationException("Address is required") } - // All validation passed - call repository - // No try-catch needed - let exceptions propagate to ViewModel return repository.createCraftsmanProfile(personalInfo, categories) } private fun isValidPhoneNumber(phone: String): Boolean { - // Basic phone validation - accepts international format return phone.matches(Regex("^\\+?[1-9]\\d{1,14}$")) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt index 4aeea2a..5597d53 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/DeleteCraftsmanAccountUseCase.kt @@ -11,7 +11,6 @@ class DeleteCraftsmanAccountUseCase( craftsmanId: String, confirmDelete: Boolean = false ) { - // Business validation if (craftsmanId.isBlank()) { throw ValidationException("Craftsman ID is required") } @@ -20,7 +19,6 @@ class DeleteCraftsmanAccountUseCase( throw ValidationException("Please confirm account deletion") } - // Direct repository call repository.deleteCraftsmanAccount(craftsmanId) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt index f3365e0..730e6d8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/GetCraftsmanStatusUseCase.kt @@ -9,12 +9,10 @@ class GetCraftsmanStatusUseCase( private val repository: CraftsmanRepository ) { suspend operator fun invoke(craftsmanId: String): CraftsmanStatus { - // Business validation if (craftsmanId.isBlank()) { throw ValidationException("Craftsman ID is required") } - // Direct repository call return repository.getCraftsmanStatus(craftsmanId) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt index 2599388..b8ab58c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt @@ -16,12 +16,10 @@ class UploadIdCardsUseCase( idCardBack: ByteArray, idCardBackFileName: String ): VerificationDocuments { - // Validate craftsman ID if (craftsmanId.isBlank()) { throw ValidationException("Craftsman ID is required") } - // Validate file sizes if (idCardFront.isEmpty()) { throw ValidationException("Please select front ID card image") } @@ -30,15 +28,14 @@ class UploadIdCardsUseCase( throw ValidationException("Please select back ID card image") } - if (idCardFront.size > org.example.project.util.ApiConstants.FileUpload.MAX_FILE_SIZE) { + if (idCardFront.size > org.example.project.util.AppConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Front ID card image size must be less than 4MB") } - if (idCardBack.size > org.example.project.util.ApiConstants.FileUpload.MAX_FILE_SIZE) { + if (idCardBack.size > org.example.project.util.AppConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Back ID card image size must be less than 4MB") } - // Validate file names (must have extensions) if (!idCardFrontFileName.contains(".")) { throw ValidationException("Invalid front ID card file name") } @@ -47,24 +44,21 @@ class UploadIdCardsUseCase( throw ValidationException("Invalid back ID card file name") } - // Validate file types val frontExtension = idCardFrontFileName.substringAfterLast('.', "").lowercase() val backExtension = idCardBackFileName.substringAfterLast('.', "").lowercase() - if (frontExtension !in org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (frontExtension !in org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( - "Front ID card must be one of: ${org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + "Front ID card must be one of: ${org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" ) } - if (backExtension !in org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (backExtension !in org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( - "Back ID card must be one of: ${org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + "Back ID card must be one of: ${org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" ) } - // All validation passed - call repository - // No error handling - let exceptions propagate return repository.uploadIdCards( craftsmanId = craftsmanId, idCardFront = idCardFront, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt index 6db213f..84086d6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt @@ -12,7 +12,6 @@ class UploadWorkPortfolioUseCase( craftsmanId: String, workImages: List ): List { - // Business validation if (craftsmanId.isBlank()) { throw ValidationException("Craftsman ID is required") } @@ -21,31 +20,29 @@ class UploadWorkPortfolioUseCase( throw ValidationException("Please select at least one work image") } - if (workImages.size > org.example.project.util.ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { + if (workImages.size > org.example.project.util.AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { throw ValidationException( - "You can upload maximum ${org.example.project.util.ApiConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" + "You can upload maximum ${org.example.project.util.AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" ) } - // Validate each image workImages.forEachIndexed { index, image -> if (image.data.isEmpty()) { throw ValidationException("Image ${index + 1} is empty") } - if (image.data.size > org.example.project.util.ApiConstants.FileUpload.MAX_FILE_SIZE) { + if (image.data.size > org.example.project.util.AppConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Image ${index + 1} size must be less than 4MB") } val extension = image.fileName.substringAfterLast('.', "").lowercase() - if (extension !in org.example.project.util.ApiConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (extension !in org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( "Image ${index + 1} must be JPEG or PNG" ) } } - // Direct repository call return repository.uploadWorkPortfolio(craftsmanId, workImages) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt deleted file mode 100644 index 897685d..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/IntegrationTestScreen.kt +++ /dev/null @@ -1,198 +0,0 @@ -package org.example.project.presentation.screens.setupScreens - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch -import org.example.project.data.datasource.local.UserPreferences -import org.example.project.domain.entity.PersonalInfo -import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase -import org.koin.compose.koinInject -import kotlin.time.Clock -import kotlin.time.ExperimentalTime - -@OptIn(ExperimentalTime::class) -@Composable -fun TestScreen() { - val scope = rememberCoroutineScope() - var result by remember { mutableStateOf("") } - var isLoading by remember { mutableStateOf(false) } - var isLoggedIn by remember { mutableStateOf(false) } - var currentUserId by remember { mutableStateOf(null) } - - // Get dependencies from Koin - val userPreferences: UserPreferences = koinInject() - val createCraftsmanUseCase: CreateCraftsmanProfileUseCase = koinInject() - - // Check login status on composition - LaunchedEffect(Unit) { - currentUserId = userPreferences.getUserId() - isLoggedIn = currentUserId != null - } - - Column( - modifier = Modifier.fillMaxSize().padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - "API Test Screen", - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) - - // Login Status Card - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (isLoggedIn) Color(0xFF4CAF50).copy(alpha = 0.1f) - else Color(0xFFF44336).copy(alpha = 0.1f) - ) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "Login Status:", - style = MaterialTheme.typography.titleMedium - ) - Text( - if (isLoggedIn) "Logged In" else "Not Logged In", - color = if (isLoggedIn) Color(0xFF4CAF50) else Color(0xFFF44336), - fontWeight = FontWeight.Bold - ) - } - - if (isLoggedIn) { - Text( - "User ID: $currentUserId", - style = MaterialTheme.typography.bodyMedium - ) - } - - Button( - onClick = { - scope.launch { - if (isLoggedIn) { - // Logout - userPreferences.clearUserId() - isLoggedIn = false - currentUserId = null - result = "Logged out successfully" - } else { - // Simulate login - val testUserId = "test-user-${Clock.System.now()}" - userPreferences.setUserId(testUserId) - currentUserId = testUserId - isLoggedIn = true - result = "Logged in with ID: $testUserId" - } - } - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isLoggedIn) Color(0xFFF44336) else Color(0xFF4CAF50) - ) - ) { - Text(if (isLoggedIn) "Logout" else "Simulate Login") - } - } - } - - Divider() - - // Test Actions - Text( - "Test Actions", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Button( - onClick = { - scope.launch { - isLoading = true - try { - val personalInfo = PersonalInfo( - firstName = "Test", - lastName = "User ${Clock.System.now()}", - phoneNumber = "+1234567890", - address = "123 Test Street" - ) - - val craftsmanId = createCraftsmanUseCase( - personalInfo, - listOf("plumbing", "electrical") - ) - - result = "Success! Craftsman ID: $craftsmanId" - } catch (e: Exception) { - result = "Error: ${e.message}" - } finally { - isLoading = false - } - } - }, - enabled = !isLoading && isLoggedIn, - modifier = Modifier.fillMaxWidth() - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Test Create Craftsman Profile") - } - } - - if (!isLoggedIn) { - Text( - "Please login first to test API calls", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall - ) - } - - // Result Display - if (result.isNotEmpty()) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (result.startsWith("Success") || result.contains("Logged")) - Color(0xFF4CAF50).copy(alpha = 0.1f) - else Color(0xFFF44336).copy(alpha = 0.1f) - ) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - "Result:", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - Text( - text = result, - style = MaterialTheme.typography.bodyMedium, - color = if (result.startsWith("Success") || result.contains("Logged")) - Color(0xFF4CAF50) - else Color(0xFFF44336) - ) - } - } - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt index cc2864e..7c7a46e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt @@ -28,6 +28,7 @@ import crafto.composeapp.generated.resources.camera import crafto.composeapp.generated.resources.plus import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData +import org.example.project.presentation.util.rememberImagePicker import org.jetbrains.compose.resources.painterResource @Composable @@ -36,7 +37,8 @@ fun IdentityVerificationPage( idCardBack: ImageData?, onIdCardSelected: (isFront: Boolean, imageData: ImageData) -> Unit, onUploadClick: () -> Unit, - onSkip: () -> Unit + onSkip: () -> Unit, + onErrorMessage: (String) -> Unit ) { Column( modifier = Modifier @@ -48,13 +50,15 @@ fun IdentityVerificationPage( UploadBox( title = "Upload Front of National ID", image = idCardFront, - onSelect = { img -> onIdCardSelected(true, img) } + onSelect = { img -> onIdCardSelected(true, img) }, + onError = onErrorMessage ) UploadBox( title = "Upload Back of National ID", image = idCardBack, - onSelect = { img -> onIdCardSelected(false, img) } + onSelect = { img -> onIdCardSelected(false, img) }, + onError = onErrorMessage ) Spacer(modifier = Modifier.weight(1f)) @@ -70,8 +74,17 @@ fun IdentityVerificationPage( private fun UploadBox( title: String, image: ImageData?, - onSelect: (ImageData) -> Unit + onSelect: (ImageData) -> Unit, + onError: (String) -> Unit ) { + val imagePicker = rememberImagePicker( + singleSelection = true, + onImagesSelected = { images -> + images.firstOrNull()?.let(onSelect) + }, + onError = onError + ) + Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(8.dp) @@ -86,7 +99,7 @@ private fun UploadBox( RoundedCornerShape(12.dp) ) .clickable { - // TODO: implement image picker + imagePicker.launch() }, contentAlignment = Alignment.Center ) { @@ -94,7 +107,7 @@ private fun UploadBox( Icon(painterResource(Res.drawable.camera), contentDescription = "upload image") else { AsyncImage( - model = image.uri, + model = image.byteArray, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(12.dp)) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt index d2cd3d4..9019d9e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.launch import org.example.project.presentation.designsystem.components.TextField import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData +import org.example.project.presentation.util.rememberImagePicker import org.jetbrains.compose.resources.painterResource import kotlin.time.Clock import kotlin.time.ExperimentalTime @@ -56,31 +57,41 @@ fun PortfolioUploadPage( val scope = rememberCoroutineScope() val context = LocalPlatformContext.current - val imagePicker = rememberFilePickerLauncher( - type = FilePickerFileType.Image, - selectionMode = FilePickerSelectionMode.Multiple, - ) { files -> - scope.launch { - val imageDataList = files.take(4 - images.size).mapNotNull { file -> - try { - val fileName = file.getName(context) ?: "image_${Clock.System.now()}.jpg" - val byteArray = file.readByteArray(context) - - ImageData( - uri = fileName, - byteArray = byteArray, - fileName = fileName - ) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - if (imageDataList.isNotEmpty()) { - onAddPhotosClicked(imageDataList) - } + val imagePicker = rememberImagePicker( + singleSelection = false, + onImagesSelected = { newImages -> + onAddPhotosClicked(newImages) + }, + onError = { errorMessage -> + // Handle error - show snackbar/toast } - } + ) + +// val imagePicker = rememberFilePickerLauncher( +// type = FilePickerFileType.Image, +// selectionMode = FilePickerSelectionMode.Multiple, +// ) { files -> +// scope.launch { +// val imageDataList = files.take(4 - images.size).mapNotNull { file -> +// try { +// val fileName = file.getName(context) ?: "image_${Clock.System.now()}.jpg" +// val byteArray = file.readByteArray(context) +// +// ImageData( +// uri = fileName, +// byteArray = byteArray, +// fileName = fileName +// ) +// } catch (e: Exception) { +// e.printStackTrace() +// null +// } +// } +// if (imageDataList.isNotEmpty()) { +// onAddPhotosClicked(imageDataList) +// } +// } +// } Column( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt index 099b172..c3d9396 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt @@ -30,7 +30,11 @@ import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.TextButton import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.setupScreens.composable.SetupScreenScaffold +import org.example.project.presentation.screens.setupScreens.composable.page.IdentityVerificationPage +import org.example.project.presentation.screens.setupScreens.composable.page.PersonalInfoPage import org.example.project.presentation.screens.setupScreens.composable.page.PortfolioUploadPage +import org.example.project.presentation.screens.setupScreens.composable.page.ServiceSelectionPage +import org.example.project.presentation.screens.setupScreens.composable.page.UserTypeSelectionPage import org.example.project.presentation.viewmodel.base.ErrorUiState import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanRegistrationEffect import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupUiState @@ -154,30 +158,30 @@ fun CraftsmanSetupContent( state = pagerState, userScrollEnabled = state.isSwipeEnabled && state.canNavigateNext ) { page -> -// when (RegistrationStep.fromIndex(page)) { -// RegistrationStep.USER_TYPE -> { -// UserTypeSelectionPage( -// selectedType = state.userType, -// onTypeSelected = viewModel::onUserTypeSelected, -// ) -// } -// RegistrationStep.SERVICE_SELECTION -> { -// ServiceSelectionPage( -// availableCategories = state.availableCategories, -// onCategoryToggled = viewModel::onCategoryToggled, -// selectedServiceIds = state.selectedCategoryIds, -// ) -// } -// -// RegistrationStep.PERSONAL_INFO -> { -// PersonalInfoPage( -// personalInfo = state.personalInfo, -// onPersonalInfoChanged = viewModel::onPersonalInfoChanged, -// isLoading = state.isLoading, -// ) -// } + when (RegistrationStep.fromIndex(page)) { + RegistrationStep.USER_TYPE -> { + UserTypeSelectionPage( + selectedType = state.userType, + onTypeSelected = viewModel::onUserTypeSelected, + ) + } + RegistrationStep.SERVICE_SELECTION -> { + ServiceSelectionPage( + availableCategories = state.availableCategories, + onCategoryToggled = viewModel::onCategoryToggled, + selectedServiceIds = state.selectedCategoryIds, + ) + } - //RegistrationStep.PORTFOLIO_UPLOAD -> { + RegistrationStep.PERSONAL_INFO -> { + PersonalInfoPage( + personalInfo = state.personalInfo, + onPersonalInfoChanged = viewModel::onPersonalInfoChanged, + isLoading = state.isLoading, + ) + } + + RegistrationStep.PORTFOLIO_UPLOAD -> { PortfolioUploadPage( images = state.portfolioImages, workDescription = state.workDescription, @@ -186,18 +190,19 @@ fun CraftsmanSetupContent( onImageRemoved = viewModel::onPortfolioImageRemoved, onDescriptionChanged = viewModel::onWorkDescriptionChanged, ) - //} + } - //RegistrationStep.IDENTITY_VERIFICATION -> { -// IdentityVerificationPage( -// idCardFront = state.idCardFront, -// idCardBack = state.idCardBack, -// onIdCardSelected = viewModel::onIdCardSelected, -// onUploadClick = viewModel::onUploadIdCards, -// onSkip = {}, -// ) - //} - //} + RegistrationStep.IDENTITY_VERIFICATION -> { + IdentityVerificationPage( + idCardFront = state.idCardFront, + idCardBack = state.idCardBack, + onIdCardSelected = viewModel::onIdCardSelected, + onUploadClick = viewModel::onUploadIdCards, + onSkip = {}, + onErrorMessage = {} , + ) + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt deleted file mode 100644 index acb81d5..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/usersetup/AccountSetupCategoryScreen.kt +++ /dev/null @@ -1,74 +0,0 @@ -//package org.example.project.presentation.screens.setupScreens.usersetup -// -//import androidx.compose.runtime.Composable -//import androidx.compose.runtime.collectAsState -//import androidx.compose.runtime.getValue -//import androidx.compose.ui.Modifier -//import crafto.composeapp.generated.resources.Res -//import crafto.composeapp.generated.resources.account_setup_craftsman_category_description -//import crafto.composeapp.generated.resources.account_setup_craftsman_category_title -//import crafto.composeapp.generated.resources.account_setup_customer_category_description -//import crafto.composeapp.generated.resources.account_setup_customer_category_title -//import org.example.project.presentation.screens.setupScreens.component.CategoryActionBox -//import org.example.project.presentation.screens.setupScreens.component.SetupScreenScaffold -//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState -//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupViewModel -//import org.jetbrains.compose.resources.stringResource -//import org.koin.compose.viewmodel.koinViewModel -//import org.koin.core.annotation.KoinExperimentalAPI -// -//@Composable -//fun AccountSetupCategoryScreen( -// viewModel: AccountSetupViewModel, -//) { -// val state by viewModel.state.collectAsState() -// AccountSetupCategoryContent( -// state = state, -// isCustomer = true, -// currentPageNumber = 2, -// onBackButtonClick = {}, -// onNextButtonClick = {}, -// onChipSelected = viewModel::onCategorySelected -// ) -//} -// -//@Composable -//fun AccountSetupCategoryContent( -// modifier: Modifier = Modifier, -// state: AccountSetupState, -// isCustomer: Boolean, -// currentPageNumber: Int, -// onBackButtonClick: () -> Unit, -// onNextButtonClick: () -> Unit, -// onChipSelected: (id: Int) -> Unit, -//) { -// SetupScreenScaffold( -// modifier = modifier, -// currentPageNumber = currentPageNumber, -// title = selectCustomerOrCraftsmanText(isCustomer).first, -// description = selectCustomerOrCraftsmanText(isCustomer).second, -// onBackButtonClick = onBackButtonClick, -// onNextButtonClick = onNextButtonClick, -// ) { -// CategoryActionBox( -// state = state, -// onChipSelected = onChipSelected, -// ) -// } -// -//} -// -//@Composable -//private fun selectCustomerOrCraftsmanText(isCustomer: Boolean): Pair { -// return if (isCustomer) { -// stringResource(Res.string.account_setup_customer_category_title) to -// stringResource(Res.string.account_setup_customer_category_description) -// } else { -// stringResource(Res.string.account_setup_craftsman_category_title) to -// stringResource(Res.string.account_setup_craftsman_category_description) -// } -//} -// -// -// -// diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt new file mode 100644 index 0000000..a185c8f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt @@ -0,0 +1,144 @@ +package org.example.project.presentation.util + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import com.mohamedrejeb.calf.core.LocalPlatformContext +import com.mohamedrejeb.calf.io.getName +import com.mohamedrejeb.calf.io.readByteArray +import com.mohamedrejeb.calf.picker.FilePickerFileType +import com.mohamedrejeb.calf.picker.FilePickerSelectionMode +import com.mohamedrejeb.calf.picker.rememberFilePickerLauncher +import kotlinx.coroutines.launch +import org.example.project.presentation.model.ImageData +import org.example.project.util.AppConstants +import org.example.project.util.AppLogger +import kotlin.time.Clock +import kotlin.time.ExperimentalTime + +class ImagePickerLauncher( + private val launch: () -> Unit +) { + fun launch() = launch.invoke() +} + +@OptIn(ExperimentalTime::class) +@Composable +fun rememberImagePicker( + singleSelection: Boolean = true, + onImagesSelected: (List) -> Unit, + onError: (String) -> Unit = {} +): ImagePickerLauncher { + val scope = rememberCoroutineScope() + val context = LocalPlatformContext.current + + val launcher = rememberFilePickerLauncher( + type = FilePickerFileType.Image, + selectionMode = if (singleSelection) { + FilePickerSelectionMode.Single + } else { + FilePickerSelectionMode.Multiple + } + ) { files -> + scope.launch { + try { + AppLogger.d("ImagePicker", " Files selected: ${files.size}") + + val imageDataList = files.mapNotNull { file -> + try { + val originalFileName = file.getName(context) + AppLogger.d("ImagePicker", "Processing file: $originalFileName") + + val byteArray = file.readByteArray(context) + AppLogger.d("ImagePicker", " Size: ${byteArray.size} bytes") + + // Basic validation + if (byteArray.size > AppConstants.FileUpload.MAX_FILE_SIZE) { + onError("Image is too large. Maximum size is ${AppConstants.FileUpload.MAX_FILE_SIZE_MB}MB") + AppLogger.e("ImagePicker", " File too large!") + return@mapNotNull null + } + + // CRITICAL: Ensure proper file extension + val fileName = ensureProperFileName(originalFileName, byteArray) + AppLogger.d("ImagePicker", " Final fileName: $fileName") + + // Validate extension + val extension = fileName.substringAfterLast('.', "").lowercase() + if (extension !in AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + AppLogger.e("ImagePicker", " Invalid extension: $extension") + onError("Invalid image format. Please select JPG or PNG") + return@mapNotNull null + } + + AppLogger.d("ImagePicker", " File validated successfully") + + ImageData( + uri = fileName, + fileName = fileName, + byteArray = byteArray + ) + } catch (e: Exception) { + AppLogger.e("ImagePicker", " Error processing file: ${e.message}") + e.printStackTrace() + null + } + } + + if (imageDataList.isNotEmpty()) { + AppLogger.d("ImagePicker", " Successfully processed ${imageDataList.size} images") + onImagesSelected(imageDataList) + } else { + AppLogger.d("ImagePicker", "⚠ No valid images processed") + } + } catch (e: Exception) { + AppLogger.e("ImagePicker", " Failed to pick images: ${e.message}") + onError("Failed to pick images") + } + } + } + + return remember { + ImagePickerLauncher { launcher.launch() } + } +} + +@OptIn(ExperimentalTime::class) +private fun ensureProperFileName(originalFileName: String?, byteArray: ByteArray): String { + // If we have a filename with valid extension, use it + if (!originalFileName.isNullOrBlank()) { + val extension = originalFileName.substringAfterLast('.', "").lowercase() + if (extension in AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + return originalFileName + } + + val detectedExt = detectImageFormat(byteArray) + val baseFileName = originalFileName.substringBeforeLast('.', originalFileName) + return "$baseFileName.$detectedExt" + } + + val extension = detectImageFormat(byteArray) + return "image_${Clock.System.now().toEpochMilliseconds()}.$extension" +} + + +private fun detectImageFormat(byteArray: ByteArray): String { + if (byteArray.size < 4) return "jpg" + + // PNG signature: 89 50 4E 47 + if (byteArray[0] == 0x89.toByte() && + byteArray[1] == 0x50.toByte() && + byteArray[2] == 0x4E.toByte() && + byteArray[3] == 0x47.toByte()) { + return "png" + } + + // JPEG signature: FF D8 FF + if (byteArray[0] == 0xFF.toByte() && + byteArray[1] == 0xD8.toByte() && + byteArray[2] == 0xFF.toByte()) { + return "jpg" + } + + return "jpg" +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt index e8b6151..a654fe7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -20,4 +20,5 @@ interface CraftsmanSetupInteractionListener { fun onPortfolioImageRemoved(index: Int) fun onWorkDescriptionChanged(description: String) fun onUploadPortfolio() + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt index 233d56d..4a390b4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt @@ -65,11 +65,11 @@ data class CraftsmanSetupUiState( } enum class RegistrationStep(val index: Int) { - USER_TYPE(0), // Choose Customer/Craftsman - SERVICE_SELECTION(1), // What services do you offer - PERSONAL_INFO(2), // Collect personal information - PORTFOLIO_UPLOAD(3), // Show your work - IDENTITY_VERIFICATION(4); // Upload ID (Optional) + USER_TYPE(0), + SERVICE_SELECTION(1), + PERSONAL_INFO(2), + PORTFOLIO_UPLOAD(3), + IDENTITY_VERIFICATION(4); companion object { fun fromIndex(index: Int): RegistrationStep = diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt index 049cd92..200c1ee 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt @@ -23,6 +23,7 @@ class CraftsmanSetupViewModel( ) : BaseViewModel( CraftsmanSetupUiState() ), CraftsmanSetupInteractionListener { + override fun onUserTypeSelected(userType: UserType) { when (userType) { UserType.CRAFTSMAN -> { @@ -42,7 +43,6 @@ class CraftsmanSetupViewModel( init { validateCurrentPage() fetchCategories() - viewModelScope.launch { isLoading.collect { loading -> updateState { it.copy(isLoading = loading) } @@ -73,10 +73,21 @@ class CraftsmanSetupViewModel( } } - override fun onIdCardSelected( - isFront: Boolean, - imageData: ImageData - ) { + override fun onIdCardSelected(isFront: Boolean, imageData: ImageData) { + AppLogger.d("IDCard", " ID Card selected") + AppLogger.d("IDCard", " Front: $isFront") + AppLogger.d("IDCard", " FileName: ${imageData.fileName}") + AppLogger.d("IDCard", " URI: ${imageData.uri}") + AppLogger.d("IDCard", " Size: ${imageData.byteArray.size} bytes") + + // Extract and validate extension + val extension = imageData.fileName.substringAfterLast('.', "").lowercase() + AppLogger.d("IDCard", " Extension: '$extension'") + + if (extension.isEmpty()) { + AppLogger.e("IDCard", " No extension found in filename!") + } + updateState { state -> if (isFront) { state.copy( @@ -93,9 +104,34 @@ class CraftsmanSetupViewModel( } override fun onUploadIdCards() { - val craftsmanId = state.value.craftsmanId ?: return - val frontCard = state.value.idCardFront ?: return - val backCard = state.value.idCardBack ?: return + val craftsmanId = state.value.craftsmanId + val frontCard = state.value.idCardFront + val backCard = state.value.idCardBack + + AppLogger.d("IDCard", " onUploadIdCards() called") + AppLogger.d("IDCard", " CraftsmanId: $craftsmanId") + + if (craftsmanId == null) { + AppLogger.e("IDCard", " CraftsmanId is null!") + updateState { it.copy(error = ErrorUiState("Profile not created yet")) } + return + } + + if (frontCard == null || backCard == null) { + AppLogger.e("IDCard", " Missing ID cards!") + updateState { it.copy(error = ErrorUiState("Please upload both ID card images")) } + return + } + + AppLogger.d("IDCard", " Front card:") + AppLogger.d("IDCard", " - FileName: ${frontCard.fileName}") + AppLogger.d("IDCard", " - Size: ${frontCard.byteArray.size} bytes") + AppLogger.d("IDCard", " - Extension: ${frontCard.fileName.substringAfterLast('.', "")}") + + AppLogger.d("IDCard", " Back card:") + AppLogger.d("IDCard", " - FileName: ${backCard.fileName}") + AppLogger.d("IDCard", " - Size: ${backCard.byteArray.size} bytes") + AppLogger.d("IDCard", " - Extension: ${backCard.fileName.substringAfterLast('.', "")}") updateState { it.copy(isSwipeEnabled = false) } @@ -110,12 +146,12 @@ class CraftsmanSetupViewModel( ) }, onSuccess = { verificationDocs -> - updateState { - it.copy(isSwipeEnabled = true) - } + AppLogger.d("IDCard", " ID Cards uploaded successfully!") + updateState { it.copy(isSwipeEnabled = true) } sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) }, onError = { error -> + AppLogger.e("IDCard", " Upload failed: ${error.message}") updateState { it.copy( error = error, @@ -137,6 +173,13 @@ class CraftsmanSetupViewModel( val totalImages = currentImages + images val limitedImages = totalImages.take(4) + AppLogger.d("Portfolio", "Added ${images.size} images. Total: ${limitedImages.size}") + + // Log each image details + limitedImages.forEachIndexed { index, img -> + AppLogger.d("Portfolio", "Image $index: ${img.fileName}, ${img.byteArray.size} bytes") + } + state.copy( portfolioImages = limitedImages, canAddMoreImages = limitedImages.size < 4, @@ -147,7 +190,9 @@ class CraftsmanSetupViewModel( override fun onPortfolioImageRemoved(index: Int) { updateState { state -> - val newImages = state.portfolioImages.toMutableList().apply { removeAt(index) } + val newImages = state.portfolioImages.toMutableList().apply { + removeAt(index) + } state.copy( portfolioImages = newImages, canAddMoreImages = true, @@ -165,34 +210,64 @@ class CraftsmanSetupViewModel( override fun onUploadPortfolio() { val craftsmanId = state.value.craftsmanId if (craftsmanId == null) { + AppLogger.e("Portfolio", "CraftsmanId is null!") updateState { it.copy(error = ErrorUiState("Profile not created yet")) } return } - updateState { it.copy(isSwipeEnabled = false) } + val portfolioImages = state.value.portfolioImages + if (portfolioImages.isEmpty()) { + AppLogger.d("Portfolio", "No images to upload, skipping to next page") + updateState { it.copy(currentPageIndex = it.currentPageIndex + 1) } + return + } + + // Check if already uploaded + if (state.value.uploadedPortfolioUrls.isNotEmpty()) { + AppLogger.d("Portfolio", "Portfolio already uploaded, skipping") + updateState { it.copy(currentPageIndex = it.currentPageIndex + 1) } + return + } + + AppLogger.d("Portfolio", "Starting upload of ${portfolioImages.size} images for craftsman $craftsmanId") + + portfolioImages.forEachIndexed { index, image -> + AppLogger.d("Portfolio", "Image $index: fileName=${image.fileName}, size=${image.byteArray.size} bytes") + } + + updateState { it.copy(isSwipeEnabled = false, isUploadingPortfolio = true) } tryToCall( call = { + val workImages = portfolioImages.toWorkImages() + AppLogger.d("Portfolio", "Converted to ${workImages.size} WorkImage objects - calling API") uploadWorkPortfolioUseCase( craftsmanId = craftsmanId, - workImages = state.value.portfolioImages.toWorkImages() + workImages = workImages ) }, onSuccess = { uploadedUrls -> + AppLogger.d("Portfolio", " SUCCESS! Received ${uploadedUrls.size} URLs") + uploadedUrls.forEach { url -> + AppLogger.d("Portfolio", " - $url") + } + updateState { it.copy( isSwipeEnabled = true, - // Store uploaded URLs if needed + isUploadingPortfolio = false, + uploadedPortfolioUrls = uploadedUrls, + currentPageIndex = it.currentPageIndex + 1 ) } - // Navigate to ID verification - navigateNext() }, onError = { error -> + AppLogger.e("Portfolio", " FAILED: ${error.message}") updateState { it.copy( error = error, - isSwipeEnabled = true + isSwipeEnabled = true, + isUploadingPortfolio = false ) } }, @@ -206,25 +281,41 @@ class CraftsmanSetupViewModel( fun navigateNext() { val currentIndex = state.value.currentPageIndex + when (state.value.currentStep) { RegistrationStep.PERSONAL_INFO -> { if (!state.value.isProfileCreated) { - AppLogger.d("CraftsmanSetupViewModel", "Creating profile") + AppLogger.d("Navigation", "Creating profile before proceeding") createCraftsmanProfile() - return + return // createCraftsmanProfile will navigate on success } } RegistrationStep.PORTFOLIO_UPLOAD -> { - if (state.value.portfolioImages.isNotEmpty()) { + val hasImages = state.value.portfolioImages.isNotEmpty() + val alreadyUploaded = state.value.uploadedPortfolioUrls.isNotEmpty() + val isCurrentlyUploading = state.value.isUploadingPortfolio + + if (isCurrentlyUploading) { + AppLogger.d("Navigation", "Upload already in progress, ignoring navigation") + return + } + + if (hasImages && !alreadyUploaded) { + AppLogger.d("Navigation", "Portfolio needs to be uploaded") onUploadPortfolio() - return // Don't navigate yet, wait for success + return // onUploadPortfolio will navigate on success } + + AppLogger.d("Navigation", "Portfolio already uploaded or no images, proceeding") } else -> { // Normal navigation for other steps } } + + // Normal navigation if (currentIndex < state.value.totalPages - 1 && state.value.canNavigateNext) { + AppLogger.d("Navigation", "Navigating from page $currentIndex to ${currentIndex + 1}") updateState { it.copy(currentPageIndex = currentIndex + 1) } } } @@ -282,30 +373,31 @@ class CraftsmanSetupViewModel( val selectedCategoryTitles = state.value.availableCategories .filter { it.id in state.value.selectedCategoryIds } .map { it.title } + updateState { it.copy(isSwipeEnabled = false) } tryToCall( call = { - AppLogger.d("CraftsmanSetupViewModel", "call createCraftsmanUseCase") + AppLogger.d("CraftsmanSetupViewModel", "Calling createCraftsmanUseCase") createCraftsmanUseCase( personalInfo = state.value.personalInfo.toDomain(), categories = selectedCategoryTitles ) }, onSuccess = { craftsmanId -> - AppLogger.d("CraftsmanSetupViewModel", "call onSuccess") + AppLogger.d("CraftsmanSetupViewModel", "Profile created successfully: $craftsmanId") updateState { it.copy( craftsmanId = craftsmanId, isProfileCreated = true, - isSwipeEnabled = true + isSwipeEnabled = true, + // Navigate to portfolio page + currentPageIndex = it.currentPageIndex + 1 ) } - // Auto navigate to portfolio after successful creation - navigateNext() }, onError = { error -> - AppLogger.d("CraftsmanSetupViewModel", error.message) + AppLogger.e("CraftsmanSetupViewModel", "Profile creation failed: ${error.message}") updateState { it.copy( error = error, @@ -316,14 +408,13 @@ class CraftsmanSetupViewModel( showLoading = true ) } -} - -private fun validatePersonalInfo(info: PersonalInfoUiModel): Boolean { - return info.firstName.length>=3 && - info.lastName.length>=3 && - info.phoneNumber.length>=10 && - info.phoneNumber.matches(Regex("^\\+?[1-9]\\d{1,14}$")) && - info.address.isNotBlank() + private fun validatePersonalInfo(info: PersonalInfoUiModel): Boolean { + return info.firstName.length >= 3 && + info.lastName.length >= 3 && + info.phoneNumber.length >= 10 && + info.phoneNumber.matches(Regex("^\\+?[1-9]\\d{1,14}$")) && + info.address.isNotBlank() + } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt index 5dbf89f..14caff0 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt @@ -7,6 +7,7 @@ import org.example.project.domain.exception.NetworkException import org.example.project.domain.exception.UnauthorizedException import org.example.project.domain.exception.ValidationException import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.util.AppLogger fun Throwable.toErrorUiState(): ErrorUiState { return when (this) { @@ -32,9 +33,13 @@ fun Throwable.toErrorUiState(): ErrorUiState { errorType = ErrorUiState.ErrorType.AUTHENTICATION ) - else -> ErrorUiState( - message = "Something went wrong. Please try again later.", - errorType = ErrorUiState.ErrorType.UNKNOWN - ) + else -> { + AppLogger.e("ThrowableEX",this.message?:"Unknown error") + println("⚠️ Unexpected error: ${this::class.simpleName} - ${this.message}") + ErrorUiState( + message = "Something went wrong. Please try again later.", + errorType = ErrorUiState.ErrorType.UNKNOWN + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt index 5ddbd70..cc6513b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt @@ -1,10 +1,15 @@ package org.example.project.util -object ApiConstants { +object AppConstants { // File Upload object FileUpload { - const val MAX_FILE_SIZE = 4 * 1024 * 1024 // 4MB + const val MAX_FILE_SIZE_MB = 4 + const val MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 // 4MB + val ALLOWED_IMAGE_TYPES = listOf("jpg", "jpeg", "png") const val MAX_PORTFOLIO_IMAGES = 4 + + const val MIME_TYPE_JPEG = "image/jpeg" + const val MIME_TYPE_PNG = "image/png" } } \ No newline at end of file From 070a697f91d5394f4729179f5d9999987c542554 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Fri, 24 Oct 2025 18:25:52 +0300 Subject: [PATCH 06/12] feat: add profile picture upload functionality and enhance craftsman data model --- .../remote/CraftsmanRemoteDataSource.kt | 8 + .../project/data/mapper/CraftsmanMapper.kt | 1 + .../CraftsmanRemoteDataSourceImpl.kt | 31 ++++ .../project/data/remote/dto/CraftsmanDto.kt | 9 + .../data/remote/network/APIConstant.kt | 3 +- .../repository/CraftsmanRepositoryImpl.kt | 19 +- .../UserPreferencesImpl.kt | 5 +- .../org/example/project/di/DataModule.kt | 4 +- .../org/example/project/di/DomainModule.kt | 2 + .../example/project/di/PresentationModule.kt | 1 + .../project/domain/entity/Craftsman.kt | 19 +- .../domain/repository/CraftsmanRepository.kt | 6 + .../repository}/UserPreferences.kt | 2 +- .../domain/service/ValidationService.kt | 19 ++ .../craftsman/UploadProfilePictureUseCase.kt | 40 +++++ .../composable/ProfilePictureSelector.kt | 168 ++++++++++++++++++ .../composable/SetupScreenScaffold.kt | 5 - .../composable/page/PersonalInfoPage.kt | 19 +- .../composable/page/PortfolioUploadPage.kt | 27 --- .../craftsmansetup/CraftsmanSetupScreen.kt | 6 + .../accountSetup/AccountSetupEffect.kt | 5 - .../AccountSetupInterActionListener.kt | 5 - .../accountSetup/AccountSetupState.kt | 16 -- .../accountSetup/AccountSetupViewModel.kt | 45 ----- .../CraftsmanSetupInteractionListener.kt | 7 +- .../craftsmansetup/CraftsmanSetupUiState.kt | 11 +- .../craftsmansetup/CraftsmanSetupViewModel.kt | 105 ++++++++++- 27 files changed, 457 insertions(+), 131 deletions(-) rename composeApp/src/commonMain/kotlin/org/example/project/data/{local/datasource => repository}/UserPreferencesImpl.kt (82%) rename composeApp/src/commonMain/kotlin/org/example/project/{data/datasource/local => domain/repository}/UserPreferences.kt (73%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupEffect.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupInterActionListener.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupState.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt index 5518ec9..48ea494 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt @@ -6,6 +6,7 @@ import org.example.project.data.remote.dto.CraftsmanStatusResponseDto import org.example.project.data.remote.dto.CreateCraftsmanRequest import org.example.project.data.remote.dto.DeleteAccountResponseDto import org.example.project.data.remote.dto.IdCardUploadResponseDto +import org.example.project.data.remote.dto.ProfilePictureUploadResponseDto import org.example.project.data.remote.dto.WorkPortfolioResponseDto import org.example.project.domain.model.WorkImage @@ -24,6 +25,13 @@ interface CraftsmanRemoteDataSource { idCardBackFileName: String ): IdCardUploadResponseDto + suspend fun uploadProfilePicture( + userId: String, + craftsmanId: String, + profilePicture: ByteArray, + profilePictureFileName: String + ): ProfilePictureUploadResponseDto + suspend fun uploadWorkPortfolio( userId: String, craftsmanId: String, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt index 08071e5..1493cb8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/mapper/CraftsmanMapper.kt @@ -19,6 +19,7 @@ fun CraftsmanProfileResponseDto.toDomain(): Craftsman { craftsmanId = craftsmanId, personalInfo = personalInfo.toDomain(), categories = categories, + profilePictureUrl = profilePictureUrl, status = CraftsmanStatus.valueOf(status), verificationStatus = VerificationStatus.valueOf(verificationInfo.status), verification = verificationInfo.toVerificationDocuments(), diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt index f210ba6..87e2459 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -19,6 +19,7 @@ import org.example.project.data.remote.dto.CraftsmanStatusResponseDto import org.example.project.data.remote.dto.CreateCraftsmanRequest import org.example.project.data.remote.dto.DeleteAccountResponseDto import org.example.project.data.remote.dto.IdCardUploadResponseDto +import org.example.project.data.remote.dto.ProfilePictureUploadResponseDto import org.example.project.data.remote.dto.WorkPortfolioResponseDto import org.example.project.data.remote.network.ApiConstants import org.example.project.data.remote.network.ApiConstants.Headers.USER_ID @@ -84,6 +85,36 @@ class CraftsmanRemoteDataSourceImpl( } } + override suspend fun uploadProfilePicture( + userId: String, + craftsmanId: String, + profilePicture: ByteArray, + profilePictureFileName: String + ): ProfilePictureUploadResponseDto { + AppLogger.d("API", "=== Starting Profile Picture Upload ===") + AppLogger.d("API", "UserId: $userId") + AppLogger.d("API", "CraftsmanId: $craftsmanId") + AppLogger.d("API", "File: $profilePictureFileName (${profilePicture.size} bytes)") + + return wrapApiCall { + httpClient.submitFormWithBinaryData( + url = ApiConstants.Endpoints.craftsmanProfilePicture(craftsmanId), + formData = formData { + val mimeType = getMimeType(profilePictureFileName) + AppLogger.d("API", "Profile picture MIME type: $mimeType") + + append("profilePicture", profilePicture, Headers.build { + append(HttpHeaders.ContentType, mimeType) + append(HttpHeaders.ContentDisposition, "filename=\"$profilePictureFileName\"") + }) + } + ) { + header(USER_ID, userId) + AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanProfilePicture(craftsmanId)}") + } + } + } + override suspend fun uploadWorkPortfolio( userId: String, craftsmanId: String, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt index f9ebc30..9d68c70 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/dto/CraftsmanDto.kt @@ -37,6 +37,7 @@ data class CraftsmanProfileResponseDto( val craftsmanId: String, val personalInfo: PersonalInfoDto, val categories: List, + val profilePictureUrl: String? = null, val status: String, val verificationInfo: VerificationInfoDto, val createdAt: String @@ -46,10 +47,18 @@ data class CraftsmanProfileResponseDto( data class CraftsmanStatusResponseDto( val craftsmanId: String, val status: String, + val profilePictureUrl: String? = null, val verificationStatus: String, val message: String ) +@Serializable +data class ProfilePictureUploadResponseDto( + val craftsmanId: String, + val profilePictureUrl: String, + val message: String +) + @Serializable data class DeleteAccountResponseDto( val success: Boolean, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt index 04293d1..cf194c0 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt @@ -1,7 +1,7 @@ package org.example.project.data.remote.network object ApiConstants { - const val BASE_URL = "http://192.168.1.53:8085" + const val BASE_URL = "http://192.168.1.51:8085" // Headers object Headers { @@ -25,6 +25,7 @@ object ApiConstants { const val CRAFTSMAN_PROFILE = "/craftsman/profile" const val ONBOARDING_END_POINT = "/onboarding" + fun craftsmanProfilePicture(craftsmanId: String) = "/craftsman/$craftsmanId/profile-picture" fun craftsmanIdCards(craftsmanId: String) = "/craftsman/$craftsmanId/verify/id-cards" fun craftsmanWorkPortfolio(craftsmanId: String) = "/craftsman/$craftsmanId/verify/work-portfolio" fun craftsmanStatus(craftsmanId: String) = "/craftsman/$craftsmanId/status" diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt index 9f894ca..a23e9f6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt @@ -1,7 +1,7 @@ package org.example.project.data.repository import org.example.project.data.remote.dto.CreateCraftsmanRequest -import org.example.project.data.datasource.local.UserPreferences +import org.example.project.domain.repository.UserPreferences import org.example.project.data.mapper.toDomain import org.example.project.data.mapper.toDto import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource @@ -70,6 +70,23 @@ class CraftsmanRepositoryImpl ( ) } + override suspend fun uploadProfilePicture( + craftsmanId: String, + profilePicture: ByteArray, + profilePictureFileName: String + ): String { + val userId = userPreferences.getUserId() + ?: throw UnauthorizedException("User must be logged in to upload profile picture") + + val response = remoteDataSource.uploadProfilePicture( + userId = userId, + craftsmanId = craftsmanId, + profilePicture = profilePicture, + profilePictureFileName = profilePictureFileName + ) + return response.profilePictureUrl + } + override suspend fun uploadWorkPortfolio( craftsmanId: String, workImages: List diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt similarity index 82% rename from composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt index adfe47c..75d977e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/UserPreferencesImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt @@ -1,8 +1,7 @@ -package org.example.project.data.local.datasource +package org.example.project.data.repository import org.example.project.data.datasource.local.StorageLocalDataSource -import org.example.project.data.datasource.local.UserPreferences - +import org.example.project.domain.repository.UserPreferences class UserPreferencesImpl( private val storage: StorageLocalDataSource diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index 8d9f015..2bd9f61 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -1,11 +1,11 @@ package org.example.project.di -import org.example.project.data.datasource.local.UserPreferences +import org.example.project.domain.repository.UserPreferences import org.example.project.data.datasource.remote.CategoryDataSource import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource import org.example.project.data.local.datasource.CategoryMemoryDataSource -import org.example.project.data.local.datasource.UserPreferencesImpl +import org.example.project.data.repository.UserPreferencesImpl import org.example.project.data.memory.categorySeed import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl import org.example.project.data.repository.CategoryRepositoryImpl diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt index 0311452..0c02e5e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt @@ -6,6 +6,7 @@ import org.example.project.domain.usecase.craftsman.DeleteCraftsmanAccountUseCas import org.example.project.domain.usecase.craftsman.GetCraftsmanProfileUseCase import org.example.project.domain.usecase.craftsman.GetCraftsmanStatusUseCase import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase +import org.example.project.domain.usecase.craftsman.UploadProfilePictureUseCase import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase import org.koin.dsl.module @@ -17,4 +18,5 @@ val domainModule = module { factory { GetCraftsmanStatusUseCase(get()) } factory { DeleteCraftsmanAccountUseCase(get()) } factory { GetCategoriesUseCase(get())} + factory { UploadProfilePictureUseCase(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt index e1790d7..aabd253 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt @@ -11,6 +11,7 @@ val presentationModule = module { uploadIdCardsUseCase = get(), uploadWorkPortfolioUseCase = get(), getCategoriesUseCase = get(), + uploadProfilePictureUseCase = get(), ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt index d078a38..0eb90b6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/entity/Craftsman.kt @@ -6,12 +6,29 @@ import kotlin.time.Instant data class Craftsman @OptIn(ExperimentalTime::class) constructor( val craftsmanId: String, val personalInfo: PersonalInfo, + val profilePictureUrl: String? = null, val categories: List, val status: CraftsmanStatus, val verificationStatus: VerificationStatus, val verification: VerificationDocuments, val createdAt: Instant, -) +){ + fun isVerified(): Boolean = verificationStatus == VerificationStatus.VERIFIED + fun canReceiveJobs(): Boolean = status == CraftsmanStatus.ACTIVE && isVerified() + fun isProfileComplete(): Boolean = hasRequiredDocuments() && personalInfo.isComplete() + + private fun hasRequiredDocuments(): Boolean { + return verification.idCardFrontUrl != null && + verification.idCardBackUrl != null && + verification.workPortfolioUrls.isNotEmpty() + } + + private fun PersonalInfo.isComplete(): Boolean { + return firstName.isNotEmpty() && + lastName.isNotEmpty() && + address.isNotEmpty() + } +} data class PersonalInfo( val firstName: String, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt index 986c870..2c44095 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/CraftsmanRepository.kt @@ -20,6 +20,12 @@ interface CraftsmanRepository { idCardBackFileName: String ): VerificationDocuments + suspend fun uploadProfilePicture( + craftsmanId: String, + profilePicture: ByteArray, + profilePictureFileName: String + ): String + suspend fun uploadWorkPortfolio( craftsmanId: String, workImages: List diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/local/UserPreferences.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt similarity index 73% rename from composeApp/src/commonMain/kotlin/org/example/project/data/datasource/local/UserPreferences.kt rename to composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt index 378d58e..7564fd0 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/local/UserPreferences.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/repository/UserPreferences.kt @@ -1,4 +1,4 @@ -package org.example.project.data.datasource.local +package org.example.project.domain.repository interface UserPreferences { suspend fun getUserId(): String? diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt new file mode 100644 index 0000000..f33c472 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt @@ -0,0 +1,19 @@ +package org.example.project.domain.service + +interface ValidationService { + fun isValidPhoneNumber(phone: String): Boolean + //fun isValidImageFile(fileName: String, data: ByteArray): ValidationResult +} + +class ValidationServiceImpl : ValidationService { + override fun isValidPhoneNumber(phone: String): Boolean { + return phone.matches(Regex("^\\+?[1-9]\\d{1,14}$")) + } + +// override fun isValidImageFile( +// fileName: String, +// data: ByteArray +// ): ValidationResult { +// TODO("Not yet implemented") +// } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt new file mode 100644 index 0000000..1f5ec98 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt @@ -0,0 +1,40 @@ +package org.example.project.domain.usecase.craftsman + +import org.example.project.domain.exception.ValidationException +import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.util.AppConstants + +class UploadProfilePictureUseCase( + private val repository: CraftsmanRepository +) { + suspend operator fun invoke( + craftsmanId: String, + profilePicture: ByteArray, + profilePictureFileName: String + ): String { + if (craftsmanId.isBlank()) { + throw ValidationException("Craftsman ID is required") + } + + if (profilePicture.isEmpty()) { + throw ValidationException("Profile picture is required") + } + + if (profilePicture.size > AppConstants.FileUpload.MAX_FILE_SIZE) { + throw ValidationException( + "Profile picture size must be less than ${AppConstants.FileUpload.MAX_FILE_SIZE_MB}MB" + ) + } + + val extension = profilePictureFileName.substringAfterLast('.', "").lowercase() + if (extension !in AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + throw ValidationException("Profile picture must be JPG or PNG") + } + + return repository.uploadProfilePicture( + craftsmanId, + profilePicture, + profilePictureFileName + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt new file mode 100644 index 0000000..79f50dc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt @@ -0,0 +1,168 @@ +package org.example.project.presentation.screens.setupScreens.composable + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.camera +import crafto.composeapp.generated.resources.plus +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.model.ImageData +import org.example.project.presentation.util.rememberImagePicker +import org.jetbrains.compose.resources.painterResource + +@Composable +fun ProfilePictureSelector( + modifier: Modifier = Modifier, + selectedImage: ImageData? = null, + onImageSelected: (ImageData) -> Unit, + onError: (String) -> Unit, + enabled: Boolean = true, + isUploading: Boolean = false +) { + val imagePicker = rememberImagePicker( + singleSelection = true, + onImagesSelected = { images -> + images.firstOrNull()?.let { imageData -> + onImageSelected(imageData) + } + }, + onError = { errorMessage -> + onError(errorMessage) + } + ) + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box( + modifier = Modifier + .size(100.dp) + .clip(CircleShape) + .background(AppTheme.craftoColors.background.card) + .border( + BorderStroke( + width = 1.dp, + color = AppTheme.craftoColors.shade.quinary + + ), + CircleShape + ) + .clickable(enabled = enabled && !isUploading) { + imagePicker.launch() + }, + contentAlignment = Alignment.Center + ) { + if (selectedImage != null) { + AsyncImage( + model = selectedImage.byteArray, + contentDescription = "Profile Picture", + modifier = Modifier + .size(96.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(AppTheme.craftoColors.background.card) + .align(Alignment.BottomEnd), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(Res.drawable.plus), + contentDescription = "Change Photo", + tint = AppTheme.craftoColors.button.onPrimary, + modifier = Modifier.size(16.dp) + ) + } + } else { + // Display placeholder - similar to EmptyPortfolioBox style + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painter = painterResource(Res.drawable.camera), + contentDescription = "Add Profile Picture", + tint = AppTheme.craftoColors.shade.secondary, + modifier = Modifier.size(32.dp) + ) + + Text( + text = "Tap here", + style = AppTheme.textStyle.body.smallMedium, + color = AppTheme.craftoColors.shade.secondary + ) + } + } +// +// // Show loading overlay if uploading +// if (isUploading) { +// Box( +// modifier = Modifier +// .size(100.dp) +// .clip(CircleShape) +// .background( +// AppTheme.craftoColors.shade.secondary.copy(alpha = 0.5f) +// ), +// contentAlignment = Alignment.Center +// ) { +// CircularProgressIndicator( +// color = AppTheme.craftoColors.primary.main, +// strokeWidth = 2.dp, +// modifier = Modifier.size(24.dp) +// ) +// } +// } +// } +// +// // Status text +// Text( +// text = when { +// isUploading -> "Uploading..." +// selectedImage != null -> "Tap to change photo" +// else -> "Add profile photo" +// }, +// style = AppTheme.textStyle.caption.medium, +// color = if (isUploading) +// AppTheme.craftoColors.primary.main +// else +// AppTheme.craftoColors.text.secondary, +// textAlign = TextAlign.Center +// ) +// +// if (selectedImage == null) { +// Text( +// text = "(Optional)", +// style = AppTheme.textStyle.caption.regular, +// color = AppTheme.craftoColors.text.tertiary, +// textAlign = TextAlign.Center +// ) +// } +// } + + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt index be8a17d..effe0b7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt @@ -17,17 +17,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import crafto.composeapp.generated.resources.Res -import crafto.composeapp.generated.resources.account_setup_craftsman_category_description -import crafto.composeapp.generated.resources.account_setup_craftsman_category_title import crafto.composeapp.generated.resources.next import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.PrimaryButton import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.util.DeviceConfiguration -import org.example.project.presentation.viewmodel.accountSetup.AccountSetupCategoryState -import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun SetupScreenScaffold( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt index 91a50c1..b7201a7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt @@ -3,6 +3,7 @@ package org.example.project.presentation.screens.setupScreens.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions @@ -16,13 +17,19 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import org.example.project.presentation.designsystem.components.TextField +import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel +import org.example.project.presentation.screens.setupScreens.composable.ProfilePictureSelector @Composable fun PersonalInfoPage( personalInfo: PersonalInfoUiModel, onPersonalInfoChanged: (PersonalInfoUiModel) -> Unit, - isLoading: Boolean + profilePicture: ImageData? = null, + onProfilePictureSelected: (ImageData) -> Unit, + onImagePickerError: (String) -> Unit, + isLoading: Boolean, + isUploadingProfilePicture: Boolean = false ) { Column( modifier = Modifier @@ -31,6 +38,16 @@ fun PersonalInfoPage( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + + ProfilePictureSelector( + modifier = Modifier.fillMaxWidth(), + selectedImage = profilePicture, + onImageSelected = onProfilePictureSelected, + onError = onImagePickerError, + enabled = !isLoading, + isUploading = isUploadingProfilePicture + ) + TextField( labelText = "First Name", text = personalInfo.firstName, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt index 9019d9e..ccb0809 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt @@ -67,33 +67,6 @@ fun PortfolioUploadPage( } ) -// val imagePicker = rememberFilePickerLauncher( -// type = FilePickerFileType.Image, -// selectionMode = FilePickerSelectionMode.Multiple, -// ) { files -> -// scope.launch { -// val imageDataList = files.take(4 - images.size).mapNotNull { file -> -// try { -// val fileName = file.getName(context) ?: "image_${Clock.System.now()}.jpg" -// val byteArray = file.readByteArray(context) -// -// ImageData( -// uri = fileName, -// byteArray = byteArray, -// fileName = fileName -// ) -// } catch (e: Exception) { -// e.printStackTrace() -// null -// } -// } -// if (imageDataList.isNotEmpty()) { -// onAddPhotosClicked(imageDataList) -// } -// } -// } - - Column( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt index c3d9396..5dc4f06 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt @@ -176,8 +176,14 @@ fun CraftsmanSetupContent( RegistrationStep.PERSONAL_INFO -> { PersonalInfoPage( personalInfo = state.personalInfo, + profilePicture = state.profilePicture, onPersonalInfoChanged = viewModel::onPersonalInfoChanged, + onProfilePictureSelected = viewModel::onProfilePictureSelected, + onImagePickerError = { errorMessage -> + viewModel.onImagePickerError(ErrorUiState(errorMessage)) + }, isLoading = state.isLoading, + isUploadingProfilePicture = state.isUploadingProfilePicture ) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupEffect.kt deleted file mode 100644 index bb4a5d1..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupEffect.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.project.presentation.viewmodel.accountSetup - -sealed class AccountSetupEffect { - -} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupInterActionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupInterActionListener.kt deleted file mode 100644 index e026867..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupInterActionListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.project.presentation.viewmodel.accountSetup - -interface AccountSetupInterActionListener { - fun onCategorySelected(id: Int) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupState.kt deleted file mode 100644 index 97d5a44..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.example.project.presentation.viewmodel.accountSetup - -import org.example.project.domain.entity.Category - -data class AccountSetupState( - val title: String = "", - val description: String = "", - val isLoading: Boolean = false, - val isError: Boolean = false, - val categoryState: AccountSetupCategoryState = AccountSetupCategoryState() -) - -data class AccountSetupCategoryState( - val categories: List = emptyList(), - val isSelected: Boolean = false, -) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt deleted file mode 100644 index 0b7cab7..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/accountSetup/AccountSetupViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -//package org.example.project.presentation.viewmodel.accountSetup -// -//import androidx.lifecycle.viewModelScope -//import kotlinx.coroutines.launch -//import org.example.project.domain.usecase.GetCategoriesUseCase -//import org.example.project.presentation.viewmodel.base.BaseViewModel -//import org.koin.android.annotation.KoinViewModel -//import org.koin.core.annotation.Provided -// -//@KoinViewModel -//class AccountSetupViewModel( -// @Provided private val getCategoriesUseCase : GetCategoriesUseCase -//) : BaseViewModel(AccountSetupState()), -// AccountSetupInterActionListener { -// -// init { -// fetchCategories() -// } -// -// private fun fetchCategories() { -// viewModelScope.launch { -// val categories = getCategoriesUseCase.invoke() -// updateState { -// it.copy( -// categoryState = it.categoryState.copy(categories = categories) -// ) -// } -// } -// } -// -// override fun onCategorySelected(id: Int) { -// updateState { -// val categories = it.categoryState.categories.map { category -> -// if (category.id == id) { -// category.copy(isSelected = !category.isSelected) -// } else { -// category -// } -// } -// it.copy( -// categoryState = it.categoryState.copy(categories = categories) -// ) -// } -// } -//} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt index a654fe7..6879b44 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -2,23 +2,24 @@ package org.example.project.presentation.viewmodel.craftsmansetup import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel +import org.example.project.presentation.viewmodel.base.ErrorUiState interface CraftsmanSetupInteractionListener { fun onUserTypeSelected(userType: UserType) - // Service Selection fun onCategoryToggled(categoryId: Int) fun onPersonalInfoChanged(personalInfo: PersonalInfoUiModel) - // Identity Verification fun onIdCardSelected(isFront: Boolean, imageData: ImageData) fun onUploadIdCards() fun onSkipIdentityVerification() - // Portfolio fun onPortfolioImagesAdded(images: List) fun onPortfolioImageRemoved(index: Int) fun onWorkDescriptionChanged(description: String) fun onUploadPortfolio() + fun onProfilePictureSelected(imageData: ImageData) // ADD THIS + fun onImagePickerError(error: ErrorUiState) // ADD THIS + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt index 4a390b4..ad30d19 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt @@ -11,7 +11,6 @@ data class CraftsmanSetupUiState( override val isLoading: Boolean = false, override val error: ErrorUiState? = null, - // Pager state val currentPageIndex: Int = 0, val totalPages: Int = 5, val canNavigateNext: Boolean = false, @@ -24,26 +23,24 @@ data class CraftsmanSetupUiState( val uploadedPortfolioUrls: List = emptyList(), val verificationDocuments: VerificationDocuments? = null, - // User type selection val userType: UserType? = null, - // Service selection val availableCategories: List = emptyList(), val selectedCategoryIds: Set = emptySet(), - // Personal info val personalInfo: PersonalInfoUiModel = PersonalInfoUiModel("", "", "", ""), - // Portfolio val portfolioImages: List = emptyList(), val workDescription: String = "", val canAddMoreImages: Boolean = true, - // Identity verification val idCardFront: ImageData? = null, val idCardBack: ImageData? = null, - // Flow state + val profilePicture: ImageData? = null, + val isUploadingProfilePicture: Boolean = false, + val profilePictureUrl: String? = null, + val craftsmanId: String? = null, val isProfileCreated: Boolean = false, ): BaseScreenState { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt index 200c1ee..d874a9b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt @@ -6,6 +6,7 @@ import org.example.project.util.AppLogger import org.example.project.domain.usecase.GetCategoriesUseCase import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase +import org.example.project.domain.usecase.craftsman.UploadProfilePictureUseCase import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel @@ -19,7 +20,8 @@ class CraftsmanSetupViewModel( private val createCraftsmanUseCase: CreateCraftsmanProfileUseCase, private val uploadIdCardsUseCase: UploadIdCardsUseCase, private val uploadWorkPortfolioUseCase: UploadWorkPortfolioUseCase, - private val getCategoriesUseCase: GetCategoriesUseCase + private val getCategoriesUseCase: GetCategoriesUseCase, + private val uploadProfilePictureUseCase: UploadProfilePictureUseCase, ) : BaseViewModel( CraftsmanSetupUiState() ), CraftsmanSetupInteractionListener { @@ -275,6 +277,80 @@ class CraftsmanSetupViewModel( ) } + override fun onProfilePictureSelected(imageData: ImageData) { + AppLogger.d("ProfilePicture", "Profile picture selected") + AppLogger.d("ProfilePicture", " FileName: ${imageData.fileName}") + AppLogger.d("ProfilePicture", " Size: ${imageData.byteArray.size} bytes") + + updateState { state -> + state.copy(profilePicture = imageData) + } + } + + override fun onImagePickerError(error: ErrorUiState) { + AppLogger.e("ImagePicker", "Image picker error: ${error.message}") + updateState { it.copy(error = error) } + } + + private fun uploadProfilePicture(craftsmanId: String, profilePicture: ImageData) { + AppLogger.d("ProfilePicture", "Starting profile picture upload") + AppLogger.d("ProfilePicture", " CraftsmanId: $craftsmanId") + AppLogger.d("ProfilePicture", " FileName: ${profilePicture.fileName}") + AppLogger.d("ProfilePicture", " Size: ${profilePicture.byteArray.size} bytes") + + updateState { it.copy(isUploadingProfilePicture = true) } + + tryToCall( + call = { + uploadProfilePictureUseCase( + craftsmanId = craftsmanId, + profilePicture = profilePicture.byteArray, + profilePictureFileName = profilePicture.fileName + ) + }, + onSuccess = { profilePictureUrl -> + AppLogger.d("ProfilePicture", "✅ Profile picture uploaded successfully!") + AppLogger.d("ProfilePicture", " URL: $profilePictureUrl") + + updateState { + it.copy( + profilePictureUrl = profilePictureUrl, + isUploadingProfilePicture = false + ) + } + + // Continue to next page after profile picture upload + AppLogger.d("Navigation", "Profile creation complete, navigating to portfolio page") + updateState { + it.copy( + isSwipeEnabled = true, + currentPageIndex = it.currentPageIndex + 1 + ) + } + }, + onError = { error -> + AppLogger.e("ProfilePicture", "❌ Profile picture upload failed: ${error.message}") + updateState { + it.copy( + error = error, + isUploadingProfilePicture = false + ) + } + + // Even if profile picture fails, allow user to continue + // They can upload it later from settings + AppLogger.d("Navigation", "Profile creation complete, navigating to portfolio page") + updateState { + it.copy( + isSwipeEnabled = true, + currentPageIndex = it.currentPageIndex + 1 + ) + } + }, + showLoading = false // Don't show loading since we're already showing it for profile creation + ) + } + fun clearError() { updateState { it.copy(error = null) } } @@ -378,26 +454,39 @@ class CraftsmanSetupViewModel( tryToCall( call = { - AppLogger.d("CraftsmanSetupViewModel", "Calling createCraftsmanUseCase") + AppLogger.d("CraftsmanSetup", "Calling createCraftsmanUseCase") createCraftsmanUseCase( personalInfo = state.value.personalInfo.toDomain(), categories = selectedCategoryTitles ) }, onSuccess = { craftsmanId -> - AppLogger.d("CraftsmanSetupViewModel", "Profile created successfully: $craftsmanId") + AppLogger.d("CraftsmanSetup", "✅ Profile created successfully: $craftsmanId") + updateState { it.copy( craftsmanId = craftsmanId, - isProfileCreated = true, - isSwipeEnabled = true, - // Navigate to portfolio page - currentPageIndex = it.currentPageIndex + 1 + isProfileCreated = true ) } + + // Check if profile picture was selected + val profilePicture = state.value.profilePicture + if (profilePicture != null) { + AppLogger.d("CraftsmanSetup", "Profile picture selected, uploading...") + uploadProfilePicture(craftsmanId, profilePicture) + } else { + AppLogger.d("CraftsmanSetup", "No profile picture selected, skipping upload") + updateState { + it.copy( + isSwipeEnabled = true, + currentPageIndex = it.currentPageIndex + 1 + ) + } + } }, onError = { error -> - AppLogger.e("CraftsmanSetupViewModel", "Profile creation failed: ${error.message}") + AppLogger.e("CraftsmanSetup", "❌ Profile creation failed: ${error.message}") updateState { it.copy( error = error, From 5ff2a5bd951eabe67a1e0dde0f0e7a65b49b0d2f Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Sat, 25 Oct 2025 21:40:30 +0300 Subject: [PATCH 07/12] refactor: update package structure and improve imports for craftsman setup --- .../datasource/DataStoreLocalDataSourceImp.kt | 1 - .../org/example/project/di/AndroidModule.kt | 2 +- .../kotlin/org/example/project/App.kt | 2 +- .../local/datasource/CategoryDataSourceImpl.kt | 2 +- .../datasource}/StorageLocalDataSource.kt | 2 +- .../datasource}/CategoryDataSource.kt | 2 +- .../datasource}/CraftsmanRemoteDataSource.kt | 2 +- .../datasource/CraftsmanRemoteDataSourceImpl.kt | 7 +++---- .../data/repository/CategoryRepositoryImpl.kt | 2 +- .../data/repository/CraftsmanRepositoryImpl.kt | 2 +- .../data/repository/UserPreferencesImpl.kt | 2 +- .../kotlin/org/example/project/di/DataModule.kt | 4 ++-- .../example/project/di/PresentationModule.kt | 2 +- .../{viewmodel => }/mapper/CraftsmanMapper.kt | 2 +- .../{viewmodel => }/mapper/Exception.kt | 4 ++-- .../screens/onboarding/OnboardingScreen.kt | 1 - .../screens/onboarding/OnboardingScreenState.kt | 2 +- .../onboarding}/OnboardingViewModel.kt | 8 ++------ .../composable/page/UserTypeSelectionPage.kt | 4 +--- .../craftsmansetup/CraftsmanSetupEffect.kt | 2 +- .../CraftsmanSetupInteractionListener.kt | 4 ++-- .../craftsmansetup/CraftsmanSetupScreen.kt | 8 ++------ .../craftsmansetup/CraftsmanSetupUiState.kt | 6 +++--- .../craftsmansetup/CraftsmanSetupViewModel.kt | 17 ++++++++--------- .../shared}/base/BaseScreenState.kt | 4 ++-- .../shared}/base/BaseViewModel.kt | 4 ++-- .../shared}/base/ErrorUiState.kt | 2 +- .../datasource/StorageLocalDataSourceImpl.kt | 2 -- .../kotlin/org/example/project/di/IosModule.kt | 2 +- 29 files changed, 44 insertions(+), 60 deletions(-) rename composeApp/src/commonMain/kotlin/org/example/project/data/{datasource/local => local/datasource}/StorageLocalDataSource.kt (91%) rename composeApp/src/commonMain/kotlin/org/example/project/data/{datasource/remote => remote/datasource}/CategoryDataSource.kt (73%) rename composeApp/src/commonMain/kotlin/org/example/project/data/{datasource/remote => remote/datasource}/CraftsmanRemoteDataSource.kt (97%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => }/mapper/CraftsmanMapper.kt (93%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => }/mapper/Exception.kt (92%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/onboarding}/OnboardingViewModel.kt (81%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/setupScreens}/craftsmansetup/CraftsmanSetupEffect.kt (60%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/setupScreens}/craftsmansetup/CraftsmanSetupInteractionListener.kt (83%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/setupScreens}/craftsmansetup/CraftsmanSetupUiState.kt (91%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/setupScreens}/craftsmansetup/CraftsmanSetupViewModel.kt (97%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/shared}/base/BaseScreenState.kt (57%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/shared}/base/BaseViewModel.kt (93%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{viewmodel => screens/shared}/base/ErrorUiState.kt (79%) diff --git a/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt b/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt index ee63782..58fcb76 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/data/local/datasource/DataStoreLocalDataSourceImp.kt @@ -6,7 +6,6 @@ import androidx.datastore.preferences.core.* import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import org.example.project.data.datasource.local.StorageLocalDataSource private val Context.dataStore: DataStore by preferencesDataStore( diff --git a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt index 87d472a..b261258 100644 --- a/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt +++ b/composeApp/src/androidMain/kotlin/org/example/project/di/AndroidModule.kt @@ -1,6 +1,6 @@ package org.example.project.di -import org.example.project.data.datasource.local.StorageLocalDataSource +import org.example.project.data.local.datasource.StorageLocalDataSource import org.example.project.data.local.datasource.DataStoreLocalDataSourceImp import org.koin.android.ext.koin.androidContext import org.koin.dsl.module diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index a473b68..fa557a7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -2,7 +2,7 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.screens.setupScreens.craftsmansetup.CraftsmanSetupScreen +import org.example.project.presentation.screens.setupscreens.craftsmansetup.CraftsmanSetupScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt index 2ce6452..3b64308 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/CategoryDataSourceImpl.kt @@ -1,6 +1,6 @@ package org.example.project.data.local.datasource -import org.example.project.data.datasource.remote.CategoryDataSource +import org.example.project.data.remote.datasource.CategoryDataSource import org.example.project.data.remote.dto.CategoryDto class CategoryMemoryDataSource( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/local/StorageLocalDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt similarity index 91% rename from composeApp/src/commonMain/kotlin/org/example/project/data/datasource/local/StorageLocalDataSource.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt index acc8efc..d2ef580 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/local/StorageLocalDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSource.kt @@ -1,4 +1,4 @@ -package org.example.project.data.datasource.local +package org.example.project.data.local.datasource interface StorageLocalDataSource { suspend fun saveString(key: String, value: String) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CategoryDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CategoryDataSource.kt similarity index 73% rename from composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CategoryDataSource.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CategoryDataSource.kt index d5ba704..0478a1b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CategoryDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CategoryDataSource.kt @@ -1,4 +1,4 @@ -package org.example.project.data.datasource.remote +package org.example.project.data.remote.datasource import org.example.project.data.remote.dto.CategoryDto diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt rename to composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt index 48ea494..0f3f932 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/datasource/remote/CraftsmanRemoteDataSource.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSource.kt @@ -1,4 +1,4 @@ -package org.example.project.data.datasource.remote +package org.example.project.data.remote.datasource import org.example.project.data.remote.dto.CraftsmanProfileResponseDto import org.example.project.data.remote.dto.CraftsmanSetupResponseDto diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt index 87e2459..82188f6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -12,7 +12,6 @@ import io.ktor.http.ContentType import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.contentType -import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource import org.example.project.data.remote.dto.CraftsmanProfileResponseDto import org.example.project.data.remote.dto.CraftsmanSetupResponseDto import org.example.project.data.remote.dto.CraftsmanStatusResponseDto @@ -37,7 +36,7 @@ class CraftsmanRemoteDataSourceImpl( ): CraftsmanSetupResponseDto { return wrapApiCall { httpClient.post(ApiConstants.Endpoints.CRAFTSMAN_SETUP) { - header(ApiConstants.Headers.USER_ID, userId) + header(USER_ID, userId) contentType(ContentType.Application.Json) setBody(request) } @@ -148,7 +147,7 @@ class CraftsmanRemoteDataSourceImpl( } } ) { - header(ApiConstants.Headers.USER_ID, userId) + header(USER_ID, userId) AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId)}") } } @@ -157,7 +156,7 @@ class CraftsmanRemoteDataSourceImpl( override suspend fun getCraftsmanProfile(userId: String): CraftsmanProfileResponseDto { return wrapApiCall { httpClient.get(ApiConstants.Endpoints.CRAFTSMAN_PROFILE) { - header(ApiConstants.Headers.USER_ID, userId) + header(USER_ID, userId) } } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt index c6b3b76..b4832a4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CategoryRepositoryImpl.kt @@ -1,7 +1,7 @@ package org.example.project.data.repository -import org.example.project.data.datasource.remote.CategoryDataSource +import org.example.project.data.remote.datasource.CategoryDataSource import org.example.project.data.mapper.toDomain import org.example.project.domain.entity.Category import org.example.project.domain.repository.CategoryRepository diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt index a23e9f6..984c0e6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt @@ -4,7 +4,7 @@ import org.example.project.data.remote.dto.CreateCraftsmanRequest import org.example.project.domain.repository.UserPreferences import org.example.project.data.mapper.toDomain import org.example.project.data.mapper.toDto -import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource +import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource import org.example.project.domain.entity.Craftsman import org.example.project.domain.entity.CraftsmanStatus import org.example.project.domain.entity.PersonalInfo diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt index 75d977e..aec8671 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/UserPreferencesImpl.kt @@ -1,6 +1,6 @@ package org.example.project.data.repository -import org.example.project.data.datasource.local.StorageLocalDataSource +import org.example.project.data.local.datasource.StorageLocalDataSource import org.example.project.domain.repository.UserPreferences class UserPreferencesImpl( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index 2bd9f61..bb2f052 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -2,8 +2,8 @@ package org.example.project.di import org.example.project.domain.repository.UserPreferences -import org.example.project.data.datasource.remote.CategoryDataSource -import org.example.project.data.datasource.remote.CraftsmanRemoteDataSource +import org.example.project.data.remote.datasource.CategoryDataSource +import org.example.project.data.remote.datasource.CraftsmanRemoteDataSource import org.example.project.data.local.datasource.CategoryMemoryDataSource import org.example.project.data.repository.UserPreferencesImpl import org.example.project.data.memory.categorySeed diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt index aabd253..1fd6afb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt @@ -1,6 +1,6 @@ package org.example.project.di -import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupViewModel +import org.example.project.presentation.screens.setupscreens.craftsmansetup.CraftsmanSetupViewModel import org.koin.core.module.dsl.viewModel import org.koin.dsl.module diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/CraftsmanMapper.kt similarity index 93% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/CraftsmanMapper.kt index 0bc674e..4de8938 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/CraftsmanMapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/CraftsmanMapper.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.viewmodel.mapper +package org.example.project.presentation.mapper import androidx.compose.ui.graphics.Color import org.example.project.domain.entity.Category diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt index 14caff0..54f5cef 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/mapper/Exception.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.viewmodel.mapper +package org.example.project.presentation.mapper import io.ktor.client.network.sockets.SocketTimeoutException import io.ktor.client.plugins.HttpRequestTimeoutException @@ -6,7 +6,7 @@ import org.example.project.domain.exception.ForbiddenException import org.example.project.domain.exception.NetworkException import org.example.project.domain.exception.UnauthorizedException import org.example.project.domain.exception.ValidationException -import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.presentation.screens.shared.base.ErrorUiState import org.example.project.util.AppLogger fun Throwable.toErrorUiState(): ErrorUiState { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt index ef38cc8..79565bc 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreen.kt @@ -38,7 +38,6 @@ import org.example.project.presentation.designsystem.components.SecondaryButton import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.onboarding.composable.OnboardingIndicator import org.example.project.presentation.screens.onboarding.composable.OnboardingItem -import org.example.project.presentation.viewmodel.OnboardingViewModel import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt index 6a3a706..f5d6f11 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt @@ -1,7 +1,7 @@ package org.example.project.presentation.screens.onboarding import org.example.project.presentation.screens.onboarding.model.OnboardingUiState -import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.presentation.screens.shared.base.ErrorUiState data class OnboardingScreenState( val onboardingData: List = emptyList(), diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt similarity index 81% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt index b6b3c6c..2cef8a3 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/OnboardingViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt @@ -1,19 +1,15 @@ -package org.example.project.presentation.viewmodel +package org.example.project.presentation.screens.onboarding import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import org.example.project.domain.entity.OnboardingItem import org.example.project.domain.repository.OnboardingRepository -import org.example.project.presentation.screens.onboarding.OnboardingScreenEffect -import org.example.project.presentation.screens.onboarding.OnboardingScreenInteractionListener -import org.example.project.presentation.screens.onboarding.OnboardingScreenState import org.example.project.presentation.screens.onboarding.model.toUiState -import org.example.project.presentation.viewmodel.base.BaseViewModel +import org.example.project.presentation.screens.shared.base.BaseViewModel import org.koin.android.annotation.KoinViewModel import org.koin.core.annotation.Provided - @KoinViewModel class OnboardingViewModel( @Provided private val repository: OnboardingRepository, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt index e85c021..888cde4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt @@ -2,9 +2,7 @@ package org.example.project.presentation.screens.setupScreens.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -12,7 +10,7 @@ import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.selection_craftsman import crafto.composeapp.generated.resources.selection_customer import org.example.project.presentation.designsystem.components.SelectionCard -import org.example.project.presentation.viewmodel.craftsmansetup.UserType +import org.example.project.presentation.screens.setupscreens.craftsmansetup.UserType import org.jetbrains.compose.resources.painterResource @Composable diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupEffect.kt similarity index 60% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupEffect.kt index 0c770e8..ab89306 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupEffect.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupEffect.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.viewmodel.craftsmansetup +package org.example.project.presentation.screens.setupscreens.craftsmansetup sealed interface CraftsmanRegistrationEffect { data object RegistrationComplete : CraftsmanRegistrationEffect diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt similarity index 83% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt index 6879b44..31a774b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -1,8 +1,8 @@ -package org.example.project.presentation.viewmodel.craftsmansetup +package org.example.project.presentation.screens.setupscreens.craftsmansetup import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.presentation.screens.shared.base.ErrorUiState interface CraftsmanSetupInteractionListener { fun onUserTypeSelected(userType: UserType) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt index 5dc4f06..b49906f 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.craftsmansetup +package org.example.project.presentation.screens.setupscreens.craftsmansetup import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition @@ -35,11 +35,7 @@ import org.example.project.presentation.screens.setupScreens.composable.page.Per import org.example.project.presentation.screens.setupScreens.composable.page.PortfolioUploadPage import org.example.project.presentation.screens.setupScreens.composable.page.ServiceSelectionPage import org.example.project.presentation.screens.setupScreens.composable.page.UserTypeSelectionPage -import org.example.project.presentation.viewmodel.base.ErrorUiState -import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanRegistrationEffect -import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupUiState -import org.example.project.presentation.viewmodel.craftsmansetup.CraftsmanSetupViewModel -import org.example.project.presentation.viewmodel.craftsmansetup.RegistrationStep +import org.example.project.presentation.screens.shared.base.ErrorUiState import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalFoundationApi::class) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt similarity index 91% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt index ad30d19..84387bb 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt @@ -1,11 +1,11 @@ -package org.example.project.presentation.viewmodel.craftsmansetup +package org.example.project.presentation.screens.setupscreens.craftsmansetup import org.example.project.domain.entity.VerificationDocuments import org.example.project.presentation.model.CategoryUi import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.viewmodel.base.BaseScreenState -import org.example.project.presentation.viewmodel.base.ErrorUiState +import org.example.project.presentation.screens.shared.base.BaseScreenState +import org.example.project.presentation.screens.shared.base.ErrorUiState data class CraftsmanSetupUiState( override val isLoading: Boolean = false, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt index d874a9b..82c2ed8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt @@ -1,8 +1,7 @@ -package org.example.project.presentation.viewmodel.craftsmansetup +package org.example.project.presentation.screens.setupscreens.craftsmansetup import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.example.project.util.AppLogger import org.example.project.domain.usecase.GetCategoriesUseCase import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCase import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase @@ -10,11 +9,12 @@ import org.example.project.domain.usecase.craftsman.UploadProfilePictureUseCase import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.viewmodel.base.BaseViewModel -import org.example.project.presentation.viewmodel.base.ErrorUiState -import org.example.project.presentation.viewmodel.mapper.toDomain -import org.example.project.presentation.viewmodel.mapper.toUi -import org.example.project.presentation.viewmodel.mapper.toWorkImages +import org.example.project.presentation.screens.shared.base.BaseViewModel +import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.mapper.toDomain +import org.example.project.presentation.mapper.toUi +import org.example.project.presentation.mapper.toWorkImages +import org.example.project.util.AppLogger class CraftsmanSetupViewModel( private val createCraftsmanUseCase: CreateCraftsmanProfileUseCase, @@ -505,5 +505,4 @@ class CraftsmanSetupViewModel( info.phoneNumber.matches(Regex("^\\+?[1-9]\\d{1,14}$")) && info.address.isNotBlank() } -} - +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseScreenState.kt similarity index 57% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseScreenState.kt index 0923509..592fa60 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseScreenState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseScreenState.kt @@ -1,6 +1,6 @@ -package org.example.project.presentation.viewmodel.base +package org.example.project.presentation.screens.shared.base interface BaseScreenState { val isLoading: Boolean val error: ErrorUiState? -} +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseViewModel.kt similarity index 93% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseViewModel.kt index 614233e..284c39d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/BaseViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseViewModel.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.viewmodel.base +package org.example.project.presentation.screens.shared.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.example.project.presentation.viewmodel.mapper.toErrorUiState +import org.example.project.presentation.mapper.toErrorUiState abstract class BaseViewModel( initialState: SCREEN_STATE, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/ErrorUiState.kt similarity index 79% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/ErrorUiState.kt index 7e2cbd1..7dfe8fd 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/viewmodel/base/ErrorUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/ErrorUiState.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.viewmodel.base +package org.example.project.presentation.screens.shared.base data class ErrorUiState ( val message: String = "", diff --git a/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt b/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt index 280c005..40a3bfd 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/data/local/datasource/StorageLocalDataSourceImpl.kt @@ -1,7 +1,5 @@ package org.example.project.data.local.datasource -import org.example.project.data.datasource.local.StorageLocalDataSource -import org.koin.core.annotation.Single import platform.Foundation.NSUserDefaults class StorageLocalDataSourceImpl : StorageLocalDataSource { diff --git a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt index a65d3d3..264c6ff 100644 --- a/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt +++ b/composeApp/src/iosMain/kotlin/org/example/project/di/IosModule.kt @@ -1,7 +1,7 @@ package org.example.project.di -import org.example.project.data.datasource.local.StorageLocalDataSource +import org.example.project.data.local.datasource.StorageLocalDataSource import org.example.project.data.local.datasource.StorageLocalDataSourceImpl import org.koin.dsl.module From 3b0378696edacfb14e6449bf450a04ac65d65310 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Sat, 25 Oct 2025 22:27:24 +0300 Subject: [PATCH 08/12] feat: enhance error handling with server unavailable exceptions and improve UI error states --- .../data/remote/network/ApiCallWrapper.kt | 20 +++++++-- .../domain/exception/DomainExceptions.kt | 1 + .../project/presentation/mapper/Exception.kt | 43 +++++++++++++++---- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt index 52e3109..461efae 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/ApiCallWrapper.kt @@ -1,6 +1,9 @@ package org.example.project.data.remote.network import io.ktor.client.call.body +import io.ktor.client.network.sockets.ConnectTimeoutException +import io.ktor.client.network.sockets.SocketTimeoutException +import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpStatusCode import org.example.project.data.remote.dto.ErrorResponseDto @@ -10,6 +13,7 @@ import org.example.project.domain.exception.CraftoException import org.example.project.domain.exception.ForbiddenException import org.example.project.domain.exception.NetworkException import org.example.project.domain.exception.NotFoundException +import org.example.project.domain.exception.ServerUnavailableException import org.example.project.domain.exception.UnauthorizedException suspend inline fun wrapApiCall( @@ -39,6 +43,12 @@ suspend inline fun wrapApiCall( } throw AlreadyExistsException(error?.message ?: "Resource already exists") } + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout -> { + throw ServerUnavailableException("Server is experiencing issues. Please try again later.") + } else -> { val error = try { response.body() @@ -50,9 +60,13 @@ suspend inline fun wrapApiCall( } } catch (e: CraftoException) { throw e - } catch (e: io.ktor.client.network.sockets.SocketTimeoutException) { - throw NetworkException("Connection timeout") - } catch (e: Exception) { + } catch (e: ConnectTimeoutException) { + throw ServerUnavailableException("Cannot connect to server. Please try again later.") + } catch (e: SocketTimeoutException) { + throw NetworkException("Connection timeout. Please check your internet connection.") + } catch (e: HttpRequestTimeoutException) { + throw NetworkException("Request timeout. Please try again.") + }catch (e: Exception) { throw NetworkException("Network error: ${e.message}") } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt index 769c148..a3a59e7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt @@ -9,4 +9,5 @@ class AlreadyExistsException(message: String?) : CraftoException(message) class ValidationException(message: String?) : CraftoException(message) class NetworkException(message: String? = "Network error occurred") : CraftoException(message) class ApiException(message: String?) : CraftoException(message) +class ServerUnavailableException(message: String? = "Server is currently unavailable") : CraftoException(message) class UnknownException(message: String? = "Unknown error occurred") : CraftoException(message) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt index 54f5cef..1a32b49 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt @@ -1,9 +1,14 @@ package org.example.project.presentation.mapper +import io.ktor.client.network.sockets.ConnectTimeoutException import io.ktor.client.network.sockets.SocketTimeoutException import io.ktor.client.plugins.HttpRequestTimeoutException +import org.example.project.domain.exception.AlreadyExistsException +import org.example.project.domain.exception.ApiException import org.example.project.domain.exception.ForbiddenException import org.example.project.domain.exception.NetworkException +import org.example.project.domain.exception.NotFoundException +import org.example.project.domain.exception.ServerUnavailableException import org.example.project.domain.exception.UnauthorizedException import org.example.project.domain.exception.ValidationException import org.example.project.presentation.screens.shared.base.ErrorUiState @@ -11,33 +16,53 @@ import org.example.project.util.AppLogger fun Throwable.toErrorUiState(): ErrorUiState { return when (this) { + // ========== CLIENT-SIDE: NETWORK ISSUES ========== is NetworkException, is SocketTimeoutException, is HttpRequestTimeoutException -> ErrorUiState( message = "Please check your internet connection and try again.", errorType = ErrorUiState.ErrorType.NETWORK ) - - is UnauthorizedException -> ErrorUiState( - message = "Please login to continue.", - errorType = ErrorUiState.ErrorType.AUTHENTICATION - ) - is ValidationException -> ErrorUiState( message = message ?: "Please check your input.", errorType = ErrorUiState.ErrorType.VALIDATION ) + // ========== SERVER-SIDE: SERVER UNAVAILABLE ========== + is ServerUnavailableException, + is ConnectTimeoutException -> ErrorUiState( + message = message ?: "Server is currently unavailable. Please try again later.", + errorType = ErrorUiState.ErrorType.SERVER + ) + + // ========== SERVER-SIDE: API ERRORS ========== + is UnauthorizedException -> ErrorUiState( + message = message ?: "Please login to continue.", + errorType = ErrorUiState.ErrorType.AUTHENTICATION + ) is ForbiddenException -> ErrorUiState( - message = "You don't have permission to perform this action.", + message = message ?: "You don't have permission to perform this action.", errorType = ErrorUiState.ErrorType.AUTHENTICATION ) + is NotFoundException -> ErrorUiState( + message = message ?: "The requested resource was not found.", + errorType = ErrorUiState.ErrorType.SERVER + ) + is AlreadyExistsException -> ErrorUiState( + message = message ?: "This resource already exists.", + errorType = ErrorUiState.ErrorType.SERVER + ) + is ApiException -> ErrorUiState( + message = message ?: "Server error occurred. Please try again.", + errorType = ErrorUiState.ErrorType.SERVER + ) + // ========== UNKNOWN ERRORS ========== else -> { - AppLogger.e("ThrowableEX",this.message?:"Unknown error") + AppLogger.e("ThrowableEX", this.message ?: "Unknown error") println("⚠️ Unexpected error: ${this::class.simpleName} - ${this.message}") ErrorUiState( - message = "Something went wrong. Please try again later.", + message = message ?: "Something went wrong. Please try again later.", errorType = ErrorUiState.ErrorType.UNKNOWN ) } From 23e64760f80dd46feddd86ad6ebe2ebeb97d0495 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Sun, 26 Oct 2025 19:41:08 +0300 Subject: [PATCH 09/12] feat: implement validation service and enhance personal info constraints in craftsman profile --- .../CraftsmanRemoteDataSourceImpl.kt | 29 -------------- .../data/remote/network/APIConstant.kt | 1 - .../repository/CraftsmanRepositoryImpl.kt | 3 +- .../data/service/ValidationServiceImpl.kt | 23 +++++++++++ .../org/example/project/di/DataModule.kt | 3 ++ .../org/example/project/di/DomainModule.kt | 6 +-- .../domain/exception/DomainExceptions.kt | 2 +- .../domain/service/ValidationService.kt | 17 ++------ .../CreateCraftsmanProfileUseCase.kt | 24 +++++++----- .../usecase/craftsman/UploadIdCardsUseCase.kt | 23 ++++++----- .../craftsman/UploadProfilePictureUseCase.kt | 13 ++++--- .../craftsman/UploadWorkPortfolioUseCase.kt | 9 +++-- .../project/domain/util/AppConstant.kt | 39 +++++++++++++++++++ .../presentation/util/ImagePickerUtils.kt | 2 +- .../org/example/project/util/AppConstant.kt | 15 ------- 15 files changed, 112 insertions(+), 97 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/data/service/ValidationServiceImpl.kt create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/domain/util/AppConstant.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt index 82188f6..4707fee 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/datasource/CraftsmanRemoteDataSourceImpl.kt @@ -24,7 +24,6 @@ import org.example.project.data.remote.network.ApiConstants import org.example.project.data.remote.network.ApiConstants.Headers.USER_ID import org.example.project.data.remote.network.wrapApiCall import org.example.project.domain.model.WorkImage -import org.example.project.util.AppLogger class CraftsmanRemoteDataSourceImpl( private val httpClient: HttpClient, @@ -51,18 +50,11 @@ class CraftsmanRemoteDataSourceImpl( idCardBack: ByteArray, idCardBackFileName: String ): IdCardUploadResponseDto { - AppLogger.d("API", "=== Starting ID Cards Upload ===") - AppLogger.d("API", "UserId: $userId") - AppLogger.d("API", "CraftsmanId: $craftsmanId") - AppLogger.d("API", "Front: $idCardFrontFileName (${idCardFront.size} bytes)") - AppLogger.d("API", "Back: $idCardBackFileName (${idCardBack.size} bytes)") - return wrapApiCall { httpClient.submitFormWithBinaryData( url = ApiConstants.Endpoints.craftsmanIdCards(craftsmanId), formData = formData { val frontMimeType = getMimeType(idCardFrontFileName) - AppLogger.d("API", "Front MIME type: $frontMimeType") append("idCardFront", idCardFront, Headers.build { append(HttpHeaders.ContentType, frontMimeType) @@ -70,7 +62,6 @@ class CraftsmanRemoteDataSourceImpl( }) val backMimeType = getMimeType(idCardBackFileName) - AppLogger.d("API", "Back MIME type: $backMimeType") append("idCardBack", idCardBack, Headers.build { append(HttpHeaders.ContentType, backMimeType) @@ -79,7 +70,6 @@ class CraftsmanRemoteDataSourceImpl( } ) { header(USER_ID, userId) - AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanIdCards(craftsmanId)}") } } } @@ -90,17 +80,11 @@ class CraftsmanRemoteDataSourceImpl( profilePicture: ByteArray, profilePictureFileName: String ): ProfilePictureUploadResponseDto { - AppLogger.d("API", "=== Starting Profile Picture Upload ===") - AppLogger.d("API", "UserId: $userId") - AppLogger.d("API", "CraftsmanId: $craftsmanId") - AppLogger.d("API", "File: $profilePictureFileName (${profilePicture.size} bytes)") - return wrapApiCall { httpClient.submitFormWithBinaryData( url = ApiConstants.Endpoints.craftsmanProfilePicture(craftsmanId), formData = formData { val mimeType = getMimeType(profilePictureFileName) - AppLogger.d("API", "Profile picture MIME type: $mimeType") append("profilePicture", profilePicture, Headers.build { append(HttpHeaders.ContentType, mimeType) @@ -109,7 +93,6 @@ class CraftsmanRemoteDataSourceImpl( } ) { header(USER_ID, userId) - AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanProfilePicture(craftsmanId)}") } } } @@ -119,23 +102,12 @@ class CraftsmanRemoteDataSourceImpl( craftsmanId: String, workImages: List ): WorkPortfolioResponseDto { - AppLogger.d("API", "=== Starting Portfolio Upload ===") - AppLogger.d("API", "UserId: $userId") - AppLogger.d("API", "CraftsmanId: $craftsmanId") - AppLogger.d("API", "Number of images: ${workImages.size}") - - workImages.forEachIndexed { index, image -> - AppLogger.d("API", "Image $index: ${image.fileName}, ${image.data.size} bytes") - } - return wrapApiCall { httpClient.submitFormWithBinaryData( url = ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId), formData = formData { workImages.forEachIndexed { index, image -> val mimeType = getMimeType(image.fileName) - AppLogger.d("API", "Appending image $index: ${image.fileName} ($mimeType)") - append( key = "workImages", value = image.data, @@ -148,7 +120,6 @@ class CraftsmanRemoteDataSourceImpl( } ) { header(USER_ID, userId) - AppLogger.d("API", "Request sent to: ${ApiConstants.Endpoints.craftsmanWorkPortfolio(craftsmanId)}") } } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt index cf194c0..56e6ebc 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/remote/network/APIConstant.kt @@ -20,7 +20,6 @@ object ApiConstants { // API Endpoints object Endpoints { - // Craftsman endpoints const val CRAFTSMAN_SETUP = "/craftsman/setup" const val CRAFTSMAN_PROFILE = "/craftsman/profile" const val ONBOARDING_END_POINT = "/onboarding" diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt index 984c0e6..ab1c950 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/repository/CraftsmanRepositoryImpl.kt @@ -115,7 +115,8 @@ class CraftsmanRepositoryImpl ( val response = remoteDataSource.getCraftsmanStatus(craftsmanId) return try { CraftsmanStatus.valueOf(response.status) - } catch (e: IllegalArgumentException) { + } catch ( + e: IllegalArgumentException) { throw ApiException("Invalid craftsman status: ${response.status}") } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/data/service/ValidationServiceImpl.kt b/composeApp/src/commonMain/kotlin/org/example/project/data/service/ValidationServiceImpl.kt new file mode 100644 index 0000000..a7b8e8a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/data/service/ValidationServiceImpl.kt @@ -0,0 +1,23 @@ +package org.example.project.data.service + +import org.example.project.domain.service.ValidationService +import org.example.project.domain.util.AppConstants + +class ValidationServiceImpl : ValidationService { + override fun isValidPhoneNumber(phone: String): Boolean { + return phone.matches(Regex(AppConstants.PersonalInfo.PHONE_REGEX)) + } + + override fun isValidEmail(email: String): Boolean { + return email.matches(Regex(AppConstants.PersonalInfo.EMAIL_REGEX)) + } + + override fun isValidImageFileName(fileName: String): Boolean { + val extension = fileName.substringAfterLast('.', "").lowercase() + return extension in AppConstants.FileUpload.ALLOWED_IMAGE_TYPES + } + + override fun isValidFileSize(sizeInBytes: Int): Boolean { + return sizeInBytes <= AppConstants.FileUpload.MAX_FILE_SIZE + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index bb2f052..28f6e7e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -10,8 +10,10 @@ import org.example.project.data.memory.categorySeed import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl import org.example.project.data.repository.CategoryRepositoryImpl import org.example.project.data.repository.CraftsmanRepositoryImpl +import org.example.project.data.service.ValidationServiceImpl import org.example.project.domain.repository.CategoryRepository import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.domain.service.ValidationService import org.koin.dsl.module val dataModule = module { @@ -26,6 +28,7 @@ val dataModule = module { single { categorySeed } single { CategoryMemoryDataSource(get()) } single { CategoryRepositoryImpl(get()) } + single { ValidationServiceImpl() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt index 0c02e5e..70c9a8c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DomainModule.kt @@ -11,12 +11,12 @@ import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase import org.koin.dsl.module val domainModule = module { - factory { CreateCraftsmanProfileUseCase(get()) } - factory { UploadIdCardsUseCase(get()) } + factory { CreateCraftsmanProfileUseCase(get(), get()) } + factory { UploadIdCardsUseCase(get(),get()) } factory { UploadWorkPortfolioUseCase(get()) } factory { GetCraftsmanProfileUseCase(get()) } factory { GetCraftsmanStatusUseCase(get()) } factory { DeleteCraftsmanAccountUseCase(get()) } factory { GetCategoriesUseCase(get())} - factory { UploadProfilePictureUseCase(get()) } + factory { UploadProfilePictureUseCase(get(),get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt index a3a59e7..a1efd43 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/exception/DomainExceptions.kt @@ -9,5 +9,5 @@ class AlreadyExistsException(message: String?) : CraftoException(message) class ValidationException(message: String?) : CraftoException(message) class NetworkException(message: String? = "Network error occurred") : CraftoException(message) class ApiException(message: String?) : CraftoException(message) -class ServerUnavailableException(message: String? = "Server is currently unavailable") : CraftoException(message) +class ServerUnavailableException(message: String? = "Server is currently unavailable") : CraftoException(message) // NEW class UnknownException(message: String? = "Unknown error occurred") : CraftoException(message) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt index f33c472..b2bda9b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/service/ValidationService.kt @@ -2,18 +2,7 @@ package org.example.project.domain.service interface ValidationService { fun isValidPhoneNumber(phone: String): Boolean - //fun isValidImageFile(fileName: String, data: ByteArray): ValidationResult -} - -class ValidationServiceImpl : ValidationService { - override fun isValidPhoneNumber(phone: String): Boolean { - return phone.matches(Regex("^\\+?[1-9]\\d{1,14}$")) - } - -// override fun isValidImageFile( -// fileName: String, -// data: ByteArray -// ): ValidationResult { -// TODO("Not yet implemented") -// } + fun isValidEmail(email: String): Boolean + fun isValidImageFileName(fileName: String): Boolean + fun isValidFileSize(sizeInBytes: Int): Boolean } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt index b50796d..dcddf9a 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/CreateCraftsmanProfileUseCase.kt @@ -3,24 +3,32 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.entity.PersonalInfo import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.domain.service.ValidationService +import org.example.project.domain.util.AppConstants +import org.example.project.domain.util.AppConstants.Categories.MAX_CATEGORIES class CreateCraftsmanProfileUseCase( - private val repository: CraftsmanRepository + private val repository: CraftsmanRepository, + private val validationService: ValidationService ) { suspend operator fun invoke( personalInfo: PersonalInfo, categories: List ): String { - if (categories.isEmpty()) { + if (categories.size < AppConstants.Categories.MIN_CATEGORIES) { throw ValidationException("Please select at least one service category") } - if (personalInfo.firstName.isBlank()) { + if (categories.size > MAX_CATEGORIES) { + throw ValidationException("Please select at most $MAX_CATEGORIES service categories") + } + + if (personalInfo.firstName.length < AppConstants.PersonalInfo.MIN_FIRST_NAME_LENGTH) { throw ValidationException("First name is required") } - if (personalInfo.lastName.isBlank()) { + if (personalInfo.lastName.length < AppConstants.PersonalInfo.MIN_LAST_NAME_LENGTH) { throw ValidationException("Last name is required") } @@ -28,18 +36,14 @@ class CreateCraftsmanProfileUseCase( throw ValidationException("Phone number is required") } - if (!isValidPhoneNumber(personalInfo.phoneNumber)) { + if (!validationService.isValidPhoneNumber(personalInfo.phoneNumber)) { throw ValidationException("Please enter a valid phone number") } - if (personalInfo.address.isBlank()) { + if (personalInfo.address.length < AppConstants.PersonalInfo.MIN_ADDRESS_LENGTH) { throw ValidationException("Address is required") } return repository.createCraftsmanProfile(personalInfo, categories) } - - private fun isValidPhoneNumber(phone: String): Boolean { - return phone.matches(Regex("^\\+?[1-9]\\d{1,14}$")) - } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt index b8ab58c..be74ab3 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadIdCardsUseCase.kt @@ -3,11 +3,13 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.entity.VerificationDocuments import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository - +import org.example.project.domain.service.ValidationService +import org.example.project.domain.util.AppConstants class UploadIdCardsUseCase( - private val repository: CraftsmanRepository + private val repository: CraftsmanRepository, + private val validationService: ValidationService ) { suspend operator fun invoke( craftsmanId: String, @@ -28,11 +30,11 @@ class UploadIdCardsUseCase( throw ValidationException("Please select back ID card image") } - if (idCardFront.size > org.example.project.util.AppConstants.FileUpload.MAX_FILE_SIZE) { - throw ValidationException("Front ID card image size must be less than 4MB") + if (!validationService.isValidFileSize(idCardFront.size)) { + throw ValidationException("Front ID card image size must be less than ${AppConstants.FileUpload.MAX_FILE_SIZE_MB} MB") } - if (idCardBack.size > org.example.project.util.AppConstants.FileUpload.MAX_FILE_SIZE) { + if (!validationService.isValidFileSize(idCardBack.size)) { throw ValidationException("Back ID card image size must be less than 4MB") } @@ -44,18 +46,15 @@ class UploadIdCardsUseCase( throw ValidationException("Invalid back ID card file name") } - val frontExtension = idCardFrontFileName.substringAfterLast('.', "").lowercase() - val backExtension = idCardBackFileName.substringAfterLast('.', "").lowercase() - - if (frontExtension !in org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (!validationService.isValidImageFileName(idCardFrontFileName)) { throw ValidationException( - "Front ID card must be one of: ${org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + "Front ID card must be one of: ${AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" ) } - if (backExtension !in org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (!validationService.isValidImageFileName(idCardBackFileName)) { throw ValidationException( - "Back ID card must be one of: ${org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" + "Back ID card must be one of: ${AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}" ) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt index 1f5ec98..6a1ba8c 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadProfilePictureUseCase.kt @@ -2,10 +2,12 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.exception.ValidationException import org.example.project.domain.repository.CraftsmanRepository -import org.example.project.util.AppConstants +import org.example.project.domain.service.ValidationService +import org.example.project.domain.util.AppConstants class UploadProfilePictureUseCase( - private val repository: CraftsmanRepository + private val repository: CraftsmanRepository, + private val validationService: ValidationService ) { suspend operator fun invoke( craftsmanId: String, @@ -20,15 +22,14 @@ class UploadProfilePictureUseCase( throw ValidationException("Profile picture is required") } - if (profilePicture.size > AppConstants.FileUpload.MAX_FILE_SIZE) { + if (!validationService.isValidFileSize(profilePicture.size)) { throw ValidationException( "Profile picture size must be less than ${AppConstants.FileUpload.MAX_FILE_SIZE_MB}MB" ) } - val extension = profilePictureFileName.substringAfterLast('.', "").lowercase() - if (extension !in AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { - throw ValidationException("Profile picture must be JPG or PNG") + if (!validationService.isValidImageFileName(profilePictureFileName)) { + throw ValidationException("Profile picture must be one of: ${AppConstants.FileUpload.ALLOWED_IMAGE_TYPES.joinToString(", ")}") } return repository.uploadProfilePicture( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt index 84086d6..9f20a22 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/usecase/craftsman/UploadWorkPortfolioUseCase.kt @@ -3,6 +3,7 @@ package org.example.project.domain.usecase.craftsman import org.example.project.domain.exception.ValidationException import org.example.project.domain.model.WorkImage import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.domain.util.AppConstants class UploadWorkPortfolioUseCase( @@ -20,9 +21,9 @@ class UploadWorkPortfolioUseCase( throw ValidationException("Please select at least one work image") } - if (workImages.size > org.example.project.util.AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { + if (workImages.size > AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES) { throw ValidationException( - "You can upload maximum ${org.example.project.util.AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" + "You can upload maximum ${AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES} images" ) } @@ -31,12 +32,12 @@ class UploadWorkPortfolioUseCase( throw ValidationException("Image ${index + 1} is empty") } - if (image.data.size > org.example.project.util.AppConstants.FileUpload.MAX_FILE_SIZE) { + if (image.data.size > AppConstants.FileUpload.MAX_FILE_SIZE) { throw ValidationException("Image ${index + 1} size must be less than 4MB") } val extension = image.fileName.substringAfterLast('.', "").lowercase() - if (extension !in org.example.project.util.AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { + if (extension !in AppConstants.FileUpload.ALLOWED_IMAGE_TYPES) { throw ValidationException( "Image ${index + 1} must be JPEG or PNG" ) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/domain/util/AppConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/domain/util/AppConstant.kt new file mode 100644 index 0000000..e4cc451 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/domain/util/AppConstant.kt @@ -0,0 +1,39 @@ +package org.example.project.domain.util + +object AppConstants { + object FileUpload { + const val MAX_FILE_SIZE_MB = 4 + const val MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 // 4MB + + val ALLOWED_IMAGE_TYPES = listOf("jpg", "jpeg", "png") + const val MAX_PORTFOLIO_IMAGES = 4 + + const val MIME_TYPE_JPEG = "image/jpeg" + const val MIME_TYPE_PNG = "image/png" + } + + object PersonalInfo { + const val MIN_FIRST_NAME_LENGTH = 3 + const val MAX_FIRST_NAME_LENGTH = 50 + const val MIN_LAST_NAME_LENGTH = 3 + const val MAX_LAST_NAME_LENGTH = 50 + const val MIN_PHONE_LENGTH = 10 + const val MAX_PHONE_LENGTH = 15 + const val MIN_ADDRESS_LENGTH = 5 + const val MAX_ADDRESS_LENGTH = 50 + const val PHONE_REGEX = "^\\+?[1-9]\\d{1,14}$" + const val EMAIL_REGEX = + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + } + + object Categories { + const val MIN_CATEGORIES = 1 + const val MAX_CATEGORIES = 7 + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt index a185c8f..c7a75d7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/util/ImagePickerUtils.kt @@ -11,7 +11,7 @@ import com.mohamedrejeb.calf.picker.FilePickerSelectionMode import com.mohamedrejeb.calf.picker.rememberFilePickerLauncher import kotlinx.coroutines.launch import org.example.project.presentation.model.ImageData -import org.example.project.util.AppConstants +import org.example.project.domain.util.AppConstants import org.example.project.util.AppLogger import kotlin.time.Clock import kotlin.time.ExperimentalTime diff --git a/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt b/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt deleted file mode 100644 index cc6513b..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/util/AppConstant.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.example.project.util - -object AppConstants { - // File Upload - object FileUpload { - const val MAX_FILE_SIZE_MB = 4 - const val MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 // 4MB - - val ALLOWED_IMAGE_TYPES = listOf("jpg", "jpeg", "png") - const val MAX_PORTFOLIO_IMAGES = 4 - - const val MIME_TYPE_JPEG = "image/jpeg" - const val MIME_TYPE_PNG = "image/png" - } -} \ No newline at end of file From e0bb89d054c7bd116797917156fd042ac17a0f8e Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Mon, 27 Oct 2025 03:54:19 +0300 Subject: [PATCH 10/12] feat: enhance craftsman setup with image removal functionality and refactor imports --- .../project/presentation/mapper/Exception.kt | 2 +- .../onboarding/OnboardingScreenState.kt | 2 +- .../screens/onboarding/OnboardingViewModel.kt | 2 +- .../setupScreens/composable/PicturePicker.kt | 136 ++++++++++++++++++ .../CraftsmanSetupInteractionListener.kt | 9 +- .../craftsmansetup/CraftsmanSetupScreen.kt | 27 ++-- .../craftsmansetup/CraftsmanSetupUiState.kt | 4 +- .../craftsmansetup/CraftsmanSetupViewModel.kt | 53 +++++-- .../shared/base/BaseScreenState.kt | 2 +- .../shared/base/BaseViewModel.kt | 2 +- .../{screens => }/shared/base/ErrorUiState.kt | 2 +- 11 files changed, 211 insertions(+), 30 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{screens => }/shared/base/BaseScreenState.kt (58%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{screens => }/shared/base/BaseViewModel.kt (96%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/{screens => }/shared/base/ErrorUiState.kt (79%) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt index 1a32b49..af9a6d0 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/mapper/Exception.kt @@ -11,7 +11,7 @@ import org.example.project.domain.exception.NotFoundException import org.example.project.domain.exception.ServerUnavailableException import org.example.project.domain.exception.UnauthorizedException import org.example.project.domain.exception.ValidationException -import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.shared.base.ErrorUiState import org.example.project.util.AppLogger fun Throwable.toErrorUiState(): ErrorUiState { diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt index f5d6f11..c4c6995 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingScreenState.kt @@ -1,7 +1,7 @@ package org.example.project.presentation.screens.onboarding import org.example.project.presentation.screens.onboarding.model.OnboardingUiState -import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.shared.base.ErrorUiState data class OnboardingScreenState( val onboardingData: List = emptyList(), diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt index 2cef8a3..f22d328 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/onboarding/OnboardingViewModel.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.IO import org.example.project.domain.entity.OnboardingItem import org.example.project.domain.repository.OnboardingRepository import org.example.project.presentation.screens.onboarding.model.toUiState -import org.example.project.presentation.screens.shared.base.BaseViewModel +import org.example.project.presentation.shared.base.BaseViewModel import org.koin.android.annotation.KoinViewModel import org.koin.core.annotation.Provided diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt new file mode 100644 index 0000000..dd3141c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt @@ -0,0 +1,136 @@ +package org.example.project.presentation.screens.setupscreens.composable + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.camera +import crafto.composeapp.generated.resources.plus +import crafto.composeapp.generated.resources.x +import io.ktor.util.internal.OpDescriptor +import org.example.project.presentation.designsystem.colors.Shade +import org.example.project.presentation.designsystem.textstyle.AppTheme +import org.example.project.presentation.model.ImageData +import org.example.project.presentation.util.rememberImagePicker +import org.jetbrains.compose.resources.painterResource + +@Composable +fun ImagePicker( + modifier: Modifier = Modifier, + selectedImage: ImageData? = null, + onImageSelected: (ImageData) -> Unit, + onRemove: () -> Unit, + shape: Shape? = null, + imageSize: Dp? = null, + contentDescriptor: String? = null, + onError: (String) -> Unit, + enabled: Boolean = true, + isUploading: Boolean = false +) { + val imagePicker = rememberImagePicker( + singleSelection = true, + onImagesSelected = { images -> + images.firstOrNull()?.let { imageData -> + onImageSelected(imageData) + } + }, + onError = { errorMessage -> + onError(errorMessage) + } + ) + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box(contentAlignment=Alignment.TopEnd) { + Box( + modifier = Modifier + .then(if (imageSize != null) Modifier.size(imageSize) else Modifier.fillMaxSize()) + .then(shape?.let { Modifier.clip(it) } ?: Modifier) + .background(AppTheme.craftoColors.background.card) + .border( + BorderStroke( + width = 1.dp, + color = AppTheme.craftoColors.shade.quinary + ), + shape = shape ?: RectangleShape + ) + .clickable(enabled = enabled && !isUploading) { + imagePicker.launch() + }, + contentAlignment = Alignment.Center + ) { + if (selectedImage != null) { + AsyncImage( + model = selectedImage.byteArray, + contentDescription = contentDescriptor, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + Column( + modifier.matchParentSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically) + ) { + Icon( + painter = painterResource(Res.drawable.camera), + contentDescription = "Add Profile Picture", + tint = AppTheme.craftoColors.shade.secondary, + modifier = Modifier.size(32.dp) + ) + + Text( + text = "Tap here", + style = AppTheme.textStyle.body.smallMedium, + color = AppTheme.craftoColors.shade.secondary + ) + } + } + } + if (selectedImage != null) { + IconButton( + onClick = onRemove, + modifier = Modifier + .size(24.dp) + .background( + AppTheme.craftoColors.background.card.copy(alpha = 0.8f), + RoundedCornerShape(8.dp) + ) + .padding(4.dp) + ) { + Icon( + painter = painterResource(Res.drawable.x), + contentDescription = "Remove Image", + tint = AppTheme.craftoColors.shade.primary + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt index 31a774b..dacf9ed 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -2,7 +2,7 @@ package org.example.project.presentation.screens.setupscreens.craftsmansetup import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.shared.base.ErrorUiState interface CraftsmanSetupInteractionListener { fun onUserTypeSelected(userType: UserType) @@ -16,10 +16,13 @@ interface CraftsmanSetupInteractionListener { fun onPortfolioImagesAdded(images: List) fun onPortfolioImageRemoved(index: Int) + fun onProfilePictureRemoved() + fun onFrontIdCardRemoved() + fun onBackIdCardRemoved() fun onWorkDescriptionChanged(description: String) fun onUploadPortfolio() - fun onProfilePictureSelected(imageData: ImageData) // ADD THIS - fun onImagePickerError(error: ErrorUiState) // ADD THIS + fun onProfilePictureSelected(imageData: ImageData) + fun onImagePickerError(error: ErrorUiState) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt index b49906f..76fdc9e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt @@ -30,12 +30,12 @@ import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.TextButton import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.screens.setupScreens.composable.SetupScreenScaffold -import org.example.project.presentation.screens.setupScreens.composable.page.IdentityVerificationPage -import org.example.project.presentation.screens.setupScreens.composable.page.PersonalInfoPage -import org.example.project.presentation.screens.setupScreens.composable.page.PortfolioUploadPage -import org.example.project.presentation.screens.setupScreens.composable.page.ServiceSelectionPage -import org.example.project.presentation.screens.setupScreens.composable.page.UserTypeSelectionPage -import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.screens.setupscreens.composable.page.IdentityVerificationPage +import org.example.project.presentation.screens.setupscreens.composable.page.PersonalInfoPage +import org.example.project.presentation.screens.setupscreens.composable.page.PortfolioUploadPage +import org.example.project.presentation.screens.setupscreens.composable.page.ServiceSelectionPage +import org.example.project.presentation.screens.setupscreens.composable.page.UserTypeSelectionPage +import org.example.project.presentation.shared.base.ErrorUiState import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalFoundationApi::class) @@ -179,7 +179,8 @@ fun CraftsmanSetupContent( viewModel.onImagePickerError(ErrorUiState(errorMessage)) }, isLoading = state.isLoading, - isUploadingProfilePicture = state.isUploadingProfilePicture + isUploadingProfilePicture = state.isUploadingProfilePicture, + onRemove = viewModel::onProfilePictureRemoved ) } @@ -191,6 +192,9 @@ fun CraftsmanSetupContent( onAddPhotosClicked = viewModel::onPortfolioImagesAdded, onImageRemoved = viewModel::onPortfolioImageRemoved, onDescriptionChanged = viewModel::onWorkDescriptionChanged, + onError = { errorMessage -> + viewModel.onImagePickerError(ErrorUiState(errorMessage)) + } , ) } @@ -199,9 +203,12 @@ fun CraftsmanSetupContent( idCardFront = state.idCardFront, idCardBack = state.idCardBack, onIdCardSelected = viewModel::onIdCardSelected, - onUploadClick = viewModel::onUploadIdCards, - onSkip = {}, - onErrorMessage = {} , + onSkip = viewModel::onSkipIdentityVerification, + onErrorMessage = { errorMessage -> + viewModel.onImagePickerError(ErrorUiState(errorMessage)) + }, + onFrontImageRemoved = viewModel::onFrontIdCardRemoved, + onBackImageRemoved = viewModel::onBackIdCardRemoved, ) } } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt index 84387bb..5ae43e7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt @@ -4,8 +4,8 @@ import org.example.project.domain.entity.VerificationDocuments import org.example.project.presentation.model.CategoryUi import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.screens.shared.base.BaseScreenState -import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.shared.base.BaseScreenState +import org.example.project.presentation.shared.base.ErrorUiState data class CraftsmanSetupUiState( override val isLoading: Boolean = false, diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt index 82c2ed8..d6cdcad 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt @@ -7,10 +7,11 @@ import org.example.project.domain.usecase.craftsman.CreateCraftsmanProfileUseCas import org.example.project.domain.usecase.craftsman.UploadIdCardsUseCase import org.example.project.domain.usecase.craftsman.UploadProfilePictureUseCase import org.example.project.domain.usecase.craftsman.UploadWorkPortfolioUseCase +import org.example.project.domain.util.AppConstants.FileUpload.MAX_PORTFOLIO_IMAGES import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.screens.shared.base.BaseViewModel -import org.example.project.presentation.screens.shared.base.ErrorUiState +import org.example.project.presentation.shared.base.BaseViewModel +import org.example.project.presentation.shared.base.ErrorUiState import org.example.project.presentation.mapper.toDomain import org.example.project.presentation.mapper.toUi import org.example.project.presentation.mapper.toWorkImages @@ -166,14 +167,15 @@ class CraftsmanSetupViewModel( } override fun onSkipIdentityVerification() { - sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) + //sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) + navigateNext() } override fun onPortfolioImagesAdded(images: List) { updateState { state -> val currentImages = state.portfolioImages val totalImages = currentImages + images - val limitedImages = totalImages.take(4) + val limitedImages = totalImages.take(MAX_PORTFOLIO_IMAGES) AppLogger.d("Portfolio", "Added ${images.size} images. Total: ${limitedImages.size}") @@ -184,7 +186,7 @@ class CraftsmanSetupViewModel( state.copy( portfolioImages = limitedImages, - canAddMoreImages = limitedImages.size < 4, + canAddMoreImages = limitedImages.size < MAX_PORTFOLIO_IMAGES, canNavigateNext = limitedImages.isNotEmpty() ) } @@ -193,9 +195,17 @@ class CraftsmanSetupViewModel( override fun onPortfolioImageRemoved(index: Int) { updateState { state -> val newImages = state.portfolioImages.toMutableList().apply { - removeAt(index) + if (index < this.size) { + removeAt(index) + } + } + val newImagesUrl= state.uploadedPortfolioUrls.toMutableList().apply { + if (index < this.size) { + removeAt(index) + } } state.copy( + uploadedPortfolioUrls = newImagesUrl, portfolioImages = newImages, canAddMoreImages = true, canNavigateNext = newImages.isNotEmpty() @@ -203,6 +213,33 @@ class CraftsmanSetupViewModel( } } + override fun onProfilePictureRemoved() { + updateState { state -> + state.copy( + profilePicture = null, + profilePictureUrl = null + ) + } + } + + override fun onFrontIdCardRemoved() { + updateState { state -> + state.copy( + idCardFront = null, + canNavigateNext = false + ) + } + } + + override fun onBackIdCardRemoved() { + updateState { state -> + state.copy( + idCardBack = null, + canNavigateNext = false + ) + } + } + override fun onWorkDescriptionChanged(description: String) { updateState { state -> state.copy(workDescription = description) @@ -337,8 +374,6 @@ class CraftsmanSetupViewModel( ) } - // Even if profile picture fails, allow user to continue - // They can upload it later from settings AppLogger.d("Navigation", "Profile creation complete, navigating to portfolio page") updateState { it.copy( @@ -347,7 +382,7 @@ class CraftsmanSetupViewModel( ) } }, - showLoading = false // Don't show loading since we're already showing it for profile creation + showLoading = true ) } diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseScreenState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/BaseScreenState.kt similarity index 58% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseScreenState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/BaseScreenState.kt index 592fa60..cadaca9 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseScreenState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/BaseScreenState.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.shared.base +package org.example.project.presentation.shared.base interface BaseScreenState { val isLoading: Boolean diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/BaseViewModel.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseViewModel.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/BaseViewModel.kt index 284c39d..28da96d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/BaseViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/BaseViewModel.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.shared.base +package org.example.project.presentation.shared.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/ErrorUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/ErrorUiState.kt similarity index 79% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/ErrorUiState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/ErrorUiState.kt index 7dfe8fd..73a6de7 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/shared/base/ErrorUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/shared/base/ErrorUiState.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.shared.base +package org.example.project.presentation.shared.base data class ErrorUiState ( val message: String = "", From 31311ddb29d26ad9e0717ae6d96fd48930fb3012 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Wed, 29 Oct 2025 23:28:01 +0300 Subject: [PATCH 11/12] feat: refactor craftsman setup to remove unnecessary logging and improve image handling --- .../composable/ProfilePictureSelector.kt | 51 +---------- .../page/IdentityVerificationPage.kt | 72 ++++++---------- .../composable/page/PersonalInfoPage.kt | 16 ++-- .../composable/page/PortfolioUploadPage.kt | 20 +---- .../composable/page/ServiceSelectionPage.kt | 6 +- .../composable/page/UserTypeSelectionPage.kt | 2 +- .../craftsmansetup/CraftsmanSetupViewModel.kt | 85 ------------------- 7 files changed, 44 insertions(+), 208 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt index 79f50dc..aef9c17 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable +package org.example.project.presentation.screens.setupscreens.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,7 +16,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import crafto.composeapp.generated.resources.Res @@ -98,7 +96,6 @@ fun ProfilePictureSelector( ) } } else { - // Display placeholder - similar to EmptyPortfolioBox style Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp) @@ -117,52 +114,6 @@ fun ProfilePictureSelector( ) } } -// -// // Show loading overlay if uploading -// if (isUploading) { -// Box( -// modifier = Modifier -// .size(100.dp) -// .clip(CircleShape) -// .background( -// AppTheme.craftoColors.shade.secondary.copy(alpha = 0.5f) -// ), -// contentAlignment = Alignment.Center -// ) { -// CircularProgressIndicator( -// color = AppTheme.craftoColors.primary.main, -// strokeWidth = 2.dp, -// modifier = Modifier.size(24.dp) -// ) -// } -// } -// } -// -// // Status text -// Text( -// text = when { -// isUploading -> "Uploading..." -// selectedImage != null -> "Tap to change photo" -// else -> "Add profile photo" -// }, -// style = AppTheme.textStyle.caption.medium, -// color = if (isUploading) -// AppTheme.craftoColors.primary.main -// else -// AppTheme.craftoColors.text.secondary, -// textAlign = TextAlign.Center -// ) -// -// if (selectedImage == null) { -// Text( -// text = "(Optional)", -// style = AppTheme.textStyle.caption.regular, -// color = AppTheme.craftoColors.text.tertiary, -// textAlign = TextAlign.Center -// ) -// } -// } - } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt index 7c7a46e..d77cf52 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt @@ -1,18 +1,17 @@ -package org.example.project.presentation.screens.setupScreens.composable.page +package org.example.project.presentation.screens.setupscreens.composable.page import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio 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.shape.RoundedCornerShape -import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -25,9 +24,9 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.camera -import crafto.composeapp.generated.resources.plus import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData +import org.example.project.presentation.screens.setupscreens.composable.ImagePicker import org.example.project.presentation.util.rememberImagePicker import org.jetbrains.compose.resources.painterResource @@ -36,7 +35,8 @@ fun IdentityVerificationPage( idCardFront: ImageData?, idCardBack: ImageData?, onIdCardSelected: (isFront: Boolean, imageData: ImageData) -> Unit, - onUploadClick: () -> Unit, + onFrontImageRemoved: () -> Unit, + onBackImageRemoved: () -> Unit, onSkip: () -> Unit, onErrorMessage: (String) -> Unit ) { @@ -48,71 +48,55 @@ fun IdentityVerificationPage( horizontalAlignment = Alignment.CenterHorizontally ) { UploadBox( + modifier = Modifier.weight(1f), title = "Upload Front of National ID", image = idCardFront, onSelect = { img -> onIdCardSelected(true, img) }, - onError = onErrorMessage + onError = onErrorMessage, + onRemove = onFrontImageRemoved ) UploadBox( + modifier = Modifier.weight(1f), title = "Upload Back of National ID", image = idCardBack, onSelect = { img -> onIdCardSelected(false, img) }, - onError = onErrorMessage + onError = onErrorMessage, + onRemove = onBackImageRemoved ) - Spacer(modifier = Modifier.weight(1f)) - - OutlinedButton( - modifier = Modifier.weight(1f), - onClick = onSkip - ) { Text("I'll Verify Later") } + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = onSkip + ) { Text("I'll Verify Later") } } } @Composable private fun UploadBox( + modifier: Modifier=Modifier, title: String, image: ImageData?, onSelect: (ImageData) -> Unit, - onError: (String) -> Unit + onError: (String) -> Unit, + onRemove: () -> Unit, ) { - val imagePicker = rememberImagePicker( - singleSelection = true, - onImagesSelected = { images -> - images.firstOrNull()?.let(onSelect) - }, - onError = onError - ) - Column( + modifier = modifier, horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text(title, style = AppTheme.textStyle.body.smallMedium) - Box( + + ImagePicker( modifier = Modifier .fillMaxWidth() - .height(150.dp) - .background( - color = AppTheme.craftoColors.background.card, - RoundedCornerShape(12.dp) - ) - .clickable { - imagePicker.launch() - }, - contentAlignment = Alignment.Center - ) { - if (image == null) - Icon(painterResource(Res.drawable.camera), contentDescription = "upload image") - else { - AsyncImage( - model = image.byteArray, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().clip(RoundedCornerShape(12.dp)) - ) - } - } + .clip(RoundedCornerShape(12.dp)), + onImageSelected = onSelect, + onError = onError, + selectedImage = image, + onRemove = onRemove, + contentDescriptor = "ID Card Image", + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt index b7201a7..a99c148 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable.page +package org.example.project.presentation.screens.setupscreens.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -6,11 +6,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction @@ -19,7 +17,7 @@ import androidx.compose.ui.unit.dp import org.example.project.presentation.designsystem.components.TextField import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.screens.setupScreens.composable.ProfilePictureSelector +import org.example.project.presentation.screens.setupscreens.composable.ImagePicker @Composable fun PersonalInfoPage( @@ -27,6 +25,7 @@ fun PersonalInfoPage( onPersonalInfoChanged: (PersonalInfoUiModel) -> Unit, profilePicture: ImageData? = null, onProfilePictureSelected: (ImageData) -> Unit, + onRemove: () -> Unit, onImagePickerError: (String) -> Unit, isLoading: Boolean, isUploadingProfilePicture: Boolean = false @@ -39,13 +38,16 @@ fun PersonalInfoPage( verticalArrangement = Arrangement.spacedBy(16.dp) ) { - ProfilePictureSelector( + ImagePicker( modifier = Modifier.fillMaxWidth(), selectedImage = profilePicture, onImageSelected = onProfilePictureSelected, onError = onImagePickerError, + imageSize = 100.dp, + shape = CircleShape, enabled = !isLoading, - isUploading = isUploadingProfilePicture + isUploading = isUploadingProfilePicture, + onRemove = onRemove, ) TextField( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt index ccb0809..f53f234 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable.page +package org.example.project.presentation.screens.setupscreens.composable.page import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -26,22 +26,15 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.mohamedrejeb.calf.core.LocalPlatformContext -import com.mohamedrejeb.calf.io.getName -import com.mohamedrejeb.calf.io.readByteArray -import com.mohamedrejeb.calf.picker.FilePickerFileType -import com.mohamedrejeb.calf.picker.FilePickerSelectionMode -import com.mohamedrejeb.calf.picker.rememberFilePickerLauncher import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.camera import crafto.composeapp.generated.resources.plus import crafto.composeapp.generated.resources.x -import kotlinx.coroutines.launch import org.example.project.presentation.designsystem.components.TextField import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData import org.example.project.presentation.util.rememberImagePicker import org.jetbrains.compose.resources.painterResource -import kotlin.time.Clock import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) @@ -52,19 +45,15 @@ fun PortfolioUploadPage( onAddPhotosClicked: (List) -> Unit, onImageRemoved: (Int) -> Unit, workDescription: String, - onDescriptionChanged: (String) -> Unit + onDescriptionChanged: (String) -> Unit, + onError: (String) -> Unit ) { - val scope = rememberCoroutineScope() - val context = LocalPlatformContext.current - val imagePicker = rememberImagePicker( singleSelection = false, onImagesSelected = { newImages -> onAddPhotosClicked(newImages) }, - onError = { errorMessage -> - // Handle error - show snackbar/toast - } + onError = onError ) Column( @@ -107,7 +96,6 @@ fun PortfolioUploadPage( } } -// Shown when no images yet @Composable private fun EmptyPortfolioBox(onAddPhotosClicked: () -> Unit) { Box( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt index f6bd32f..88dd735 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt @@ -1,12 +1,8 @@ -package org.example.project.presentation.screens.setupScreens.composable.page +package org.example.project.presentation.screens.setupscreens.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt index 888cde4..9187612 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable.page +package org.example.project.presentation.screens.setupscreens.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt index d6cdcad..d97eaab 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt @@ -77,20 +77,6 @@ class CraftsmanSetupViewModel( } override fun onIdCardSelected(isFront: Boolean, imageData: ImageData) { - AppLogger.d("IDCard", " ID Card selected") - AppLogger.d("IDCard", " Front: $isFront") - AppLogger.d("IDCard", " FileName: ${imageData.fileName}") - AppLogger.d("IDCard", " URI: ${imageData.uri}") - AppLogger.d("IDCard", " Size: ${imageData.byteArray.size} bytes") - - // Extract and validate extension - val extension = imageData.fileName.substringAfterLast('.', "").lowercase() - AppLogger.d("IDCard", " Extension: '$extension'") - - if (extension.isEmpty()) { - AppLogger.e("IDCard", " No extension found in filename!") - } - updateState { state -> if (isFront) { state.copy( @@ -111,31 +97,15 @@ class CraftsmanSetupViewModel( val frontCard = state.value.idCardFront val backCard = state.value.idCardBack - AppLogger.d("IDCard", " onUploadIdCards() called") - AppLogger.d("IDCard", " CraftsmanId: $craftsmanId") - if (craftsmanId == null) { - AppLogger.e("IDCard", " CraftsmanId is null!") updateState { it.copy(error = ErrorUiState("Profile not created yet")) } return } if (frontCard == null || backCard == null) { - AppLogger.e("IDCard", " Missing ID cards!") updateState { it.copy(error = ErrorUiState("Please upload both ID card images")) } return } - - AppLogger.d("IDCard", " Front card:") - AppLogger.d("IDCard", " - FileName: ${frontCard.fileName}") - AppLogger.d("IDCard", " - Size: ${frontCard.byteArray.size} bytes") - AppLogger.d("IDCard", " - Extension: ${frontCard.fileName.substringAfterLast('.', "")}") - - AppLogger.d("IDCard", " Back card:") - AppLogger.d("IDCard", " - FileName: ${backCard.fileName}") - AppLogger.d("IDCard", " - Size: ${backCard.byteArray.size} bytes") - AppLogger.d("IDCard", " - Extension: ${backCard.fileName.substringAfterLast('.', "")}") - updateState { it.copy(isSwipeEnabled = false) } tryToCall( @@ -149,12 +119,10 @@ class CraftsmanSetupViewModel( ) }, onSuccess = { verificationDocs -> - AppLogger.d("IDCard", " ID Cards uploaded successfully!") updateState { it.copy(isSwipeEnabled = true) } sendNewEffect(CraftsmanRegistrationEffect.RegistrationComplete) }, onError = { error -> - AppLogger.e("IDCard", " Upload failed: ${error.message}") updateState { it.copy( error = error, @@ -177,9 +145,6 @@ class CraftsmanSetupViewModel( val totalImages = currentImages + images val limitedImages = totalImages.take(MAX_PORTFOLIO_IMAGES) - AppLogger.d("Portfolio", "Added ${images.size} images. Total: ${limitedImages.size}") - - // Log each image details limitedImages.forEachIndexed { index, img -> AppLogger.d("Portfolio", "Image $index: ${img.fileName}, ${img.byteArray.size} bytes") } @@ -249,48 +214,31 @@ class CraftsmanSetupViewModel( override fun onUploadPortfolio() { val craftsmanId = state.value.craftsmanId if (craftsmanId == null) { - AppLogger.e("Portfolio", "CraftsmanId is null!") updateState { it.copy(error = ErrorUiState("Profile not created yet")) } return } val portfolioImages = state.value.portfolioImages if (portfolioImages.isEmpty()) { - AppLogger.d("Portfolio", "No images to upload, skipping to next page") - updateState { it.copy(currentPageIndex = it.currentPageIndex + 1) } return } - // Check if already uploaded if (state.value.uploadedPortfolioUrls.isNotEmpty()) { - AppLogger.d("Portfolio", "Portfolio already uploaded, skipping") updateState { it.copy(currentPageIndex = it.currentPageIndex + 1) } return } - AppLogger.d("Portfolio", "Starting upload of ${portfolioImages.size} images for craftsman $craftsmanId") - - portfolioImages.forEachIndexed { index, image -> - AppLogger.d("Portfolio", "Image $index: fileName=${image.fileName}, size=${image.byteArray.size} bytes") - } - updateState { it.copy(isSwipeEnabled = false, isUploadingPortfolio = true) } tryToCall( call = { val workImages = portfolioImages.toWorkImages() - AppLogger.d("Portfolio", "Converted to ${workImages.size} WorkImage objects - calling API") uploadWorkPortfolioUseCase( craftsmanId = craftsmanId, workImages = workImages ) }, onSuccess = { uploadedUrls -> - AppLogger.d("Portfolio", " SUCCESS! Received ${uploadedUrls.size} URLs") - uploadedUrls.forEach { url -> - AppLogger.d("Portfolio", " - $url") - } - updateState { it.copy( isSwipeEnabled = true, @@ -301,7 +249,6 @@ class CraftsmanSetupViewModel( } }, onError = { error -> - AppLogger.e("Portfolio", " FAILED: ${error.message}") updateState { it.copy( error = error, @@ -315,26 +262,16 @@ class CraftsmanSetupViewModel( } override fun onProfilePictureSelected(imageData: ImageData) { - AppLogger.d("ProfilePicture", "Profile picture selected") - AppLogger.d("ProfilePicture", " FileName: ${imageData.fileName}") - AppLogger.d("ProfilePicture", " Size: ${imageData.byteArray.size} bytes") - updateState { state -> state.copy(profilePicture = imageData) } } override fun onImagePickerError(error: ErrorUiState) { - AppLogger.e("ImagePicker", "Image picker error: ${error.message}") updateState { it.copy(error = error) } } private fun uploadProfilePicture(craftsmanId: String, profilePicture: ImageData) { - AppLogger.d("ProfilePicture", "Starting profile picture upload") - AppLogger.d("ProfilePicture", " CraftsmanId: $craftsmanId") - AppLogger.d("ProfilePicture", " FileName: ${profilePicture.fileName}") - AppLogger.d("ProfilePicture", " Size: ${profilePicture.byteArray.size} bytes") - updateState { it.copy(isUploadingProfilePicture = true) } tryToCall( @@ -346,18 +283,12 @@ class CraftsmanSetupViewModel( ) }, onSuccess = { profilePictureUrl -> - AppLogger.d("ProfilePicture", "✅ Profile picture uploaded successfully!") - AppLogger.d("ProfilePicture", " URL: $profilePictureUrl") - updateState { it.copy( profilePictureUrl = profilePictureUrl, isUploadingProfilePicture = false ) } - - // Continue to next page after profile picture upload - AppLogger.d("Navigation", "Profile creation complete, navigating to portfolio page") updateState { it.copy( isSwipeEnabled = true, @@ -366,15 +297,12 @@ class CraftsmanSetupViewModel( } }, onError = { error -> - AppLogger.e("ProfilePicture", "❌ Profile picture upload failed: ${error.message}") updateState { it.copy( error = error, isUploadingProfilePicture = false ) } - - AppLogger.d("Navigation", "Profile creation complete, navigating to portfolio page") updateState { it.copy( isSwipeEnabled = true, @@ -396,7 +324,6 @@ class CraftsmanSetupViewModel( when (state.value.currentStep) { RegistrationStep.PERSONAL_INFO -> { if (!state.value.isProfileCreated) { - AppLogger.d("Navigation", "Creating profile before proceeding") createCraftsmanProfile() return // createCraftsmanProfile will navigate on success } @@ -407,24 +334,20 @@ class CraftsmanSetupViewModel( val isCurrentlyUploading = state.value.isUploadingPortfolio if (isCurrentlyUploading) { - AppLogger.d("Navigation", "Upload already in progress, ignoring navigation") return } if (hasImages && !alreadyUploaded) { - AppLogger.d("Navigation", "Portfolio needs to be uploaded") onUploadPortfolio() return // onUploadPortfolio will navigate on success } - AppLogger.d("Navigation", "Portfolio already uploaded or no images, proceeding") } else -> { // Normal navigation for other steps } } - // Normal navigation if (currentIndex < state.value.totalPages - 1 && state.value.canNavigateNext) { AppLogger.d("Navigation", "Navigating from page $currentIndex to ${currentIndex + 1}") updateState { it.copy(currentPageIndex = currentIndex + 1) } @@ -434,7 +357,6 @@ class CraftsmanSetupViewModel( fun navigateBack() { val currentIndex = state.value.currentPageIndex if (currentIndex > 0) { - // Prevent going back after profile creation val targetIndex = if (state.value.isProfileCreated && currentIndex == 3) { currentIndex // Stay on current page } else { @@ -489,15 +411,12 @@ class CraftsmanSetupViewModel( tryToCall( call = { - AppLogger.d("CraftsmanSetup", "Calling createCraftsmanUseCase") createCraftsmanUseCase( personalInfo = state.value.personalInfo.toDomain(), categories = selectedCategoryTitles ) }, onSuccess = { craftsmanId -> - AppLogger.d("CraftsmanSetup", "✅ Profile created successfully: $craftsmanId") - updateState { it.copy( craftsmanId = craftsmanId, @@ -505,13 +424,10 @@ class CraftsmanSetupViewModel( ) } - // Check if profile picture was selected val profilePicture = state.value.profilePicture if (profilePicture != null) { - AppLogger.d("CraftsmanSetup", "Profile picture selected, uploading...") uploadProfilePicture(craftsmanId, profilePicture) } else { - AppLogger.d("CraftsmanSetup", "No profile picture selected, skipping upload") updateState { it.copy( isSwipeEnabled = true, @@ -521,7 +437,6 @@ class CraftsmanSetupViewModel( } }, onError = { error -> - AppLogger.e("CraftsmanSetup", "❌ Profile creation failed: ${error.message}") updateState { it.copy( error = error, From 9c35add3200a738fc2df602dcb58fe49c3385e24 Mon Sep 17 00:00:00 2001 From: Amr Ashraf Date: Tue, 11 Nov 2025 13:03:59 +0200 Subject: [PATCH 12/12] feat: reorganize package structure and enhance string resources for setup screens --- .gitignore | 7 +- .../composeResources/values-ar/string.xml | 32 +++++++ .../composeResources/values/string.xml | 31 +++++++ .../kotlin/org/example/project/App.kt | 4 +- .../org/example/project/di/CraftoModule.kt | 11 --- .../org/example/project/di/DataModule.kt | 4 +- .../example/project/di/PresentationModule.kt | 4 +- .../org/example/project/di/ViewModelModule.kt | 11 --- .../composable/AccountSetupTopBar.kt | 2 +- .../composable/PicturePicker.kt | 6 +- .../composable/ProfilePictureSelector.kt | 2 +- .../composable/SetupScreenScaffold.kt | 2 +- .../composable/TitleDescriptionBox.kt | 2 +- .../page/IdentityVerificationPage.kt | 30 +++--- .../composable/page/PersonalInfoPage.kt | 18 ++-- .../composable/page/PortfolioUploadPage.kt | 14 +-- .../composable/page/ServiceSelectionPage.kt | 2 +- .../composable/page/UserTypeSelectionPage.kt | 17 ++-- .../craftsmansetup/CraftsmanSetupEffect.kt | 2 +- .../CraftsmanSetupInteractionListener.kt | 2 +- .../craftsmansetup/CraftsmanSetupScreen.kt | 66 +++++++++---- .../craftsmansetup/CraftsmanSetupUiState.kt | 2 +- .../craftsmansetup/CraftsmanSetupViewModel.kt | 2 +- .../screens/setup/customersetup/init | 1 + .../location/AccountSetupTopBar.kt | 2 +- .../location/LocationEffect.kt | 2 +- .../location/LocationSetupScreen.kt | 2 +- .../location/LocationUiState.kt | 2 +- .../location/LocationViewModel.kt | 2 +- .../AccountSetupCategoryScreen.kt | 75 --------------- .../setupScreens/AccountSetupEffect.kt | 5 - .../AccountSetupInterActionListener.kt | 5 - .../screens/setupScreens/AccountSetupState.kt | 16 ---- .../setupScreens/AccountSetupViewModel.kt | 45 --------- .../component/CategoryActionBox.kt | 92 ------------------- .../composable/CategoryActionBox.kt | 92 ------------------- 36 files changed, 184 insertions(+), 430 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/AccountSetupTopBar.kt (97%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/PicturePicker.kt (94%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/ProfilePictureSelector.kt (98%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/SetupScreenScaffold.kt (98%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/TitleDescriptionBox.kt (95%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/page/IdentityVerificationPage.kt (73%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/page/PersonalInfoPage.kt (80%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/page/PortfolioUploadPage.kt (91%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/page/ServiceSelectionPage.kt (94%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/composable/page/UserTypeSelectionPage.kt (66%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/craftsmansetup/CraftsmanSetupEffect.kt (60%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/craftsmansetup/CraftsmanSetupInteractionListener.kt (92%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/craftsmansetup/CraftsmanSetupScreen.kt (76%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/craftsmansetup/CraftsmanSetupUiState.kt (97%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/craftsmansetup/CraftsmanSetupViewModel.kt (99%) create mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/location/AccountSetupTopBar.kt (96%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/location/LocationEffect.kt (67%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/location/LocationSetupScreen.kt (98%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/location/LocationUiState.kt (89%) rename composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/{setupScreens => setup}/location/LocationViewModel.kt (97%) delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupEffect.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupInterActionListener.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupState.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupViewModel.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt delete mode 100644 composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt diff --git a/.gitignore b/.gitignore index 225937e..a4228d2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,9 @@ captures !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings -# Project exclude paths - -### Google Service ### +# Google Service +composeApp/google-service.json **/google-service.json google-service.json + +# Project exclude paths \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ar/string.xml b/composeApp/src/commonMain/composeResources/values-ar/string.xml index 0e01b6b..0dc7e89 100644 --- a/composeApp/src/commonMain/composeResources/values-ar/string.xml +++ b/composeApp/src/commonMain/composeResources/values-ar/string.xml @@ -39,5 +39,37 @@ السهم لأسفل + + How would you like to use Crafto? + What services do you offer? + Let’s personalize your profile + Show Us Your Work + Verify Your Identity\n(Optional) + I'll Verify Later + Next Step + + You can switch roles anytime from your profile. + Choose your specialties to get relevant job requests. You can change this later. + We’ll use this to personalize your experience. You can add a profile photo too, or skip for now. + Add photos of your past work. This helps build trust with customers. + Uploading your ID helps build trust with customers. Verified craftsmen get more jobs and a special badge on their profile. + + Upload front of national ID + Upload back of national ID + ID card image + + First name + Last name + Phone number + Address + + Describe your work (optional) + You can mention your years of experience, tools you use, or types of jobs you usually handle. + Tap to add photos + + Customer + I need help with a\n problem + Craftsman + I offer service \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/string.xml b/composeApp/src/commonMain/composeResources/values/string.xml index 2d14a09..a8033d4 100644 --- a/composeApp/src/commonMain/composeResources/values/string.xml +++ b/composeApp/src/commonMain/composeResources/values/string.xml @@ -48,6 +48,37 @@ back arrow + + How would you like to use Crafto? + What services do you offer? + Let’s personalize your profile + Show Us Your Work + Verify Your Identity\n(Optional) + I'll Verify Later + Next Step + You can switch roles anytime from your profile. + Choose your specialties to get relevant job requests. You can change this later. + We’ll use this to personalize your experience. You can add a profile photo too, or skip for now. + Add photos of your past work. This helps build trust with customers. + Uploading your ID helps build trust with customers. Verified craftsmen get more jobs and a special badge on their profile. + + Upload front of national ID + Upload back of national ID + ID card image + + First name + Last name + Phone number + Address + + Describe your work (optional) + You can mention your years of experience, tools you use, or types of jobs you usually handle. + Tap to add photos + + Customer + I need help with a\n problem + Craftsman + I offer service \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/App.kt b/composeApp/src/commonMain/kotlin/org/example/project/App.kt index fa557a7..e767ad8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/App.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/App.kt @@ -2,7 +2,8 @@ package org.example.project import androidx.compose.runtime.Composable import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.screens.setupscreens.craftsmansetup.CraftsmanSetupScreen +import org.example.project.presentation.screens.setup.craftsmansetup.CraftsmanSetupScreen +import org.example.project.presentation.screens.splash.SplashScreen import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @@ -10,6 +11,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun App() { AppTheme { //OnboardingScreen() + SplashScreen { } CraftsmanSetupScreen() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt deleted file mode 100644 index 91df6ba..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/CraftoModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -import org.example.project.data.repository.LocationRepositoryImpl -import org.example.project.domain.repository.LocationRepository -import org.koin.core.annotation.Module -import org.koin.dsl.module - -@Module -class CraftoModule { - val module = module { - single { LocationRepositoryImpl(get()) } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt index 28f6e7e..4ab09b4 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/DataModule.kt @@ -10,9 +10,11 @@ import org.example.project.data.memory.categorySeed import org.example.project.data.remote.datasource.CraftsmanRemoteDataSourceImpl import org.example.project.data.repository.CategoryRepositoryImpl import org.example.project.data.repository.CraftsmanRepositoryImpl +import org.example.project.data.repository.LocationRepositoryImpl import org.example.project.data.service.ValidationServiceImpl import org.example.project.domain.repository.CategoryRepository import org.example.project.domain.repository.CraftsmanRepository +import org.example.project.domain.repository.LocationRepository import org.example.project.domain.service.ValidationService import org.koin.dsl.module @@ -29,6 +31,6 @@ val dataModule = module { single { CategoryMemoryDataSource(get()) } single { CategoryRepositoryImpl(get()) } single { ValidationServiceImpl() } - + single { LocationRepositoryImpl(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt index 1fd6afb..9a1adec 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/di/PresentationModule.kt @@ -1,6 +1,7 @@ package org.example.project.di -import org.example.project.presentation.screens.setupscreens.craftsmansetup.CraftsmanSetupViewModel +import org.example.project.presentation.screens.setup.craftsmansetup.CraftsmanSetupViewModel +import org.example.project.presentation.screens.setup.location.LocationViewModel import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -14,4 +15,5 @@ val presentationModule = module { uploadProfilePictureUseCase = get(), ) } + viewModel {LocationViewModel(get())} } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt b/composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt deleted file mode 100644 index 25a13a5..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/di/ViewModelModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.project.di - -import org.example.project.presentation.screens.setupScreens.location.LocationViewModel -import org.koin.core.module.dsl.viewModel -import org.koin.dsl.module - -class ViewModelModule { - val module = module { - viewModel { LocationViewModel(get()) } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/AccountSetupTopBar.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/AccountSetupTopBar.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/AccountSetupTopBar.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/AccountSetupTopBar.kt index 90592b1..d05684d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/AccountSetupTopBar.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/AccountSetupTopBar.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable +package org.example.project.presentation.screens.setup.composable import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/PicturePicker.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/PicturePicker.kt index dd3141c..ba447da 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/PicturePicker.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/PicturePicker.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.composable +package org.example.project.presentation.screens.setup.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -27,10 +26,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import crafto.composeapp.generated.resources.Res import crafto.composeapp.generated.resources.camera -import crafto.composeapp.generated.resources.plus import crafto.composeapp.generated.resources.x -import io.ktor.util.internal.OpDescriptor -import org.example.project.presentation.designsystem.colors.Shade import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData import org.example.project.presentation.util.rememberImagePicker diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/ProfilePictureSelector.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/ProfilePictureSelector.kt index aef9c17..50f2ff8 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/ProfilePictureSelector.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/ProfilePictureSelector.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.composable +package org.example.project.presentation.screens.setup.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt index effe0b7..04994fa 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/SetupScreenScaffold.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/SetupScreenScaffold.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable +package org.example.project.presentation.screens.setup.composable import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/TitleDescriptionBox.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/TitleDescriptionBox.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/TitleDescriptionBox.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/TitleDescriptionBox.kt index ecbd456..ae8034b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/TitleDescriptionBox.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/TitleDescriptionBox.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.composable +package org.example.project.presentation.screens.setup.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/IdentityVerificationPage.kt similarity index 73% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/IdentityVerificationPage.kt index d77cf52..4c0e16e 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/IdentityVerificationPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/IdentityVerificationPage.kt @@ -1,34 +1,28 @@ -package org.example.project.presentation.screens.setupscreens.composable.page +package org.example.project.presentation.screens.setup.composable.page -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio 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.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import crafto.composeapp.generated.resources.Res -import crafto.composeapp.generated.resources.camera +import crafto.composeapp.generated.resources.id_card_image +import crafto.composeapp.generated.resources.personal_info +import crafto.composeapp.generated.resources.skip_for_now +import crafto.composeapp.generated.resources.upload_back_of_national_id +import crafto.composeapp.generated.resources.upload_front_of_national_id import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData -import org.example.project.presentation.screens.setupscreens.composable.ImagePicker -import org.example.project.presentation.util.rememberImagePicker -import org.jetbrains.compose.resources.painterResource +import org.example.project.presentation.screens.setup.composable.ImagePicker +import org.jetbrains.compose.resources.stringResource @Composable fun IdentityVerificationPage( @@ -49,7 +43,7 @@ fun IdentityVerificationPage( ) { UploadBox( modifier = Modifier.weight(1f), - title = "Upload Front of National ID", + title = stringResource(Res.string.upload_front_of_national_id), image = idCardFront, onSelect = { img -> onIdCardSelected(true, img) }, onError = onErrorMessage, @@ -58,7 +52,7 @@ fun IdentityVerificationPage( UploadBox( modifier = Modifier.weight(1f), - title = "Upload Back of National ID", + title = stringResource(Res.string.upload_back_of_national_id), image = idCardBack, onSelect = { img -> onIdCardSelected(false, img) }, onError = onErrorMessage, @@ -68,7 +62,7 @@ fun IdentityVerificationPage( OutlinedButton( modifier = Modifier.fillMaxWidth(), onClick = onSkip - ) { Text("I'll Verify Later") } + ) { Text(stringResource(Res.string.skip_for_now)) } } } @@ -96,7 +90,7 @@ private fun UploadBox( onError = onError, selectedImage = image, onRemove = onRemove, - contentDescriptor = "ID Card Image", + contentDescriptor = stringResource(Res.string.id_card_image), ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/PersonalInfoPage.kt similarity index 80% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/PersonalInfoPage.kt index a99c148..61a0b2d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PersonalInfoPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/PersonalInfoPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.composable.page +package org.example.project.presentation.screens.setup.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -14,10 +14,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.address +import crafto.composeapp.generated.resources.first_name +import crafto.composeapp.generated.resources.last_name +import crafto.composeapp.generated.resources.phone_number import org.example.project.presentation.designsystem.components.TextField import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel -import org.example.project.presentation.screens.setupscreens.composable.ImagePicker +import org.example.project.presentation.screens.setup.composable.ImagePicker +import org.jetbrains.compose.resources.stringResource @Composable fun PersonalInfoPage( @@ -51,14 +57,14 @@ fun PersonalInfoPage( ) TextField( - labelText = "First Name", + labelText = stringResource(Res.string.first_name), text = personalInfo.firstName, onTextChange = { onPersonalInfoChanged(personalInfo.copy(firstName = it)) }, enabledState = !isLoading, inputKeyboard = KeyboardOptions(imeAction = ImeAction.Next) ) TextField( - labelText = "Last Name", + labelText = stringResource(resource = Res.string.last_name), text = personalInfo.lastName, onTextChange = { onPersonalInfoChanged(personalInfo.copy(lastName = it)) }, enabledState = !isLoading, @@ -67,14 +73,14 @@ fun PersonalInfoPage( TextField( text = personalInfo.phoneNumber, onTextChange = { onPersonalInfoChanged(personalInfo.copy(phoneNumber = it)) }, - labelText = "Phone Number", + labelText = stringResource(Res.string.phone_number), enabledState = !isLoading, inputKeyboard = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Phone) ) TextField( text = personalInfo.address, onTextChange = { onPersonalInfoChanged(personalInfo.copy(address = it)) }, - labelText = "Address", + labelText = stringResource(Res.string.address), maxLines = 3, enabledState = !isLoading, ) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/PortfolioUploadPage.kt similarity index 91% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/PortfolioUploadPage.kt index f53f234..b6f7943 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/PortfolioUploadPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/PortfolioUploadPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.composable.page +package org.example.project.presentation.screens.setup.composable.page import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -18,16 +18,17 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.mohamedrejeb.calf.core.LocalPlatformContext import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.add_photos import crafto.composeapp.generated.resources.camera +import crafto.composeapp.generated.resources.describe_your_work +import crafto.composeapp.generated.resources.describe_your_work_hint import crafto.composeapp.generated.resources.plus import crafto.composeapp.generated.resources.x import org.example.project.presentation.designsystem.components.TextField @@ -35,6 +36,7 @@ import org.example.project.presentation.designsystem.textstyle.AppTheme import org.example.project.presentation.model.ImageData import org.example.project.presentation.util.rememberImagePicker import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) @@ -86,10 +88,10 @@ fun PortfolioUploadPage( } TextField( - labelText = "Describe Your Work (Optional)", + labelText = stringResource(Res.string.describe_your_work), text = workDescription, onTextChange = onDescriptionChanged, - hint = "You can mention your years of experience, tools you use, or types of jobs you usually handle.", + hint = stringResource(Res.string.describe_your_work_hint), modifier = Modifier.fillMaxWidth(), maxLines = 4 ) @@ -118,7 +120,7 @@ private fun EmptyPortfolioBox(onAddPhotosClicked: () -> Unit) { ) Spacer(Modifier.height(8.dp)) Text( - "Tap to add photos", + text = stringResource(Res.string.add_photos), style = AppTheme.textStyle.body.smallMedium, color = AppTheme.craftoColors.shade.secondary ) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/ServiceSelectionPage.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/ServiceSelectionPage.kt index 88dd735..c2e4e89 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/ServiceSelectionPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/ServiceSelectionPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.composable.page +package org.example.project.presentation.screens.setup.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.FlowRow diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt similarity index 66% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt index 9187612..62a8677 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/page/UserTypeSelectionPage.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/composable/page/UserTypeSelectionPage.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.composable.page +package org.example.project.presentation.screens.setup.composable.page import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -7,11 +7,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.craftsman +import crafto.composeapp.generated.resources.craftsman_description +import crafto.composeapp.generated.resources.customer +import crafto.composeapp.generated.resources.customer_description import crafto.composeapp.generated.resources.selection_craftsman import crafto.composeapp.generated.resources.selection_customer import org.example.project.presentation.designsystem.components.SelectionCard -import org.example.project.presentation.screens.setupscreens.craftsmansetup.UserType +import org.example.project.presentation.screens.setup.craftsmansetup.UserType import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource @Composable fun UserTypeSelectionPage( @@ -25,16 +30,16 @@ fun UserTypeSelectionPage( ) { SelectionCard( img = painterResource(Res.drawable.selection_customer), - title = "Customer", - caption = "I need help with a\nservice", + title = stringResource(Res.string.customer), + caption = stringResource(Res.string.customer_description), isSelected = selectedType == UserType.CUSTOMER, onCardClick = { onTypeSelected(UserType.CUSTOMER) }, modifier = Modifier.weight(1f) ) SelectionCard( img = painterResource(Res.drawable.selection_craftsman), - title = "Craftsman", - caption = "I offer services", + title = stringResource(Res.string.craftsman), + caption = stringResource(Res.string.craftsman_description), isSelected = selectedType == UserType.CRAFTSMAN, onCardClick = { onTypeSelected(UserType.CRAFTSMAN) }, modifier = Modifier.weight(1f) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupEffect.kt similarity index 60% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupEffect.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupEffect.kt index ab89306..1372a6d 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupEffect.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupEffect.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.craftsmansetup +package org.example.project.presentation.screens.setup.craftsmansetup sealed interface CraftsmanRegistrationEffect { data object RegistrationComplete : CraftsmanRegistrationEffect diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt index dacf9ed..bc33d78 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupInteractionListener.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupInteractionListener.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.craftsmansetup +package org.example.project.presentation.screens.setup.craftsmansetup import org.example.project.presentation.model.ImageData import org.example.project.presentation.model.PersonalInfoUiModel diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt similarity index 76% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt index 76fdc9e..d9d4369 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupScreen.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.craftsmansetup +package org.example.project.presentation.screens.setup.craftsmansetup import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition @@ -26,16 +26,29 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import crafto.composeapp.generated.resources.Res +import crafto.composeapp.generated.resources.identity_verification +import crafto.composeapp.generated.resources.location_hint +import crafto.composeapp.generated.resources.personal_info +import crafto.composeapp.generated.resources.portfolio_upload +import crafto.composeapp.generated.resources.registration_step_1_description +import crafto.composeapp.generated.resources.registration_step_2_description +import crafto.composeapp.generated.resources.registration_step_3_description +import crafto.composeapp.generated.resources.registration_step_4_description +import crafto.composeapp.generated.resources.registration_step_5_description +import crafto.composeapp.generated.resources.service_selection +import crafto.composeapp.generated.resources.user_type import org.example.project.presentation.designsystem.components.ButtonState import org.example.project.presentation.designsystem.components.TextButton import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.screens.setupScreens.composable.SetupScreenScaffold -import org.example.project.presentation.screens.setupscreens.composable.page.IdentityVerificationPage -import org.example.project.presentation.screens.setupscreens.composable.page.PersonalInfoPage -import org.example.project.presentation.screens.setupscreens.composable.page.PortfolioUploadPage -import org.example.project.presentation.screens.setupscreens.composable.page.ServiceSelectionPage -import org.example.project.presentation.screens.setupscreens.composable.page.UserTypeSelectionPage +import org.example.project.presentation.screens.setup.composable.SetupScreenScaffold +import org.example.project.presentation.screens.setup.composable.page.IdentityVerificationPage +import org.example.project.presentation.screens.setup.composable.page.PersonalInfoPage +import org.example.project.presentation.screens.setup.composable.page.PortfolioUploadPage +import org.example.project.presentation.screens.setup.composable.page.ServiceSelectionPage +import org.example.project.presentation.screens.setup.composable.page.UserTypeSelectionPage import org.example.project.presentation.shared.base.ErrorUiState +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @OptIn(ExperimentalFoundationApi::class) @@ -131,18 +144,37 @@ fun CraftsmanSetupContent( } }, title = when (state.currentStep) { - RegistrationStep.USER_TYPE -> { "How would you like to use Crafto?" } - RegistrationStep.SERVICE_SELECTION -> {"What services do you offer?"} - RegistrationStep.PERSONAL_INFO -> {"Let’s personalize your profile"} - RegistrationStep.PORTFOLIO_UPLOAD -> {"Show Us Your Work"} - RegistrationStep.IDENTITY_VERIFICATION -> {"Verify Your Identity\n(Optional)"} + RegistrationStep.USER_TYPE -> { + stringResource(Res.string.user_type) } + RegistrationStep.SERVICE_SELECTION -> { + stringResource(Res.string.service_selection) + } + RegistrationStep.PERSONAL_INFO -> { + stringResource(Res.string.personal_info) + } + RegistrationStep.PORTFOLIO_UPLOAD -> { + stringResource(Res.string.portfolio_upload) + } + RegistrationStep.IDENTITY_VERIFICATION -> { + stringResource(Res.string.identity_verification) + } }, description =when (state.currentStep) { - RegistrationStep.USER_TYPE -> { "You can switch roles anytime from your profile." } - RegistrationStep.SERVICE_SELECTION -> {"Choose your specialties to get relevant job requests. You can change this later."} - RegistrationStep.PERSONAL_INFO -> {"We’ll use this to personalize your experience. You can add a profile photo too, or skip for now."} - RegistrationStep.PORTFOLIO_UPLOAD -> {"Add photos or a video of your past work. This helps build trust with customers."} - RegistrationStep.IDENTITY_VERIFICATION -> {"Uploading your ID helps build trust with customers. Verified craftsmen get more jobs and a special badge on their profile."} + RegistrationStep.USER_TYPE -> { + stringResource(Res.string.registration_step_1_description) + } + RegistrationStep.SERVICE_SELECTION -> { + stringResource(Res.string.registration_step_2_description) + } + RegistrationStep.PERSONAL_INFO -> { + stringResource(Res.string.registration_step_3_description) + } + RegistrationStep.PORTFOLIO_UPLOAD -> { + stringResource(Res.string.registration_step_4_description) + } + RegistrationStep.IDENTITY_VERIFICATION -> { + stringResource(Res.string.registration_step_5_description) + } } ) { Column( diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt index 5ae43e7..ac71fa9 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupUiState.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.craftsmansetup +package org.example.project.presentation.screens.setup.craftsmansetup import org.example.project.domain.entity.VerificationDocuments import org.example.project.presentation.model.CategoryUi diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt similarity index 99% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt index d97eaab..35e6f2b 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/craftsmansetup/CraftsmanSetupViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/craftsmansetup/CraftsmanSetupViewModel.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupscreens.craftsmansetup +package org.example.project.presentation.screens.setup.craftsmansetup import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init new file mode 100644 index 0000000..6a8de1b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/customersetup/init @@ -0,0 +1 @@ +TODO() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/AccountSetupTopBar.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/AccountSetupTopBar.kt index 28fd946..5d87caa 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/AccountSetupTopBar.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/AccountSetupTopBar.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.location +package org.example.project.presentation.screens.setup.location import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationEffect.kt similarity index 67% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationEffect.kt index 2d5e88b..b571c42 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationEffect.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationEffect.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.location +package org.example.project.presentation.screens.setup.location sealed class LocationEffect { object NavigateToNextScreen : LocationEffect() diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationSetupScreen.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationSetupScreen.kt index bef097b..4548566 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationSetupScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationSetupScreen.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.location +package org.example.project.presentation.screens.setup.location import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationUiState.kt similarity index 89% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationUiState.kt index f403a7d..70c6db6 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationUiState.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationUiState.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.location +package org.example.project.presentation.screens.setup.location import org.example.project.domain.entity.District import org.example.project.domain.entity.Governorates diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationViewModel.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt rename to composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationViewModel.kt index 1fea3cb..1af1fca 100644 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/location/LocationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setup/location/LocationViewModel.kt @@ -1,4 +1,4 @@ -package org.example.project.presentation.screens.setupScreens.location +package org.example.project.presentation.screens.setup.location import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt deleted file mode 100644 index 054fc87..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupCategoryScreen.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.example.project.presentation.ui.screens.setupScreens - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import crafto.composeapp.generated.resources.Res -import crafto.composeapp.generated.resources.account_setup_craftsman_category_description -import crafto.composeapp.generated.resources.account_setup_craftsman_category_title -import crafto.composeapp.generated.resources.account_setup_customer_category_description -import crafto.composeapp.generated.resources.account_setup_customer_category_title -import org.example.project.presentation.ui.screens.setupScreens.component.CategoryActionBox -import org.example.project.presentation.ui.screens.setupScreens.component.SetupScreenScaffold -import org.example.project.presentation.screens.setupScreens.AccountSetupState -import org.example.project.presentation.screens.setupScreens.AccountSetupViewModel -import org.jetbrains.compose.resources.stringResource -import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI - -@OptIn(KoinExperimentalAPI::class) -@Composable -fun AccountSetupCategoryScreen( - viewModel: AccountSetupViewModel = koinViewModel(), -) { - val state by viewModel.state.collectAsState() - AccountSetupCategoryContent( - state = state, - isCustomer = true, - currentPageNumber = 2, - onBackButtonClick = {}, - onNextButtonClick = {}, - onChipSelected = viewModel::onCategorySelected - ) -} - -@Composable -fun AccountSetupCategoryContent( - modifier: Modifier = Modifier, - state: AccountSetupState, - isCustomer: Boolean, - currentPageNumber: Int, - onBackButtonClick: () -> Unit, - onNextButtonClick: () -> Unit, - onChipSelected: (id: Int) -> Unit, -) { - SetupScreenScaffold( - modifier = modifier, - currentPageNumber = currentPageNumber, - title = selectCustomerOrCraftsmanText(isCustomer).first, - description = selectCustomerOrCraftsmanText(isCustomer).second, - onBackButtonClick = onBackButtonClick, - onNextButtonClick = onNextButtonClick, - ) { - CategoryActionBox( - state = state, - onChipSelected = onChipSelected, - ) - } - -} - -@Composable -private fun selectCustomerOrCraftsmanText(isCustomer: Boolean): Pair { - return if (isCustomer) { - stringResource(Res.string.account_setup_customer_category_title) to - stringResource(Res.string.account_setup_customer_category_description) - } else { - stringResource(Res.string.account_setup_craftsman_category_title) to - stringResource(Res.string.account_setup_craftsman_category_description) - } -} - - - - diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupEffect.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupEffect.kt deleted file mode 100644 index e98c9ec..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupEffect.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.project.presentation.screens.setupScreens - -sealed class AccountSetupEffect { - -} diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupInterActionListener.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupInterActionListener.kt deleted file mode 100644 index 4bf163e..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupInterActionListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.project.presentation.screens.setupScreens - -interface AccountSetupInterActionListener { - fun onCategorySelected(id: Int) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupState.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupState.kt deleted file mode 100644 index b9eb309..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.example.project.presentation.screens.setupScreens - -import org.example.project.domain.entity.Category - -data class AccountSetupState( - val title: String = "", - val description: String = "", - val isLoading: Boolean = false, - val isError: Boolean = false, - val categoryState: AccountSetupCategoryState = AccountSetupCategoryState() -) - -data class AccountSetupCategoryState( - val categories: List = emptyList(), - val isSelected: Boolean = false, -) diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupViewModel.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupViewModel.kt deleted file mode 100644 index 5041221..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/AccountSetupViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.example.project.presentation.screens.setupScreens - -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch -import org.example.project.domain.usecase.GetCategoriesUseCase -import org.example.project.presentation.shared.base.BaseViewModel -import org.koin.android.annotation.KoinViewModel -import org.koin.core.annotation.Provided - -@KoinViewModel -class AccountSetupViewModel( - @Provided private val getCategoriesUseCase : GetCategoriesUseCase -) : BaseViewModel(AccountSetupState()), - AccountSetupInterActionListener { - - init { - fetchCategories() - } - - private fun fetchCategories() { - viewModelScope.launch { - val categories = getCategoriesUseCase.invoke() - updateState { - it.copy( - categoryState = it.categoryState.copy(categories = categories) - ) - } - } - } - - override fun onCategorySelected(id: Int) { - updateState { - val categories = it.categoryState.categories.map { category -> - if (category.id == id) { - category.copy(isSelected = !category.isSelected) - } else { - category - } - } - it.copy( - categoryState = it.categoryState.copy(categories = categories) - ) - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt deleted file mode 100644 index dddeb96..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/component/CategoryActionBox.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.example.project.presentation.ui.screens.setupScreens.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import org.example.project.data.memory.dataSource.categoryList -import org.example.project.presentation.designsystem.components.Chip -import org.example.project.presentation.designsystem.textstyle.AppTheme -import org.example.project.presentation.util.extension.toAnimatedColor -import org.example.project.presentation.screens.setupScreens.AccountSetupCategoryState -import org.example.project.presentation.screens.setupScreens.AccountSetupState -import org.jetbrains.compose.ui.tooling.preview.Preview - -@Composable -fun CategoryActionBox( - modifier: Modifier = Modifier, - state: AccountSetupState, - onChipSelected: (id: Int) -> Unit, -) { - Box(modifier = modifier.fillMaxWidth()) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - - state.categoryState.categories.forEach { category -> - val isSelected = category.isSelected - Chip( - text = category.title, - isSelected = isSelected, - onChipSelected = { onChipSelected(category.id) }, - modifier = Modifier.background( - color = category.color.toAnimatedColor( - isSelected, - AppTheme.craftoColors.background.card - ), - shape = RoundedCornerShape(AppTheme.craftoRadius.full) - ), - textColor = AppTheme.craftoColors.background.card - .toAnimatedColor( - isSelected, - AppTheme.craftoColors.shade.secondary - ), - borderColor = category.color.toAnimatedColor( - condition = isSelected, - falseConditionColor = Color.Transparent, - duration = 100 - - ) - ) - } - } - } -} - -@Preview -@Composable -fun CategoryActionBoxLightPreview() { - AppTheme { - CategoryActionBox( - state = AccountSetupState( - categoryState = AccountSetupCategoryState( - categories = categoryList - ) - ), - onChipSelected = {} - ) - - } -} - -@Preview -@Composable -fun CategoryActionBoxDarkPreview() { - AppTheme(isDarkTheme = true) { - CategoryActionBox( - state = AccountSetupState( - categoryState = AccountSetupCategoryState( - categories = categoryList - ) - ), - onChipSelected = {} - ) - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt b/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt deleted file mode 100644 index 439c83d..0000000 --- a/composeApp/src/commonMain/kotlin/org/example/project/presentation/screens/setupScreens/composable/CategoryActionBox.kt +++ /dev/null @@ -1,92 +0,0 @@ -//package org.example.project.presentation.screens.setupScreens.component -// -//import androidx.compose.foundation.background -//import androidx.compose.foundation.layout.Arrangement -//import androidx.compose.foundation.layout.Box -//import androidx.compose.foundation.layout.FlowRow -//import androidx.compose.foundation.layout.fillMaxWidth -//import androidx.compose.foundation.shape.RoundedCornerShape -//import androidx.compose.runtime.Composable -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.graphics.Color -//import androidx.compose.ui.unit.dp -//import org.example.project.data.memory.categorySeed -//import org.example.project.presentation.designsystem.components.Chip -//import org.example.project.presentation.designsystem.textstyle.AppTheme -//import org.example.project.presentation.util.extension.toAnimatedColor -//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupCategoryState -//import org.example.project.presentation.viewmodel.accountSetup.AccountSetupState -//import org.jetbrains.compose.ui.tooling.preview.Preview -// -//@Composable -//fun CategoryActionBox( -// modifier: Modifier = Modifier, -// state: AccountSetupState, -// onChipSelected: (id: Int) -> Unit, -//) { -// Box(modifier = modifier.fillMaxWidth()) { -// FlowRow( -// horizontalArrangement = Arrangement.spacedBy(12.dp), -// verticalArrangement = Arrangement.spacedBy(12.dp) -// ) { -// -// state.categoryState.categories.forEach { category -> -// val isSelected = category.isSelected -// Chip( -// text = category.title, -// isSelected = isSelected, -// onChipSelected = { onChipSelected(category.id) }, -// modifier = Modifier.background( -// color = category.colorHex.toAnimatedColor( -// isSelected, -// AppTheme.craftoColors.background.card -// ), -// shape = RoundedCornerShape(AppTheme.craftoRadius.full) -// ), -// textColor = AppTheme.craftoColors.background.card -// .toAnimatedColor( -// isSelected, -// AppTheme.craftoColors.shade.secondary -// ), -// borderColor = category.colorHex.toAnimatedColor( -// condition = isSelected, -// falseConditionColor = Color.Transparent, -// duration = 100 -// -// ) -// ) -// } -// } -// } -//} -// -//@Preview -//@Composable -//fun CategoryActionBoxLightPreview() { -// AppTheme { -// CategoryActionBox( -// state = AccountSetupState( -// categoryState = AccountSetupCategoryState( -// categories = categorySeed -// ) -// ), -// onChipSelected = {} -// ) -// -// } -//} -// -//@Preview -//@Composable -//fun CategoryActionBoxDarkPreview() { -// AppTheme(isDarkTheme = true) { -// CategoryActionBox( -// state = AccountSetupState( -// categoryState = AccountSetupCategoryState( -// categories = categorySeed -// ) -// ), -// onChipSelected = {} -// ) -// } -//} \ No newline at end of file