From 6edd98a268b2923f7055938e2ccf884f513aaa77 Mon Sep 17 00:00:00 2001 From: Wira Irawan Date: Thu, 21 May 2026 23:36:13 +0700 Subject: [PATCH] Initial mobile app implementation --- .gitignore | 8 + CODEX_HANDOFF.md | 98 + app/build.gradle.kts | 86 + app/proguard-rules.pro | 1 + app/src/main/AndroidManifest.xml | 25 + .../id/abelbirdnest/mobile/MainActivity.kt | 25 + .../mobile/data/MobileRepository.kt | 324 + .../id/abelbirdnest/mobile/data/Models.kt | 1152 +++ .../abelbirdnest/mobile/data/SessionStore.kt | 27 + .../abelbirdnest/mobile/network/ApiService.kt | 352 + .../abelbirdnest/mobile/ui/AbelbirdnestApp.kt | 7667 +++++++++++++++++ .../abelbirdnest/mobile/ui/MainViewModel.kt | 3231 +++++++ .../id/abelbirdnest/mobile/ui/theme/Color.kt | 21 + .../id/abelbirdnest/mobile/ui/theme/Theme.kt | 87 + .../id/abelbirdnest/mobile/ui/theme/Type.kt | 3 + .../main/res/drawable/logo_abelbirdnest.png | Bin 0 -> 123393 bytes app/src/main/res/values/strings.xml | 3 + build.gradle.kts | 5 + gradle.properties | 5 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 248 + gradlew.bat | 82 + logo_abelbirdnest.png | Bin 0 -> 123393 bytes mobile-api-blueprint.md | 268 + ...irdnest-mobile-api.postman_collection.json | 569 ++ ...nest-mobile-local.postman_environment.json | 93 + settings.gradle.kts | 18 + 28 files changed, 14407 insertions(+) create mode 100644 .gitignore create mode 100644 CODEX_HANDOFF.md create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/id/abelbirdnest/mobile/MainActivity.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/data/MobileRepository.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/data/Models.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/data/SessionStore.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/network/ApiService.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/ui/AbelbirdnestApp.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/ui/MainViewModel.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/ui/theme/Color.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/ui/theme/Theme.kt create mode 100644 app/src/main/java/id/abelbirdnest/mobile/ui/theme/Type.kt create mode 100755 app/src/main/res/drawable/logo_abelbirdnest.png create mode 100644 app/src/main/res/values/strings.xml create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100755 logo_abelbirdnest.png create mode 100644 mobile-api-blueprint.md create mode 100644 postman/abelbirdnest-mobile-api.postman_collection.json create mode 100644 postman/abelbirdnest-mobile-local.postman_environment.json create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f46c0f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle/ +.kotlin/ +.idea/ +build/ +app/build/ +local.properties +*.iml +.DS_Store diff --git a/CODEX_HANDOFF.md b/CODEX_HANDOFF.md new file mode 100644 index 0000000..0974297 --- /dev/null +++ b/CODEX_HANDOFF.md @@ -0,0 +1,98 @@ +# Codex Handoff + +## Project +- Native Android app +- Package: `id.abelbirdnest.mobile` +- App name: `Abelbirdnest Stock` +- Stack: Kotlin + Jetpack Compose +- Blueprint source of truth: [mobile-api-blueprint.md](./mobile-api-blueprint.md) + +## API / Environment +- Production base URL is used from Postman env. +- Active API host used during testing: `https://abelbirdnest.id/api/v1` + +## Test Accounts +- `OWNER` -> `abel@zappcare.id` / `abel1234` +- `PURCHASING` -> `tini@zappcare.id` / `tini1234` +- `SALES` -> `budi@zappcare.id` / `budi1234` +- `QC` -> `edi@zappcare.id` / `edi12345` +- `WAREHOUSE` -> `jhon@zappcare.id` / `jhon1234` + +## Implemented Modules +- `dashboard` +- `lots` +- `washing` +- `stock_adjustments` +- `purchases` +- `purchase_analyses` +- `purchase_realizations` +- `fund_requests` +- `sales_regular` +- `sales_jit` +- `consignments` +- `lot_transformations` + +## Hidden / Removed From Mobile Navigation +- `receipts` + - intentionally hidden because newer server/mobile flow no longer uses it for active role navigation + +## Important Behavior +- App is server-driven from `mobile/bootstrap`, but module ordering and UI prioritization were aligned to the blueprint where needed. +- `PURCHASING` login issue was fixed by not forcing `/mobile/lots` fetch for roles that do not have `lots`. +- Quick actions now only show modules that are not already present in the bottom bar. +- For `OWNER`, quick actions were reduced to match actual mobile need. Extra actions like `dana`, `sales`, `jit`, `titip jual`, and `washing` were removed from quick actions. + +## Recent UI Changes + +### Login +- Brand text changed to `Abelbirdnest` +- Centered vertically +- `Lupa Kata Sandi?` restored +- Removed extra elements below the main login CTA except forgot password + +### Lots +- Lot detail top gap was reduced by: + - removing extra spacer + - disabling nested scaffold content insets in detail +- Lot detail footer actions were removed entirely: + - `Print Ulang` + - `Pindah Lokasi` + - `Ubah Status` + - reason: no real mobile action flow yet, avoid dead buttons + +### Scan Lot +- Scanner layout was reworked: + - `Tutup` moved outside the camera panel + - scanner panel wrapped as its own card/section + - manual input visually separated from camera section + - scanner corner markers fixed so their directions are correct + +### Washing +- `Lot` picker uses search / scan workflow +- `Tempat Cuci` changed to simple dropdown +- card action button `Selesaikan` forced to one line by reducing font and spacing + +## Known Gaps / Things Not Yet Wired +- Some modules are present as minimum mobile flows only, aligned to blueprint, not full web parity. +- `lot_transformations` still needs full create flow audit if expanded beyond current scope. +- Some modules are read-only where blueprint only required read paths. +- `pindah lokasi` and `ubah status` for lot detail are not implemented, and UI was intentionally removed. + +## Build / Deploy +- Android SDK path used locally: `/opt/android-sdk` +- Typical build command: + - `./gradlew assembleDebug` +- Typical install command: + - `adb install -r app/build/outputs/apk/debug/app-debug.apk` +- Last confirmed installed package timestamp during this session: + - `lastUpdateTime=2026-05-21 23:29:49` + +## Current Device +- Frequently used connected device: + - `CPH2781` (Oppo) + +## Notes For Next Codex +- Use `mobile-api-blueprint.md` as the primary spec. +- Use web project only as a secondary reference for payload shape or flow hints, not as the source of truth. +- Be careful with nested `Scaffold` padding/insets; several UI gaps came from double insets. +- Many server responses differ slightly between endpoints; avoid assuming detail response shape matches scan/list response shape. diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..af9ac30 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,86 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "id.abelbirdnest.mobile" + compileSdk = 35 + + defaultConfig { + applicationId = "id.abelbirdnest.mobile" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + compose = true + buildConfig = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2024.12.01") + + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.activity:activity-compose:1.10.1") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.squareup.retrofit2:converter-gson:2.11.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + implementation("androidx.camera:camera-camera2:1.4.1") + implementation("androidx.camera:camera-lifecycle:1.4.1") + implementation("androidx.camera:camera-view:1.4.1") + implementation("com.google.mlkit:barcode-scanning:17.3.0") + + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..eb4aef7 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +# Intentionally empty for now. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..30a1768 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/id/abelbirdnest/mobile/MainActivity.kt b/app/src/main/java/id/abelbirdnest/mobile/MainActivity.kt new file mode 100644 index 0000000..ce60248 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/MainActivity.kt @@ -0,0 +1,25 @@ +package id.abelbirdnest.mobile + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import id.abelbirdnest.mobile.ui.AbelbirdnestApp +import id.abelbirdnest.mobile.ui.MainViewModel +import id.abelbirdnest.mobile.ui.theme.AbelbirdnestTheme + +class MainActivity : ComponentActivity() { + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + AbelbirdnestTheme { + AbelbirdnestApp(viewModel = viewModel) + } + } + } +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/data/MobileRepository.kt b/app/src/main/java/id/abelbirdnest/mobile/data/MobileRepository.kt new file mode 100644 index 0000000..e6a220b --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/data/MobileRepository.kt @@ -0,0 +1,324 @@ +package id.abelbirdnest.mobile.data + +import android.content.Context +import id.abelbirdnest.mobile.data.toLotDetailData +import id.abelbirdnest.mobile.network.ApiFactory +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import com.google.gson.Gson +import retrofit2.HttpException + +class MobileRepository(context: Context) { + private val api = ApiFactory.create() + private val sessionStore = SessionStore(context) + private val gson = Gson() + + fun sessionToken(): String? = sessionStore.token() + + suspend fun login(identity: String, password: String): LoginData { + val response = api.login(LoginRequest(identity = identity, password = password)) + sessionStore.save(response.data) + return response.data + } + + suspend fun loadDashboardBundle(): DashboardBundle = coroutineScope { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + val auth = "Bearer $token" + + val bootstrap = async { api.bootstrap(auth).data } + val dashboard = async { api.dashboard(auth).data } + + val bootstrapData = bootstrap.await() + val dashboardData = dashboard.await() + val lotItems = if (bootstrapData.modules.contains("lots")) { + try { + api.lots(auth).data + } catch (error: HttpException) { + if (error.code() == 403) emptyList() else throw error + } + } else { + emptyList() + } + + DashboardBundle( + user = bootstrapData.user, + modules = bootstrapData.modules, + summary = bootstrapData.summary, + metrics = dashboardData.metrics, + criticalLots = dashboardData.criticalLots, + recentActivity = dashboardData.recentActivity, + gradeDistribution = lotItems + .filter { it.availableQty > 0 } + .groupBy { it.grade } + .mapValues { entry -> entry.value.sumOf { it.availableQty } } + .entries + .sortedByDescending { it.value } + .take(3) + .let { topGrades -> + val total = topGrades.sumOf { it.value }.takeIf { it > 0 } ?: 1.0 + topGrades.map { grade -> + GradeDistribution( + grade = grade.key, + quantity = grade.value, + percentage = ((grade.value / total) * 100).toInt(), + ) + } + }, + transformationTypes = bootstrapData.transformationTypes, + remainderModes = bootstrapData.remainderModes, + processingLossModes = bootstrapData.processingLossModes, + grades = bootstrapData.grades, + warehouses = bootstrapData.warehouses, + ) + } + + suspend fun loadLots(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.lots("Bearer $token").data + } + + suspend fun loadLotDetail(id: String): LotDetailData { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.lotDetail("Bearer $token", id).data.toLotDetailData() + } + + suspend fun scanLot(code: String): LotScanResult { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return LotScanResult( + scannedCode = code, + scannedAtMillis = System.currentTimeMillis(), + payload = api.scanLot("Bearer $token", code).data, + ) + } + + suspend fun loadAdjustmentReasons(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.adjustmentReasons("Bearer $token").data + } + + suspend fun loadStockAdjustments(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.stockAdjustments("Bearer $token").data + } + + suspend fun createStockAdjustment(payload: StockAdjustmentCreatePayload): StockAdjustmentListItem { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.createStockAdjustment("Bearer $token", payload).data + } + + suspend fun loadPurchases(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.purchases("Bearer $token").data + } + + suspend fun loadUnits(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.units("Bearer $token").data + } + + suspend fun loadEmployees(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.employees("Bearer $token").data + } + + suspend fun createPurchase(payload: PurchaseCreatePayload): PurchaseDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.createPurchase("Bearer $token", payload).data + } + + suspend fun loadPurchaseDetail(id: String): PurchaseDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.purchaseDetail("Bearer $token", id).data + } + + suspend fun updatePurchase(id: String, payload: PurchaseCreatePayload): PurchaseDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.updatePurchase("Bearer $token", id, payload).data + } + + suspend fun loadPurchaseAnalyses(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.purchaseAnalyses("Bearer $token").data + } + + suspend fun loadPurchaseAnalysisDetail(purchaseId: String): PurchaseAnalysisDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.purchaseAnalysisDetail("Bearer $token", purchaseId).data + } + + suspend fun loadPurchaseRealizations(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.purchaseRealizations("Bearer $token").data + } + + suspend fun loadPurchaseRealizationDetail(purchaseId: String): PurchaseRealizationDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.purchaseRealizationDetail("Bearer $token", purchaseId).data + } + + suspend fun loadRegularSales(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.regularSales("Bearer $token").data + } + + suspend fun loadRegularSaleDetail(id: String): RegularSaleDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.regularSaleDetail("Bearer $token", id).data + } + + suspend fun closeRegularSale(id: String, payload: RegularSaleClosePayload): RegularSaleDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.closeRegularSale("Bearer $token", id, payload).data + } + + suspend fun loadJitSales(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.jitSales("Bearer $token").data + } + + suspend fun loadJitSaleDetail(id: String): JitSaleDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.jitSaleDetail("Bearer $token", id).data + } + + suspend fun closeJitSale(id: String, payload: JitSaleClosePayload): JitSaleDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.closeJitSale("Bearer $token", id, payload).data + } + + suspend fun loadConsignmentsBootstrap(): ConsignmentBootstrapData { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.consignmentsBootstrap("Bearer $token").data + } + + suspend fun loadConsignments(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.consignments("Bearer $token").data + } + + suspend fun loadConsignmentDetail(id: String): ConsignmentDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.consignmentDetail("Bearer $token", id).data + } + + suspend fun closeConsignmentLine(lineId: String, payload: ConsignmentCloseLinePayload) { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + api.closeConsignmentLine("Bearer $token", lineId, payload) + } + + suspend fun loadFundRequestsBootstrap(): FundRequestsBootstrapData { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.fundRequestsBootstrap("Bearer $token").data + } + + suspend fun loadFundRequests(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.fundRequests("Bearer $token").data + } + + suspend fun createFundRequest( + transferType: String, + referenceNo: String, + agentId: String, + agentBankAccountId: String, + companyBankAccountId: String, + amount: String, + transferredAt: String, + ): FundRequestListItem { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + val plain = "text/plain".toMediaType() + val fields = linkedMapOf( + "transfer_type" to transferType.toRequestBody(plain), + "reference_no" to referenceNo.toRequestBody(plain), + "agent_id" to agentId.toRequestBody(plain), + "agent_bank_account_id" to agentBankAccountId.toRequestBody(plain), + "company_bank_account_id" to companyBankAccountId.toRequestBody(plain), + "amount" to amount.toRequestBody(plain), + "transferred_at" to transferredAt.toRequestBody(plain), + ) + return api.createFundRequest("Bearer $token", fields).data + } + + suspend fun submitPurchase(id: String): PurchaseDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.submitPurchase("Bearer $token", id).data + } + + suspend fun cancelPurchase(id: String) { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + api.cancelPurchase("Bearer $token", id) + } + + suspend fun loadLotTransformations(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.lotTransformations("Bearer $token").data + } + + suspend fun loadLotTransformationDetail(id: String): LotTransformationDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.lotTransformationDetail("Bearer $token", id).data + } + + suspend fun createLotTransformation(payload: LotTransformationCreatePayload): LotTransformationDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.createLotTransformation("Bearer $token", payload).data + } + + suspend fun loadWashingBootstrap(): WashingBootstrapData { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.washingBootstrap("Bearer $token").data + } + + suspend fun loadWashings(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.washings("Bearer $token").data + } + + suspend fun createWashing(payload: WashingCreatePayload): WashingListItem { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + val body = gson.toJson(payload).toRequestBody("text/plain".toMediaType()) + return api.createWashing("Bearer $token", body).data + } + + suspend fun updateWashing(id: String, payload: WashingCreatePayload): WashingListItem { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + val body = gson.toJson(payload).toRequestBody("text/plain".toMediaType()) + return api.updateWashing("Bearer $token", id, body).data + } + + suspend fun completeWashing(id: String, payload: CompleteWashingPayload): WashingListItem { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.completeWashing("Bearer $token", id, payload).data + } + + suspend fun loadReceiptsBootstrap(): ReceiptBootstrapData { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.receiptsBootstrap("Bearer $token").data + } + + suspend fun loadReceipts(): List { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.receipts("Bearer $token").data + } + + suspend fun loadReceiptDetail(id: String): ReceiptDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.receiptDetail("Bearer $token", id).data + } + + suspend fun createReceipt(payload: ReceiptCreatePayload): ReceiptDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.createReceipt("Bearer $token", payload).data + } + + suspend fun generateReceiptLots(id: String): ReceiptDetail { + val token = sessionStore.token() ?: error("Sesi tidak tersedia") + return api.generateReceiptLots("Bearer $token", id).data + } + + fun logout() { + sessionStore.clear() + } +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/data/Models.kt b/app/src/main/java/id/abelbirdnest/mobile/data/Models.kt new file mode 100644 index 0000000..8b58284 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/data/Models.kt @@ -0,0 +1,1152 @@ +package id.abelbirdnest.mobile.data + +import com.google.gson.annotations.SerializedName + +data class ApiEnvelope( + val data: T, +) + +data class LoginResponse( + val message: String, + val data: LoginData, +) + +data class LoginData( + val user: User, + @SerializedName("redirect_to") val redirectTo: String?, + @SerializedName("session_token") val sessionToken: String, + @SerializedName("token_type") val tokenType: String?, + @SerializedName("expires_in") val expiresIn: Int?, +) + +data class User( + val id: String, + val name: String, + val role: String, + val email: String?, + val username: String?, +) + +data class BootstrapData( + val user: User, + val modules: List = emptyList(), + val summary: Summary = Summary(), + @SerializedName("transformation_types") val transformationTypes: List = emptyList(), + @SerializedName("remainder_modes") val remainderModes: List = emptyList(), + @SerializedName("processing_loss_modes") val processingLossModes: List = emptyList(), + val grades: List = emptyList(), + val warehouses: List = emptyList(), +) + +data class Summary( + @SerializedName("active_lot_count") val activeLotCount: Int = 0, + @SerializedName("draft_purchase_count") val draftPurchaseCount: Int = 0, + @SerializedName("submitted_purchase_count") val submittedPurchaseCount: Int = 0, + @SerializedName("open_regular_sale_count") val openRegularSaleCount: Int = 0, + @SerializedName("open_consignment_line_count") val openConsignmentLineCount: Int = 0, + @SerializedName("in_progress_washing_count") val inProgressWashingCount: Int = 0, +) + +data class DashboardData( + val metrics: List = emptyList(), + @SerializedName("criticalLots") val criticalLots: List = emptyList(), + @SerializedName("recentActivity") val recentActivity: List = emptyList(), +) + +data class DashboardMetric( + val label: String, + val value: String, + val delta: String, +) + +data class CriticalLot( + val id: String, + @SerializedName("lotCode") val lotCode: String, + val supplier: String, + val item: String, + @SerializedName("availableQty") val availableQty: String, + @SerializedName("attentionStatus") val attentionStatus: String, + val reason: String, +) + +data class RecentActivity( + val id: String, + val summary: String, + @SerializedName("occurredAt") val occurredAt: String, +) + +data class LotItem( + val id: String, + @SerializedName("lot_code") val lotCode: String, + val supplier: String, + val grade: String, + @SerializedName("original_qty") val originalQty: Double, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("unit_code") val unitCode: String, + val warehouse: String, + val location: String, + @SerializedName("aging_days") val agingDays: Int, + val status: String, +) + +data class LotDetailEnvelope( + val data: LotDetailData, +) + +data class LotDetailData( + @SerializedName("summary_card") val summaryCard: LotSummaryCard?, + val lot: LotDetail, + val procurement: ProcurementInfo?, + @SerializedName("mobile_actions") val mobileActions: MobileActions?, +) + +data class LotDetailResponse( + @SerializedName("summary_card") val summaryCard: LotSummaryCard?, + val id: String, + @SerializedName("lot_code") val lotCode: String, + @SerializedName("source_type") val sourceType: String?, + val supplier: NamedEntity?, + val grade: NamedEntity?, + @SerializedName("original_qty") val originalQty: Double, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("reserved_qty") val reservedQty: Double, + @SerializedName("damaged_qty") val damagedQty: Double, + @SerializedName("shrinkage_qty") val shrinkageQty: Double, + @SerializedName("unit_code") val unitCode: String?, + @SerializedName("unit_cost") val unitCost: Double, + val warehouse: NamedEntity?, + val location: NamedEntity?, + val status: String, + @SerializedName("parent_lot") val parentLot: ParentLot?, + @SerializedName("child_lots") val childLots: List = emptyList(), + val labels: List = emptyList(), + val purchase: PurchaseInfo?, + val receipt: ReceiptInfo?, + @SerializedName("received_at") val receivedAt: String?, + val procurement: ProcurementInfo? = null, + @SerializedName("mobile_actions") val mobileActions: MobileActions? = null, +) + +data class LotSummaryCard( + @SerializedName("lot_code") val lotCode: String, + @SerializedName("source_type") val sourceType: String?, + val status: String, + val grade: String?, + @SerializedName("supplier_name") val supplierName: String?, + @SerializedName("warehouse_name") val warehouseName: String?, + @SerializedName("warehouse_location_name") val warehouseLocationName: String?, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("original_qty") val originalQty: Double, + @SerializedName("reserved_qty") val reservedQty: Double, + @SerializedName("damaged_qty") val damagedQty: Double, + @SerializedName("shrinkage_qty") val shrinkageQty: Double, + @SerializedName("unit_code") val unitCode: String?, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("estimated_value") val estimatedValue: Double, + @SerializedName("purchase_no") val purchaseNo: String?, + @SerializedName("purchase_date") val purchaseDate: String?, + @SerializedName("receipt_no") val receiptNo: String?, + @SerializedName("receipt_date") val receiptDate: String?, + @SerializedName("received_at") val receivedAt: String?, + @SerializedName("qr_code_value") val qrCodeValue: String?, + @SerializedName("barcode_value") val barcodeValue: String?, + @SerializedName("parent_lot_code") val parentLotCode: String?, + @SerializedName("child_lot_count") val childLotCount: Int, + @SerializedName("transformation_count") val transformationCount: Int, +) + +data class LotDetail( + val id: String, + @SerializedName("lot_code") val lotCode: String, + @SerializedName("source_type") val sourceType: String?, + val supplier: NamedEntity?, + val grade: NamedEntity?, + @SerializedName("original_qty") val originalQty: Double, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("reserved_qty") val reservedQty: Double, + @SerializedName("damaged_qty") val damagedQty: Double, + @SerializedName("shrinkage_qty") val shrinkageQty: Double, + @SerializedName("unit_code") val unitCode: String?, + @SerializedName("unit_cost") val unitCost: Double, + val warehouse: NamedEntity?, + val location: NamedEntity?, + val status: String, + @SerializedName("parent_lot") val parentLot: ParentLot?, + @SerializedName("child_lots") val childLots: List = emptyList(), + val labels: List = emptyList(), + val purchase: PurchaseInfo?, + val receipt: ReceiptInfo?, + @SerializedName("received_at") val receivedAt: String?, +) + +data class NamedEntity( + val id: String?, + val name: String?, +) + +data class ParentLot( + val id: String?, + @SerializedName("lot_code") val lotCode: String?, +) + +data class LotLabel( + val type: String, + val value: String, +) + +data class PurchaseInfo( + val id: String?, + @SerializedName("purchase_no") val purchaseNo: String?, + @SerializedName("purchase_date") val purchaseDate: String?, +) + +data class ReceiptInfo( + val id: String?, + @SerializedName("receipt_no") val receiptNo: String?, + @SerializedName("receipt_date") val receiptDate: String?, +) + +data class ProcurementInfo( + @SerializedName("supplier_name") val supplierName: String?, + @SerializedName("purchase_no") val purchaseNo: String?, + @SerializedName("purchase_date") val purchaseDate: String?, + @SerializedName("receipt_no") val receiptNo: String?, + @SerializedName("receipt_date") val receiptDate: String?, + @SerializedName("received_at") val receivedAt: String?, + @SerializedName("source_type") val sourceType: String?, +) + +data class MobileActions( + @SerializedName("can_mix") val canMix: Boolean = false, + @SerializedName("can_regrade") val canRegrade: Boolean = false, + @SerializedName("can_adjust") val canAdjust: Boolean = false, +) + +data class LotScanPayload( + @SerializedName("summary_card") val summaryCard: LotSummaryCard, + val lot: LotDetail, + val procurement: ProcurementInfo?, + @SerializedName("mobile_actions") val mobileActions: MobileActions?, +) + +data class LotScanResult( + val scannedCode: String, + val scannedAtMillis: Long, + val payload: LotScanPayload, +) + +data class LoginRequest( + val identity: String, + val password: String, +) + +data class DashboardBundle( + val user: User, + val modules: List, + val summary: Summary, + val metrics: List, + val criticalLots: List, + val recentActivity: List, + val gradeDistribution: List, + val transformationTypes: List, + val remainderModes: List, + val processingLossModes: List, + val grades: List, + val warehouses: List, +) + +data class GradeDistribution( + val grade: String, + val quantity: Double, + val percentage: Int, +) + +data class QuickAction( + val module: String, + val label: String, + val iconName: String, +) + +data class AdjustmentReason( + val id: String, + val code: String, + val name: String, + val category: String, + val status: String? = null, +) + +data class StockAdjustmentListItem( + val id: String, + @SerializedName("adjustment_no") val adjustmentNo: String, + @SerializedName("adjustment_date") val adjustmentDate: String, + val lot: StockAdjustmentLotRef, + val reason: AdjustmentReason, + @SerializedName("qty_change") val qtyChange: Double, + @SerializedName("available_qty_before") val availableQtyBefore: Double, + @SerializedName("available_qty_after") val availableQtyAfter: Double, + @SerializedName("created_by") val createdBy: StockAdjustmentCreator, + val notes: String?, +) + +data class StockAdjustmentLotRef( + val id: String, + @SerializedName("lot_code") val lotCode: String, + val grade: String, + @SerializedName("unit_code") val unitCode: String, +) + +data class StockAdjustmentCreator( + val id: String, + val name: String, + val role: String, +) + +data class StockAdjustmentCreatePayload( + @SerializedName("lot_id") val lotId: String, + @SerializedName("adjustment_reason_id") val adjustmentReasonId: String, + @SerializedName("adjustment_date") val adjustmentDate: String, + @SerializedName("qty_change") val qtyChange: Double, + val notes: String?, +) + +data class PurchaseListItem( + val id: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("received_at") val receivedAt: String?, + val agent: NamedEntity?, + val status: String, + @SerializedName("line_count") val lineCount: Int, + @SerializedName("grand_total") val grandTotal: Double, +) + +data class PurchaseDetail( + val id: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("supplier_invoice_no") val supplierInvoiceNo: String?, + val status: String, + val notes: String?, + @SerializedName("received_at") val receivedAt: String?, + @SerializedName("received_by_employee") val receivedByEmployee: NamedEntity?, + val agent: NamedEntity?, + @SerializedName("profit_share_scheme") val profitShareScheme: NamedEntity?, + val courier: NamedEntity?, + @SerializedName("moisture_buy_percent") val moistureBuyPercent: Double?, + @SerializedName("moisture_received_percent") val moistureReceivedPercent: Double?, + @SerializedName("above_average_ratio_percent") val aboveAverageRatioPercent: Double?, + @SerializedName("mk_share_percent") val mkSharePercent: Double?, + @SerializedName("non_mk_share_percent") val nonMkSharePercent: Double?, + @SerializedName("shipping_cost") val shippingCost: Double?, + @SerializedName("incoming_operational_cost") val incomingOperationalCost: Double?, + @SerializedName("after_arrival_operational_cost") val afterArrivalOperationalCost: Double?, + val analysis: PurchaseAnalysisSnapshot?, + val lines: List = emptyList(), +) + +data class PurchaseAnalysisSnapshot( + val id: String, + val status: String, + @SerializedName("weight_buy") val weightBuy: Double?, + @SerializedName("weight_received") val weightReceived: Double?, + @SerializedName("weight_final") val weightFinal: Double?, + @SerializedName("average_price") val averagePrice: Double?, + @SerializedName("modal_beli") val modalBeli: Double?, + @SerializedName("modal_masuk") val modalMasuk: Double?, + @SerializedName("modal_jual") val modalJual: Double?, + @SerializedName("modal_barang") val modalBarang: Double?, + @SerializedName("total_modal_beli") val totalModalBeli: Double?, + @SerializedName("total_modal_mal") val totalModalMal: Double?, + @SerializedName("market_reference_price") val marketReferencePrice: Double?, + @SerializedName("cost_lines") val costLines: List = emptyList(), +) + +data class PurchaseCostLine( + val id: String, + @SerializedName("cost_name") val costName: String, + val amount: Double, + val notes: String?, + @SerializedName("proof_file_url") val proofFileUrl: String?, +) + +data class PurchaseLineDetail( + val id: String, + val grade: NamedEntity?, + @SerializedName("qty_ordered") val qtyOrdered: Double, + @SerializedName("qty_received") val qtyReceived: Double, + @SerializedName("qty_accepted") val qtyAccepted: Double, + @SerializedName("qty_rejected") val qtyRejected: Double, + @SerializedName("purchase_moisture_percent") val purchaseMoisturePercent: Double?, + @SerializedName("moisture_received_percent") val moistureReceivedPercent: Double?, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("mal_unit_price") val malUnitPrice: Double?, + val unit: UnitLookup, + @SerializedName("unit_price") val unitPrice: Double, + val subtotal: Double, + @SerializedName("classification_status") val classificationStatus: String, + val warehouse: WarehouseLookup?, + @SerializedName("warehouse_location") val warehouseLocation: WarehouseLocationLookup?, + val notes: String?, +) + +data class PurchaseCreatePayload( + @SerializedName("received_by_employee_id") val receivedByEmployeeId: String, + @SerializedName("received_at") val receivedAt: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("warehouse_id") val warehouseId: String?, + @SerializedName("warehouse_location_id") val warehouseLocationId: String?, + val notes: String?, + val lines: List, +) + +data class PurchaseCreateLinePayload( + @SerializedName("grade_id") val gradeId: String?, + @SerializedName("qty_ordered") val qtyOrdered: Double, + @SerializedName("qty_received") val qtyReceived: Double, + @SerializedName("qty_accepted") val qtyAccepted: Double, + @SerializedName("qty_rejected") val qtyRejected: Double, + @SerializedName("unit_id") val unitId: String, + @SerializedName("unit_price") val unitPrice: Double, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("classification_status") val classificationStatus: String, + @SerializedName("warehouse_id") val warehouseId: String?, + @SerializedName("warehouse_location_id") val warehouseLocationId: String?, + val notes: String?, +) + +data class PurchaseAnalysisListItem( + @SerializedName("purchase_id") val purchaseId: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("supplier_name") val supplierName: String, + @SerializedName("purchase_status") val purchaseStatus: String, + @SerializedName("analysis_status") val analysisStatus: String, + @SerializedName("weight_buy") val weightBuy: Double?, + @SerializedName("weight_received") val weightReceived: Double?, + @SerializedName("weight_final") val weightFinal: Double?, + @SerializedName("total_modal_beli") val totalModalBeli: Double, + @SerializedName("total_modal_mal") val totalModalMal: Double?, + @SerializedName("total_laba_rugi") val totalLabaRugi: Double?, +) + +data class PurchaseAnalysisDetail( + val purchase: PurchaseAnalysisPurchaseRef, + val actuals: PurchaseAnalysisActuals, + val inputs: PurchaseAnalysisInputs, + val summary: PurchaseAnalysisSummary, +) + +data class PurchaseAnalysisPurchaseRef( + val id: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("supplier_name") val supplierName: String, + val status: String, + @SerializedName("grand_total") val grandTotal: Double, + @SerializedName("line_count") val lineCount: Int, +) + +data class PurchaseAnalysisActuals( + @SerializedName("weight_buy") val weightBuy: Double, + @SerializedName("weight_received") val weightReceived: Double, + @SerializedName("weight_final") val weightFinal: Double, + @SerializedName("moisture_buy_percent") val moistureBuyPercent: Double?, + @SerializedName("moisture_received_percent") val moistureReceivedPercent: Double?, + @SerializedName("moisture_final_percent") val moistureFinalPercent: Double?, + @SerializedName("above_average_ratio_percent") val aboveAverageRatioPercent: Double?, + @SerializedName("market_reference_price") val marketReferencePrice: Double?, +) + +data class PurchaseAnalysisInputs( + val status: String, + @SerializedName("weight_buy") val weightBuy: Double?, + @SerializedName("weight_received") val weightReceived: Double?, + @SerializedName("weight_final") val weightFinal: Double?, + @SerializedName("moisture_buy_percent") val moistureBuyPercent: Double?, + @SerializedName("moisture_received_percent") val moistureReceivedPercent: Double?, + @SerializedName("moisture_final_percent") val moistureFinalPercent: Double?, + @SerializedName("above_average_ratio_percent") val aboveAverageRatioPercent: Double?, + @SerializedName("average_price") val averagePrice: Double?, + @SerializedName("modal_beli") val modalBeli: Double?, + @SerializedName("modal_masuk") val modalMasuk: Double?, + @SerializedName("modal_jual") val modalJual: Double?, + @SerializedName("modal_barang") val modalBarang: Double?, + @SerializedName("total_modal_beli") val totalModalBeli: Double?, + @SerializedName("total_modal_mal") val totalModalMal: Double?, + @SerializedName("market_reference_price") val marketReferencePrice: Double?, + @SerializedName("market_valuation_total") val marketValuationTotal: Double?, + @SerializedName("agent_profit_share_total") val agentProfitShareTotal: Double, + val notes: String?, + @SerializedName("cost_entries") val costEntries: List = emptyList(), +) + +data class PurchaseAnalysisCostEntry( + val id: String, + @SerializedName("cost_type") val costType: String, + val description: String?, + val amount: Double, +) + +data class PurchaseAnalysisSummary( + @SerializedName("berat_beli") val beratBeli: Double?, + @SerializedName("berat_masuk") val beratMasuk: Double?, + @SerializedName("berat_akhir") val beratAkhir: Double?, + @SerializedName("berat_naik_percent") val beratNaikPercent: Double?, + @SerializedName("susut_tambah") val susutTambah: Double?, + @SerializedName("kadar_beli_percent") val kadarBeliPercent: Double?, + @SerializedName("kadar_masuk_percent") val kadarMasukPercent: Double?, + @SerializedName("kadar_akhir_percent") val kadarAkhirPercent: Double?, + @SerializedName("barang_atas_rata2_percent") val barangAtasRata2Percent: Double?, + @SerializedName("mk_percent") val mkPercent: Double?, + @SerializedName("non_mk_percent") val nonMkPercent: Double?, + @SerializedName("harga_mk_ar") val hargaMkAr: Double?, + @SerializedName("harga_rata2") val hargaRata2: Double?, + @SerializedName("modal_beli") val modalBeli: Double?, + @SerializedName("modal_masuk") val modalMasuk: Double?, + @SerializedName("modal_jual") val modalJual: Double?, + @SerializedName("modal_barang") val modalBarang: Double, + val operasional: Double, + @SerializedName("total_modal_beli") val totalModalBeli: Double, + @SerializedName("total_modal_mal") val totalModalMal: Double?, + @SerializedName("total_laba_rugi") val totalLabaRugi: Double?, + @SerializedName("laba_rugi_agen") val labaRugiAgen: Double, + @SerializedName("laba_total_per_kg") val labaTotalPerKg: Double?, + @SerializedName("laba_agen_per_kg") val labaAgenPerKg: Double?, +) + +data class PurchaseRealizationListItem( + @SerializedName("purchase_id") val purchaseId: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("purchase_type") val purchaseType: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("supplier_name") val supplierName: String, + val status: String, + @SerializedName("qty_opening") val qtyOpening: Double, + @SerializedName("qty_remaining") val qtyRemaining: Double, + @SerializedName("qty_sold") val qtySold: Double, + @SerializedName("qty_shrinkage") val qtyShrinkage: Double, + @SerializedName("revenue_total") val revenueTotal: Double, + @SerializedName("profit_loss_total") val profitLossTotal: Double, + @SerializedName("agent_profit_total") val agentProfitTotal: Double, +) + +data class PurchaseRealizationDetail( + val purchase: PurchaseRealizationPurchaseRef, + val summary: PurchaseRealizationSummary, + val entries: List = emptyList(), +) + +data class PurchaseRealizationPurchaseRef( + val id: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("purchase_type") val purchaseType: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("supplier_name") val supplierName: String, + val status: String, + @SerializedName("agent_name") val agentName: String?, + @SerializedName("buyout_source_agent_name") val buyoutSourceAgentName: String?, + @SerializedName("profit_share_scheme_name") val profitShareSchemeName: String?, + @SerializedName("share_agent_percent") val shareAgentPercent: Double?, +) + +data class PurchaseRealizationSummary( + val status: String, + @SerializedName("qty_opening") val qtyOpening: Double, + @SerializedName("qty_remaining") val qtyRemaining: Double, + @SerializedName("qty_sold") val qtySold: Double, + @SerializedName("qty_returned") val qtyReturned: Double, + @SerializedName("qty_shrinkage") val qtyShrinkage: Double, + @SerializedName("cost_opening_total") val costOpeningTotal: Double, + @SerializedName("cost_additional_total") val costAdditionalTotal: Double, + @SerializedName("revenue_total") val revenueTotal: Double, + @SerializedName("profit_loss_total") val profitLossTotal: Double, + @SerializedName("agent_share_percent") val agentSharePercent: Double?, + @SerializedName("agent_profit_total") val agentProfitTotal: Double, + @SerializedName("closed_at") val closedAt: String?, +) + +data class PurchaseRealizationEntryItem( + val id: String, + @SerializedName("event_type") val eventType: String, + @SerializedName("reference_type") val referenceType: String, + @SerializedName("reference_id") val referenceId: String?, + @SerializedName("lot_id") val lotId: String?, + @SerializedName("lot_code") val lotCode: String?, + @SerializedName("occurred_at") val occurredAt: String, + @SerializedName("qty_in") val qtyIn: Double, + @SerializedName("qty_out") val qtyOut: Double, + @SerializedName("qty_shrinkage") val qtyShrinkage: Double, + @SerializedName("amount_cost") val amountCost: Double, + @SerializedName("amount_revenue") val amountRevenue: Double, + @SerializedName("amount_expense") val amountExpense: Double, + @SerializedName("amount_profit") val amountProfit: Double, + @SerializedName("agent_amount") val agentAmount: Double, + val notes: String?, +) + +data class RegularSalePartyRef( + val id: String, + val code: String, + val name: String, +) + +data class RegularSaleListItem( + val id: String, + @SerializedName("sale_no") val saleNo: String, + @SerializedName("sale_date") val saleDate: String, + val buyer: RegularSalePartyRef, + @SerializedName("buyer_currency_code") val buyerCurrencyCode: String, + @SerializedName("company_currency_code") val companyCurrencyCode: String, + @SerializedName("exchange_rate") val exchangeRate: Double?, + val status: String, + @SerializedName("item_count") val itemCount: Int, + @SerializedName("total_nominal_buyer") val totalNominalBuyer: Double, + @SerializedName("total_nominal_company") val totalNominalCompany: Double, + @SerializedName("total_agent_commission") val totalAgentCommission: Double, +) + +data class RegularSaleDetail( + val id: String, + @SerializedName("sale_no") val saleNo: String, + @SerializedName("sale_date") val saleDate: String, + @SerializedName("close_date") val closeDate: String?, + val buyer: RegularSalePartyRef, + val courier: RegularSalePartyRef?, + @SerializedName("buyer_currency_code") val buyerCurrencyCode: String, + @SerializedName("company_currency_code") val companyCurrencyCode: String, + @SerializedName("exchange_rate") val exchangeRate: Double?, + @SerializedName("shipping_cost_buyer") val shippingCostBuyer: Double, + @SerializedName("shipping_cost_company") val shippingCostCompany: Double, + @SerializedName("shipping_receipt_file_url") val shippingReceiptFileUrl: String?, + @SerializedName("total_nominal_buyer") val totalNominalBuyer: Double, + @SerializedName("total_nominal_company") val totalNominalCompany: Double, + @SerializedName("total_agent_commission") val totalAgentCommission: Double, + val status: String, + val notes: String?, + @SerializedName("created_at") val createdAt: String, + val lines: List = emptyList(), +) + +data class RegularSaleLineDetail( + val id: String, + @SerializedName("lot_id") val lotId: String, + @SerializedName("lot_code") val lotCode: String, + val grade: String, + @SerializedName("unit_code") val unitCode: String, + val warehouse: String, + val location: String?, + @SerializedName("available_qty_snapshot") val availableQtySnapshot: Double, + @SerializedName("current_available_qty") val currentAvailableQty: Double, + @SerializedName("mal_unit_price_snapshot") val malUnitPriceSnapshot: Double?, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("agent_name") val agentName: String?, + @SerializedName("agent_share_percent") val agentSharePercent: Double?, + @SerializedName("qty_planned") val qtyPlanned: Double, + @SerializedName("selling_price_planned") val sellingPricePlanned: Double, + @SerializedName("qty_actual_sold") val qtyActualSold: Double?, + @SerializedName("qty_returned") val qtyReturned: Double?, + @SerializedName("qty_shrinkage") val qtyShrinkage: Double?, + @SerializedName("selling_price_actual") val sellingPriceActual: Double?, + val notes: String?, +) + +data class RegularSaleClosePayload( + @SerializedName("close_date") val closeDate: String, + val lines: List, +) + +data class RegularSaleCloseLinePayload( + @SerializedName("line_id") val lineId: String, + @SerializedName("qty_actual_sold") val qtyActualSold: Double, + @SerializedName("qty_returned") val qtyReturned: Double, + @SerializedName("selling_price_actual") val sellingPriceActual: Double, +) + +data class JitSaleGradeRef( + val id: String, + val code: String, + val name: String, +) + +data class JitSaleListItem( + val id: String, + @SerializedName("sale_no") val saleNo: String, + @SerializedName("sale_date") val saleDate: String, + val buyer: RegularSalePartyRef, + @SerializedName("buyer_currency_code") val buyerCurrencyCode: String, + @SerializedName("company_currency_code") val companyCurrencyCode: String, + @SerializedName("exchange_rate") val exchangeRate: Double?, + val status: String, + @SerializedName("item_count") val itemCount: Int, + @SerializedName("total_nominal_buyer") val totalNominalBuyer: Double, + @SerializedName("total_nominal_company") val totalNominalCompany: Double, + @SerializedName("total_agent_commission") val totalAgentCommission: Double, +) + +data class JitSaleDetail( + val id: String, + @SerializedName("sale_no") val saleNo: String, + @SerializedName("sale_date") val saleDate: String, + @SerializedName("close_date") val closeDate: String?, + val buyer: RegularSalePartyRef, + val courier: RegularSalePartyRef?, + @SerializedName("buyer_currency_code") val buyerCurrencyCode: String, + @SerializedName("company_currency_code") val companyCurrencyCode: String, + @SerializedName("exchange_rate") val exchangeRate: Double?, + @SerializedName("shipping_cost_buyer") val shippingCostBuyer: Double, + @SerializedName("shipping_cost_company") val shippingCostCompany: Double, + @SerializedName("shipping_receipt_file_url") val shippingReceiptFileUrl: String?, + @SerializedName("total_nominal_buyer") val totalNominalBuyer: Double, + @SerializedName("total_nominal_company") val totalNominalCompany: Double, + @SerializedName("total_agent_commission") val totalAgentCommission: Double, + val status: String, + val notes: String?, + @SerializedName("created_at") val createdAt: String, + val lines: List = emptyList(), +) + +data class JitSaleLineDetail( + val id: String, + val grade: JitSaleGradeRef, + @SerializedName("qty_planned") val qtyPlanned: Double, + @SerializedName("qty_actual_sold") val qtyActualSold: Double?, + @SerializedName("mal_unit_price") val malUnitPrice: Double, + @SerializedName("selling_price_planned") val sellingPricePlanned: Double, + @SerializedName("selling_price_actual") val sellingPriceActual: Double?, + val agent: RegularSalePartyRef?, + @SerializedName("profit_share_scheme") val profitShareScheme: RegularSalePartyRef?, + @SerializedName("agent_name") val agentName: String?, + @SerializedName("agent_share_percent") val agentSharePercent: Double?, + val notes: String?, +) + +data class JitSaleClosePayload( + @SerializedName("close_date") val closeDate: String, + val lines: List, +) + +data class JitSaleCloseLinePayload( + @SerializedName("line_id") val lineId: String, + @SerializedName("selling_price_actual") val sellingPriceActual: Double, +) + +data class ConsignmentCandidateLot( + @SerializedName("lot_id") val lotId: String, + @SerializedName("lot_code") val lotCode: String, + val grade: String, + @SerializedName("unit_code") val unitCode: String, + val warehouse: String, + val location: String?, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("mal_unit_price") val malUnitPrice: Double?, +) + +data class ConsignmentListItem( + val id: String, + @SerializedName("consignment_no") val consignmentNo: String, + @SerializedName("consignment_date") val consignmentDate: String, + val sales: RegularSalePartyRef, + val buyer: RegularSalePartyRef, + val status: String, + @SerializedName("item_count") val itemCount: Int, + @SerializedName("open_item_count") val openItemCount: Int, + @SerializedName("total_qty_consigned") val totalQtyConsigned: Double, + @SerializedName("total_qty_sold") val totalQtySold: Double, + @SerializedName("total_qty_returned") val totalQtyReturned: Double, + @SerializedName("total_qty_shrinkage") val totalQtyShrinkage: Double, +) + +data class ConsignmentDetail( + val id: String, + @SerializedName("consignment_no") val consignmentNo: String, + @SerializedName("consignment_date") val consignmentDate: String, + val sales: RegularSalePartyRef, + val buyer: RegularSalePartyRef, + val status: String, + val notes: String?, + @SerializedName("created_at") val createdAt: String, + val lines: List = emptyList(), +) + +data class ConsignmentLineDetail( + val id: String, + @SerializedName("lot_id") val lotId: String, + @SerializedName("lot_code") val lotCode: String, + val grade: String, + @SerializedName("unit_code") val unitCode: String, + val warehouse: String, + val location: String?, + @SerializedName("available_qty_snapshot") val availableQtySnapshot: Double, + @SerializedName("current_available_qty") val currentAvailableQty: Double, + @SerializedName("qty_consigned") val qtyConsigned: Double, + @SerializedName("mal_unit_price_snapshot") val malUnitPriceSnapshot: Double?, + @SerializedName("agent_name_snapshot") val agentNameSnapshot: String?, + @SerializedName("agent_share_percent") val agentSharePercent: Double?, + @SerializedName("unit_cost") val unitCost: Double, + val status: String, + val notes: String?, + @SerializedName("close_date") val closeDate: String?, + @SerializedName("selling_price") val sellingPrice: Double?, + @SerializedName("qty_sold") val qtySold: Double, + @SerializedName("qty_returned") val qtyReturned: Double, + @SerializedName("qty_shrinkage") val qtyShrinkage: Double, + @SerializedName("sales_commission") val salesCommission: Double, + @SerializedName("agent_commission") val agentCommission: Double, +) + +data class ConsignmentBootstrapData( + val sales: List = emptyList(), + val buyers: List = emptyList(), + val lots: List = emptyList(), +) + +data class ConsignmentCloseLinePayload( + @SerializedName("close_date") val closeDate: String, + @SerializedName("selling_price") val sellingPrice: Double, + @SerializedName("qty_sold") val qtySold: Double, + @SerializedName("qty_returned") val qtyReturned: Double, + @SerializedName("sales_commission") val salesCommission: Double, +) + +data class FundRequestBankAccountOption( + val id: String, + @SerializedName("bank_id") val bankId: String?, + @SerializedName("bank_code") val bankCode: String?, + @SerializedName("bank_name") val bankName: String, + @SerializedName("account_number") val accountNumber: String, +) + +data class FundRequestAgentOption( + val id: String, + val code: String, + val name: String, + @SerializedName("profit_share_balance") val profitShareBalance: Double = 0.0, + @SerializedName("capital_balance") val capitalBalance: Double = 0.0, + @SerializedName("bank_accounts") val bankAccounts: List = emptyList(), +) + +data class FundRequestsBootstrapData( + val agents: List = emptyList(), + @SerializedName("company_bank_name") val companyBankName: String?, + @SerializedName("company_bank_account_number") val companyBankAccountNumber: String?, + @SerializedName("company_bank_accounts") val companyBankAccounts: List = emptyList(), + @SerializedName("currency_code") val currencyCode: String = "IDR", +) + +data class FundRequestListItem( + val id: String, + @SerializedName("request_no") val requestNo: String, + @SerializedName("reference_no") val referenceNo: String, + @SerializedName("transfer_type") val transferType: String, + val agent: RegularSalePartyRef, + @SerializedName("agent_bank_account") val agentBankAccount: FundRequestBankAccountOption, + @SerializedName("company_bank_account") val companyBankAccount: FundRequestBankAccountOption?, + @SerializedName("company_bank_name") val companyBankName: String, + @SerializedName("company_bank_account_number") val companyBankAccountNumber: String, + val amount: Double, + @SerializedName("currency_code") val currencyCode: String, + @SerializedName("transferred_at") val transferredAt: String, + @SerializedName("transfer_proof_file_url") val transferProofFileUrl: String?, + val status: String, + @SerializedName("created_at") val createdAt: String, +) + +data class LotTransformationListItem( + val id: String, + @SerializedName("transformation_no") val transformationNo: String, + @SerializedName("transformation_type") val transformationType: String, + @SerializedName("transformation_date") val transformationDate: String, + val status: String, + @SerializedName("input_count") val inputCount: Int, + @SerializedName("output_count") val outputCount: Int, + @SerializedName("total_input_qty") val totalInputQty: Double, + @SerializedName("total_output_qty") val totalOutputQty: Double, +) + +data class LotTransformationDetail( + val id: String, + @SerializedName("transformation_no") val transformationNo: String, + @SerializedName("transformation_type") val transformationType: String, + @SerializedName("transformation_date") val transformationDate: String, + val status: String, + @SerializedName("remainder_mode") val remainderMode: String?, + @SerializedName("remainder_qty") val remainderQty: Double?, + @SerializedName("processing_loss_mode") val processingLossMode: String?, + @SerializedName("processing_loss_qty") val processingLossQty: Double?, + val notes: String?, + val inputs: List = emptyList(), + val outputs: List = emptyList(), +) + +data class LotTransformationCreatePayload( + @SerializedName("transformation_type") val transformationType: String, + @SerializedName("transformation_date") val transformationDate: String, + @SerializedName("remainder_mode") val remainderMode: String?, + @SerializedName("processing_loss_mode") val processingLossMode: String?, + val notes: String?, + val inputs: List, + val outputs: List, +) + +data class LotTransformationCreateInputPayload( + @SerializedName("source_lot_code") val sourceLotCode: String, + @SerializedName("qty_used") val qtyUsed: Double, + val notes: String?, +) + +data class LotTransformationCreateOutputPayload( + @SerializedName("grade_id") val gradeId: String, + @SerializedName("warehouse_id") val warehouseId: String, + @SerializedName("warehouse_location_id") val warehouseLocationId: String?, + @SerializedName("qty_produced") val qtyProduced: Double, + val notes: String?, +) + +data class LotTransformationInput( + val id: String, + @SerializedName("source_lot") val sourceLot: LotTransformationLotRef, + @SerializedName("qty_used") val qtyUsed: Double, + @SerializedName("unit_cost_snapshot") val unitCostSnapshot: Double, + val notes: String?, +) + +data class LotTransformationOutput( + val id: String, + @SerializedName("result_lot") val resultLot: LotTransformationResultLotRef, + @SerializedName("qty_produced") val qtyProduced: Double, + @SerializedName("unit_cost") val unitCost: Double, + val notes: String?, +) + +data class LotTransformationLotRef( + val id: String, + @SerializedName("lot_code") val lotCode: String, + val grade: String, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("unit_code") val unitCode: String, + val warehouse: String, + val location: String?, +) + +data class LotTransformationResultLotRef( + val id: String, + @SerializedName("lot_code") val lotCode: String, + val grade: String, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("unit_code") val unitCode: String, + val warehouse: String, + val location: String?, + val status: String, +) + +data class ReceiptBootstrapData( + val purchases: List = emptyList(), + val warehouses: List = emptyList(), +) + +data class ReceiptPurchaseOption( + val id: String, + @SerializedName("purchase_no") val purchaseNo: String, + @SerializedName("purchase_date") val purchaseDate: String, + @SerializedName("received_at") val receivedAt: String?, + @SerializedName("supplier_name") val supplierName: String?, + val notes: String?, + val lines: List = emptyList(), +) + +data class ReceiptPurchaseLineOption( + val id: String, + @SerializedName("purchase_line_id") val purchaseLineId: String, + val grade: LookupRecord?, + val unit: UnitLookup, + @SerializedName("qty_ordered") val qtyOrdered: Double, + @SerializedName("qty_received") val qtyReceived: Double, + @SerializedName("qty_accepted") val qtyAccepted: Double, + @SerializedName("qty_rejected") val qtyRejected: Double, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("warehouse_id") val warehouseId: String?, + @SerializedName("warehouse_location_id") val warehouseLocationId: String?, +) + +data class UnitLookup( + val id: String, + val code: String, + val name: String? = null, +) + +data class ReceiptListItem( + val id: String, + @SerializedName("receipt_no") val receiptNo: String, + val purchase: PurchaseInfo, + @SerializedName("receipt_date") val receiptDate: String, + val status: String, + @SerializedName("line_count") val lineCount: Int, + @SerializedName("lot_count") val lotCount: Int, +) + +data class ReceiptDetail( + val id: String, + @SerializedName("receipt_no") val receiptNo: String, + val purchase: PurchaseInfo, + @SerializedName("receipt_date") val receiptDate: String, + val status: String, + val notes: String?, + val lines: List = emptyList(), + @SerializedName("generated_lots") val generatedLots: List = emptyList(), +) + +data class ReceiptDetailLine( + val id: String, + @SerializedName("purchase_line_id") val purchaseLineId: String, + val grade: NamedEntity?, + @SerializedName("qty_received") val qtyReceived: Double, + @SerializedName("qty_accepted") val qtyAccepted: Double, + @SerializedName("qty_rejected") val qtyRejected: Double, + val unit: UnitLookup, + @SerializedName("unit_cost") val unitCost: Double, + val warehouse: NamedEntity?, + val location: NamedEntity?, + val notes: String?, +) + +data class ReceiptGeneratedLot( + val id: String, + @SerializedName("lot_code") val lotCode: String, + val status: String, +) + +data class ReceiptCreatePayload( + @SerializedName("purchase_id") val purchaseId: String, + @SerializedName("receipt_date") val receiptDate: String, + val notes: String?, + val lines: List, +) + +data class ReceiptCreateLinePayload( + @SerializedName("purchase_line_id") val purchaseLineId: String, + @SerializedName("grade_id") val gradeId: String?, + @SerializedName("qty_received") val qtyReceived: Double, + @SerializedName("qty_accepted") val qtyAccepted: Double, + @SerializedName("qty_rejected") val qtyRejected: Double, + @SerializedName("unit_id") val unitId: String, + @SerializedName("unit_cost") val unitCost: Double, + @SerializedName("warehouse_id") val warehouseId: String, + @SerializedName("warehouse_location_id") val warehouseLocationId: String?, + val notes: String?, +) + +data class WashingListItem( + val id: String, + @SerializedName("washing_no") val washingNo: String, + val status: String, + @SerializedName("started_at") val startedAt: String, + @SerializedName("expected_done_at") val expectedDoneAt: String, + @SerializedName("completed_at") val completedAt: String?, + @SerializedName("washing_cost") val washingCost: Double, + @SerializedName("duration_hours") val durationHours: Int, + @SerializedName("receipt_file_url") val receiptFileUrl: String?, + @SerializedName("before_qty") val beforeQty: Double, + @SerializedName("after_qty") val afterQty: Double?, + @SerializedName("shrinkage_qty") val shrinkageQty: Double?, + @SerializedName("before_grade_name") val beforeGradeName: String?, + @SerializedName("after_grade_name") val afterGradeName: String?, + @SerializedName("before_warehouse_name") val beforeWarehouseName: String, + @SerializedName("before_location_name") val beforeLocationName: String?, + @SerializedName("after_warehouse_name") val afterWarehouseName: String?, + @SerializedName("after_location_name") val afterLocationName: String?, + val lot: WashingLotRef, + @SerializedName("washing_place") val washingPlace: WashingPlaceRef, +) + +data class WashingLotRef( + val id: String, + @SerializedName("lot_code") val lotCode: String, + val supplier: String, + @SerializedName("grade_name") val gradeName: String?, + @SerializedName("available_qty") val availableQty: Double, + @SerializedName("unit_code") val unitCode: String, + @SerializedName("warehouse_name") val warehouseName: String, + @SerializedName("location_name") val locationName: String?, +) + +data class WashingPlaceRef( + val id: String, + val name: String, +) + +data class WashingBootstrapData( + @SerializedName("washing_places") val washingPlaces: List = emptyList(), + val grades: List = emptyList(), + val warehouses: List = emptyList(), +) + +data class LookupRecord( + val id: String, + val code: String? = null, + val name: String, +) + +data class CodeLabelOption( + val code: String, + val label: String, +) + +data class WarehouseLookup( + val id: String, + val code: String? = null, + val name: String, + val locations: List = emptyList(), +) + +data class WarehouseLocationLookup( + val id: String, + val code: String? = null, + val name: String, + @SerializedName("location_type") val locationType: String? = null, +) + +data class WashingCreatePayload( + @SerializedName("lot_id") val lotId: String, + @SerializedName("washing_place_id") val washingPlaceId: String, + @SerializedName("washing_cost") val washingCost: Double, + @SerializedName("duration_hours") val durationHours: Int, +) + +data class CompleteWashingPayload( + @SerializedName("after_qty") val afterQty: Double, + @SerializedName("grade_id") val gradeId: String?, + @SerializedName("warehouse_id") val warehouseId: String, + @SerializedName("warehouse_location_id") val warehouseLocationId: String?, +) + +fun LotDetailResponse.toLotDetailData(): LotDetailData { + return LotDetailData( + summaryCard = summaryCard, + lot = LotDetail( + id = id, + lotCode = lotCode, + sourceType = sourceType, + supplier = supplier, + grade = grade, + originalQty = originalQty, + availableQty = availableQty, + reservedQty = reservedQty, + damagedQty = damagedQty, + shrinkageQty = shrinkageQty, + unitCode = unitCode, + unitCost = unitCost, + warehouse = warehouse, + location = location, + status = status, + parentLot = parentLot, + childLots = childLots, + labels = labels, + purchase = purchase, + receipt = receipt, + receivedAt = receivedAt, + ), + procurement = procurement, + mobileActions = mobileActions, + ) +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/data/SessionStore.kt b/app/src/main/java/id/abelbirdnest/mobile/data/SessionStore.kt new file mode 100644 index 0000000..668c095 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/data/SessionStore.kt @@ -0,0 +1,27 @@ +package id.abelbirdnest.mobile.data + +import android.content.Context + +class SessionStore(context: Context) { + private val prefs = context.getSharedPreferences("abelbirdnest_stock_session", Context.MODE_PRIVATE) + + fun save(loginData: LoginData) { + prefs.edit() + .putString(KEY_TOKEN, loginData.sessionToken) + .putString(KEY_NAME, loginData.user.name) + .putString(KEY_ROLE, loginData.user.role) + .apply() + } + + fun token(): String? = prefs.getString(KEY_TOKEN, null) + + fun clear() { + prefs.edit().clear().apply() + } + + companion object { + private const val KEY_TOKEN = "token" + private const val KEY_NAME = "name" + private const val KEY_ROLE = "role" + } +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/network/ApiService.kt b/app/src/main/java/id/abelbirdnest/mobile/network/ApiService.kt new file mode 100644 index 0000000..917c720 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/network/ApiService.kt @@ -0,0 +1,352 @@ +package id.abelbirdnest.mobile.network + +import id.abelbirdnest.mobile.data.ApiEnvelope +import id.abelbirdnest.mobile.data.BootstrapData +import id.abelbirdnest.mobile.data.CompleteWashingPayload +import id.abelbirdnest.mobile.data.ConsignmentBootstrapData +import id.abelbirdnest.mobile.data.ConsignmentCloseLinePayload +import id.abelbirdnest.mobile.data.ConsignmentDetail +import id.abelbirdnest.mobile.data.ConsignmentListItem +import id.abelbirdnest.mobile.data.DashboardData +import id.abelbirdnest.mobile.data.FundRequestListItem +import id.abelbirdnest.mobile.data.FundRequestsBootstrapData +import id.abelbirdnest.mobile.data.JitSaleClosePayload +import id.abelbirdnest.mobile.data.JitSaleDetail +import id.abelbirdnest.mobile.data.JitSaleListItem +import id.abelbirdnest.mobile.data.LookupRecord +import id.abelbirdnest.mobile.data.LotDetailResponse +import id.abelbirdnest.mobile.data.LotTransformationCreatePayload +import id.abelbirdnest.mobile.data.LotTransformationDetail +import id.abelbirdnest.mobile.data.LotTransformationListItem +import id.abelbirdnest.mobile.data.AdjustmentReason +import id.abelbirdnest.mobile.data.PurchaseAnalysisDetail +import id.abelbirdnest.mobile.data.PurchaseAnalysisListItem +import id.abelbirdnest.mobile.data.PurchaseCreatePayload +import id.abelbirdnest.mobile.data.PurchaseDetail +import id.abelbirdnest.mobile.data.PurchaseListItem +import id.abelbirdnest.mobile.data.PurchaseRealizationDetail +import id.abelbirdnest.mobile.data.PurchaseRealizationListItem +import id.abelbirdnest.mobile.data.RegularSaleClosePayload +import id.abelbirdnest.mobile.data.RegularSaleDetail +import id.abelbirdnest.mobile.data.RegularSaleListItem +import id.abelbirdnest.mobile.data.ReceiptBootstrapData +import id.abelbirdnest.mobile.data.StockAdjustmentCreatePayload +import id.abelbirdnest.mobile.data.StockAdjustmentListItem +import id.abelbirdnest.mobile.data.ReceiptCreatePayload +import id.abelbirdnest.mobile.data.ReceiptDetail +import id.abelbirdnest.mobile.data.ReceiptListItem +import id.abelbirdnest.mobile.data.LoginRequest +import id.abelbirdnest.mobile.data.LoginResponse +import id.abelbirdnest.mobile.data.LotItem +import id.abelbirdnest.mobile.data.LotScanPayload +import id.abelbirdnest.mobile.data.UnitLookup +import id.abelbirdnest.mobile.data.WashingBootstrapData +import id.abelbirdnest.mobile.data.WashingListItem +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.Part +import retrofit2.http.PartMap +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.PUT + +interface ApiService { + @POST("auth/login") + suspend fun login( + @Body request: LoginRequest, + ): LoginResponse + + @GET("mobile/bootstrap") + suspend fun bootstrap( + @Header("Authorization") authorization: String, + ): ApiEnvelope + + @GET("mobile/dashboard") + suspend fun dashboard( + @Header("Authorization") authorization: String, + @Query("locale") locale: String = "id", + ): ApiEnvelope + + @GET("mobile/lots") + suspend fun lots( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/lots/{id}") + suspend fun lotDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @GET("mobile/lots/scan") + suspend fun scanLot( + @Header("Authorization") authorization: String, + @Query("code") code: String, + ): ApiEnvelope + + @GET("adjustment-reasons") + suspend fun adjustmentReasons( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/stock-adjustments") + suspend fun stockAdjustments( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @POST("mobile/stock-adjustments") + suspend fun createStockAdjustment( + @Header("Authorization") authorization: String, + @Body payload: StockAdjustmentCreatePayload, + ): ApiEnvelope + + @GET("mobile/purchases") + suspend fun purchases( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("units") + suspend fun units( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("employees") + suspend fun employees( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @POST("mobile/purchases") + suspend fun createPurchase( + @Header("Authorization") authorization: String, + @Body payload: PurchaseCreatePayload, + ): ApiEnvelope + + @GET("mobile/purchases/{id}") + suspend fun purchaseDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @PUT("mobile/purchases/{id}") + suspend fun updatePurchase( + @Header("Authorization") authorization: String, + @Path("id") id: String, + @Body payload: PurchaseCreatePayload, + ): ApiEnvelope + + @GET("mobile/purchase-analyses") + suspend fun purchaseAnalyses( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/purchase-analyses/{purchaseId}") + suspend fun purchaseAnalysisDetail( + @Header("Authorization") authorization: String, + @Path("purchaseId") purchaseId: String, + ): ApiEnvelope + + @GET("mobile/purchase-realizations") + suspend fun purchaseRealizations( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/purchase-realizations/{purchaseId}") + suspend fun purchaseRealizationDetail( + @Header("Authorization") authorization: String, + @Path("purchaseId") purchaseId: String, + ): ApiEnvelope + + @GET("mobile/sales-regular") + suspend fun regularSales( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/sales-regular/{id}") + suspend fun regularSaleDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @POST("mobile/sales-regular/{id}/close") + suspend fun closeRegularSale( + @Header("Authorization") authorization: String, + @Path("id") id: String, + @Body payload: RegularSaleClosePayload, + ): ApiEnvelope + + @GET("mobile/sales-jit") + suspend fun jitSales( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/sales-jit/{id}") + suspend fun jitSaleDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @POST("mobile/sales-jit/{id}/close") + suspend fun closeJitSale( + @Header("Authorization") authorization: String, + @Path("id") id: String, + @Body payload: JitSaleClosePayload, + ): ApiEnvelope + + @GET("mobile/consignments/bootstrap") + suspend fun consignmentsBootstrap( + @Header("Authorization") authorization: String, + ): ApiEnvelope + + @GET("mobile/consignments") + suspend fun consignments( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/consignments/{id}") + suspend fun consignmentDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @POST("consignments/lines/{lineId}/close") + suspend fun closeConsignmentLine( + @Header("Authorization") authorization: String, + @Path("lineId") lineId: String, + @Body payload: ConsignmentCloseLinePayload, + ): ApiEnvelope> + + @GET("mobile/fund-requests/bootstrap") + suspend fun fundRequestsBootstrap( + @Header("Authorization") authorization: String, + ): ApiEnvelope + + @GET("mobile/fund-requests") + suspend fun fundRequests( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @Multipart + @POST("mobile/fund-requests") + suspend fun createFundRequest( + @Header("Authorization") authorization: String, + @PartMap fields: Map, + ): ApiEnvelope + + @POST("mobile/purchases/{id}/submit") + suspend fun submitPurchase( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @POST("mobile/purchases/{id}/cancel") + suspend fun cancelPurchase( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope> + + @GET("mobile/lot-transformations") + suspend fun lotTransformations( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @GET("mobile/lot-transformations/{id}") + suspend fun lotTransformationDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @POST("mobile/lot-transformations") + suspend fun createLotTransformation( + @Header("Authorization") authorization: String, + @Body payload: LotTransformationCreatePayload, + ): ApiEnvelope + + @GET("mobile/washing/bootstrap") + suspend fun washingBootstrap( + @Header("Authorization") authorization: String, + ): ApiEnvelope + + @GET("mobile/washing") + suspend fun washings( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @Multipart + @POST("mobile/washing") + suspend fun createWashing( + @Header("Authorization") authorization: String, + @Part("payload") payload: RequestBody, + ): ApiEnvelope + + @Multipart + @PUT("mobile/washing/{id}") + suspend fun updateWashing( + @Header("Authorization") authorization: String, + @Path("id") id: String, + @Part("payload") payload: RequestBody, + ): ApiEnvelope + + @POST("mobile/washing/{id}/complete") + suspend fun completeWashing( + @Header("Authorization") authorization: String, + @Path("id") id: String, + @Body payload: CompleteWashingPayload, + ): ApiEnvelope + + @GET("mobile/receipts/bootstrap") + suspend fun receiptsBootstrap( + @Header("Authorization") authorization: String, + ): ApiEnvelope + + @GET("mobile/receipts") + suspend fun receipts( + @Header("Authorization") authorization: String, + ): ApiEnvelope> + + @POST("mobile/receipts") + suspend fun createReceipt( + @Header("Authorization") authorization: String, + @Body payload: ReceiptCreatePayload, + ): ApiEnvelope + + @GET("mobile/receipts/{id}") + suspend fun receiptDetail( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope + + @POST("mobile/receipts/{id}/generate-lots") + suspend fun generateReceiptLots( + @Header("Authorization") authorization: String, + @Path("id") id: String, + ): ApiEnvelope +} + +object ApiFactory { + private const val BASE_URL = "https://abelbirdnest.id/api/v1/" + + fun create(): ApiService { + val logger = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + } + + val client = OkHttpClient.Builder() + .addInterceptor(logger) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ApiService::class.java) + } +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/ui/AbelbirdnestApp.kt b/app/src/main/java/id/abelbirdnest/mobile/ui/AbelbirdnestApp.kt new file mode 100644 index 0000000..02b448d --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/ui/AbelbirdnestApp.kt @@ -0,0 +1,7667 @@ +package id.abelbirdnest.mobile.ui + +import android.Manifest +import android.content.pm.PackageManager +import androidx.compose.foundation.Image +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.WindowInsets +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.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items as gridItems +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccountTree +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.ChevronRight +import androidx.compose.material.icons.outlined.ContentPasteSearch +import androidx.compose.material.icons.outlined.Dashboard +import androidx.compose.material.icons.outlined.EditNote +import androidx.compose.material.icons.outlined.ExitToApp +import androidx.compose.material.icons.outlined.Inventory2 +import androidx.compose.material.icons.outlined.LocalShipping +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.LocationOn +import androidx.compose.material.icons.outlined.Login +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Print +import androidx.compose.material.icons.outlined.QrCode2 +import androidx.compose.material.icons.outlined.QrCodeScanner +import androidx.compose.material.icons.outlined.ReceiptLong +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.ShowChart +import androidx.compose.material.icons.outlined.SwapHoriz +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.WaterDrop +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner +import id.abelbirdnest.mobile.BuildConfig +import id.abelbirdnest.mobile.R +import id.abelbirdnest.mobile.data.CriticalLot +import id.abelbirdnest.mobile.data.DashboardBundle +import id.abelbirdnest.mobile.data.GradeDistribution +import id.abelbirdnest.mobile.data.AdjustmentReason +import id.abelbirdnest.mobile.data.ConsignmentDetail +import id.abelbirdnest.mobile.data.ConsignmentLineDetail +import id.abelbirdnest.mobile.data.ConsignmentListItem +import id.abelbirdnest.mobile.data.FundRequestAgentOption +import id.abelbirdnest.mobile.data.FundRequestBankAccountOption +import id.abelbirdnest.mobile.data.FundRequestListItem +import id.abelbirdnest.mobile.data.JitSaleDetail +import id.abelbirdnest.mobile.data.JitSaleLineDetail +import id.abelbirdnest.mobile.data.JitSaleListItem +import id.abelbirdnest.mobile.data.LotDetail +import id.abelbirdnest.mobile.data.LotDetailData +import id.abelbirdnest.mobile.data.LotItem +import id.abelbirdnest.mobile.data.LotScanResult +import id.abelbirdnest.mobile.data.LotTransformationDetail +import id.abelbirdnest.mobile.data.LotTransformationInput +import id.abelbirdnest.mobile.data.LotTransformationListItem +import id.abelbirdnest.mobile.data.LotTransformationOutput +import id.abelbirdnest.mobile.data.PurchaseAnalysisDetail +import id.abelbirdnest.mobile.data.PurchaseAnalysisListItem +import id.abelbirdnest.mobile.data.PurchaseDetail +import id.abelbirdnest.mobile.data.PurchaseLineDetail +import id.abelbirdnest.mobile.data.PurchaseListItem +import id.abelbirdnest.mobile.data.PurchaseRealizationDetail +import id.abelbirdnest.mobile.data.PurchaseRealizationEntryItem +import id.abelbirdnest.mobile.data.PurchaseRealizationListItem +import id.abelbirdnest.mobile.data.QuickAction +import id.abelbirdnest.mobile.data.RegularSaleDetail +import id.abelbirdnest.mobile.data.RegularSaleLineDetail +import id.abelbirdnest.mobile.data.RegularSaleListItem +import id.abelbirdnest.mobile.data.LookupRecord +import id.abelbirdnest.mobile.data.UnitLookup +import id.abelbirdnest.mobile.data.WarehouseLookup +import id.abelbirdnest.mobile.data.WashingListItem +import id.abelbirdnest.mobile.data.ReceiptDetail +import id.abelbirdnest.mobile.data.ReceiptDetailLine +import id.abelbirdnest.mobile.data.ReceiptGeneratedLot +import id.abelbirdnest.mobile.data.ReceiptListItem +import id.abelbirdnest.mobile.data.ReceiptPurchaseOption +import id.abelbirdnest.mobile.data.StockAdjustmentListItem +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.common.InputImage +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.Executors +import kotlin.math.roundToInt +import androidx.compose.ui.window.Dialog + +@Composable +fun AbelbirdnestApp(viewModel: MainViewModel) { + val state = viewModel.uiState + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.errorMessage) { + val message = state.errorMessage ?: return@LaunchedEffect + snackbarHostState.showSnackbar(message) + viewModel.clearError() + } + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(innerPadding), + ) { + when { + state.isCheckingSession -> LoadingScreen() + state.isAuthenticated && state.dashboard != null -> ShellScreen( + data = state.dashboard, + currentModule = state.currentModule, + isRefreshing = state.isRefreshing, + inlineError = state.errorMessage, + lotsState = state.lotsState, + salesRegularState = state.salesRegularState, + salesJitState = state.salesJitState, + consignmentsState = state.consignmentsState, + fundRequestsState = state.fundRequestsState, + purchasesState = state.purchasesState, + purchaseAnalysesState = state.purchaseAnalysesState, + purchaseRealizationsState = state.purchaseRealizationsState, + lotTransformationsState = state.lotTransformationsState, + stockAdjustmentsState = state.stockAdjustmentsState, + washingState = state.washingState, + receiptsState = state.receiptsState, + onRefresh = viewModel::refreshDashboard, + onLogout = viewModel::logout, + onModuleSelected = viewModel::selectModule, + onRefreshLots = viewModel::refreshLots, + onLotQueryChanged = viewModel::updateLotQuery, + onLotScanInputChanged = viewModel::updateScanInput, + onScanLot = viewModel::scanLot, + onScanLotCode = viewModel::scanLotCode, + onOpenLotDetail = viewModel::openLotDetail, + onCloseLotDetail = viewModel::closeLotDetail, + onOpenRecentScan = viewModel::openRecentScan, + onRefreshRegularSales = viewModel::refreshRegularSales, + onOpenRegularSaleDetail = viewModel::openRegularSaleDetail, + onCloseRegularSaleDetail = viewModel::closeRegularSaleDetail, + onUpdateRegularSaleCloseDate = viewModel::updateRegularSaleCloseDate, + onUpdateRegularSaleQtySold = viewModel::updateRegularSaleQtySold, + onUpdateRegularSaleQtyReturned = viewModel::updateRegularSaleQtyReturned, + onUpdateRegularSalePriceActual = viewModel::updateRegularSalePriceActual, + onCloseRegularSale = viewModel::closeRegularSale, + onRefreshJitSales = viewModel::refreshJitSales, + onOpenJitSaleDetail = viewModel::openJitSaleDetail, + onCloseJitSaleDetail = viewModel::closeJitSaleDetail, + onUpdateJitSaleCloseDate = viewModel::updateJitSaleCloseDate, + onUpdateJitSalePriceActual = viewModel::updateJitSalePriceActual, + onCloseJitSale = viewModel::closeJitSale, + onRefreshConsignments = viewModel::refreshConsignments, + onOpenConsignmentDetail = viewModel::openConsignmentDetail, + onCloseConsignmentDetail = viewModel::closeConsignmentDetail, + onSelectConsignmentLine = viewModel::selectConsignmentLine, + onUpdateConsignmentCloseDate = viewModel::updateConsignmentCloseDate, + onUpdateConsignmentSellingPrice = viewModel::updateConsignmentSellingPrice, + onUpdateConsignmentQtySold = viewModel::updateConsignmentQtySold, + onUpdateConsignmentQtyReturned = viewModel::updateConsignmentQtyReturned, + onUpdateConsignmentSalesCommission = viewModel::updateConsignmentSalesCommission, + onCloseConsignmentLine = viewModel::closeConsignmentLine, + onRefreshFundRequests = viewModel::refreshFundRequests, + onUpdateFundRequestTransferType = viewModel::updateFundRequestTransferType, + onUpdateFundRequestReferenceNo = viewModel::updateFundRequestReferenceNo, + onSelectFundRequestAgent = viewModel::selectFundRequestAgent, + onSelectFundRequestAgentBank = viewModel::selectFundRequestAgentBank, + onSelectFundRequestCompanyBank = viewModel::selectFundRequestCompanyBank, + onUpdateFundRequestAmount = viewModel::updateFundRequestAmount, + onUpdateFundRequestTransferredAt = viewModel::updateFundRequestTransferredAt, + onSaveFundRequest = viewModel::saveFundRequest, + onRefreshPurchases = viewModel::refreshPurchases, + onOpenCreatePurchase = viewModel::openCreatePurchase, + onOpenPurchaseDetail = viewModel::openPurchaseDetail, + onClosePurchaseDetail = viewModel::closePurchaseDetail, + onOpenEditPurchase = viewModel::openEditPurchase, + onClosePurchaseEditor = viewModel::closePurchaseEditor, + onUpdatePurchaseDate = viewModel::updatePurchaseDate, + onUpdatePurchaseReceivedAt = viewModel::updatePurchaseReceivedAt, + onSelectPurchaseEmployee = viewModel::selectPurchaseEmployee, + onSelectPurchaseWarehouse = viewModel::selectPurchaseWarehouse, + onSelectPurchaseWarehouseLocation = viewModel::selectPurchaseWarehouseLocation, + onUpdatePurchaseNotes = viewModel::updatePurchaseNotes, + onAddPurchaseLine = viewModel::addPurchaseLine, + onRemovePurchaseLine = viewModel::removePurchaseLine, + onSelectPurchaseLineGrade = viewModel::selectPurchaseLineGrade, + onUpdatePurchaseLineQty = viewModel::updatePurchaseLineQty, + onSelectPurchaseLineUnit = viewModel::selectPurchaseLineUnit, + onUpdatePurchaseLineUnitPrice = viewModel::updatePurchaseLineUnitPrice, + onUpdatePurchaseLineUnitCost = viewModel::updatePurchaseLineUnitCost, + onUpdatePurchaseLineNotes = viewModel::updatePurchaseLineNotes, + onSavePurchase = viewModel::savePurchase, + onSubmitPurchase = viewModel::submitPurchase, + onCancelPurchase = viewModel::cancelPurchase, + onRefreshPurchaseAnalyses = viewModel::refreshPurchaseAnalyses, + onOpenPurchaseAnalysisDetail = viewModel::openPurchaseAnalysisDetail, + onClosePurchaseAnalysisDetail = viewModel::closePurchaseAnalysisDetail, + onRefreshPurchaseRealizations = viewModel::refreshPurchaseRealizations, + onOpenPurchaseRealizationDetail = viewModel::openPurchaseRealizationDetail, + onClosePurchaseRealizationDetail = viewModel::closePurchaseRealizationDetail, + onRefreshLotTransformations = viewModel::refreshLotTransformations, + onOpenCreateLotTransformation = viewModel::openCreateLotTransformation, + onOpenLotTransformationDetail = viewModel::openLotTransformationDetail, + onCloseLotTransformationScreen = viewModel::closeLotTransformationScreen, + onUpdateLotTransformationType = viewModel::updateLotTransformationType, + onUpdateLotTransformationDate = viewModel::updateLotTransformationDate, + onUpdateLotTransformationRemainderMode = viewModel::updateLotTransformationRemainderMode, + onUpdateLotTransformationProcessingLossMode = viewModel::updateLotTransformationProcessingLossMode, + onUpdateLotTransformationNotes = viewModel::updateLotTransformationNotes, + onAddLotTransformationInput = viewModel::addLotTransformationInput, + onRemoveLotTransformationInput = viewModel::removeLotTransformationInput, + onUpdateLotTransformationInputQuery = viewModel::updateLotTransformationInputQuery, + onSelectLotTransformationInputLot = viewModel::selectLotTransformationInputLot, + onClearLotTransformationInputLot = viewModel::clearLotTransformationInputLot, + onUpdateLotTransformationInputQty = viewModel::updateLotTransformationInputQty, + onUpdateLotTransformationInputNotes = viewModel::updateLotTransformationInputNotes, + onAddLotTransformationOutput = viewModel::addLotTransformationOutput, + onRemoveLotTransformationOutput = viewModel::removeLotTransformationOutput, + onSelectLotTransformationOutputGrade = viewModel::selectLotTransformationOutputGrade, + onSelectLotTransformationOutputWarehouse = viewModel::selectLotTransformationOutputWarehouse, + onSelectLotTransformationOutputLocation = viewModel::selectLotTransformationOutputLocation, + onUpdateLotTransformationOutputQty = viewModel::updateLotTransformationOutputQty, + onUpdateLotTransformationOutputNotes = viewModel::updateLotTransformationOutputNotes, + onSaveLotTransformation = viewModel::saveLotTransformation, + onRefreshStockAdjustments = viewModel::refreshStockAdjustments, + onSelectStockAdjustmentLot = viewModel::selectStockAdjustmentLot, + onSelectStockAdjustmentReason = viewModel::selectStockAdjustmentReason, + onUpdateStockAdjustmentDate = viewModel::updateStockAdjustmentDate, + onUpdateStockAdjustmentQty = viewModel::updateStockAdjustmentQty, + onUpdateStockAdjustmentNotes = viewModel::updateStockAdjustmentNotes, + onSaveStockAdjustment = viewModel::saveStockAdjustment, + onRefreshWashing = viewModel::refreshWashing, + onOpenCreateWashing = viewModel::openCreateWashing, + onOpenEditWashing = viewModel::openEditWashing, + onOpenCompleteWashing = viewModel::openCompleteWashing, + onCloseWashingScreen = viewModel::closeWashingScreen, + onSelectWashingLot = viewModel::selectWashingLot, + onSelectWashingPlace = viewModel::selectWashingPlace, + onUpdateWashingCost = viewModel::updateWashingCost, + onUpdateWashingDuration = viewModel::updateWashingDuration, + onUpdateAfterQty = viewModel::updateAfterQty, + onSelectCompleteGrade = viewModel::selectCompleteGrade, + onSelectCompleteWarehouse = viewModel::selectCompleteWarehouse, + onSelectCompleteWarehouseLocation = viewModel::selectCompleteWarehouseLocation, + onSaveWashing = viewModel::saveWashing, + onCompleteWashing = viewModel::completeWashing, + onRefreshReceipts = viewModel::refreshReceipts, + onOpenCreateReceipt = viewModel::openCreateReceipt, + onCloseReceiptScreen = viewModel::closeReceiptScreen, + onSelectReceiptPurchase = viewModel::selectReceiptPurchase, + onUpdateReceiptDate = viewModel::updateReceiptDate, + onUpdateReceiptNotes = viewModel::updateReceiptNotes, + onUpdateReceiptLineQtyReceived = viewModel::updateReceiptLineQtyReceived, + onUpdateReceiptLineQtyAccepted = viewModel::updateReceiptLineQtyAccepted, + onUpdateReceiptLineQtyRejected = viewModel::updateReceiptLineQtyRejected, + onUpdateReceiptLineUnitCost = viewModel::updateReceiptLineUnitCost, + onUpdateReceiptLineWarehouse = viewModel::updateReceiptLineWarehouse, + onUpdateReceiptLineLocation = viewModel::updateReceiptLineLocation, + onUpdateReceiptLineNotes = viewModel::updateReceiptLineNotes, + onOpenReceiptDetail = viewModel::openReceiptDetail, + onSaveReceipt = viewModel::saveReceipt, + onGenerateReceiptLots = viewModel::generateReceiptLots, + ) + + else -> LoginScreen( + identity = viewModel.identity, + password = viewModel.password, + isSubmitting = state.isSubmitting, + onIdentityChanged = viewModel::onIdentityChanged, + onPasswordChanged = viewModel::onPasswordChanged, + onSubmit = viewModel::login, + ) + } + } + } +} + +@Composable +private fun LoadingScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + Spacer(modifier = Modifier.height(16.dp)) + Text("Memuat data operasional...", color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} + +@Composable +private fun LoginScreen( + identity: String, + password: String, + isSubmitting: Boolean, + onIdentityChanged: (String) -> Unit, + onPasswordChanged: (String) -> Unit, + onSubmit: () -> Unit, +) { + var showPassword by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(R.drawable.logo_abelbirdnest), + contentDescription = "Logo Abelbirdnest", + modifier = Modifier.size(width = 170.dp, height = 130.dp), + contentScale = ContentScale.Fit, + ) + Text( + text = "AbelBirdnest", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "Selamat Datang Kembali", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "Silakan masuk untuk mengelola inventaris walet Anda", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp), + ) + Spacer(modifier = Modifier.height(28.dp)) + OutlinedTextField( + value = identity, + onValueChange = onIdentityChanged, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + singleLine = true, + leadingIcon = { Icon(Icons.Outlined.Person, contentDescription = null) }, + label = { Text("Email / Nomor HP / Username") }, + placeholder = { Text("contoh@email.com") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + ) + Spacer(modifier = Modifier.height(14.dp)) + OutlinedTextField( + value = password, + onValueChange = onPasswordChanged, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + singleLine = true, + leadingIcon = { Icon(Icons.Outlined.Lock, contentDescription = null) }, + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = null, + ) + } + }, + label = { Text("Kata Sandi") }, + placeholder = { Text("Masukkan kata sandi") }, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = "Lupa Kata Sandi?", + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End, + ) + Spacer(modifier = Modifier.height(18.dp)) + Button( + onClick = onSubmit, + modifier = Modifier.fillMaxWidth().height(52.dp), + shape = RoundedCornerShape(14.dp), + enabled = !isSubmitting, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + if (isSubmitting) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimaryContainer, + strokeWidth = 2.dp, + ) + } else { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Masuk", fontWeight = FontWeight.SemiBold) + Icon(Icons.Outlined.Login, contentDescription = null) + } + } + } + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShellScreen( + data: DashboardBundle, + currentModule: String, + isRefreshing: Boolean, + inlineError: String?, + lotsState: LotsUiState, + salesRegularState: SalesRegularUiState, + salesJitState: SalesJitUiState, + consignmentsState: ConsignmentsUiState, + fundRequestsState: FundRequestsUiState, + purchasesState: PurchasesUiState, + purchaseAnalysesState: PurchaseAnalysesUiState, + purchaseRealizationsState: PurchaseRealizationsUiState, + lotTransformationsState: LotTransformationsUiState, + stockAdjustmentsState: StockAdjustmentsUiState, + washingState: WashingUiState, + receiptsState: ReceiptsUiState, + onRefresh: () -> Unit, + onLogout: () -> Unit, + onModuleSelected: (String) -> Unit, + onRefreshLots: () -> Unit, + onLotQueryChanged: (String) -> Unit, + onLotScanInputChanged: (String) -> Unit, + onScanLot: () -> Unit, + onScanLotCode: (String) -> Unit, + onOpenLotDetail: (String) -> Unit, + onCloseLotDetail: () -> Unit, + onOpenRecentScan: (LotScanResult) -> Unit, + onRefreshRegularSales: () -> Unit, + onOpenRegularSaleDetail: (String) -> Unit, + onCloseRegularSaleDetail: () -> Unit, + onUpdateRegularSaleCloseDate: (String) -> Unit, + onUpdateRegularSaleQtySold: (Int, String) -> Unit, + onUpdateRegularSaleQtyReturned: (Int, String) -> Unit, + onUpdateRegularSalePriceActual: (Int, String) -> Unit, + onCloseRegularSale: () -> Unit, + onRefreshJitSales: () -> Unit, + onOpenJitSaleDetail: (String) -> Unit, + onCloseJitSaleDetail: () -> Unit, + onUpdateJitSaleCloseDate: (String) -> Unit, + onUpdateJitSalePriceActual: (Int, String) -> Unit, + onCloseJitSale: () -> Unit, + onRefreshConsignments: () -> Unit, + onOpenConsignmentDetail: (String) -> Unit, + onCloseConsignmentDetail: () -> Unit, + onSelectConsignmentLine: (String?) -> Unit, + onUpdateConsignmentCloseDate: (String) -> Unit, + onUpdateConsignmentSellingPrice: (String) -> Unit, + onUpdateConsignmentQtySold: (String) -> Unit, + onUpdateConsignmentQtyReturned: (String) -> Unit, + onUpdateConsignmentSalesCommission: (String) -> Unit, + onCloseConsignmentLine: () -> Unit, + onRefreshFundRequests: () -> Unit, + onUpdateFundRequestTransferType: (String) -> Unit, + onUpdateFundRequestReferenceNo: (String) -> Unit, + onSelectFundRequestAgent: (String) -> Unit, + onSelectFundRequestAgentBank: (String) -> Unit, + onSelectFundRequestCompanyBank: (String) -> Unit, + onUpdateFundRequestAmount: (String) -> Unit, + onUpdateFundRequestTransferredAt: (String) -> Unit, + onSaveFundRequest: () -> Unit, + onRefreshPurchases: () -> Unit, + onOpenCreatePurchase: () -> Unit, + onOpenPurchaseDetail: (String) -> Unit, + onClosePurchaseDetail: () -> Unit, + onOpenEditPurchase: () -> Unit, + onClosePurchaseEditor: () -> Unit, + onUpdatePurchaseDate: (String) -> Unit, + onUpdatePurchaseReceivedAt: (String) -> Unit, + onSelectPurchaseEmployee: (String) -> Unit, + onSelectPurchaseWarehouse: (String) -> Unit, + onSelectPurchaseWarehouseLocation: (String?) -> Unit, + onUpdatePurchaseNotes: (String) -> Unit, + onAddPurchaseLine: () -> Unit, + onRemovePurchaseLine: (Int) -> Unit, + onSelectPurchaseLineGrade: (Int, String?) -> Unit, + onUpdatePurchaseLineQty: (Int, String) -> Unit, + onSelectPurchaseLineUnit: (Int, String) -> Unit, + onUpdatePurchaseLineUnitPrice: (Int, String) -> Unit, + onUpdatePurchaseLineUnitCost: (Int, String) -> Unit, + onUpdatePurchaseLineNotes: (Int, String) -> Unit, + onSavePurchase: () -> Unit, + onSubmitPurchase: () -> Unit, + onCancelPurchase: () -> Unit, + onRefreshPurchaseAnalyses: () -> Unit, + onOpenPurchaseAnalysisDetail: (String) -> Unit, + onClosePurchaseAnalysisDetail: () -> Unit, + onRefreshPurchaseRealizations: () -> Unit, + onOpenPurchaseRealizationDetail: (String) -> Unit, + onClosePurchaseRealizationDetail: () -> Unit, + onRefreshLotTransformations: () -> Unit, + onOpenCreateLotTransformation: () -> Unit, + onOpenLotTransformationDetail: (String) -> Unit, + onCloseLotTransformationScreen: () -> Unit, + onUpdateLotTransformationType: (String) -> Unit, + onUpdateLotTransformationDate: (String) -> Unit, + onUpdateLotTransformationRemainderMode: (String?) -> Unit, + onUpdateLotTransformationProcessingLossMode: (String?) -> Unit, + onUpdateLotTransformationNotes: (String) -> Unit, + onAddLotTransformationInput: () -> Unit, + onRemoveLotTransformationInput: (Int) -> Unit, + onUpdateLotTransformationInputQuery: (Int, String) -> Unit, + onSelectLotTransformationInputLot: (Int, String) -> Unit, + onClearLotTransformationInputLot: (Int) -> Unit, + onUpdateLotTransformationInputQty: (Int, String) -> Unit, + onUpdateLotTransformationInputNotes: (Int, String) -> Unit, + onAddLotTransformationOutput: () -> Unit, + onRemoveLotTransformationOutput: (Int) -> Unit, + onSelectLotTransformationOutputGrade: (Int, String) -> Unit, + onSelectLotTransformationOutputWarehouse: (Int, String) -> Unit, + onSelectLotTransformationOutputLocation: (Int, String?) -> Unit, + onUpdateLotTransformationOutputQty: (Int, String) -> Unit, + onUpdateLotTransformationOutputNotes: (Int, String) -> Unit, + onSaveLotTransformation: () -> Unit, + onRefreshStockAdjustments: () -> Unit, + onSelectStockAdjustmentLot: (String) -> Unit, + onSelectStockAdjustmentReason: (String) -> Unit, + onUpdateStockAdjustmentDate: (String) -> Unit, + onUpdateStockAdjustmentQty: (String) -> Unit, + onUpdateStockAdjustmentNotes: (String) -> Unit, + onSaveStockAdjustment: () -> Unit, + onRefreshWashing: () -> Unit, + onOpenCreateWashing: () -> Unit, + onOpenEditWashing: (String) -> Unit, + onOpenCompleteWashing: (String) -> Unit, + onCloseWashingScreen: () -> Unit, + onSelectWashingLot: (String) -> Unit, + onSelectWashingPlace: (String) -> Unit, + onUpdateWashingCost: (String) -> Unit, + onUpdateWashingDuration: (String) -> Unit, + onUpdateAfterQty: (String) -> Unit, + onSelectCompleteGrade: (String?) -> Unit, + onSelectCompleteWarehouse: (String) -> Unit, + onSelectCompleteWarehouseLocation: (String?) -> Unit, + onSaveWashing: () -> Unit, + onCompleteWashing: () -> Unit, + onRefreshReceipts: () -> Unit, + onOpenCreateReceipt: () -> Unit, + onCloseReceiptScreen: () -> Unit, + onSelectReceiptPurchase: (String) -> Unit, + onUpdateReceiptDate: (String) -> Unit, + onUpdateReceiptNotes: (String) -> Unit, + onUpdateReceiptLineQtyReceived: (Int, String) -> Unit, + onUpdateReceiptLineQtyAccepted: (Int, String) -> Unit, + onUpdateReceiptLineQtyRejected: (Int, String) -> Unit, + onUpdateReceiptLineUnitCost: (Int, String) -> Unit, + onUpdateReceiptLineWarehouse: (Int, String) -> Unit, + onUpdateReceiptLineLocation: (Int, String) -> Unit, + onUpdateReceiptLineNotes: (Int, String) -> Unit, + onOpenReceiptDetail: (String) -> Unit, + onSaveReceipt: () -> Unit, + onGenerateReceiptLots: () -> Unit, +) { + val quickActions = remember(data.user.role, data.modules) { + buildQuickActions(role = data.user.role, modules = data.modules) + } + val bottomItems = remember(currentModule, data.modules, data.user.role) { + buildBottomBarItems(role = data.user.role, currentModule = currentModule, modules = data.modules) + } + + Scaffold( + topBar = { + Surface(shadowElevation = 2.dp, color = MaterialTheme.colorScheme.surface) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Text( + text = data.user.name.split(" ").mapNotNull { it.firstOrNull()?.toString() }.take(2).joinToString(""), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + ) + } + Column { + Text( + text = "Abelbirdnest Stock", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = "${data.user.role} • ${moduleLabel(currentModule)}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Row { + IconButton(onClick = onRefresh) { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh") + } + IconButton(onClick = onLogout) { + Icon(Icons.Outlined.ExitToApp, contentDescription = "Logout") + } + } + } + if (isRefreshing) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ) + } + } + } + }, + bottomBar = { + BottomBar( + items = bottomItems, + currentModule = currentModule, + onModuleSelected = onModuleSelected, + ) + }, + ) { innerPadding -> + when (currentModule) { + "dashboard" -> DashboardScreen( + data = data, + quickActions = quickActions, + bottomItems = bottomItems, + inlineError = inlineError, + onModuleSelected = onModuleSelected, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) + + "lots" -> LotsModuleScreen( + role = data.user.role, + state = lotsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshLots, + onQueryChanged = onLotQueryChanged, + onScanInputChanged = onLotScanInputChanged, + onScan = onScanLot, + onCameraScan = onScanLotCode, + onOpenLotDetail = onOpenLotDetail, + onCloseLotDetail = onCloseLotDetail, + onOpenRecentScan = onOpenRecentScan, + ) + + "sales_regular" -> RegularSalesModuleScreen( + role = data.user.role, + state = salesRegularState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshRegularSales, + onOpenDetail = onOpenRegularSaleDetail, + onBack = onCloseRegularSaleDetail, + onUpdateCloseDate = onUpdateRegularSaleCloseDate, + onUpdateQtySold = onUpdateRegularSaleQtySold, + onUpdateQtyReturned = onUpdateRegularSaleQtyReturned, + onUpdatePriceActual = onUpdateRegularSalePriceActual, + onCloseSale = onCloseRegularSale, + ) + + "sales_jit" -> JitSalesModuleScreen( + role = data.user.role, + state = salesJitState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshJitSales, + onOpenDetail = onOpenJitSaleDetail, + onBack = onCloseJitSaleDetail, + onUpdateCloseDate = onUpdateJitSaleCloseDate, + onUpdatePriceActual = onUpdateJitSalePriceActual, + onCloseSale = onCloseJitSale, + ) + + "consignments" -> ConsignmentsModuleScreen( + role = data.user.role, + state = consignmentsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshConsignments, + onOpenDetail = onOpenConsignmentDetail, + onBack = onCloseConsignmentDetail, + onSelectLine = onSelectConsignmentLine, + onUpdateCloseDate = onUpdateConsignmentCloseDate, + onUpdateSellingPrice = onUpdateConsignmentSellingPrice, + onUpdateQtySold = onUpdateConsignmentQtySold, + onUpdateQtyReturned = onUpdateConsignmentQtyReturned, + onUpdateSalesCommission = onUpdateConsignmentSalesCommission, + onCloseLine = onCloseConsignmentLine, + ) + + "fund_requests" -> FundRequestsModuleScreen( + state = fundRequestsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshFundRequests, + onUpdateTransferType = onUpdateFundRequestTransferType, + onUpdateReferenceNo = onUpdateFundRequestReferenceNo, + onSelectAgent = onSelectFundRequestAgent, + onSelectAgentBank = onSelectFundRequestAgentBank, + onSelectCompanyBank = onSelectFundRequestCompanyBank, + onUpdateAmount = onUpdateFundRequestAmount, + onUpdateTransferredAt = onUpdateFundRequestTransferredAt, + onSave = onSaveFundRequest, + ) + + "purchases" -> PurchasesModuleScreen( + role = data.user.role, + state = purchasesState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshPurchases, + onOpenCreate = onOpenCreatePurchase, + onOpenDetail = onOpenPurchaseDetail, + onBack = onClosePurchaseDetail, + onOpenEdit = onOpenEditPurchase, + onCloseEditor = onClosePurchaseEditor, + onUpdatePurchaseDate = onUpdatePurchaseDate, + onUpdateReceivedAt = onUpdatePurchaseReceivedAt, + onSelectEmployee = onSelectPurchaseEmployee, + onSelectWarehouse = onSelectPurchaseWarehouse, + onSelectWarehouseLocation = onSelectPurchaseWarehouseLocation, + onUpdateNotes = onUpdatePurchaseNotes, + onAddLine = onAddPurchaseLine, + onRemoveLine = onRemovePurchaseLine, + onSelectLineGrade = onSelectPurchaseLineGrade, + onUpdateLineQty = onUpdatePurchaseLineQty, + onSelectLineUnit = onSelectPurchaseLineUnit, + onUpdateLineUnitPrice = onUpdatePurchaseLineUnitPrice, + onUpdateLineUnitCost = onUpdatePurchaseLineUnitCost, + onUpdateLineNotes = onUpdatePurchaseLineNotes, + onSave = onSavePurchase, + onSubmit = onSubmitPurchase, + onCancel = onCancelPurchase, + ) + + "purchase_analyses" -> PurchaseAnalysesModuleScreen( + state = purchaseAnalysesState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshPurchaseAnalyses, + onOpenDetail = onOpenPurchaseAnalysisDetail, + onBack = onClosePurchaseAnalysisDetail, + ) + + "purchase_realizations" -> PurchaseRealizationsModuleScreen( + state = purchaseRealizationsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshPurchaseRealizations, + onOpenDetail = onOpenPurchaseRealizationDetail, + onBack = onClosePurchaseRealizationDetail, + ) + + "lot_transformations" -> LotTransformationsModuleScreen( + state = lotTransformationsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshLotTransformations, + onOpenCreate = onOpenCreateLotTransformation, + onOpenDetail = onOpenLotTransformationDetail, + onBack = onCloseLotTransformationScreen, + onUpdateType = onUpdateLotTransformationType, + onUpdateDate = onUpdateLotTransformationDate, + onUpdateRemainderMode = onUpdateLotTransformationRemainderMode, + onUpdateProcessingLossMode = onUpdateLotTransformationProcessingLossMode, + onUpdateNotes = onUpdateLotTransformationNotes, + onAddInput = onAddLotTransformationInput, + onRemoveInput = onRemoveLotTransformationInput, + onUpdateInputQuery = onUpdateLotTransformationInputQuery, + onSelectInputLot = onSelectLotTransformationInputLot, + onClearInputLot = onClearLotTransformationInputLot, + onUpdateInputQty = onUpdateLotTransformationInputQty, + onUpdateInputNotes = onUpdateLotTransformationInputNotes, + onAddOutput = onAddLotTransformationOutput, + onRemoveOutput = onRemoveLotTransformationOutput, + onSelectOutputGrade = onSelectLotTransformationOutputGrade, + onSelectOutputWarehouse = onSelectLotTransformationOutputWarehouse, + onSelectOutputLocation = onSelectLotTransformationOutputLocation, + onUpdateOutputQty = onUpdateLotTransformationOutputQty, + onUpdateOutputNotes = onUpdateLotTransformationOutputNotes, + onSave = onSaveLotTransformation, + ) + + "stock_adjustments" -> StockAdjustmentsModuleScreen( + state = stockAdjustmentsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshStockAdjustments, + onSelectLot = onSelectStockAdjustmentLot, + onSelectReason = onSelectStockAdjustmentReason, + onUpdateDate = onUpdateStockAdjustmentDate, + onUpdateQty = onUpdateStockAdjustmentQty, + onUpdateNotes = onUpdateStockAdjustmentNotes, + onSave = onSaveStockAdjustment, + ) + + "washing" -> WashingModuleScreen( + role = data.user.role, + state = washingState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshWashing, + onOpenCreate = onOpenCreateWashing, + onOpenEdit = onOpenEditWashing, + onOpenComplete = onOpenCompleteWashing, + onBack = onCloseWashingScreen, + onSelectLot = onSelectWashingLot, + onSelectPlace = onSelectWashingPlace, + onUpdateCost = onUpdateWashingCost, + onUpdateDuration = onUpdateWashingDuration, + onUpdateAfterQty = onUpdateAfterQty, + onSelectGrade = onSelectCompleteGrade, + onSelectWarehouse = onSelectCompleteWarehouse, + onSelectWarehouseLocation = onSelectCompleteWarehouseLocation, + onSave = onSaveWashing, + onComplete = onCompleteWashing, + ) + + "receipts" -> ReceiptsModuleScreen( + state = receiptsState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + onRefresh = onRefreshReceipts, + onOpenCreate = onOpenCreateReceipt, + onBack = onCloseReceiptScreen, + onSelectPurchase = onSelectReceiptPurchase, + onUpdateReceiptDate = onUpdateReceiptDate, + onUpdateReceiptNotes = onUpdateReceiptNotes, + onUpdateQtyReceived = onUpdateReceiptLineQtyReceived, + onUpdateQtyAccepted = onUpdateReceiptLineQtyAccepted, + onUpdateQtyRejected = onUpdateReceiptLineQtyRejected, + onUpdateUnitCost = onUpdateReceiptLineUnitCost, + onUpdateWarehouse = onUpdateReceiptLineWarehouse, + onUpdateLocation = onUpdateReceiptLineLocation, + onUpdateLineNotes = onUpdateReceiptLineNotes, + onOpenDetail = onOpenReceiptDetail, + onSave = onSaveReceipt, + onGenerateLots = onGenerateReceiptLots, + ) + + else -> ModulePlaceholderScreen( + module = currentModule, + role = data.user.role, + availableModules = data.modules, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) + } + } +} + +@Composable +private fun DashboardScreen( + data: DashboardBundle, + quickActions: List, + bottomItems: List, + inlineError: String?, + onModuleSelected: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val bottomModules = bottomItems.filter { it != "dashboard" }.distinct() + val extraQuickActions = quickActions.filter { it.module !in bottomModules }.distinctBy { it.module } + + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Halo, ${data.user.name}", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "Update terkini inventaris dan operasional untuk role ${data.user.role.lowercase().replaceFirstChar { it.uppercase() }}.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + if (!inlineError.isNullOrBlank()) { + item { + InlineErrorCard(message = inlineError) + } + } + + item { + MetricSection(data = data) + } + + if (extraQuickActions.isNotEmpty()) { + item { + QuickActionSection( + actions = extraQuickActions, + onActionClick = { onModuleSelected(it.module) }, + ) + } + } + + item { + AlertSection(criticalLots = data.criticalLots) + } + + item { + DistributionSection(items = data.gradeDistribution) + } + + item { + Spacer(modifier = Modifier.height(6.dp)) + Image( + painter = painterResource(R.drawable.logo_abelbirdnest), + contentDescription = null, + modifier = Modifier + .size(width = 120.dp, height = 44.dp) + .padding(bottom = 4.dp), + alpha = 0.48f, + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Composable +private fun MetricSection(data: DashboardBundle) { + val showPurchaseValue = data.user.role in setOf("OWNER", "PURCHASING") + val cards = buildList { + addAll( + data.metrics + .filterNot { metric -> + !showPurchaseValue && metric.label.contains("pembelian", ignoreCase = true) + } + .take(3), + ) + if (none { it.label.contains("Lot", ignoreCase = true) }) { + add( + id.abelbirdnest.mobile.data.DashboardMetric( + label = "Lot Aktif", + value = data.summary.activeLotCount.toString(), + delta = "Unit produksi aktif", + ), + ) + } + }.take(3) + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + cards.forEachIndexed { index, metric -> + MetricCard( + modifier = if (index == 0) Modifier.fillMaxWidth() else Modifier.fillMaxWidth(), + label = metric.label, + value = metric.value, + delta = metric.delta, + highlighted = index == 0, + ) + } + } +} + +@Composable +private fun MetricCard( + modifier: Modifier = Modifier, + label: String, + value: String, + delta: String, + highlighted: Boolean, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + tonalElevation = if (highlighted) 2.dp else 0.dp, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(label.uppercase(), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + text = value, + style = if (highlighted) MaterialTheme.typography.displaySmall else MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + ) + Text(delta, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} + +@Composable +private fun QuickActionSection( + actions: List, + onActionClick: (QuickAction) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Aksi Cepat", style = MaterialTheme.typography.titleMedium) + LazyVerticalGrid( + columns = GridCells.Fixed(4), + modifier = Modifier.height(((actions.chunked(4).size.coerceAtLeast(1)) * 104).dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + userScrollEnabled = false, + ) { + gridItems(actions) { action -> + Column( + modifier = Modifier.clickable { onActionClick(action) }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Surface( + shape = RoundedCornerShape(18.dp), + color = if (action == actions.first()) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = action.icon(), + contentDescription = null, + tint = if (action == actions.first()) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp), + ) + } + } + Text( + text = action.label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } + } +} + +@Composable +private fun AlertSection(criticalLots: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Peringatan Lot", style = MaterialTheme.typography.titleMedium) + AssistChip( + onClick = {}, + label = { + Text("${criticalLots.size} perlu tindakan") + }, + ) + } + + if (criticalLots.isEmpty()) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + ) { + Text( + text = "Tidak ada lot kritis saat ini.", + modifier = Modifier.padding(16.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + criticalLots.take(3).forEach { lot -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(alertColor(lot.attentionStatus).copy(alpha = 0.14f)), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Outlined.Inventory2, contentDescription = null, tint = alertColor(lot.attentionStatus)) + } + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("${lot.lotCode} • ${lot.item}", fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) + Text(lot.reason, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Text( + text = lot.availableQty, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + } + } +} + +@Composable +private fun DistributionSection(items: List) { + Surface( + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + Text("Distribusi Stok Grade", style = MaterialTheme.typography.titleMedium) + if (items.isEmpty()) { + Text( + text = "Belum ada distribusi grade yang bisa dihitung.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } else { + items.forEachIndexed { index, item -> + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(item.grade, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold) + Text( + "${formatQuantity(item.quantity)} kg (${item.percentage}%)", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodySmall, + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + ) { + Box( + modifier = Modifier + .fillMaxWidth(item.percentage.coerceIn(3, 100) / 100f) + .height(8.dp) + .clip(CircleShape) + .background( + if (index == 0) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.primary.copy(alpha = 0.62f), + ), + ) + } + } + } + } + } + } +} + +@Composable +private fun BottomBar( + items: List, + currentModule: String, + onModuleSelected: (String) -> Unit, +) { + Surface(shadowElevation = 8.dp, color = MaterialTheme.colorScheme.surface) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + ) { + items.forEach { module -> + val active = module == currentModule + Surface( + color = if (active) MaterialTheme.colorScheme.primaryContainer else Color.Transparent, + shape = RoundedCornerShape(999.dp), + modifier = Modifier.clip(RoundedCornerShape(999.dp)).clickable { onModuleSelected(module) }, + ) { + Column( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = moduleIcon(module), + contentDescription = null, + tint = if (active) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = moduleLabel(module), + style = MaterialTheme.typography.labelSmall, + color = if (active) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun LotsModuleScreen( + role: String, + state: LotsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onQueryChanged: (String) -> Unit, + onScanInputChanged: (String) -> Unit, + onScan: () -> Unit, + onCameraScan: (String) -> Unit, + onOpenLotDetail: (String) -> Unit, + onCloseLotDetail: () -> Unit, + onOpenRecentScan: (LotScanResult) -> Unit, +) { + var activeTab by remember { mutableStateOf("inventory") } + + when { + state.selectedLotId != null -> { + LotDetailScreen( + detail = state.lotDetail, + isLoading = state.isLoadingDetail, + inlineError = state.inlineError, + modifier = modifier, + onBack = onCloseLotDetail, + ) + } + + activeTab == "scan" -> { + LotScanScreen( + role = role, + state = state, + modifier = modifier, + onTabChanged = { activeTab = it }, + onScanInputChanged = onScanInputChanged, + onScan = onScan, + onCameraScan = onCameraScan, + onOpenRecentScan = onOpenRecentScan, + ) + } + + else -> { + LotInventoryScreen( + state = state, + modifier = modifier, + onTabChanged = { activeTab = it }, + onRefresh = onRefresh, + onQueryChanged = onQueryChanged, + onOpenLotDetail = onOpenLotDetail, + ) + } + } +} + +@Composable +private fun WashingModuleScreen( + role: String, + state: WashingUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenEdit: (String) -> Unit, + onOpenComplete: (String) -> Unit, + onBack: () -> Unit, + onSelectLot: (String) -> Unit, + onSelectPlace: (String) -> Unit, + onUpdateCost: (String) -> Unit, + onUpdateDuration: (String) -> Unit, + onUpdateAfterQty: (String) -> Unit, + onSelectGrade: (String?) -> Unit, + onSelectWarehouse: (String) -> Unit, + onSelectWarehouseLocation: (String?) -> Unit, + onSave: () -> Unit, + onComplete: () -> Unit, +) { + when (state.screen) { + WashingScreen.List -> WashingListScreen( + role = role, + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenCreate = onOpenCreate, + onOpenEdit = onOpenEdit, + onOpenComplete = onOpenComplete, + ) + + WashingScreen.Create, + WashingScreen.Edit, + -> WashingFormScreen( + state = state, + modifier = modifier, + onBack = onBack, + onSelectLot = onSelectLot, + onSelectPlace = onSelectPlace, + onUpdateCost = onUpdateCost, + onUpdateDuration = onUpdateDuration, + onSave = onSave, + ) + + WashingScreen.Complete -> WashingCompleteScreen( + state = state, + modifier = modifier, + onBack = onBack, + onUpdateAfterQty = onUpdateAfterQty, + onSelectGrade = onSelectGrade, + onSelectWarehouse = onSelectWarehouse, + onSelectWarehouseLocation = onSelectWarehouseLocation, + onComplete = onComplete, + ) + } +} + +@Composable +private fun ReceiptsModuleScreen( + state: ReceiptsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onBack: () -> Unit, + onSelectPurchase: (String) -> Unit, + onUpdateReceiptDate: (String) -> Unit, + onUpdateReceiptNotes: (String) -> Unit, + onUpdateQtyReceived: (Int, String) -> Unit, + onUpdateQtyAccepted: (Int, String) -> Unit, + onUpdateQtyRejected: (Int, String) -> Unit, + onUpdateUnitCost: (Int, String) -> Unit, + onUpdateWarehouse: (Int, String) -> Unit, + onUpdateLocation: (Int, String) -> Unit, + onUpdateLineNotes: (Int, String) -> Unit, + onOpenDetail: (String) -> Unit, + onSave: () -> Unit, + onGenerateLots: () -> Unit, +) { + when (state.screen) { + ReceiptsScreen.List -> ReceiptsListScreen( + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenCreate = onOpenCreate, + onOpenDetail = onOpenDetail, + ) + + ReceiptsScreen.Create -> ReceiptCreateScreen( + state = state, + modifier = modifier, + onBack = onBack, + onSelectPurchase = onSelectPurchase, + onUpdateReceiptDate = onUpdateReceiptDate, + onUpdateReceiptNotes = onUpdateReceiptNotes, + onUpdateQtyReceived = onUpdateQtyReceived, + onUpdateQtyAccepted = onUpdateQtyAccepted, + onUpdateQtyRejected = onUpdateQtyRejected, + onUpdateUnitCost = onUpdateUnitCost, + onUpdateWarehouse = onUpdateWarehouse, + onUpdateLocation = onUpdateLocation, + onUpdateLineNotes = onUpdateLineNotes, + onSave = onSave, + ) + + ReceiptsScreen.Detail -> ReceiptDetailScreen( + state = state, + modifier = modifier, + onBack = onBack, + onGenerateLots = onGenerateLots, + ) + } +} + +@Composable +private fun ReceiptsListScreen( + state: ReceiptsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Receipts", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + "Buat penerimaan dari purchase yang sudah submitted lalu generate lot hasil terima.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Button( + onClick = onOpenCreate, + shape = RoundedCornerShape(16.dp), + ) { + Icon(Icons.Outlined.Add, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("Baru") + } + } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Ringkasan Receipt", style = MaterialTheme.typography.titleMedium) + Text( + "${state.receipts.size} receipt • ${state.purchases.size} purchase siap diterima", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh receipts") + } + } + } + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + if (state.isLoading && state.receipts.isEmpty()) { + item { LoadingCard("Memuat receipt...") } + } else if (state.receipts.isEmpty()) { + item { EmptyStateCard("Belum ada receipt. Buat receipt pertama dari purchase yang sudah submitted.") } + } else { + items(state.receipts, key = { it.id }) { item -> + ReceiptListCard(item = item, onClick = { onOpenDetail(item.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun ReceiptCreateScreen( + state: ReceiptsUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onSelectPurchase: (String) -> Unit, + onUpdateReceiptDate: (String) -> Unit, + onUpdateReceiptNotes: (String) -> Unit, + onUpdateQtyReceived: (Int, String) -> Unit, + onUpdateQtyAccepted: (Int, String) -> Unit, + onUpdateQtyRejected: (Int, String) -> Unit, + onUpdateUnitCost: (Int, String) -> Unit, + onUpdateWarehouse: (Int, String) -> Unit, + onUpdateLocation: (Int, String) -> Unit, + onUpdateLineNotes: (Int, String) -> Unit, + onSave: () -> Unit, +) { + var purchaseExpanded by remember { mutableStateOf(false) } + val selectedPurchase = remember(state.purchases, state.selectedPurchaseId) { + state.purchases.find { it.id == state.selectedPurchaseId } + } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Buat Receipt", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text("Purchase", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (state.purchases.isEmpty()) { + EmptyStateCard("Belum ada purchase submitted yang siap dibuat receipt.") + } else { + Box(modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { purchaseExpanded = true }, + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Pilih Purchase", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + selectedPurchase?.purchaseNo ?: "Tap untuk memilih purchase", + color = if (selectedPurchase != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = if (selectedPurchase != null) FontWeight.SemiBold else FontWeight.Normal, + ) + selectedPurchase?.supplierName?.let { + Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + } + DropdownMenu( + expanded = purchaseExpanded, + onDismissRequest = { purchaseExpanded = false }, + modifier = Modifier.fillMaxWidth(0.92f), + ) { + state.purchases.forEach { purchase -> + DropdownMenuItem( + text = { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(purchase.purchaseNo, fontWeight = FontWeight.SemiBold) + Text( + listOfNotNull(purchase.supplierName, purchase.purchaseDate).joinToString(" • "), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + onClick = { + onSelectPurchase(purchase.id) + purchaseExpanded = false + }, + ) + } + } + } + } + OutlinedTextField( + value = state.receiptDate, + onValueChange = onUpdateReceiptDate, + modifier = Modifier.fillMaxWidth(), + label = { Text("Tanggal Receipt") }, + placeholder = { Text("2026-05-20") }, + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + OutlinedTextField( + value = state.notes, + onValueChange = onUpdateReceiptNotes, + modifier = Modifier.fillMaxWidth(), + label = { Text("Catatan") }, + minLines = 3, + shape = RoundedCornerShape(16.dp), + ) + } + } + } + + if (state.lines.isEmpty()) { + item { EmptyStateCard("Pilih purchase untuk memuat line receipt.") } + } else { + items(state.lines.indices.toList(), key = { it }) { index -> + ReceiptLineFormCard( + line = state.lines[index], + warehouses = state.warehouses, + onQtyReceived = { onUpdateQtyReceived(index, it) }, + onQtyAccepted = { onUpdateQtyAccepted(index, it) }, + onQtyRejected = { onUpdateQtyRejected(index, it) }, + onUnitCost = { onUpdateUnitCost(index, it) }, + onWarehouse = { onUpdateWarehouse(index, it) }, + onLocation = { onUpdateLocation(index, it) }, + onNotes = { onUpdateLineNotes(index, it) }, + ) + } + } + + item { + Button( + onClick = onSave, + enabled = !state.isSaving, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isSaving) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Icon(Icons.Outlined.ReceiptLong, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Simpan Receipt") + } + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } +} + +@Composable +private fun ReceiptDetailScreen( + state: ReceiptsUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onGenerateLots: () -> Unit, +) { + val detail = state.selectedReceipt + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail receipt...") + } + + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail receipt belum tersedia.") + } + + else -> { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Receipt", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { ReceiptDetailHeader(detail = detail, isGeneratingLots = state.isGeneratingLots, onGenerateLots = onGenerateLots) } + item { ReceiptLinesSection(lines = detail.lines) } + item { ReceiptGeneratedLotsSection(lots = detail.generatedLots) } + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } + } +} + +@Composable +private fun ReceiptListCard( + item: ReceiptListItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("RECEIPT", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.receiptNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("PURCHASE", item.purchase.purchaseNo ?: "-") + LotMetaCell("TANGGAL", item.receiptDate, alignEnd = true) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("LINES", item.lineCount.toString(), emphasize = true) + LotMetaCell("LOTS", item.lotCount.toString(), alignEnd = true, emphasize = true) + } + } + } +} + +@Composable +private fun ReceiptLineFormCard( + line: ReceiptLineFormState, + warehouses: List, + onQtyReceived: (String) -> Unit, + onQtyAccepted: (String) -> Unit, + onQtyRejected: (String) -> Unit, + onUnitCost: (String) -> Unit, + onWarehouse: (String) -> Unit, + onLocation: (String) -> Unit, + onNotes: (String) -> Unit, +) { + var warehouseExpanded by remember { mutableStateOf(false) } + var locationExpanded by remember { mutableStateOf(false) } + val selectedWarehouse = warehouses.find { it.id == line.warehouseId } + val selectedLocation = selectedWarehouse?.locations?.find { it.id == line.warehouseLocationId } + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text(line.gradeName, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Text("Qty order: ${line.qtyOrdered} ${line.unitCode}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + OutlinedTextField( + value = line.qtyReceived, + onValueChange = onQtyReceived, + modifier = Modifier.weight(1f), + label = { Text("Received") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = RoundedCornerShape(14.dp), + ) + OutlinedTextField( + value = line.qtyAccepted, + onValueChange = onQtyAccepted, + modifier = Modifier.weight(1f), + label = { Text("Accepted") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = RoundedCornerShape(14.dp), + ) + } + + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + OutlinedTextField( + value = line.qtyRejected, + onValueChange = onQtyRejected, + modifier = Modifier.weight(1f), + label = { Text("Rejected") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = RoundedCornerShape(14.dp), + ) + OutlinedTextField( + value = line.unitCost, + onValueChange = onUnitCost, + modifier = Modifier.weight(1f), + label = { Text("Unit Cost") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = RoundedCornerShape(14.dp), + ) + } + + Box(modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .clickable { warehouseExpanded = true }, + shape = RoundedCornerShape(14.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(selectedWarehouse?.name ?: "Pilih gudang", color = if (selectedWarehouse != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant) + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + } + DropdownMenu(expanded = warehouseExpanded, onDismissRequest = { warehouseExpanded = false }, modifier = Modifier.fillMaxWidth(0.92f)) { + warehouses.forEach { warehouse -> + DropdownMenuItem( + text = { Text(warehouse.name) }, + onClick = { + onWarehouse(warehouse.id) + warehouseExpanded = false + }, + ) + } + } + } + + if (!selectedWarehouse?.locations.isNullOrEmpty()) { + Box(modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .clickable { locationExpanded = true }, + shape = RoundedCornerShape(14.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(selectedLocation?.name ?: "Pilih lokasi", color = if (selectedLocation != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant) + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + } + DropdownMenu(expanded = locationExpanded, onDismissRequest = { locationExpanded = false }, modifier = Modifier.fillMaxWidth(0.92f)) { + selectedWarehouse?.locations?.forEach { location -> + DropdownMenuItem( + text = { Text(location.name) }, + onClick = { + onLocation(location.id) + locationExpanded = false + }, + ) + } + } + } + } + + OutlinedTextField( + value = line.notes, + onValueChange = onNotes, + modifier = Modifier.fillMaxWidth(), + label = { Text("Catatan Line") }, + minLines = 2, + shape = RoundedCornerShape(14.dp), + ) + } + } +} + +@Composable +private fun ReceiptDetailHeader( + detail: ReceiptDetail, + isGeneratingLots: Boolean, + onGenerateLots: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("RECEIPT", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(detail.receiptNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = detail.status) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("PURCHASE", detail.purchase.purchaseNo ?: "-") + LotMetaCell("TANGGAL", detail.receiptDate, alignEnd = true) + } + if (!detail.notes.isNullOrBlank()) { + Text(detail.notes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + if (detail.status != "FINALIZED") { + Button( + onClick = onGenerateLots, + enabled = !isGeneratingLots, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + if (isGeneratingLots) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Icon(Icons.Outlined.Inventory2, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Generate Lots") + } + } + } + } + } +} + +@Composable +private fun ReceiptLinesSection(lines: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Receipt Lines", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (lines.isEmpty()) { + EmptyStateCard("Belum ada line receipt.") + } else { + lines.forEach { line -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(line.grade?.name ?: "-", fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("RECEIVED", "${formatQuantity(line.qtyReceived)} ${line.unit.code}", emphasize = true) + LotMetaCell("ACCEPTED", "${formatQuantity(line.qtyAccepted)} ${line.unit.code}", alignEnd = true, emphasize = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("REJECTED", "${formatQuantity(line.qtyRejected)} ${line.unit.code}") + LotMetaCell("UNIT COST", formatCurrency(line.unitCost), alignEnd = true) + } + Text( + listOfNotNull(line.warehouse?.name, line.location?.name).joinToString(" • ").ifBlank { "-" }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun ReceiptGeneratedLotsSection(lots: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Generated Lots", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (lots.isEmpty()) { + EmptyStateCard("Belum ada lot yang dihasilkan dari receipt ini.") + } else { + lots.forEach { lot -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(lot.lotCode, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.SemiBold) + StatusPill(status = lot.status) + } + } + } + } + } +} + +@Composable +private fun StockAdjustmentsModuleScreen( + state: StockAdjustmentsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onSelectLot: (String) -> Unit, + onSelectReason: (String) -> Unit, + onUpdateDate: (String) -> Unit, + onUpdateQty: (String) -> Unit, + onUpdateNotes: (String) -> Unit, + onSave: () -> Unit, +) { + var lotQuery by remember(state.selectedLotId, state.selectableLots) { + mutableStateOf(state.selectableLots.find { it.id == state.selectedLotId }?.lotCode.orEmpty()) + } + var reasonExpanded by remember { mutableStateOf(false) } + val selectedLot = remember(state.selectableLots, state.selectedLotId) { + state.selectableLots.find { it.id == state.selectedLotId } + } + val selectedReason = remember(state.reasons, state.selectedReasonId) { + state.reasons.find { it.id == state.selectedReasonId } + } + val filteredLots = remember(state.selectableLots, lotQuery) { + val query = lotQuery.trim().lowercase() + if (query.isBlank()) { + emptyList() + } else { + state.selectableLots.filter { + it.lotCode.lowercase().contains(query) || + it.supplier.lowercase().contains(query) || + it.grade.lowercase().contains(query) || + it.warehouse.lowercase().contains(query) || + it.location.lowercase().contains(query) + }.take(8) + } + } + val isSearchingLots = remember(lotQuery, selectedLot?.lotCode) { + val query = lotQuery.trim() + query.isNotBlank() && !query.equals(selectedLot?.lotCode.orEmpty(), ignoreCase = true) + } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Stock Adjustment", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + "Catat penambahan, shrinkage, atau koreksi stok langsung dari lot aktif.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh stock adjustments") + } + } + } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${state.items.size} adjustment") }) + AssistChip(onClick = {}, label = { Text("${state.selectableLots.size} lot aktif") }) + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text("Buat Adjustment", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OutlinedTextField( + value = lotQuery, + onValueChange = { lotQuery = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Cari Lot") }, + placeholder = { Text("Kode lot / supplier / grade") }, + leadingIcon = { Icon(Icons.Outlined.Search, contentDescription = null) }, + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + when { + state.selectableLots.isEmpty() -> EmptyStateCard("Belum ada lot aktif yang bisa di-adjust.") + lotQuery.isBlank() -> EmptyStateCard("Ketik kata kunci untuk mencari lot yang akan di-adjust.") + selectedLot != null && !isSearchingLots -> {} + filteredLots.isEmpty() -> EmptyStateCard("Tidak ada lot yang cocok dengan pencarian.") + else -> { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + filteredLots.forEach { lot -> + Surface( + shape = RoundedCornerShape(16.dp), + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + 1.dp, + if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, + ), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { + onSelectLot(lot.id) + lotQuery = lot.lotCode + }, + ) { + Column( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + lot.lotCode, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + Text( + "${formatQuantity(lot.availableQty)} ${lot.unitCode}", + style = MaterialTheme.typography.bodySmall, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, + ) + } + Text( + "${lot.supplier} • ${lot.grade}", + style = MaterialTheme.typography.bodySmall, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.86f) else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } + } + + selectedLot?.let { lot -> + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primary, + ) { + Column( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text(lot.lotCode, color = MaterialTheme.colorScheme.onPrimary, fontWeight = FontWeight.SemiBold) + Text( + "${lot.supplier} • ${lot.grade} • ${formatQuantity(lot.availableQty)} ${lot.unitCode}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.86f), + ) + Text( + "${lot.warehouse} • ${lot.location}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.86f), + ) + } + } + } + + Box(modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { reasonExpanded = true }, + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Alasan Adjustment", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + selectedReason?.let { "${it.code} - ${it.name}" } ?: "Pilih alasan adjustment", + color = if (selectedReason != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + fontWeight = if (selectedReason != null) FontWeight.SemiBold else FontWeight.Normal, + ) + } + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + } + DropdownMenu( + expanded = reasonExpanded, + onDismissRequest = { reasonExpanded = false }, + modifier = Modifier.fillMaxWidth(0.92f), + ) { + state.reasons.forEach { reason -> + DropdownMenuItem( + text = { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text("${reason.code} - ${reason.name}", fontWeight = FontWeight.SemiBold) + Text(reason.category, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + }, + onClick = { + onSelectReason(reason.id) + reasonExpanded = false + }, + ) + } + } + } + + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + OutlinedTextField( + value = state.adjustmentDate, + onValueChange = onUpdateDate, + modifier = Modifier.weight(1f), + label = { Text("Tanggal") }, + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + OutlinedTextField( + value = state.qtyChange, + onValueChange = onUpdateQty, + modifier = Modifier.weight(1f), + label = { Text("Qty +/-") }, + placeholder = { Text("-0.500 atau 1.250") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(16.dp), + ) + } + + OutlinedTextField( + value = state.notes, + onValueChange = onUpdateNotes, + modifier = Modifier.fillMaxWidth(), + label = { Text("Catatan") }, + minLines = 3, + shape = RoundedCornerShape(16.dp), + ) + + Button( + onClick = onSave, + enabled = !state.isSaving, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isSaving) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Icon(Icons.Outlined.ShowChart, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(8.dp)) + Text("Simpan Adjustment") + } + } + } + } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat riwayat stock adjustment...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada stock adjustment untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + StockAdjustmentCard(item = item) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun RegularSalesModuleScreen( + role: String, + state: SalesRegularUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, + onUpdateCloseDate: (String) -> Unit, + onUpdateQtySold: (Int, String) -> Unit, + onUpdateQtyReturned: (Int, String) -> Unit, + onUpdatePriceActual: (Int, String) -> Unit, + onCloseSale: () -> Unit, +) { + if (state.selectedSaleId != null || state.selectedSale != null) { + RegularSaleDetailScreen( + role = role, + state = state, + modifier = modifier, + onBack = onBack, + onUpdateCloseDate = onUpdateCloseDate, + onUpdateQtySold = onUpdateQtySold, + onUpdateQtyReturned = onUpdateQtyReturned, + onUpdatePriceActual = onUpdatePriceActual, + onCloseSale = onCloseSale, + ) + } else { + RegularSalesListScreen( + role = role, + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenDetail = onOpenDetail, + ) + } +} + +@Composable +private fun RegularSalesListScreen( + role: String, + state: SalesRegularUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Sales Regular", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + if (role == "SALES") "Pantau penjualan reguler lalu tutup transaksi saat qty aktual dan retur sudah final." + else "Lihat transaksi regular sale yang sedang berjalan beserta total penjualannya.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh sales") + } + } + } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${state.items.size} sales") }) + AssistChip(onClick = {}, label = { Text("${state.items.count { it.status != "CLOSED" }} open") }) + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat daftar regular sale...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada regular sale untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + RegularSaleListCard(item = item, onClick = { onOpenDetail(item.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun RegularSaleListCard( + item: RegularSaleListItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("REGULAR SALE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.saleNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("BUYER", item.buyer.name) + LotMetaCell("TANGGAL", item.saleDate, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("ITEM", item.itemCount.toString(), emphasize = true) + LotMetaCell("TOTAL", formatCurrency(item.totalNominalBuyer), alignEnd = true, emphasize = true) + } + if (item.totalAgentCommission > 0) { + Text( + "Komisi agen ${formatCurrency(item.totalAgentCommission)}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Composable +private fun RegularSaleDetailScreen( + role: String, + state: SalesRegularUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onUpdateCloseDate: (String) -> Unit, + onUpdateQtySold: (Int, String) -> Unit, + onUpdateQtyReturned: (Int, String) -> Unit, + onUpdatePriceActual: (Int, String) -> Unit, + onCloseSale: () -> Unit, +) { + val detail = state.selectedSale + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail regular sale...") + } + + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail regular sale belum tersedia.") + } + + else -> { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Sales", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("REGULAR SALE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(detail.saleNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = detail.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("BUYER", detail.buyer.name) + LotMetaCell("TANGGAL", detail.saleDate, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("COURIER", detail.courier?.name ?: "-") + LotMetaCell("CLOSE", detail.closeDate ?: "-", alignEnd = true) + } + if (!detail.notes.isNullOrBlank()) { + Text(detail.notes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } + + item { + RegularSaleSummarySection(detail = detail) + } + + item { + RegularSaleLinesSection(lines = detail.lines) + } + + if (role == "SALES" && detail.status != "CLOSED") { + item { + RegularSaleCloseSection( + state = state, + lines = detail.lines, + onUpdateCloseDate = onUpdateCloseDate, + onUpdateQtySold = onUpdateQtySold, + onUpdateQtyReturned = onUpdateQtyReturned, + onUpdatePriceActual = onUpdatePriceActual, + onCloseSale = onCloseSale, + ) + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } + } +} + +@Composable +private fun RegularSaleSummarySection(detail: RegularSaleDetail) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Ringkasan Penjualan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TOTAL BUYER", formatCurrency(detail.totalNominalBuyer), emphasize = true) + LotMetaCell("TOTAL COMPANY", formatCurrency(detail.totalNominalCompany), alignEnd = true, emphasize = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SHIPPING", formatCurrency(detail.shippingCostBuyer)) + LotMetaCell("KOMISI", formatCurrency(detail.totalAgentCommission), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("CURR", detail.buyerCurrencyCode) + LotMetaCell("RATE", detail.exchangeRate?.toString() ?: "-", alignEnd = true) + } + } + } +} + +@Composable +private fun RegularSaleLinesSection(lines: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Line Penjualan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (lines.isEmpty()) { + EmptyStateCard("Belum ada line penjualan.") + } else { + lines.forEach { line -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(line.lotCode, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("GRADE", line.grade) + LotMetaCell("WAREHOUSE", line.warehouse, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("PLANNED", "${formatQuantity(line.qtyPlanned)} ${line.unitCode}", emphasize = true) + LotMetaCell("CURRENT", "${formatQuantity(line.currentAvailableQty)} ${line.unitCode}", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("PRICE PLAN", formatCurrency(line.sellingPricePlanned)) + LotMetaCell("UNIT COST", formatCurrency(line.unitCost), alignEnd = true) + } + Text( + listOfNotNull(line.location, line.agentName?.let { "Agen: $it" }).joinToString(" • ").ifBlank { "-" }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun RegularSaleCloseSection( + state: SalesRegularUiState, + lines: List, + onUpdateCloseDate: (String) -> Unit, + onUpdateQtySold: (Int, String) -> Unit, + onUpdateQtyReturned: (Int, String) -> Unit, + onUpdatePriceActual: (Int, String) -> Unit, + onCloseSale: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text("Tutup Penjualan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OutlinedTextField( + value = state.closeDate, + onValueChange = onUpdateCloseDate, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Tanggal Close") }, + placeholder = { Text("YYYY-MM-DD") }, + shape = RoundedCornerShape(14.dp), + ) + state.closeLines.forEachIndexed { index, line -> + val sourceLine = lines.getOrNull(index) + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(line.lotCode, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) + Text( + "Planned ${formatQuantity(line.qtyPlanned)} ${sourceLine?.unitCode ?: "kg"}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + OutlinedTextField( + value = line.qtyActualSold, + onValueChange = { onUpdateQtySold(index, it) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Qty Terjual Aktual") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + OutlinedTextField( + value = line.qtyReturned, + onValueChange = { onUpdateQtyReturned(index, it) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Qty Retur") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + OutlinedTextField( + value = line.sellingPriceActual, + onValueChange = { onUpdatePriceActual(index, it) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Harga Jual Aktual") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + } + } + } + Button( + onClick = onCloseSale, + enabled = !state.isClosing, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isClosing) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Tutup Penjualan") + } + } + } + } +} + +@Composable +private fun JitSalesModuleScreen( + role: String, + state: SalesJitUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, + onUpdateCloseDate: (String) -> Unit, + onUpdatePriceActual: (Int, String) -> Unit, + onCloseSale: () -> Unit, +) { + if (state.selectedSaleId != null || state.selectedSale != null) { + JitSaleDetailScreen( + role = role, + state = state, + modifier = modifier, + onBack = onBack, + onUpdateCloseDate = onUpdateCloseDate, + onUpdatePriceActual = onUpdatePriceActual, + onCloseSale = onCloseSale, + ) + } else { + JitSalesListScreen( + role = role, + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenDetail = onOpenDetail, + ) + } +} + +@Composable +private fun JitSalesListScreen( + role: String, + state: SalesJitUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Sales JIT", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + if (role == "SALES") "Pantau penjualan just in time dan tutup saat harga jual aktual sudah final." + else "Lihat transaksi just in time yang sedang berjalan beserta nilainya.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh jit sales") + } + } + } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${state.items.size} jit sales") }) + AssistChip(onClick = {}, label = { Text("${state.items.count { it.status != "CLOSED" }} open") }) + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat daftar sales JIT...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada sales JIT untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + JitSaleListCard(item = item, onClick = { onOpenDetail(item.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun JitSaleListCard( + item: JitSaleListItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("JIT SALE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.saleNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("BUYER", item.buyer.name) + LotMetaCell("TANGGAL", item.saleDate, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("ITEM", item.itemCount.toString(), emphasize = true) + LotMetaCell("TOTAL", formatCurrency(item.totalNominalBuyer), alignEnd = true, emphasize = true) + } + if (item.totalAgentCommission > 0) { + Text( + "Komisi agen ${formatCurrency(item.totalAgentCommission)}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Composable +private fun JitSaleDetailScreen( + role: String, + state: SalesJitUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onUpdateCloseDate: (String) -> Unit, + onUpdatePriceActual: (Int, String) -> Unit, + onCloseSale: () -> Unit, +) { + val detail = state.selectedSale + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail sales JIT...") + } + + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail sales JIT belum tersedia.") + } + + else -> { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Sales JIT", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("JIT SALE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(detail.saleNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = detail.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("BUYER", detail.buyer.name) + LotMetaCell("TANGGAL", detail.saleDate, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("COURIER", detail.courier?.name ?: "-") + LotMetaCell("CLOSE", detail.closeDate ?: "-", alignEnd = true) + } + } + } + } + + item { JitSaleSummarySection(detail = detail) } + item { JitSaleLinesSection(lines = detail.lines) } + + if (role == "SALES" && detail.status != "CLOSED") { + item { + JitSaleCloseSection( + state = state, + onUpdateCloseDate = onUpdateCloseDate, + onUpdatePriceActual = onUpdatePriceActual, + onCloseSale = onCloseSale, + ) + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } + } +} + +@Composable +private fun JitSaleSummarySection(detail: JitSaleDetail) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Ringkasan Penjualan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TOTAL BUYER", formatCurrency(detail.totalNominalBuyer), emphasize = true) + LotMetaCell("TOTAL COMPANY", formatCurrency(detail.totalNominalCompany), alignEnd = true, emphasize = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SHIPPING", formatCurrency(detail.shippingCostBuyer)) + LotMetaCell("KOMISI", formatCurrency(detail.totalAgentCommission), alignEnd = true) + } + } + } +} + +@Composable +private fun JitSaleLinesSection(lines: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Line Penjualan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (lines.isEmpty()) { + EmptyStateCard("Belum ada line penjualan.") + } else { + lines.forEach { line -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(line.grade.name, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("QTY", "${formatQuantity(line.qtyPlanned)} kg", emphasize = true) + LotMetaCell("MAL", formatCurrency(line.malUnitPrice), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("PLAN", formatCurrency(line.sellingPricePlanned)) + LotMetaCell("ACTUAL", line.sellingPriceActual?.let(::formatCurrency) ?: "-", alignEnd = true) + } + Text( + listOfNotNull(line.agentName, line.profitShareScheme?.name).joinToString(" • ").ifBlank { "-" }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun JitSaleCloseSection( + state: SalesJitUiState, + onUpdateCloseDate: (String) -> Unit, + onUpdatePriceActual: (Int, String) -> Unit, + onCloseSale: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text("Tutup Penjualan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OutlinedTextField( + value = state.closeDate, + onValueChange = onUpdateCloseDate, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Tanggal Close") }, + placeholder = { Text("YYYY-MM-DD") }, + shape = RoundedCornerShape(14.dp), + ) + state.closeLines.forEachIndexed { index, line -> + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(line.gradeName, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) + Text( + "Qty ${formatQuantity(line.qtyPlanned)} kg", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + OutlinedTextField( + value = line.sellingPriceActual, + onValueChange = { onUpdatePriceActual(index, it) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Harga Jual Aktual") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + } + } + } + Button( + onClick = onCloseSale, + enabled = !state.isClosing, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isClosing) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Tutup Penjualan") + } + } + } + } +} + +@Composable +private fun ConsignmentsModuleScreen( + role: String, + state: ConsignmentsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, + onSelectLine: (String?) -> Unit, + onUpdateCloseDate: (String) -> Unit, + onUpdateSellingPrice: (String) -> Unit, + onUpdateQtySold: (String) -> Unit, + onUpdateQtyReturned: (String) -> Unit, + onUpdateSalesCommission: (String) -> Unit, + onCloseLine: () -> Unit, +) { + if (state.selectedConsignmentId != null || state.selectedConsignment != null) { + ConsignmentDetailScreen( + role = role, + state = state, + modifier = modifier, + onBack = onBack, + onSelectLine = onSelectLine, + onUpdateCloseDate = onUpdateCloseDate, + onUpdateSellingPrice = onUpdateSellingPrice, + onUpdateQtySold = onUpdateQtySold, + onUpdateQtyReturned = onUpdateQtyReturned, + onUpdateSalesCommission = onUpdateSalesCommission, + onCloseLine = onCloseLine, + ) + } else { + ConsignmentsListScreen( + role = role, + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenDetail = onOpenDetail, + ) + } +} + +@Composable +private fun ConsignmentsListScreen( + role: String, + state: ConsignmentsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Titip Jual", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + if (role == "SALES") "Pantau barang titip jual dan tutup tiap item saat hasil penjualan sudah final." + else "Lihat transaksi titip jual beserta jumlah item yang masih terbuka.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh consignments") + } + } + } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${state.items.size} consignments") }) + AssistChip(onClick = {}, label = { Text("${state.items.sumOf { it.openItemCount }} open items") }) + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat daftar titip jual...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada transaksi titip jual untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + ConsignmentListCard(item = item, onClick = { onOpenDetail(item.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun ConsignmentListCard( + item: ConsignmentListItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("TITIP JUAL", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.consignmentNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SALES", item.sales.name) + LotMetaCell("BUYER", item.buyer.name, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("ITEM", "${item.openItemCount}/${item.itemCount}", emphasize = true) + LotMetaCell("TITIP", "${formatQuantity(item.totalQtyConsigned)} kg", alignEnd = true, emphasize = true) + } + } + } +} + +@Composable +private fun ConsignmentDetailScreen( + role: String, + state: ConsignmentsUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onSelectLine: (String?) -> Unit, + onUpdateCloseDate: (String) -> Unit, + onUpdateSellingPrice: (String) -> Unit, + onUpdateQtySold: (String) -> Unit, + onUpdateQtyReturned: (String) -> Unit, + onUpdateSalesCommission: (String) -> Unit, + onCloseLine: () -> Unit, +) { + val detail = state.selectedConsignment + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail titip jual...") + } + + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail titip jual belum tersedia.") + } + + else -> { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Titip Jual", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("TITIP JUAL", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(detail.consignmentNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = detail.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SALES", detail.sales.name) + LotMetaCell("BUYER", detail.buyer.name, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TANGGAL", detail.consignmentDate) + LotMetaCell("LINES", detail.lines.size.toString(), alignEnd = true) + } + } + } + } + + item { ConsignmentLinesSection(lines = detail.lines, onSelectLine = onSelectLine) } + + if (role == "SALES" && state.selectedLineId != null) { + item { + ConsignmentCloseSection( + state = state, + line = detail.lines.find { it.id == state.selectedLineId }, + onUpdateCloseDate = onUpdateCloseDate, + onUpdateSellingPrice = onUpdateSellingPrice, + onUpdateQtySold = onUpdateQtySold, + onUpdateQtyReturned = onUpdateQtyReturned, + onUpdateSalesCommission = onUpdateSalesCommission, + onCloseLine = onCloseLine, + ) + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } + } +} + +@Composable +private fun ConsignmentLinesSection( + lines: List, + onSelectLine: (String?) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Line Titip Jual", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (lines.isEmpty()) { + EmptyStateCard("Belum ada line titip jual.") + } else { + lines.forEach { line -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + modifier = Modifier + .fillMaxWidth() + .clickable { if (line.status != "CLOSED") onSelectLine(line.id) }, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Text(line.lotCode, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) + StatusPill(status = line.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("GRADE", line.grade) + LotMetaCell("TITIP", "${formatQuantity(line.qtyConsigned)} ${line.unitCode}", alignEnd = true, emphasize = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SOLD", "${formatQuantity(line.qtySold)} ${line.unitCode}") + LotMetaCell("RETURN", "${formatQuantity(line.qtyReturned)} ${line.unitCode}", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SELL", line.sellingPrice?.let(::formatCurrency) ?: "-") + LotMetaCell("COMM", formatCurrency(line.salesCommission), alignEnd = true) + } + } + } + } + } + } +} + +@Composable +private fun ConsignmentCloseSection( + state: ConsignmentsUiState, + line: ConsignmentLineDetail?, + onUpdateCloseDate: (String) -> Unit, + onUpdateSellingPrice: (String) -> Unit, + onUpdateQtySold: (String) -> Unit, + onUpdateQtyReturned: (String) -> Unit, + onUpdateSalesCommission: (String) -> Unit, + onCloseLine: () -> Unit, +) { + if (line == null) return + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text("Tutup Item Titip Jual", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Text("${line.lotCode} • ${formatQuantity(line.qtyConsigned)} ${line.unitCode}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + OutlinedTextField( + value = state.closeDate, + onValueChange = onUpdateCloseDate, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Tanggal Close") }, + shape = RoundedCornerShape(14.dp), + ) + OutlinedTextField( + value = state.sellingPrice, + onValueChange = onUpdateSellingPrice, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Harga Jual") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + OutlinedTextField( + value = state.qtySold, + onValueChange = onUpdateQtySold, + modifier = Modifier.weight(1f), + singleLine = true, + label = { Text("Qty Sold") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + OutlinedTextField( + value = state.qtyReturned, + onValueChange = onUpdateQtyReturned, + modifier = Modifier.weight(1f), + singleLine = true, + label = { Text("Qty Return") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + } + OutlinedTextField( + value = state.salesCommission, + onValueChange = onUpdateSalesCommission, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Komisi Sales") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + Button( + onClick = onCloseLine, + enabled = !state.isClosing, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isClosing) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Tutup Item") + } + } + } + } +} + +@Composable +private fun FundRequestsModuleScreen( + state: FundRequestsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onUpdateTransferType: (String) -> Unit, + onUpdateReferenceNo: (String) -> Unit, + onSelectAgent: (String) -> Unit, + onSelectAgentBank: (String) -> Unit, + onSelectCompanyBank: (String) -> Unit, + onUpdateAmount: (String) -> Unit, + onUpdateTransferredAt: (String) -> Unit, + onSave: () -> Unit, +) { + val selectedAgent = remember(state.agents, state.selectedAgentId) { + state.agents.find { it.id == state.selectedAgentId } + } + val agentBanks = selectedAgent?.bankAccounts.orEmpty() + var transferTypeExpanded by remember { mutableStateOf(false) } + var agentExpanded by remember { mutableStateOf(false) } + var agentBankExpanded by remember { mutableStateOf(false) } + var companyBankExpanded by remember { mutableStateOf(false) } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Permintaan Dana", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + "Catat transfer modal atau bagi hasil ke agen, lalu pantau riwayat pengirimannya.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh fund requests") + } + } + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text("Buat Permintaan Dana", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + + Box(modifier = Modifier.fillMaxWidth()) { + PickerField( + label = "Tipe Transfer", + value = if (state.transferType == "PROFIT_SHARE") "Bagi Hasil" else "Modal", + onClick = { transferTypeExpanded = true }, + ) + DropdownMenu(expanded = transferTypeExpanded, onDismissRequest = { transferTypeExpanded = false }) { + listOf("CAPITAL" to "Modal", "PROFIT_SHARE" to "Bagi Hasil").forEach { (code, label) -> + DropdownMenuItem( + text = { Text(label) }, + onClick = { + onUpdateTransferType(code) + transferTypeExpanded = false + }, + ) + } + } + } + + OutlinedTextField( + value = state.referenceNo, + onValueChange = onUpdateReferenceNo, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("No Referensi") }, + shape = RoundedCornerShape(14.dp), + ) + + Box(modifier = Modifier.fillMaxWidth()) { + PickerField( + label = "Agen", + value = selectedAgent?.let { "${it.code} · ${it.name}" } ?: "Pilih agen", + onClick = { agentExpanded = true }, + ) + DropdownMenu(expanded = agentExpanded, onDismissRequest = { agentExpanded = false }) { + state.agents.forEach { agent -> + DropdownMenuItem( + text = { + Column { + Text("${agent.code} · ${agent.name}", fontWeight = FontWeight.SemiBold) + Text( + "Profit ${formatCurrency(agent.profitShareBalance)} • Modal ${formatCurrency(agent.capitalBalance)}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + onClick = { + onSelectAgent(agent.id) + agentExpanded = false + }, + ) + } + } + } + + Box(modifier = Modifier.fillMaxWidth()) { + PickerField( + label = "Rekening Agen", + value = agentBanks.find { it.id == state.selectedAgentBankAccountId }?.let { "${it.bankName} · ${it.accountNumber}" } ?: "Pilih rekening agen", + onClick = { agentBankExpanded = true }, + ) + DropdownMenu(expanded = agentBankExpanded, onDismissRequest = { agentBankExpanded = false }) { + agentBanks.forEach { bank -> + DropdownMenuItem( + text = { Text("${bank.bankName} · ${bank.accountNumber}") }, + onClick = { + onSelectAgentBank(bank.id) + agentBankExpanded = false + }, + ) + } + } + } + + Box(modifier = Modifier.fillMaxWidth()) { + PickerField( + label = "Rekening Kantor", + value = state.companyBankAccounts.find { it.id == state.selectedCompanyBankAccountId }?.let { "${it.bankName} · ${it.accountNumber}" } ?: "Pilih rekening kantor", + onClick = { companyBankExpanded = true }, + ) + DropdownMenu(expanded = companyBankExpanded, onDismissRequest = { companyBankExpanded = false }) { + state.companyBankAccounts.forEach { bank -> + DropdownMenuItem( + text = { Text("${bank.bankName} · ${bank.accountNumber}") }, + onClick = { + onSelectCompanyBank(bank.id) + companyBankExpanded = false + }, + ) + } + } + } + + OutlinedTextField( + value = state.amount, + onValueChange = onUpdateAmount, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Nominal ${state.currencyCode}") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + shape = RoundedCornerShape(14.dp), + ) + + OutlinedTextField( + value = state.transferredAt, + onValueChange = onUpdateTransferredAt, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + label = { Text("Waktu Transfer") }, + placeholder = { Text("2026-05-21T09:30") }, + shape = RoundedCornerShape(14.dp), + ) + + Button( + onClick = onSave, + enabled = !state.isSaving, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isSaving) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Simpan Permintaan Dana") + } + } + } + } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${state.items.size} request") }) + AssistChip(onClick = {}, label = { Text(state.currencyCode) }) + } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat riwayat fund request...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada fund request untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + FundRequestCard(item = item) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun PickerField( + label: String, + value: String, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(value, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyMedium) + } + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + } +} + +@Composable +private fun FundRequestCard(item: FundRequestListItem) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("FUND REQUEST", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.requestNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TIPE", prettifyStatus(item.transferType)) + LotMetaCell("REFERENSI", item.referenceNo, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("AGEN", item.agent.name) + LotMetaCell("NOMINAL", formatCurrency(item.amount), alignEnd = true, emphasize = true) + } + Text( + "${item.agentBankAccount.bankName} ${item.agentBankAccount.accountNumber} • ${item.companyBankName} ${item.companyBankAccountNumber}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun PurchasesModuleScreen( + role: String, + state: PurchasesUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, + onOpenEdit: () -> Unit, + onCloseEditor: () -> Unit, + onUpdatePurchaseDate: (String) -> Unit, + onUpdateReceivedAt: (String) -> Unit, + onSelectEmployee: (String) -> Unit, + onSelectWarehouse: (String) -> Unit, + onSelectWarehouseLocation: (String?) -> Unit, + onUpdateNotes: (String) -> Unit, + onAddLine: () -> Unit, + onRemoveLine: (Int) -> Unit, + onSelectLineGrade: (Int, String?) -> Unit, + onUpdateLineQty: (Int, String) -> Unit, + onSelectLineUnit: (Int, String) -> Unit, + onUpdateLineUnitPrice: (Int, String) -> Unit, + onUpdateLineUnitCost: (Int, String) -> Unit, + onUpdateLineNotes: (Int, String) -> Unit, + onSave: () -> Unit, + onSubmit: () -> Unit, + onCancel: () -> Unit, +) { + when (state.screen) { + PurchasesScreen.Create, PurchasesScreen.Edit -> PurchaseEditorScreen( + state = state, + modifier = modifier, + onBack = onCloseEditor, + onUpdatePurchaseDate = onUpdatePurchaseDate, + onUpdateReceivedAt = onUpdateReceivedAt, + onSelectEmployee = onSelectEmployee, + onSelectWarehouse = onSelectWarehouse, + onSelectWarehouseLocation = onSelectWarehouseLocation, + onUpdateNotes = onUpdateNotes, + onAddLine = onAddLine, + onRemoveLine = onRemoveLine, + onSelectLineGrade = onSelectLineGrade, + onUpdateLineQty = onUpdateLineQty, + onSelectLineUnit = onSelectLineUnit, + onUpdateLineUnitPrice = onUpdateLineUnitPrice, + onUpdateLineUnitCost = onUpdateLineUnitCost, + onUpdateLineNotes = onUpdateLineNotes, + onSave = onSave, + ) + + PurchasesScreen.Detail -> { + PurchaseDetailScreen( + role = role, + state = state, + modifier = modifier, + onBack = onBack, + onEdit = onOpenEdit, + onSubmit = onSubmit, + onCancel = onCancel, + ) + } + + else -> { + PurchasesListScreen( + role = role, + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenCreate = onOpenCreate, + onOpenDetail = onOpenDetail, + ) + } + } +} + +@Composable +private fun PurchasesListScreen( + role: String, + state: PurchasesUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Purchases", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + if (role == "PURCHASING") "Pantau draft pembelian, lalu submit saat data sudah lengkap." + else "Lihat status pembelian dan grand total transaksi yang sedang berjalan.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + if (role == "PURCHASING") { + Button( + onClick = onOpenCreate, + shape = RoundedCornerShape(16.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 14.dp, vertical = 10.dp), + ) { + Icon(Icons.Outlined.Add, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("Baru") + } + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh purchases") + } + } + } + } + } + + item { + AssistChip(onClick = {}, label = { Text("${state.items.size} purchase") }) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat daftar purchase...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada purchase untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + PurchaseListCard(item = item, onClick = { onOpenDetail(item.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun PurchaseListCard( + item: PurchaseListItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("PURCHASE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.purchaseNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("AGEN", item.agent?.name ?: "-") + LotMetaCell("TANGGAL", item.purchaseDate, alignEnd = true) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("LINES", item.lineCount.toString(), emphasize = true) + LotMetaCell("TOTAL", formatCurrency(item.grandTotal), alignEnd = true, emphasize = true) + } + } + } +} + +@Composable +private fun PurchaseDetailScreen( + role: String, + state: PurchasesUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onEdit: () -> Unit, + onSubmit: () -> Unit, + onCancel: () -> Unit, +) { + val detail = state.selectedPurchase + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail purchase...") + } + + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail purchase belum tersedia.") + } + + else -> { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Purchase", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("PURCHASE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(detail.purchaseNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = detail.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("AGEN", detail.agent?.name ?: "-") + LotMetaCell("TANGGAL", detail.purchaseDate, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("RECEIVED", detail.receivedAt?.let(::formatApiDateTime) ?: "-") + LotMetaCell("LINES", detail.lines.size.toString(), alignEnd = true, emphasize = true) + } + if (!detail.notes.isNullOrBlank()) { + Text(detail.notes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } + + item { + PurchaseSummarySection(detail = detail) + } + + item { + PurchaseLinesSection(lines = detail.lines) + } + + if (role == "PURCHASING" && detail.status == "DRAFT") { + item { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Button( + onClick = onEdit, + enabled = !state.isActing, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + ) { + Text("Edit Draft") + } + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Button( + onClick = onCancel, + enabled = !state.isActing, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Text("Batalkan") + } + Button( + onClick = onSubmit, + enabled = !state.isActing, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isActing) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Submit") + } + } + } + } + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } + } +} + +@Composable +private fun PurchaseSummarySection(detail: PurchaseDetail) { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Ringkasan Biaya", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("SHIPPING", detail.shippingCost?.let(::formatCurrency) ?: "-") + LotMetaCell("OPERASIONAL", detail.incomingOperationalCost?.let(::formatCurrency) ?: "-", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("AFTER ARRIVAL", detail.afterArrivalOperationalCost?.let(::formatCurrency) ?: "-") + LotMetaCell( + "AVG PRICE", + detail.analysis?.averagePrice?.let(::formatCurrency) ?: "-", + alignEnd = true, + ) + } + detail.analysis?.let { analysis -> + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("WEIGHT BUY", analysis.weightBuy?.let(::formatQuantity) ?: "-") + LotMetaCell("WEIGHT FINAL", analysis.weightFinal?.let(::formatQuantity) ?: "-", alignEnd = true) + } + } + } + } +} + +@Composable +private fun PurchaseEditorScreen( + state: PurchasesUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onUpdatePurchaseDate: (String) -> Unit, + onUpdateReceivedAt: (String) -> Unit, + onSelectEmployee: (String) -> Unit, + onSelectWarehouse: (String) -> Unit, + onSelectWarehouseLocation: (String?) -> Unit, + onUpdateNotes: (String) -> Unit, + onAddLine: () -> Unit, + onRemoveLine: (Int) -> Unit, + onSelectLineGrade: (Int, String?) -> Unit, + onUpdateLineQty: (Int, String) -> Unit, + onSelectLineUnit: (Int, String) -> Unit, + onUpdateLineUnitPrice: (Int, String) -> Unit, + onUpdateLineUnitCost: (Int, String) -> Unit, + onUpdateLineNotes: (Int, String) -> Unit, + onSave: () -> Unit, +) { + var employeeExpanded by remember { mutableStateOf(false) } + var warehouseExpanded by remember { mutableStateOf(false) } + var locationExpanded by remember { mutableStateOf(false) } + val selectedWarehouse = state.warehouses.find { it.id == state.selectedWarehouseId } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = if (state.screen == PurchasesScreen.Edit) "Edit Purchase" else "Purchase Baru", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text("Header Purchase", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OutlinedTextField( + value = state.purchaseDate, + onValueChange = onUpdatePurchaseDate, + label = { Text("Tanggal Pembelian") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + OutlinedTextField( + value = state.receivedAt, + onValueChange = onUpdateReceivedAt, + label = { Text("Waktu Diterima") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + Box { + PickerField( + label = "Diterima Oleh", + value = state.employees.find { it.id == state.selectedEmployeeId }?.name ?: "Pilih karyawan", + onClick = { employeeExpanded = true }, + ) + DropdownMenu(expanded = employeeExpanded, onDismissRequest = { employeeExpanded = false }) { + state.employees.forEach { employee -> + DropdownMenuItem( + text = { Text(employee.name) }, + onClick = { + employeeExpanded = false + onSelectEmployee(employee.id) + }, + ) + } + } + } + Box { + PickerField( + label = "Gudang Default", + value = selectedWarehouse?.name ?: "Pilih gudang", + onClick = { warehouseExpanded = true }, + ) + DropdownMenu(expanded = warehouseExpanded, onDismissRequest = { warehouseExpanded = false }) { + state.warehouses.forEach { warehouse -> + DropdownMenuItem( + text = { Text(warehouse.name) }, + onClick = { + warehouseExpanded = false + onSelectWarehouse(warehouse.id) + }, + ) + } + } + } + if (selectedWarehouse?.locations?.isNotEmpty() == true) { + Box { + PickerField( + label = "Lokasi Default", + value = selectedWarehouse.locations.find { it.id == state.selectedWarehouseLocationId }?.name ?: "Pilih lokasi", + onClick = { locationExpanded = true }, + ) + DropdownMenu(expanded = locationExpanded, onDismissRequest = { locationExpanded = false }) { + selectedWarehouse.locations.forEach { location -> + DropdownMenuItem( + text = { Text(location.name) }, + onClick = { + locationExpanded = false + onSelectWarehouseLocation(location.id) + }, + ) + } + } + } + } + OutlinedTextField( + value = state.notes, + onValueChange = onUpdateNotes, + label = { Text("Catatan Purchase") }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + ) + } + } + } + + item { + SectionActionHeader( + title = "Line Purchase", + buttonLabel = "Tambah Line", + onClick = onAddLine, + ) + } + + items(state.lines.size, key = { "purchase-line-$it" }) { index -> + PurchaseLineEditor( + index = index, + line = state.lines[index], + grades = state.grades, + units = state.units, + warehouses = state.warehouses, + canRemove = state.lines.size > 1, + onRemove = { onRemoveLine(index) }, + onSelectGrade = { onSelectLineGrade(index, it) }, + onQtyChange = { onUpdateLineQty(index, it) }, + onSelectUnit = { onSelectLineUnit(index, it) }, + onUnitPriceChange = { onUpdateLineUnitPrice(index, it) }, + onUnitCostChange = { onUpdateLineUnitCost(index, it) }, + onNotesChange = { onUpdateLineNotes(index, it) }, + ) + } + + item { + Button( + onClick = onSave, + enabled = !state.isSaving, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 14.dp), + ) { + if (state.isSaving) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text(if (state.screen == PurchasesScreen.Edit) "Simpan Perubahan" else "Buat Draft Purchase") + } + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } +} + +@Composable +private fun PurchaseLineEditor( + index: Int, + line: PurchaseLineFormState, + grades: List, + units: List, + warehouses: List, + canRemove: Boolean, + onRemove: () -> Unit, + onSelectGrade: (String?) -> Unit, + onQtyChange: (String) -> Unit, + onSelectUnit: (String) -> Unit, + onUnitPriceChange: (String) -> Unit, + onUnitCostChange: (String) -> Unit, + onNotesChange: (String) -> Unit, +) { + var gradeExpanded by remember { mutableStateOf(false) } + var unitExpanded by remember { mutableStateOf(false) } + val warehouse = warehouses.find { it.id == line.selectedWarehouseId } + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text("Line ${index + 1}", style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary) + if (canRemove) { + Text("Hapus", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelLarge, modifier = Modifier.clickable(onClick = onRemove)) + } + } + + Box { + PickerField( + label = "Grade", + value = grades.find { it.id == line.selectedGradeId }?.name ?: "Pilih grade", + onClick = { gradeExpanded = true }, + ) + DropdownMenu(expanded = gradeExpanded, onDismissRequest = { gradeExpanded = false }) { + DropdownMenuItem( + text = { Text("Tanpa grade") }, + onClick = { + gradeExpanded = false + onSelectGrade(null) + }, + ) + grades.forEach { grade -> + DropdownMenuItem( + text = { Text(grade.name) }, + onClick = { + gradeExpanded = false + onSelectGrade(grade.id) + }, + ) + } + } + } + + OutlinedTextField( + value = line.qtyOrdered, + onValueChange = onQtyChange, + label = { Text("Qty") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + + Box { + PickerField( + label = "Unit", + value = units.find { it.id == line.selectedUnitId }?.let { item -> listOfNotNull(item.code, item.name).joinToString(" • ") } ?: "Pilih unit", + onClick = { unitExpanded = true }, + ) + DropdownMenu(expanded = unitExpanded, onDismissRequest = { unitExpanded = false }) { + units.forEach { unit -> + DropdownMenuItem( + text = { Text(listOfNotNull(unit.code, unit.name).joinToString(" • ")) }, + onClick = { + unitExpanded = false + onSelectUnit(unit.id) + }, + ) + } + } + } + + OutlinedTextField( + value = line.unitPrice, + onValueChange = onUnitPriceChange, + label = { Text("Harga Beli") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + OutlinedTextField( + value = line.unitCost, + onValueChange = onUnitCostChange, + label = { Text("Unit Cost") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + if (warehouse != null) { + Text( + "Gudang line mengikuti default: ${listOfNotNull(warehouse.name, warehouse.locations.find { it.id == line.selectedWarehouseLocationId }?.name).joinToString(" • ")}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + OutlinedTextField( + value = line.notes, + onValueChange = onNotesChange, + label = { Text("Catatan Line") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + ) + } + } +} + +@Composable +private fun PurchaseAnalysesModuleScreen( + state: PurchaseAnalysesUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, +) { + if (state.selectedPurchaseId != null || state.selectedDetail != null) { + PurchaseAnalysisDetailScreen(state = state, modifier = modifier, onBack = onBack) + } else { + PurchaseAnalysesListScreen(state = state, modifier = modifier, onRefresh = onRefresh, onOpenDetail = onOpenDetail) + } +} + +@Composable +private fun PurchaseAnalysesListScreen( + state: PurchaseAnalysesUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn(modifier = modifier.padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top) { + Column(modifier = Modifier.weight(1f)) { + Text("Purchase Analyses", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text("Pantau analisis berat, modal, dan laba rugi setiap purchase.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + else Icon(Icons.Outlined.Refresh, contentDescription = "Refresh analyses") + } + } + } + if (!state.inlineError.isNullOrBlank()) item { InlineErrorCard(message = state.inlineError) } + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat analisis purchase...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada analisis purchase.") } + } else { + items(state.items, key = { it.purchaseId }) { item -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth().clickable { onOpenDetail(item.purchaseId) }, + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top) { + Column { + Text("PURCHASE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.purchaseNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.analysisStatus) + } + Text(item.supplierName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("BUY", item.weightBuy?.let(::formatQuantity) ?: "-") + LotMetaCell("FINAL", item.weightFinal?.let(::formatQuantity) ?: "-", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("MODAL", formatCurrency(item.totalModalBeli), emphasize = true) + LotMetaCell("L/R", item.totalLabaRugi?.let(::formatCurrency) ?: "-", alignEnd = true, emphasize = true) + } + } + } + } + } + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun PurchaseAnalysisDetailScreen( + state: PurchaseAnalysesUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, +) { + val detail = state.selectedDetail + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { LoadingCard("Memuat detail analisis...") } + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { EmptyStateCard("Detail analisis belum tersedia.") } + else -> LazyColumn(modifier = modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Analisis", onBack = onBack) + } + if (!state.inlineError.isNullOrBlank()) item { InlineErrorCard(message = state.inlineError) } + item { + Surface(shape = RoundedCornerShape(20.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest, shadowElevation = 3.dp) { + Column(modifier = Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(detail.purchase.purchaseNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text(detail.purchase.supplierName, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("BUY", formatQuantity(detail.actuals.weightBuy)) + LotMetaCell("FINAL", formatQuantity(detail.actuals.weightFinal), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TOTAL MODAL", formatCurrency(detail.summary.totalModalBeli), emphasize = true) + LotMetaCell("L/R", detail.summary.totalLabaRugi?.let(::formatCurrency) ?: "-", alignEnd = true, emphasize = true) + } + } + } + } + item { + Surface(shape = RoundedCornerShape(18.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest, shadowElevation = 2.dp) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text("Ringkasan Analisis", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("AVG PRICE", detail.inputs.averagePrice?.let(::formatCurrency) ?: "-") + LotMetaCell("OPERASIONAL", formatCurrency(detail.summary.operasional), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("MODAL BARANG", formatCurrency(detail.summary.modalBarang)) + LotMetaCell("MODAL MAL", detail.summary.totalModalMal?.let(::formatCurrency) ?: "-", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("LABA/KG", detail.summary.labaTotalPerKg?.let(::formatCurrency) ?: "-") + LotMetaCell("AGEN/KG", detail.summary.labaAgenPerKg?.let(::formatCurrency) ?: "-", alignEnd = true) + } + } + } + } + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } +} + +@Composable +private fun PurchaseRealizationsModuleScreen( + state: PurchaseRealizationsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, +) { + if (state.selectedPurchaseId != null || state.selectedDetail != null) { + PurchaseRealizationDetailScreen(state = state, modifier = modifier, onBack = onBack) + } else { + PurchaseRealizationsListScreen(state = state, modifier = modifier, onRefresh = onRefresh, onOpenDetail = onOpenDetail) + } +} + +@Composable +private fun PurchaseRealizationsListScreen( + state: PurchaseRealizationsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn(modifier = modifier.padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top) { + Column(modifier = Modifier.weight(1f)) { + Text("Purchase Realizations", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text("Lihat sisa qty, revenue, dan profit dari purchase yang sudah berjalan.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + else Icon(Icons.Outlined.Refresh, contentDescription = "Refresh realizations") + } + } + } + if (!state.inlineError.isNullOrBlank()) item { InlineErrorCard(message = state.inlineError) } + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat realisasi purchase...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada realisasi purchase.") } + } else { + items(state.items, key = { it.purchaseId }) { item -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + modifier = Modifier.fillMaxWidth().clickable { onOpenDetail(item.purchaseId) }, + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top) { + Column { + Text("PURCHASE", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.purchaseNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Text(item.supplierName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("OPENING", formatQuantity(item.qtyOpening)) + LotMetaCell("SISA", formatQuantity(item.qtyRemaining), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("REVENUE", formatCurrency(item.revenueTotal), emphasize = true) + LotMetaCell("PROFIT", formatCurrency(item.profitLossTotal), alignEnd = true, emphasize = true) + } + } + } + } + } + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun PurchaseRealizationDetailScreen( + state: PurchaseRealizationsUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, +) { + val detail = state.selectedDetail + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { LoadingCard("Memuat detail realisasi...") } + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { EmptyStateCard("Detail realisasi belum tersedia.") } + else -> LazyColumn(modifier = modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Realisasi", onBack = onBack) + } + if (!state.inlineError.isNullOrBlank()) item { InlineErrorCard(message = state.inlineError) } + item { + Surface(shape = RoundedCornerShape(20.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest, shadowElevation = 3.dp) { + Column(modifier = Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(detail.purchase.purchaseNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text(detail.purchase.supplierName, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("OPENING", formatQuantity(detail.summary.qtyOpening)) + LotMetaCell("SISA", formatQuantity(detail.summary.qtyRemaining), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("REVENUE", formatCurrency(detail.summary.revenueTotal), emphasize = true) + LotMetaCell("PROFIT", formatCurrency(detail.summary.profitLossTotal), alignEnd = true, emphasize = true) + } + } + } + } + item { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Riwayat Entry", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (detail.entries.isEmpty()) { + EmptyStateCard("Belum ada entry realisasi.") + } else { + detail.entries.forEach { entry -> + PurchaseRealizationEntryCard(entry = entry) + } + } + } + } + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } +} + +@Composable +private fun PurchaseRealizationEntryCard(entry: PurchaseRealizationEntryItem) { + Surface(shape = RoundedCornerShape(18.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest, shadowElevation = 2.dp) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("EVENT", prettifyStatus(entry.eventType), emphasize = true) + LotMetaCell("WAKTU", formatApiDateTime(entry.occurredAt), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("LOT", entry.lotCode ?: "-") + LotMetaCell("REF", entry.referenceType, alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("QTY IN", formatQuantity(entry.qtyIn)) + LotMetaCell("QTY OUT", formatQuantity(entry.qtyOut), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("COST", formatCurrency(entry.amountCost)) + LotMetaCell("PROFIT", formatCurrency(entry.amountProfit), alignEnd = true) + } + if (!entry.notes.isNullOrBlank()) { + Text(entry.notes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } +} + +@Composable +private fun PurchaseLinesSection(lines: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Purchase Lines", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (lines.isEmpty()) { + EmptyStateCard("Belum ada line purchase.") + } else { + lines.forEach { line -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(line.grade?.name ?: "-", fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("ORDERED", "${formatQuantity(line.qtyOrdered)} ${line.unit.code}", emphasize = true) + LotMetaCell("RECEIVED", "${formatQuantity(line.qtyReceived)} ${line.unit.code}", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("ACCEPTED", "${formatQuantity(line.qtyAccepted)} ${line.unit.code}") + LotMetaCell("REJECTED", "${formatQuantity(line.qtyRejected)} ${line.unit.code}", alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("UNIT PRICE", formatCurrency(line.unitPrice)) + LotMetaCell("SUBTOTAL", formatCurrency(line.subtotal), alignEnd = true, emphasize = true) + } + Text( + listOfNotNull(line.warehouse?.name, line.warehouseLocation?.name).joinToString(" • ").ifBlank { "-" }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun LotTransformationsModuleScreen( + state: LotTransformationsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenDetail: (String) -> Unit, + onBack: () -> Unit, + onUpdateType: (String) -> Unit, + onUpdateDate: (String) -> Unit, + onUpdateRemainderMode: (String?) -> Unit, + onUpdateProcessingLossMode: (String?) -> Unit, + onUpdateNotes: (String) -> Unit, + onAddInput: () -> Unit, + onRemoveInput: (Int) -> Unit, + onUpdateInputQuery: (Int, String) -> Unit, + onSelectInputLot: (Int, String) -> Unit, + onClearInputLot: (Int) -> Unit, + onUpdateInputQty: (Int, String) -> Unit, + onUpdateInputNotes: (Int, String) -> Unit, + onAddOutput: () -> Unit, + onRemoveOutput: (Int) -> Unit, + onSelectOutputGrade: (Int, String) -> Unit, + onSelectOutputWarehouse: (Int, String) -> Unit, + onSelectOutputLocation: (Int, String?) -> Unit, + onUpdateOutputQty: (Int, String) -> Unit, + onUpdateOutputNotes: (Int, String) -> Unit, + onSave: () -> Unit, +) { + when { + state.screen == LotTransformationsScreen.Create -> LotTransformationCreateScreen( + state = state, + modifier = modifier, + onBack = onBack, + onUpdateType = onUpdateType, + onUpdateDate = onUpdateDate, + onUpdateRemainderMode = onUpdateRemainderMode, + onUpdateProcessingLossMode = onUpdateProcessingLossMode, + onUpdateNotes = onUpdateNotes, + onAddInput = onAddInput, + onRemoveInput = onRemoveInput, + onUpdateInputQuery = onUpdateInputQuery, + onSelectInputLot = onSelectInputLot, + onClearInputLot = onClearInputLot, + onUpdateInputQty = onUpdateInputQty, + onUpdateInputNotes = onUpdateInputNotes, + onAddOutput = onAddOutput, + onRemoveOutput = onRemoveOutput, + onSelectOutputGrade = onSelectOutputGrade, + onSelectOutputWarehouse = onSelectOutputWarehouse, + onSelectOutputLocation = onSelectOutputLocation, + onUpdateOutputQty = onUpdateOutputQty, + onUpdateOutputNotes = onUpdateOutputNotes, + onSave = onSave, + ) + + state.selectedTransformationId != null || state.selectedTransformation != null || state.screen == LotTransformationsScreen.Detail -> { + LotTransformationDetailScreen( + state = state, + modifier = modifier, + onBack = onBack, + ) + } + + else -> { + LotTransformationsListScreen( + state = state, + modifier = modifier, + onRefresh = onRefresh, + onOpenCreate = onOpenCreate, + onOpenDetail = onOpenDetail, + ) + } + } +} + +@Composable +private fun LotTransformationsListScreen( + state: LotTransformationsUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenDetail: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Lot Transformations", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + "Lihat riwayat mixing dan regrade, lalu buat transformasi baru dari lot aktif.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Button( + onClick = onOpenCreate, + shape = RoundedCornerShape(16.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 14.dp, vertical = 10.dp), + ) { + Icon(Icons.Outlined.Add, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("Buat") + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh transformations") + } + } + } + } + } + + item { + AssistChip(onClick = {}, label = { Text("${state.items.size} transformasi") }) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + if (state.isLoading && state.items.isEmpty()) { + item { LoadingCard("Memuat daftar transformasi lot...") } + } else if (state.items.isEmpty()) { + item { EmptyStateCard("Belum ada lot transformation untuk role ini.") } + } else { + items(state.items, key = { it.id }) { item -> + LotTransformationListCard(item = item, onClick = { onOpenDetail(item.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun LotTransformationCreateScreen( + state: LotTransformationsUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onUpdateType: (String) -> Unit, + onUpdateDate: (String) -> Unit, + onUpdateRemainderMode: (String?) -> Unit, + onUpdateProcessingLossMode: (String?) -> Unit, + onUpdateNotes: (String) -> Unit, + onAddInput: () -> Unit, + onRemoveInput: (Int) -> Unit, + onUpdateInputQuery: (Int, String) -> Unit, + onSelectInputLot: (Int, String) -> Unit, + onClearInputLot: (Int) -> Unit, + onUpdateInputQty: (Int, String) -> Unit, + onUpdateInputNotes: (Int, String) -> Unit, + onAddOutput: () -> Unit, + onRemoveOutput: (Int) -> Unit, + onSelectOutputGrade: (Int, String) -> Unit, + onSelectOutputWarehouse: (Int, String) -> Unit, + onSelectOutputLocation: (Int, String?) -> Unit, + onUpdateOutputQty: (Int, String) -> Unit, + onUpdateOutputNotes: (Int, String) -> Unit, + onSave: () -> Unit, +) { + var typeExpanded by remember { mutableStateOf(false) } + var remainderExpanded by remember { mutableStateOf(false) } + var lossExpanded by remember { mutableStateOf(false) } + val totalInput = state.inputs.sumOf { it.qtyUsed.toDoubleOrNull() ?: 0.0 } + val totalOutput = state.outputs.sumOf { it.qtyProduced.toDoubleOrNull() ?: 0.0 } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Buat Transformasi", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text("Header Transformasi", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + + Box { + PickerField( + label = "Tipe Transformasi", + value = state.transformationTypes.find { it.code == state.transformationType }?.label ?: state.transformationType, + onClick = { typeExpanded = true }, + ) + DropdownMenu(expanded = typeExpanded, onDismissRequest = { typeExpanded = false }) { + state.transformationTypes.forEach { option -> + DropdownMenuItem( + text = { Text(option.label) }, + onClick = { + typeExpanded = false + onUpdateType(option.code) + }, + ) + } + } + } + + OutlinedTextField( + value = state.transformationDate, + onValueChange = onUpdateDate, + label = { Text("Tanggal Transformasi") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + if (state.transformationType == "REGRADE") { + Box { + PickerField( + label = "Mode Sisa Lot Sumber", + value = state.remainderModes.find { it.code == state.remainderMode }?.label ?: "Pilih mode sisa", + onClick = { remainderExpanded = true }, + ) + DropdownMenu(expanded = remainderExpanded, onDismissRequest = { remainderExpanded = false }) { + state.remainderModes.forEach { option -> + DropdownMenuItem( + text = { Text(option.label) }, + onClick = { + remainderExpanded = false + onUpdateRemainderMode(option.code) + }, + ) + } + } + } + + Box { + PickerField( + label = "Mode Selisih / Loss", + value = state.processingLossModes.find { it.code == state.processingLossMode }?.label ?: "Pilih mode loss", + onClick = { lossExpanded = true }, + ) + DropdownMenu(expanded = lossExpanded, onDismissRequest = { lossExpanded = false }) { + state.processingLossModes.forEach { option -> + DropdownMenuItem( + text = { Text(option.label) }, + onClick = { + lossExpanded = false + onUpdateProcessingLossMode(option.code) + }, + ) + } + } + } + } + + OutlinedTextField( + value = state.notes, + onValueChange = onUpdateNotes, + label = { Text("Catatan") }, + modifier = Modifier.fillMaxWidth(), + minLines = 3, + ) + } + } + } + + item { + SectionActionHeader( + title = "Lot Sumber", + buttonLabel = if (state.transformationType == "REGRADE") null else "Tambah Lot", + onClick = if (state.transformationType == "REGRADE") null else onAddInput, + ) + } + + items(state.inputs.size, key = { "input-$it" }) { index -> + val input = state.inputs[index] + LotTransformationInputEditor( + index = index, + input = input, + allLots = state.selectableLots, + canRemove = state.inputs.size > if (state.transformationType == "REGRADE") 1 else 2, + onRemove = { onRemoveInput(index) }, + onQueryChange = { onUpdateInputQuery(index, it) }, + onSelectLot = { onSelectInputLot(index, it) }, + onClearLot = { onClearInputLot(index) }, + onQtyChange = { onUpdateInputQty(index, it) }, + onNotesChange = { onUpdateInputNotes(index, it) }, + ) + } + + item { + SectionActionHeader( + title = "Lot Hasil", + buttonLabel = "Tambah Output", + onClick = onAddOutput, + ) + } + + items(state.outputs.size, key = { "output-$it" }) { index -> + LotTransformationOutputEditor( + index = index, + output = state.outputs[index], + grades = state.grades, + warehouses = state.warehouses, + canRemove = state.outputs.size > 1, + onRemove = { onRemoveOutput(index) }, + onSelectGrade = { onSelectOutputGrade(index, it) }, + onSelectWarehouse = { onSelectOutputWarehouse(index, it) }, + onSelectLocation = { onSelectOutputLocation(index, it) }, + onQtyChange = { onUpdateOutputQty(index, it) }, + onNotesChange = { onUpdateOutputNotes(index, it) }, + ) + } + + item { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.primaryContainer, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text("Ringkasan Qty", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onPrimaryContainer) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Column { + Text("TOTAL INPUT", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)) + Text( + "${formatQuantity(totalInput)} kg", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + fontWeight = FontWeight.Bold, + ) + } + Column(horizontalAlignment = Alignment.End) { + Text("TOTAL OUTPUT", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)) + Text( + "${formatQuantity(totalOutput)} kg", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End, + ) + } + } + if (state.transformationType == "REGRADE" && totalInput > totalOutput) { + Text( + "Selisih hasil regrade akan mengikuti mode loss yang dipilih.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } + } + } + + item { + Button( + onClick = onSave, + enabled = !state.isSaving, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 14.dp), + ) { + if (state.isSaving) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Text("Simpan Transformasi") + } + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } +} + +@Composable +private fun SectionActionHeader( + title: String, + buttonLabel: String?, + onClick: (() -> Unit)?, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(title, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (buttonLabel != null && onClick != null) { + Button(onClick = onClick, shape = RoundedCornerShape(14.dp)) { + Text(buttonLabel) + } + } + } +} + +@Composable +private fun LotTransformationInputEditor( + index: Int, + input: LotTransformationInputFormState, + allLots: List, + canRemove: Boolean, + onRemove: () -> Unit, + onQueryChange: (String) -> Unit, + onSelectLot: (String) -> Unit, + onClearLot: () -> Unit, + onQtyChange: (String) -> Unit, + onNotesChange: (String) -> Unit, +) { + val selectedLot = allLots.find { it.id == input.selectedLotId } + val filteredLots = remember(input.lotQuery, allLots) { + val query = input.lotQuery.trim() + if (query.isBlank() || selectedLot != null) { + emptyList() + } else { + allLots + .filter { it.lotCode.contains(query, ignoreCase = true) } + .take(6) + } + } + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text("Source ${index + 1}", style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary) + if (canRemove) { + Text( + "Hapus", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.clickable(onClick = onRemove), + ) + } + } + + if (selectedLot == null) { + OutlinedTextField( + value = input.lotQuery, + onValueChange = onQueryChange, + label = { Text("Cari Kode Lot") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + if (filteredLots.isEmpty() && input.lotQuery.isNotBlank()) { + Text("Tidak ada lot aktif yang cocok.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } else { + filteredLots.forEach { lot -> + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .clickable { onSelectLot(lot.id) }, + shape = RoundedCornerShape(14.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Column(modifier = Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(lot.lotCode, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary) + Text( + "${lot.grade} • ${formatQuantity(lot.availableQty)} ${lot.unitCode}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + listOfNotNull(lot.warehouse, lot.location).joinToString(" • "), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } else { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primaryContainer, + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text(selectedLot.lotCode, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onPrimaryContainer) + Text( + "Ganti", + color = MaterialTheme.colorScheme.onPrimaryContainer, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.clickable(onClick = onClearLot), + ) + } + Text( + "${selectedLot.grade} • ${formatQuantity(selectedLot.availableQty)} ${selectedLot.unitCode}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Text( + listOfNotNull(selectedLot.warehouse, selectedLot.location).joinToString(" • "), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + } + } + + OutlinedTextField( + value = input.qtyUsed, + onValueChange = onQtyChange, + label = { Text("Qty Digunakan") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + OutlinedTextField( + value = input.notes, + onValueChange = onNotesChange, + label = { Text("Catatan Input") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + ) + } + } +} + +@Composable +private fun LotTransformationOutputEditor( + index: Int, + output: LotTransformationOutputFormState, + grades: List, + warehouses: List, + canRemove: Boolean, + onRemove: () -> Unit, + onSelectGrade: (String) -> Unit, + onSelectWarehouse: (String) -> Unit, + onSelectLocation: (String?) -> Unit, + onQtyChange: (String) -> Unit, + onNotesChange: (String) -> Unit, +) { + var gradeExpanded by remember { mutableStateOf(false) } + var warehouseExpanded by remember { mutableStateOf(false) } + var locationExpanded by remember { mutableStateOf(false) } + val warehouse = warehouses.find { it.id == output.selectedWarehouseId } + val locations = warehouse?.locations.orEmpty() + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Text("Output ${index + 1}", style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary) + if (canRemove) { + Text( + "Hapus", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.clickable(onClick = onRemove), + ) + } + } + + Box { + PickerField( + label = "Grade Hasil", + value = grades.find { it.id == output.selectedGradeId }?.name ?: "Pilih grade", + onClick = { gradeExpanded = true }, + ) + DropdownMenu(expanded = gradeExpanded, onDismissRequest = { gradeExpanded = false }) { + grades.forEach { grade -> + DropdownMenuItem( + text = { Text(grade.name) }, + onClick = { + gradeExpanded = false + onSelectGrade(grade.id) + }, + ) + } + } + } + + Box { + PickerField( + label = "Gudang Tujuan", + value = warehouse?.name ?: "Pilih gudang", + onClick = { warehouseExpanded = true }, + ) + DropdownMenu(expanded = warehouseExpanded, onDismissRequest = { warehouseExpanded = false }) { + warehouses.forEach { item -> + DropdownMenuItem( + text = { Text(item.name) }, + onClick = { + warehouseExpanded = false + onSelectWarehouse(item.id) + }, + ) + } + } + } + + if (locations.isNotEmpty()) { + Box { + PickerField( + label = "Lokasi Tujuan", + value = locations.find { it.id == output.selectedWarehouseLocationId }?.name ?: "Pilih lokasi", + onClick = { locationExpanded = true }, + ) + DropdownMenu(expanded = locationExpanded, onDismissRequest = { locationExpanded = false }) { + locations.forEach { item -> + DropdownMenuItem( + text = { Text(item.name) }, + onClick = { + locationExpanded = false + onSelectLocation(item.id) + }, + ) + } + } + } + } + + OutlinedTextField( + value = output.qtyProduced, + onValueChange = onQtyChange, + label = { Text("Qty Hasil") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + ) + OutlinedTextField( + value = output.notes, + onValueChange = onNotesChange, + label = { Text("Catatan Output") }, + modifier = Modifier.fillMaxWidth(), + minLines = 2, + ) + } + } +} + +@Composable +private fun LotTransformationListCard( + item: LotTransformationListItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("TRANSFORMATION", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.transformationNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TIPE", prettifyStatus(item.transformationType)) + LotMetaCell("TANGGAL", formatApiDateTime(item.transformationDate), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("INPUT", "${item.inputCount} • ${formatQuantity(item.totalInputQty)} kg", emphasize = true) + LotMetaCell("OUTPUT", "${item.outputCount} • ${formatQuantity(item.totalOutputQty)} kg", alignEnd = true, emphasize = true) + } + } + } +} + +@Composable +private fun LotTransformationDetailScreen( + state: LotTransformationsUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, +) { + val detail = state.selectedTransformation + when { + state.isLoading && detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail transformasi...") + } + + detail == null -> Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail transformasi belum tersedia.") + } + + else -> { + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Detail Transformasi", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("TRANSFORMATION", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(detail.transformationNo, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = detail.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("TIPE", prettifyStatus(detail.transformationType)) + LotMetaCell("TANGGAL", formatApiDateTime(detail.transformationDate), alignEnd = true) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("REMAINDER", detail.remainderQty?.let { "${formatQuantity(it)} kg" } ?: "-") + LotMetaCell("LOSS", detail.processingLossQty?.let { "${formatQuantity(it)} kg" } ?: "-", alignEnd = true) + } + if (!detail.notes.isNullOrBlank()) { + Text(detail.notes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } + + item { TransformationInputsSection(inputs = detail.inputs) } + item { TransformationOutputsSection(outputs = detail.outputs) } + item { Spacer(modifier = Modifier.height(20.dp)) } + } + } + } +} + +@Composable +private fun TransformationInputsSection(inputs: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Input Lots", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (inputs.isEmpty()) { + EmptyStateCard("Belum ada input lot.") + } else { + inputs.forEach { input -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(input.sourceLot.lotCode, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("GRADE", input.sourceLot.grade) + LotMetaCell("USED", "${formatQuantity(input.qtyUsed)} ${input.sourceLot.unitCode}", alignEnd = true, emphasize = true) + } + Text( + listOfNotNull(input.sourceLot.warehouse, input.sourceLot.location).joinToString(" • "), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun TransformationOutputsSection(outputs: List) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Output Lots", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (outputs.isEmpty()) { + EmptyStateCard("Belum ada output lot.") + } else { + outputs.forEach { output -> + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 2.dp, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Text(output.resultLot.lotCode, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.primary) + StatusPill(status = output.resultLot.status) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + LotMetaCell("GRADE", output.resultLot.grade) + LotMetaCell("PRODUCED", "${formatQuantity(output.qtyProduced)} ${output.resultLot.unitCode}", alignEnd = true, emphasize = true) + } + Text( + listOfNotNull(output.resultLot.warehouse, output.resultLot.location).joinToString(" • "), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +@Composable +private fun StockAdjustmentCard(item: StockAdjustmentListItem) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("ADJUSTMENT", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.adjustmentNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.reason.category) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("LOT", item.lot.lotCode) + LotMetaCell("TANGGAL", formatApiDateTime(item.adjustmentDate), alignEnd = true) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("SEBELUM", "${formatQuantity(item.availableQtyBefore)} ${item.lot.unitCode}") + LotMetaCell("SESUDAH", "${formatQuantity(item.availableQtyAfter)} ${item.lot.unitCode}", alignEnd = true) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell( + "QTY CHANGE", + "${if (item.qtyChange > 0) "+" else ""}${formatQuantity(item.qtyChange)} ${item.lot.unitCode}", + emphasize = true, + ) + LotMetaCell("ALASAN", item.reason.code, alignEnd = true) + } + Text( + "${item.reason.name} • ${item.createdBy.name}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (!item.notes.isNullOrBlank()) { + Text(item.notes, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } +} + +@Composable +private fun WashingListScreen( + role: String, + state: WashingUiState, + modifier: Modifier = Modifier, + onRefresh: () -> Unit, + onOpenCreate: () -> Unit, + onOpenEdit: (String) -> Unit, + onOpenComplete: (String) -> Unit, +) { + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Washing", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + "Kelola proses pencucian lot aktif untuk role $role.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Button( + onClick = onOpenCreate, + shape = RoundedCornerShape(16.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 14.dp, vertical = 10.dp), + ) { + Icon(Icons.Outlined.Add, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("Baru") + } + } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Ringkasan Washing", style = MaterialTheme.typography.titleMedium) + Text( + "${state.washings.count { it.status == "IN_PROGRESS" }} proses berjalan • ${state.washings.count { it.status == "DONE" }} selesai", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh washing") + } + } + } + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${state.washings.size} washing") }) + AssistChip(onClick = {}, label = { Text("${state.selectableLots.size} lot siap") }) + } + } + + if (state.isLoading && state.washings.isEmpty()) { + item { LoadingCard("Memuat daftar washing...") } + } else if (state.washings.isEmpty()) { + item { EmptyStateCard("Belum ada data washing untuk role ini.") } + } else { + items(state.washings, key = { it.id }) { item -> + WashingCard( + item = item, + onEdit = { onOpenEdit(item.id) }, + onComplete = { onOpenComplete(item.id) }, + ) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun WashingCard( + item: WashingListItem, + onEdit: () -> Unit, + onComplete: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("NO WASHING", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.washingNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("LOT", item.lot.lotCode) + LotMetaCell("TEMPAT", item.washingPlace.name, alignEnd = true) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("SEBELUM", "${formatQuantity(item.beforeQty)} ${item.lot.unitCode}", emphasize = true) + LotMetaCell( + "SESUDAH", + item.afterQty?.let { "${formatQuantity(it)} ${item.lot.unitCode}" } ?: "-", + alignEnd = true, + emphasize = item.afterQty != null, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("GRADE", item.beforeGradeName ?: item.lot.gradeName ?: "-") + LotMetaCell("DURASI", "${item.durationHours} jam", alignEnd = true) + } + + Text( + "${item.beforeWarehouseName}${item.beforeLocationName?.let { " • $it" } ?: ""}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + onClick = onEdit, + enabled = item.status == "IN_PROGRESS", + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.primary, + ), + shape = RoundedCornerShape(14.dp), + ) { + Icon(Icons.Outlined.EditNote, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("Edit", maxLines = 1) + } + Button( + onClick = onComplete, + enabled = item.status == "IN_PROGRESS", + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(14.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 10.dp, vertical = 8.dp), + ) { + Icon(Icons.Outlined.WaterDrop, contentDescription = null, modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "Selesaikan", + maxLines = 1, + style = MaterialTheme.typography.labelLarge.copy(fontSize = 13.sp), + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun WashingFormScreen( + state: WashingUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onSelectLot: (String) -> Unit, + onSelectPlace: (String) -> Unit, + onUpdateCost: (String) -> Unit, + onUpdateDuration: (String) -> Unit, + onSave: () -> Unit, +) { + val title = if (state.screen == WashingScreen.Edit) "Edit Washing" else "Buat Washing" + val selectedLot = remember(state.selectableLots, state.selectedLotId) { + state.selectableLots.find { it.id == state.selectedLotId } + } + val selectedPlace = remember(state.washingPlaces, state.selectedWashingPlaceId) { + state.washingPlaces.find { it.id == state.selectedWashingPlaceId } + } + val context = androidx.compose.ui.platform.LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var lotQuery by remember(state.selectedLotId, selectedLot?.lotCode) { + mutableStateOf(selectedLot?.lotCode.orEmpty()) + } + var placeExpanded by remember { mutableStateOf(false) } + var scanDialogOpen by remember { mutableStateOf(false) } + var hasCameraPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED, + ) + } + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + hasCameraPermission = granted + if (granted) { + scanDialogOpen = true + } + } + val filteredLots = remember(state.selectableLots, lotQuery) { + val query = lotQuery.trim().lowercase() + if (query.isBlank()) { + emptyList() + } else { + state.selectableLots.filter { + it.lotCode.lowercase().contains(query) || + it.supplier.lowercase().contains(query) || + it.grade.lowercase().contains(query) || + it.location.lowercase().contains(query) || + it.warehouse.lowercase().contains(query) + }.take(8) + } + } + val isSearchingLots = remember(lotQuery, selectedLot?.lotCode) { + val query = lotQuery.trim() + query.isNotBlank() && !query.equals(selectedLot?.lotCode.orEmpty(), ignoreCase = true) + } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = title, onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text("Pilih Lot", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OutlinedTextField( + value = lotQuery, + onValueChange = { lotQuery = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Cari Lot") }, + placeholder = { Text("Kode lot / supplier / grade") }, + leadingIcon = { Icon(Icons.Outlined.Search, contentDescription = null) }, + trailingIcon = { + IconButton( + onClick = { + if (hasCameraPermission) { + scanDialogOpen = true + } else { + permissionLauncher.launch(Manifest.permission.CAMERA) + } + }, + ) { + Icon(Icons.Outlined.QrCodeScanner, contentDescription = "Scan barcode lot") + } + }, + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + if (selectedLot != null) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primary, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + selectedLot.lotCode, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.SemiBold, + ) + Text( + "${selectedLot.supplier} • ${selectedLot.grade} • ${formatQuantity(selectedLot.availableQty)} ${selectedLot.unitCode}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.86f), + ) + } + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary) + } + } + } + when { + state.selectableLots.isEmpty() -> EmptyStateCard("Tidak ada lot yang siap diproses washing.") + lotQuery.isBlank() -> EmptyStateCard("Ketik kata kunci atau scan barcode untuk mencari lot.") + selectedLot != null && !isSearchingLots -> {} + filteredLots.isEmpty() -> EmptyStateCard("Tidak ada lot yang cocok dengan pencarian.") + else -> { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + filteredLots.forEach { lot -> + Surface( + shape = RoundedCornerShape(16.dp), + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + 1.dp, + if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, + ), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { + onSelectLot(lot.id) + lotQuery = lot.lotCode + }, + ) { + Column( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + lot.lotCode, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + Text( + "${formatQuantity(lot.availableQty)} ${lot.unitCode}", + style = MaterialTheme.typography.bodySmall, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, + ) + } + Text( + "${lot.supplier} • ${lot.grade}", + style = MaterialTheme.typography.bodySmall, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.86f) else MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + "${lot.warehouse} • ${lot.location}", + style = MaterialTheme.typography.bodySmall, + color = if (lot.id == state.selectedLotId) MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.86f) else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant) + Text("Tempat Cuci", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + if (state.washingPlaces.isEmpty()) { + EmptyStateCard("Belum ada tempat cuci aktif.") + } else { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { placeExpanded = true }, + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + "Tempat Cuci", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + selectedPlace?.name ?: "Pilih tempat cuci", + color = if (selectedPlace != null) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + fontWeight = if (selectedPlace != null) FontWeight.SemiBold else FontWeight.Normal, + ) + } + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + } + DropdownMenu( + expanded = placeExpanded, + onDismissRequest = { placeExpanded = false }, + modifier = Modifier.fillMaxWidth(0.92f), + ) { + state.washingPlaces.forEach { place -> + DropdownMenuItem( + text = { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(place.name, fontWeight = FontWeight.SemiBold) + if (!place.code.isNullOrBlank()) { + Text( + place.code.orEmpty(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + onClick = { + onSelectPlace(place.id) + placeExpanded = false + }, + ) + } + } + } + } + OutlinedTextField( + value = state.washingCost, + onValueChange = onUpdateCost, + modifier = Modifier.fillMaxWidth(), + label = { Text("Biaya Washing") }, + placeholder = { Text("Contoh: 50000") }, + leadingIcon = { Icon(Icons.Outlined.ReceiptLong, contentDescription = null) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + OutlinedTextField( + value = state.durationHours, + onValueChange = onUpdateDuration, + modifier = Modifier.fillMaxWidth(), + label = { Text("Durasi (jam)") }, + placeholder = { Text("24") }, + leadingIcon = { Icon(Icons.Outlined.WaterDrop, contentDescription = null) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + } + } + } + + item { + Button( + onClick = onSave, + enabled = !state.isSaving, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isSaving) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Icon(Icons.Outlined.EditNote, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text(if (state.screen == WashingScreen.Edit) "Simpan Perubahan" else "Mulai Washing") + } + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } + + if (scanDialogOpen) { + Dialog(onDismissRequest = { scanDialogOpen = false }) { + Surface( + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Scan Barcode Lot", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + IconButton(onClick = { scanDialogOpen = false }) { + Icon(Icons.Outlined.ArrowBack, contentDescription = "Tutup") + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(18.dp)) + .background(MaterialTheme.colorScheme.inverseSurface), + ) { + if (hasCameraPermission) { + CameraScannerPreview( + lifecycleOwner = lifecycleOwner, + modifier = Modifier.matchParentSize(), + onCodeDetected = { code -> + val matchedLot = state.selectableLots.firstOrNull { + it.lotCode.equals(code.trim(), ignoreCase = true) + } + lotQuery = code.trim() + matchedLot?.let { onSelectLot(it.id) } + scanDialogOpen = false + }, + ) + } + } + Text( + "Arahkan kamera ke barcode atau QR lot. Jika cocok, lot akan dipilih otomatis.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} + +@Composable +private fun WashingCompleteScreen( + state: WashingUiState, + modifier: Modifier = Modifier, + onBack: () -> Unit, + onUpdateAfterQty: (String) -> Unit, + onSelectGrade: (String?) -> Unit, + onSelectWarehouse: (String) -> Unit, + onSelectWarehouseLocation: (String?) -> Unit, + onComplete: () -> Unit, +) { + val selectedItem = state.washings.find { it.id == state.selectedWashingId } + val selectedWarehouse = state.warehouses.find { it.id == state.selectedWarehouseId } + + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + BackHeader(title = "Selesaikan Washing", onBack = onBack) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + selectedItem?.let { item -> + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column { + Text("NO WASHING", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(item.washingNo, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = item.status) + } + LotMetaCell("LOT", item.lot.lotCode) + Text( + "${item.beforeWarehouseName}${item.beforeLocationName?.let { " • $it" } ?: ""}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + + item { + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedTextField( + value = state.afterQty, + onValueChange = onUpdateAfterQty, + modifier = Modifier.fillMaxWidth(), + label = { Text("After Qty") }, + placeholder = { Text("Contoh: 2.5") }, + leadingIcon = { Icon(Icons.Outlined.Inventory2, contentDescription = null) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + singleLine = true, + shape = RoundedCornerShape(16.dp), + ) + Text("Grade Hasil", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OptionChipGroup( + items = listOf("" to "Tetap") + state.grades.map { it.id to it.name }, + selectedId = state.selectedGradeId ?: "", + onSelect = { id -> onSelectGrade(id.ifBlank { null }) }, + emptyLabel = "Belum ada grade aktif.", + ) + Divider(color = MaterialTheme.colorScheme.outlineVariant) + Text("Gudang Tujuan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OptionChipGroup( + items = state.warehouses.map { it.id to it.name }, + selectedId = state.selectedWarehouseId, + onSelect = onSelectWarehouse, + emptyLabel = "Belum ada gudang tujuan.", + ) + Text("Lokasi Tujuan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + OptionChipGroup( + items = listOf("" to "Tanpa lokasi") + (selectedWarehouse?.locations ?: emptyList()).map { it.id to it.name }, + selectedId = state.selectedWarehouseLocationId ?: "", + onSelect = { id -> onSelectWarehouseLocation(id.ifBlank { null }) }, + emptyLabel = "Gudang ini tidak memiliki lokasi aktif.", + ) + } + } + } + + item { + Button( + onClick = onComplete, + enabled = !state.isCompleting, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(16.dp), + ) { + if (state.isCompleting) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + } else { + Icon(Icons.Outlined.WaterDrop, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Selesaikan Washing") + } + } + } + + item { Spacer(modifier = Modifier.height(20.dp)) } + } +} + +@Composable +private fun BackHeader( + title: String, + onBack: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onBack) { + Icon(Icons.Outlined.ArrowBack, contentDescription = "Kembali") + } + Text(title, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } +} + +@Composable +private fun OptionChipGroup( + items: List>, + selectedId: String?, + onSelect: (String) -> Unit, + emptyLabel: String, +) { + if (items.isEmpty()) { + EmptyStateCard(emptyLabel) + return + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + items.forEach { (id, label) -> + Surface( + shape = RoundedCornerShape(16.dp), + color = if (selectedId == id) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke( + 1.dp, + if (selectedId == id) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) else MaterialTheme.colorScheme.outlineVariant, + ), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { onSelect(id) }, + ) { + Row( + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + modifier = Modifier.weight(1f), + color = if (selectedId == id) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + ) + if (selectedId == id) { + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer) + } + } + } + } + } +} + +@Composable +private fun LotInventoryScreen( + state: LotsUiState, + modifier: Modifier = Modifier, + onTabChanged: (String) -> Unit, + onRefresh: () -> Unit, + onQueryChanged: (String) -> Unit, + onOpenLotDetail: (String) -> Unit, +) { + val filteredLots = remember(state.lots, state.query) { + val query = state.query.trim().lowercase() + if (query.isBlank()) state.lots else state.lots.filter { + it.lotCode.lowercase().contains(query) || + it.grade.lowercase().contains(query) || + it.supplier.lowercase().contains(query) || + it.location.lowercase().contains(query) || + it.warehouse.lowercase().contains(query) + } + } + + LazyColumn( + modifier = modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + LotsModeTabs(activeTab = "inventory", onTabChanged = onTabChanged) + Spacer(modifier = Modifier.height(14.dp)) + Text("Daftar Lot", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + Text( + "Cari dan buka detail lot aktif dari gudang atau proses QC.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + item { + OutlinedTextField( + value = state.query, + onValueChange = onQueryChanged, + modifier = Modifier.fillMaxWidth(), + leadingIcon = { Icon(Icons.Outlined.Search, contentDescription = null) }, + trailingIcon = { + IconButton(onClick = onRefresh) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh lots") + } + } + }, + shape = RoundedCornerShape(16.dp), + singleLine = true, + label = { Text("Cari Kode Lot / Grade / Lokasi") }, + placeholder = { Text("Contoh: LOT-260518 atau Mangkok") }, + ) + } + + if (!state.inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = state.inlineError) } + } + + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AssistChip(onClick = {}, label = { Text("${filteredLots.size} lot tampil") }) + AssistChip(onClick = {}, label = { Text("${state.lots.count { it.status == "ACTIVE" }} active") }) + } + } + + if (state.isLoading && state.lots.isEmpty()) { + item { LoadingCard("Memuat daftar lot...") } + } else if (filteredLots.isEmpty()) { + item { EmptyStateCard("Tidak ada lot yang cocok dengan pencarian.") } + } else { + items(filteredLots, key = { it.id }) { lot -> + LotInventoryCard(lot = lot, onClick = { onOpenLotDetail(lot.id) }) + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun LotScanScreen( + role: String, + state: LotsUiState, + modifier: Modifier = Modifier, + onTabChanged: (String) -> Unit, + onScanInputChanged: (String) -> Unit, + onScan: () -> Unit, + onCameraScan: (String) -> Unit, + onOpenRecentScan: (LotScanResult) -> Unit, +) { + val context = androidx.compose.ui.platform.LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var hasCameraPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED, + ) + } + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + hasCameraPermission = granted + } + var lastCameraCode by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + if (!hasCameraPermission) { + permissionLauncher.launch(Manifest.permission.CAMERA) + } + } + + LaunchedEffect(state.isScanning) { + if (!state.isScanning) { + lastCameraCode = null + } + } + + LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(0.dp), + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Column(modifier = Modifier.padding(horizontal = 20.dp)) { + LotsModeTabs(activeTab = "scan", onTabChanged = onTabChanged) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + Surface( + shape = RoundedCornerShape(999.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + modifier = Modifier + .clip(RoundedCornerShape(999.dp)) + .clickable { onTabChanged("inventory") }, + ) { + Text( + text = "Tutup", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + } + } + Spacer(modifier = Modifier.height(14.dp)) + } + } + + item { + Surface( + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.inverseSurface, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(4f / 3f) + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.inverseSurface), + contentAlignment = Alignment.Center, + ) { + if (hasCameraPermission) { + CameraScannerPreview( + lifecycleOwner = lifecycleOwner, + modifier = Modifier.matchParentSize(), + onCodeDetected = { code -> + if (!state.isScanning && code.isNotBlank() && code != lastCameraCode) { + lastCameraCode = code + onCameraScan(code) + } + }, + ) + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + Icons.Outlined.QrCodeScanner, + contentDescription = null, + tint = MaterialTheme.colorScheme.surface, + modifier = Modifier.size(42.dp), + ) + Text( + "Izin kamera diperlukan untuk scan lot", + color = MaterialTheme.colorScheme.surface, + fontWeight = FontWeight.SemiBold, + ) + Button(onClick = { permissionLauncher.launch(Manifest.permission.CAMERA) }) { + Text("Aktifkan Kamera") + } + } + } + Box( + modifier = Modifier + .size(240.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.Transparent), + ) { + Box( + modifier = Modifier + .matchParentSize() + .clip(RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)), + ) + Box( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth() + .height(2.dp) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.75f)), + ) + CornerMarker(Modifier.align(Alignment.TopStart)) + CornerMarker(Modifier.align(Alignment.TopEnd), flippedX = true) + CornerMarker(Modifier.align(Alignment.BottomStart), flippedY = true) + CornerMarker(Modifier.align(Alignment.BottomEnd), flippedX = true, flippedY = true) + } + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 22.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + "Scanner siap untuk validasi lot", + color = MaterialTheme.colorScheme.surface, + fontWeight = FontWeight.SemiBold, + ) + Text( + if (hasCameraPermission) "Arahkan kamera ke QR atau barcode lot." + else "Izin kamera belum diberikan. Gunakan input manual dahulu.", + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.72f), + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + } + + item { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Input Manual Kode Lot", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + OutlinedTextField( + value = state.scanInput, + onValueChange = onScanInputChanged, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + singleLine = true, + leadingIcon = { Icon(Icons.Outlined.QrCode2, contentDescription = null) }, + trailingIcon = { + Button( + onClick = onScan, + enabled = !state.isScanning, + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 14.dp, vertical = 8.dp), + ) { + if (state.isScanning) { + CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp) + } else { + Text("Cari") + } + } + }, + label = { Text("Masukkan kode lot") }, + placeholder = { Text("Contoh: LOT-260518-DIMA-018") }, + ) + AssistChip(onClick = {}, label = { Text("Role $role") }) + } + } + + if (!state.inlineError.isNullOrBlank()) { + item { + Column(modifier = Modifier.padding(horizontal = 20.dp)) { + InlineErrorCard(message = state.inlineError) + } + } + } + + item { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Riwayat Scan Terakhir", style = MaterialTheme.typography.titleMedium) + Text( + "${state.recentScans.size} item", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + ) + } + if (state.recentScans.isEmpty()) { + EmptyStateCard("Belum ada hasil scan di sesi ini.") + } else { + state.recentScans.forEach { result -> + RecentScanCard(result = result, onClick = { onOpenRecentScan(result) }) + } + } + } + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + } +} + +@Composable +private fun CameraScannerPreview( + lifecycleOwner: androidx.lifecycle.LifecycleOwner, + modifier: Modifier = Modifier, + onCodeDetected: (String) -> Unit, +) { + val context = androidx.compose.ui.platform.LocalContext.current + val previewView = remember { + PreviewView(context).apply { + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + scaleType = PreviewView.ScaleType.FILL_CENTER + } + } + val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } + val analysisExecutor = remember { Executors.newSingleThreadExecutor() } + + DisposableEffect(lifecycleOwner) { + val cameraProvider = cameraProviderFuture.get() + val preview = Preview.Builder().build().apply { + surfaceProvider = previewView.surfaceProvider + } + val scanner = BarcodeScanning.getClient( + BarcodeScannerOptions.Builder() + .setBarcodeFormats( + com.google.mlkit.vision.barcode.common.Barcode.FORMAT_QR_CODE, + com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_128, + com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_39, + com.google.mlkit.vision.barcode.common.Barcode.FORMAT_EAN_13, + com.google.mlkit.vision.barcode.common.Barcode.FORMAT_DATA_MATRIX, + ) + .build(), + ) + val analyzer = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .apply { + setAnalyzer(analysisExecutor) { imageProxy -> + processImageProxy( + imageProxy = imageProxy, + scanner = scanner, + onCodeDetected = onCodeDetected, + ) + } + } + + runCatching { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + CameraSelector.DEFAULT_BACK_CAMERA, + preview, + analyzer, + ) + } + + onDispose { + cameraProvider.unbindAll() + scanner.close() + analysisExecutor.shutdown() + } + } + + AndroidView( + factory = { previewView }, + modifier = modifier, + ) +} + +@Composable +private fun LotDetailScreen( + detail: LotDetailData?, + isLoading: Boolean, + inlineError: String?, + modifier: Modifier = Modifier, + onBack: () -> Unit, +) { + when { + isLoading && detail == null -> { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + LoadingCard("Memuat detail lot...") + } + } + + detail == null -> { + Box(modifier = modifier.padding(20.dp), contentAlignment = Alignment.Center) { + EmptyStateCard("Detail lot belum tersedia.") + } + } + + else -> { + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsets(0.dp), + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + IconButton(onClick = onBack) { + Icon(Icons.Outlined.ArrowBack, contentDescription = "Kembali") + } + Text("Detail Lot", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + } + } + + if (!inlineError.isNullOrBlank()) { + item { InlineErrorCard(message = inlineError) } + } + + item { LotHeaderCard(detail) } + item { LotQrCard(detail) } + item { LotTimelineSection(detail) } + item { LotInternalNotes(detail) } + item { Spacer(modifier = Modifier.height(8.dp)) } + } + } + } + } +} + +@Composable +private fun InlineErrorCard(message: String) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.error.copy(alpha = 0.10f), + ) { + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Pembaruan terakhir tidak sempurna", + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = message, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + ) + } + } +} + +@Composable +private fun LotInventoryCard( + lot: LotItem, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("KODE LOT", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text(lot.lotCode, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = lot.status) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("SUPPLIER", lot.supplier) + LotMetaCell("GRADE", lot.grade) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("QUANTITY", "${formatQuantity(lot.availableQty)} ${lot.unitCode}", emphasize = true) + Column(horizontalAlignment = Alignment.End) { + Text("LOKASI", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Icon(Icons.Outlined.LocationOn, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(14.dp)) + Text(lot.location, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface) + } + } + } + } + } +} + +@Composable +private fun LotHeaderCard(detail: LotDetailData) { + val summary = detail.summaryCardOrFallback() + Surface( + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 3.dp, + ) { + Column(modifier = Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Kode Lot", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(summary.lotCode, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary) + } + StatusPill(status = summary.status) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("SUPPLIER", summary.supplierName ?: "-") + LotMetaCell("KUANTITAS", "${formatQuantity(summary.availableQty)} ${summary.unitCode.orEmpty()}", alignEnd = true, emphasize = true) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + LotMetaCell("LOKASI SAAT INI", listOfNotNull(summary.warehouseName, summary.warehouseLocationName).joinToString(" • ").ifBlank { "-" }) + LotMetaCell("GRADE", summary.grade ?: "-", alignEnd = true) + } + } + } +} + +@Composable +private fun LotQrCard(detail: LotDetailData) { + val summary = detail.summaryCardOrFallback() + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.primaryContainer, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Outlined.QrCode2, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(30.dp)) + Column { + Text("Verifikasi Identitas Lot", color = MaterialTheme.colorScheme.onPrimaryContainer, fontWeight = FontWeight.Bold) + Text(summary.qrCodeValue ?: summary.lotCode, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.82f), style = MaterialTheme.typography.bodySmall) + } + } + Icon(Icons.Outlined.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer) + } + } +} + +@Composable +private fun LotTimelineSection(detail: LotDetailData) { + val events = buildLotTimeline(detail) + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Riwayat Pergerakan", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + Surface( + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + ) { + Column(modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp)) { + events.forEachIndexed { index, event -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Top, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(12.dp) + .clip(CircleShape) + .background(if (index == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant), + ) + if (index != events.lastIndex) { + Box( + modifier = Modifier + .width(2.dp) + .height(42.dp) + .background(MaterialTheme.colorScheme.outlineVariant), + ) + } + } + Column( + modifier = Modifier + .weight(1f) + .padding(bottom = if (index == events.lastIndex) 6.dp else 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text(event.timestampLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(event.title, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold) + if (event.description.isNotBlank()) { + Text(event.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } + } + } + } + } +} + +@Composable +private fun LotInternalNotes(detail: LotDetailData) { + val procurement = detail.procurement + val notes = buildList { + procurement?.purchaseNo?.let { add("Referensi pembelian: $it") } + procurement?.receiptNo?.let { add("Referensi receipt: $it") } + if (detail.mobileActions?.canAdjust == true) add("Role aktif dapat melakukan stock adjustment untuk lot ini.") + if (detail.mobileActions?.canMix == true) add("Lot ini eligible untuk mixing.") + if (detail.mobileActions?.canRegrade == true) add("Lot ini eligible untuk regrade.") + } + + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("CATATAN INTERNAL", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text( + notes.joinToString(" ") .ifBlank { "Belum ada catatan internal tambahan untuk lot ini." }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +private fun RecentScanCard( + result: LotScanResult, + onClick: () -> Unit, +) { + val summary = result.payload.summaryCard + Surface( + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.secondaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Outlined.Inventory2, contentDescription = null, tint = MaterialTheme.colorScheme.primary) + } + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(summary.lotCode, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold) + Text( + "${summary.warehouseLocationName ?: "-"} • ${formatQuantity(summary.availableQty)} ${summary.unitCode.orEmpty()}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Column(horizontalAlignment = Alignment.End) { + StatusPill(status = summary.status) + Spacer(modifier = Modifier.height(4.dp)) + Text(relativeTimeLabel(result.scannedAtMillis), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } +} + +@Composable +private fun LotsModeTabs( + activeTab: String, + onTabChanged: (String) -> Unit, +) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + LotsModeChip( + label = "Inventory", + active = activeTab == "inventory", + onClick = { onTabChanged("inventory") }, + ) + LotsModeChip( + label = "Scan", + active = activeTab == "scan", + onClick = { onTabChanged("scan") }, + ) + } +} + +@Composable +private fun LotsModeChip( + label: String, + active: Boolean, + onClick: () -> Unit, +) { + Surface( + shape = RoundedCornerShape(999.dp), + color = if (active) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerLow, + modifier = Modifier.clip(RoundedCornerShape(999.dp)).clickable(onClick = onClick), + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp), + color = if (active) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + ) + } +} + +@Composable +private fun StatusPill(status: String) { + val (bg, fg) = when (status.uppercase()) { + "ACTIVE" -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.onPrimaryContainer + "PROCESSING" -> MaterialTheme.colorScheme.secondaryContainer to MaterialTheme.colorScheme.primary + "QC_PENDING", "HOLD" -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.16f) to MaterialTheme.colorScheme.tertiary + "EXPORT_READY" -> MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) to MaterialTheme.colorScheme.primary + "DEPLETED", "CLOSED", "REJECT" -> MaterialTheme.colorScheme.error.copy(alpha = 0.12f) to MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.surfaceContainerHigh to MaterialTheme.colorScheme.onSurfaceVariant + } + Surface(shape = RoundedCornerShape(999.dp), color = bg) { + Text( + text = prettifyStatus(status), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + color = fg, + style = MaterialTheme.typography.labelSmall, + ) + } +} + +@Composable +private fun LotMetaCell( + label: String, + value: String, + alignEnd: Boolean = false, + emphasize: Boolean = false, +) { + Column(horizontalAlignment = if (alignEnd) Alignment.End else Alignment.Start) { + Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline) + Text( + value, + style = MaterialTheme.typography.bodySmall, + color = if (emphasize) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + fontWeight = if (emphasize) FontWeight.Bold else FontWeight.Normal, + textAlign = if (alignEnd) TextAlign.End else TextAlign.Start, + ) + } +} + +@Composable +private fun FooterActionButton( + modifier: Modifier = Modifier, + icon: ImageVector, + label: String, + primary: Boolean, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + color = if (primary) MaterialTheme.colorScheme.primary else Color.Transparent, + border = if (primary) null else androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + ) { + Column( + modifier = Modifier.padding(vertical = 12.dp, horizontal = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon(icon, contentDescription = null, tint = if (primary) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary) + Text( + label, + style = MaterialTheme.typography.labelSmall, + color = if (primary) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun LoadingCard(label: String) { + Surface(shape = RoundedCornerShape(18.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(18.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.4.dp) + Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} + +@Composable +private fun EmptyStateCard(label: String) { + Surface(shape = RoundedCornerShape(18.dp), color = MaterialTheme.colorScheme.surfaceContainerLowest) { + Text( + text = label, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun CornerMarker( + modifier: Modifier = Modifier, + flippedX: Boolean = false, + flippedY: Boolean = false, +) { + val markerColor = MaterialTheme.colorScheme.primary + Canvas( + modifier = modifier + .size(34.dp) + .padding(4.dp), + ) { + val stroke = 4.dp.toPx() + val arm = size.minDimension * 0.62f + val left = 0f + val right = size.width + val top = 0f + val bottom = size.height + val x = if (flippedX) right else left + val y = if (flippedY) bottom else top + val horizontalEnd = if (flippedX) x - arm else x + arm + val verticalEnd = if (flippedY) y - arm else y + arm + + drawLine( + color = markerColor, + start = Offset(x, y), + end = Offset(horizontalEnd, y), + strokeWidth = stroke, + cap = StrokeCap.Round, + ) + drawLine( + color = markerColor, + start = Offset(x, y), + end = Offset(x, verticalEnd), + strokeWidth = stroke, + cap = StrokeCap.Round, + ) + } +} + +@Composable +private fun ModulePlaceholderScreen( + module: String, + role: String, + availableModules: List, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.padding(20.dp), + contentAlignment = Alignment.Center, + ) { + Surface( + shape = RoundedCornerShape(26.dp), + color = MaterialTheme.colorScheme.surfaceContainerLowest, + shadowElevation = 4.dp, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Box( + modifier = Modifier + .size(72.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = moduleIcon(module), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(34.dp), + ) + } + Text( + text = moduleLabel(module), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "Modul ini sudah dikenali dari bootstrap untuk role $role dan siap diisi implementasi layar detail berikutnya.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + AssistChip( + onClick = {}, + label = { + Text("${availableModules.size} modul aktif") + }, + ) + } + } + } +} + +private fun buildQuickActions(role: String, modules: List): List { + if (role == "OWNER") { + return buildBottomBarItems(role = role, currentModule = "dashboard", modules = modules) + .filter { it != "dashboard" } + .map { + QuickAction( + module = it, + label = moduleLabel(it), + iconName = it, + ) + } + } + val source = if (modules.isNotEmpty()) orderModulesForRole(role, modules) else roleDefaults(role) + return source + .filter { it != "dashboard" } + .map { + QuickAction( + module = it, + label = moduleLabel(it), + iconName = it, + ) + } + .take(8) + .ifEmpty { + roleDefaults(role).map { + QuickAction(module = it, label = moduleLabel(it), iconName = it) + } + } +} + +private fun buildBottomBarItems(role: String, currentModule: String, modules: List): List { + val normalized = orderModulesForRole(role, modules).filter { it != "dashboard" }.distinct() + val baseItems = mutableListOf("dashboard") + baseItems += normalized.take(3) + + if (currentModule != "dashboard" && currentModule !in baseItems && normalized.contains(currentModule)) { + if (baseItems.size >= 4) { + baseItems[baseItems.lastIndex] = currentModule + } else { + baseItems += currentModule + } + } + + return baseItems.distinct() +} + +private fun roleDefaults(role: String): List = when (role) { + "WAREHOUSE" -> listOf("lots", "stock_adjustments", "washing", "purchases") + "QC" -> listOf("lots", "lot_transformations", "washing", "stock_adjustments") + "SALES" -> listOf("lots", "sales_regular", "sales_jit", "consignments") + "PURCHASING" -> listOf("purchases", "fund_requests", "purchase_analyses", "purchase_realizations") + "OWNER" -> listOf( + "purchases", + "purchase_analyses", + "purchase_realizations", + ) + else -> listOf("dashboard", "lots") +} + +private val hiddenMobileModules = setOf("receipts") + +private fun orderModulesForRole(role: String, modules: List): List { + val normalized = modules + .filterNot { it in hiddenMobileModules } + .distinct() + val priority = roleDefaults(role) + .filter { it != "dashboard" && it !in hiddenMobileModules } + val activePriority = priority.filter { it in normalized } + val extras = normalized.filter { it != "dashboard" && it !in activePriority } + return buildList { + add("dashboard") + addAll(activePriority) + addAll(extras) + }.distinct() +} + +private fun moduleLabel(module: String): String = when (module) { + "dashboard" -> "Dashboard" + "lots" -> "Stok" + "receipts" -> "Terima" + "washing" -> "Washing" + "stock_adjustments" -> "Adjust" + "lot_transformations" -> "Sortasi" + "sales_regular" -> "Sales" + "sales_jit" -> "JIT" + "consignments" -> "Titip Jual" + "purchases" -> "Beli" + "fund_requests" -> "Dana" + "purchase_analyses" -> "Analisis" + "purchase_realizations" -> "Realisasi" + else -> module.replace('_', ' ').replaceFirstChar { it.uppercase() } +} + +private fun moduleIcon(module: String): ImageVector = when (module) { + "dashboard" -> Icons.Outlined.Dashboard + "lots" -> Icons.Outlined.QrCodeScanner + "receipts" -> Icons.Outlined.ReceiptLong + "washing" -> Icons.Outlined.WaterDrop + "stock_adjustments" -> Icons.Outlined.ShowChart + "lot_transformations" -> Icons.Outlined.AccountTree + "sales_regular", "sales_jit", "consignments" -> Icons.Outlined.LocalShipping + "purchases", "fund_requests" -> Icons.Outlined.Inventory2 + "purchase_analyses", "purchase_realizations" -> Icons.Outlined.ContentPasteSearch + else -> Icons.Outlined.Settings +} + +private fun QuickAction.icon() = moduleIcon(iconName) + +@Composable +private fun alertColor(status: String): Color { + return when (status) { + "LOW_STOCK" -> MaterialTheme.colorScheme.error + "ON_HOLD" -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.primary + } +} + +private fun formatQuantity(value: Double): String { + return if (value % 1.0 == 0.0) { + value.roundToInt().toString() + } else { + String.format("%.1f", value) + } +} + +private fun formatCurrency(value: Double): String { + return NumberFormat.getCurrencyInstance(Locale("id", "ID")).format(value) +} + +private data class LotTimelineEvent( + val timestampLabel: String, + val title: String, + val description: String = "", +) + +private fun buildLotTimeline(detail: LotDetailData): List { + val summary = detail.summaryCardOrFallback() + val timeline = mutableListOf() + + summary.receivedAt?.let { + timeline += LotTimelineEvent( + timestampLabel = formatApiDateTime(it), + title = "Lot diterima di ${summary.warehouseLocationName ?: summary.warehouseName ?: "gudang"}", + description = listOfNotNull(summary.supplierName, summary.purchaseNo).joinToString(" • "), + ) + } + + summary.receiptDate?.let { + timeline += LotTimelineEvent( + timestampLabel = formatApiDateTime(it), + title = "Receipt tercatat", + description = summary.receiptNo ?: "Receipt mobile / web", + ) + } + + summary.purchaseDate?.let { + timeline += LotTimelineEvent( + timestampLabel = formatApiDateTime(it), + title = "Pembelian dibuat", + description = summary.purchaseNo ?: "Sumber pembelian", + ) + } + + if (timeline.isEmpty()) { + timeline += LotTimelineEvent( + timestampLabel = "Belum tersedia", + title = "Riwayat lot belum tersedia", + description = "Endpoint detail saat ini belum mengembalikan movement history lengkap.", + ) + } + + return timeline +} + +private fun prettifyStatus(status: String): String { + return status.lowercase() + .split('_') + .joinToString(" ") { it.replaceFirstChar { ch -> ch.uppercase() } } +} + +private fun formatApiDateTime(value: String): String { + return runCatching { + val input = when { + value.contains('T') -> SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.US) + else -> SimpleDateFormat("yyyy-MM-dd", Locale.US) + }.apply { timeZone = TimeZone.getTimeZone("UTC") } + val output = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale("id", "ID")) + val date = input.parse(value) ?: return value + output.format(date) + }.getOrElse { value } +} + +private fun relativeTimeLabel(timestampMillis: Long): String { + val diff = (System.currentTimeMillis() - timestampMillis).coerceAtLeast(0L) + val minutes = diff / 60_000L + return when { + minutes < 1 -> "baru saja" + minutes < 60 -> "$minutes mnt lalu" + minutes < 1_440 -> "${minutes / 60} jam lalu" + else -> "${minutes / 1_440} hari lalu" + } +} + +private fun LotDetailData.summaryCardOrFallback(): id.abelbirdnest.mobile.data.LotSummaryCard { + return summaryCard ?: id.abelbirdnest.mobile.data.LotSummaryCard( + lotCode = lot.lotCode, + sourceType = lot.sourceType, + status = lot.status, + grade = lot.grade?.name, + supplierName = lot.supplier?.name, + warehouseName = lot.warehouse?.name, + warehouseLocationName = lot.location?.name, + availableQty = lot.availableQty, + originalQty = lot.originalQty, + reservedQty = lot.reservedQty, + damagedQty = lot.damagedQty, + shrinkageQty = lot.shrinkageQty, + unitCode = lot.unitCode, + unitCost = lot.unitCost, + estimatedValue = lot.availableQty * lot.unitCost, + purchaseNo = lot.purchase?.purchaseNo, + purchaseDate = lot.purchase?.purchaseDate, + receiptNo = lot.receipt?.receiptNo, + receiptDate = lot.receipt?.receiptDate, + receivedAt = lot.receivedAt, + qrCodeValue = lot.labels.firstOrNull { it.type.equals("QR", ignoreCase = true) }?.value, + barcodeValue = lot.labels.firstOrNull { it.type.equals("BARCODE", ignoreCase = true) }?.value, + parentLotCode = lot.parentLot?.lotCode, + childLotCount = lot.childLots.size, + transformationCount = 0, + ) +} + +private fun processImageProxy( + imageProxy: androidx.camera.core.ImageProxy, + scanner: com.google.mlkit.vision.barcode.BarcodeScanner, + onCodeDetected: (String) -> Unit, +) { + val mediaImage = imageProxy.image + if (mediaImage == null) { + imageProxy.close() + return + } + + val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + scanner.process(image) + .addOnSuccessListener { barcodes -> + barcodes.firstNotNullOfOrNull { it.rawValue?.trim()?.takeIf(String::isNotBlank) }?.let(onCodeDetected) + } + .addOnCompleteListener { + imageProxy.close() + } +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/ui/MainViewModel.kt b/app/src/main/java/id/abelbirdnest/mobile/ui/MainViewModel.kt new file mode 100644 index 0000000..8a73921 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/ui/MainViewModel.kt @@ -0,0 +1,3231 @@ +package id.abelbirdnest.mobile.ui + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import id.abelbirdnest.mobile.data.AdjustmentReason +import id.abelbirdnest.mobile.data.CodeLabelOption +import id.abelbirdnest.mobile.data.CompleteWashingPayload +import id.abelbirdnest.mobile.data.ConsignmentBootstrapData +import id.abelbirdnest.mobile.data.ConsignmentCloseLinePayload +import id.abelbirdnest.mobile.data.ConsignmentDetail +import id.abelbirdnest.mobile.data.ConsignmentLineDetail +import id.abelbirdnest.mobile.data.ConsignmentListItem +import id.abelbirdnest.mobile.data.DashboardBundle +import id.abelbirdnest.mobile.data.FundRequestAgentOption +import id.abelbirdnest.mobile.data.FundRequestBankAccountOption +import id.abelbirdnest.mobile.data.FundRequestListItem +import id.abelbirdnest.mobile.data.FundRequestsBootstrapData +import id.abelbirdnest.mobile.data.JitSaleCloseLinePayload +import id.abelbirdnest.mobile.data.JitSaleClosePayload +import id.abelbirdnest.mobile.data.JitSaleDetail +import id.abelbirdnest.mobile.data.JitSaleLineDetail +import id.abelbirdnest.mobile.data.JitSaleListItem +import id.abelbirdnest.mobile.data.LookupRecord +import id.abelbirdnest.mobile.data.LotDetailData +import id.abelbirdnest.mobile.data.LotItem +import id.abelbirdnest.mobile.data.LotScanResult +import id.abelbirdnest.mobile.data.LotTransformationCreateInputPayload +import id.abelbirdnest.mobile.data.LotTransformationCreateOutputPayload +import id.abelbirdnest.mobile.data.LotTransformationCreatePayload +import id.abelbirdnest.mobile.data.MobileRepository +import id.abelbirdnest.mobile.data.PurchaseAnalysisDetail +import id.abelbirdnest.mobile.data.PurchaseAnalysisListItem +import id.abelbirdnest.mobile.data.PurchaseCreateLinePayload +import id.abelbirdnest.mobile.data.PurchaseCreatePayload +import id.abelbirdnest.mobile.data.PurchaseDetail +import id.abelbirdnest.mobile.data.PurchaseListItem +import id.abelbirdnest.mobile.data.PurchaseRealizationDetail +import id.abelbirdnest.mobile.data.PurchaseRealizationListItem +import id.abelbirdnest.mobile.data.RegularSaleCloseLinePayload +import id.abelbirdnest.mobile.data.RegularSaleClosePayload +import id.abelbirdnest.mobile.data.RegularSaleDetail +import id.abelbirdnest.mobile.data.RegularSaleLineDetail +import id.abelbirdnest.mobile.data.RegularSaleListItem +import id.abelbirdnest.mobile.data.ReceiptBootstrapData +import id.abelbirdnest.mobile.data.ReceiptCreateLinePayload +import id.abelbirdnest.mobile.data.ReceiptCreatePayload +import id.abelbirdnest.mobile.data.ReceiptDetail +import id.abelbirdnest.mobile.data.ReceiptDetailLine +import id.abelbirdnest.mobile.data.ReceiptListItem +import id.abelbirdnest.mobile.data.ReceiptPurchaseLineOption +import id.abelbirdnest.mobile.data.ReceiptPurchaseOption +import id.abelbirdnest.mobile.data.LotTransformationDetail +import id.abelbirdnest.mobile.data.LotTransformationListItem +import id.abelbirdnest.mobile.data.StockAdjustmentCreatePayload +import id.abelbirdnest.mobile.data.StockAdjustmentListItem +import id.abelbirdnest.mobile.data.UnitLookup +import id.abelbirdnest.mobile.data.WarehouseLookup +import id.abelbirdnest.mobile.data.WashingCreatePayload +import id.abelbirdnest.mobile.data.WashingListItem +import kotlinx.coroutines.launch +import retrofit2.HttpException +import java.io.IOException +import kotlin.math.roundToInt + +data class MainUiState( + val isCheckingSession: Boolean = true, + val isSubmitting: Boolean = false, + val isRefreshing: Boolean = false, + val isAuthenticated: Boolean = false, + val dashboard: DashboardBundle? = null, + val currentModule: String = "dashboard", + val lotsState: LotsUiState = LotsUiState(), + val salesRegularState: SalesRegularUiState = SalesRegularUiState(), + val salesJitState: SalesJitUiState = SalesJitUiState(), + val consignmentsState: ConsignmentsUiState = ConsignmentsUiState(), + val fundRequestsState: FundRequestsUiState = FundRequestsUiState(), + val purchasesState: PurchasesUiState = PurchasesUiState(), + val purchaseAnalysesState: PurchaseAnalysesUiState = PurchaseAnalysesUiState(), + val purchaseRealizationsState: PurchaseRealizationsUiState = PurchaseRealizationsUiState(), + val lotTransformationsState: LotTransformationsUiState = LotTransformationsUiState(), + val stockAdjustmentsState: StockAdjustmentsUiState = StockAdjustmentsUiState(), + val washingState: WashingUiState = WashingUiState(), + val receiptsState: ReceiptsUiState = ReceiptsUiState(), + val errorMessage: String? = null, +) + +data class LotsUiState( + val isLoading: Boolean = false, + val isScanning: Boolean = false, + val isLoadingDetail: Boolean = false, + val query: String = "", + val lots: List = emptyList(), + val selectedLotId: String? = null, + val lotDetail: LotDetailData? = null, + val recentScans: List = emptyList(), + val scanInput: String = "", + val inlineError: String? = null, +) + +data class WashingUiState( + val isLoading: Boolean = false, + val isSaving: Boolean = false, + val isCompleting: Boolean = false, + val washings: List = emptyList(), + val washingPlaces: List = emptyList(), + val grades: List = emptyList(), + val warehouses: List = emptyList(), + val selectableLots: List = emptyList(), + val screen: WashingScreen = WashingScreen.List, + val selectedWashingId: String? = null, + val selectedLotId: String? = null, + val selectedWashingPlaceId: String? = null, + val washingCost: String = "", + val durationHours: String = "24", + val afterQty: String = "", + val selectedGradeId: String? = null, + val selectedWarehouseId: String? = null, + val selectedWarehouseLocationId: String? = null, + val inlineError: String? = null, +) + +data class StockAdjustmentsUiState( + val isLoading: Boolean = false, + val isSaving: Boolean = false, + val items: List = emptyList(), + val reasons: List = emptyList(), + val selectableLots: List = emptyList(), + val selectedLotId: String? = null, + val selectedReasonId: String? = null, + val adjustmentDate: String = "", + val qtyChange: String = "", + val notes: String = "", + val inlineError: String? = null, +) + +data class PurchasesUiState( + val isLoading: Boolean = false, + val isSaving: Boolean = false, + val isActing: Boolean = false, + val items: List = emptyList(), + val employees: List = emptyList(), + val units: List = emptyList(), + val grades: List = emptyList(), + val warehouses: List = emptyList(), + val screen: PurchasesScreen = PurchasesScreen.List, + val selectedPurchaseId: String? = null, + val selectedPurchase: PurchaseDetail? = null, + val purchaseDate: String = "", + val receivedAt: String = "", + val selectedEmployeeId: String? = null, + val selectedWarehouseId: String? = null, + val selectedWarehouseLocationId: String? = null, + val notes: String = "", + val lines: List = emptyList(), + val inlineError: String? = null, +) + +data class PurchaseLineFormState( + val selectedGradeId: String? = null, + val qtyOrdered: String = "", + val selectedUnitId: String? = null, + val unitPrice: String = "", + val unitCost: String = "", + val selectedWarehouseId: String? = null, + val selectedWarehouseLocationId: String? = null, + val notes: String = "", +) + +data class PurchaseAnalysesUiState( + val isLoading: Boolean = false, + val items: List = emptyList(), + val selectedPurchaseId: String? = null, + val selectedDetail: PurchaseAnalysisDetail? = null, + val inlineError: String? = null, +) + +data class PurchaseRealizationsUiState( + val isLoading: Boolean = false, + val items: List = emptyList(), + val selectedPurchaseId: String? = null, + val selectedDetail: PurchaseRealizationDetail? = null, + val inlineError: String? = null, +) + +data class SalesRegularUiState( + val isLoading: Boolean = false, + val isClosing: Boolean = false, + val items: List = emptyList(), + val selectedSaleId: String? = null, + val selectedSale: RegularSaleDetail? = null, + val closeDate: String = "", + val closeLines: List = emptyList(), + val inlineError: String? = null, +) + +data class RegularSaleCloseLineFormState( + val lineId: String, + val lotCode: String, + val qtyPlanned: Double, + val qtyActualSold: String, + val qtyReturned: String, + val sellingPriceActual: String, +) + +data class SalesJitUiState( + val isLoading: Boolean = false, + val isClosing: Boolean = false, + val items: List = emptyList(), + val selectedSaleId: String? = null, + val selectedSale: JitSaleDetail? = null, + val closeDate: String = "", + val closeLines: List = emptyList(), + val inlineError: String? = null, +) + +data class JitSaleCloseLineFormState( + val lineId: String, + val gradeName: String, + val qtyPlanned: Double, + val sellingPriceActual: String, +) + +data class ConsignmentsUiState( + val isLoading: Boolean = false, + val isClosing: Boolean = false, + val items: List = emptyList(), + val salesOptions: List = emptyList(), + val buyerOptions: List = emptyList(), + val lotOptions: List = emptyList(), + val selectedConsignmentId: String? = null, + val selectedConsignment: ConsignmentDetail? = null, + val selectedLineId: String? = null, + val closeDate: String = "", + val sellingPrice: String = "", + val qtySold: String = "", + val qtyReturned: String = "", + val salesCommission: String = "", + val inlineError: String? = null, +) + +data class FundRequestsUiState( + val isLoading: Boolean = false, + val isSaving: Boolean = false, + val items: List = emptyList(), + val agents: List = emptyList(), + val companyBankAccounts: List = emptyList(), + val currencyCode: String = "IDR", + val transferType: String = "CAPITAL", + val referenceNo: String = "", + val selectedAgentId: String? = null, + val selectedAgentBankAccountId: String? = null, + val selectedCompanyBankAccountId: String? = null, + val amount: String = "", + val transferredAt: String = "", + val inlineError: String? = null, +) + +data class LotTransformationsUiState( + val isLoading: Boolean = false, + val isSaving: Boolean = false, + val items: List = emptyList(), + val selectableLots: List = emptyList(), + val transformationTypes: List = emptyList(), + val remainderModes: List = emptyList(), + val processingLossModes: List = emptyList(), + val grades: List = emptyList(), + val warehouses: List = emptyList(), + val screen: LotTransformationsScreen = LotTransformationsScreen.List, + val selectedTransformationId: String? = null, + val selectedTransformation: LotTransformationDetail? = null, + val transformationType: String = "MIX", + val transformationDate: String = "", + val remainderMode: String? = null, + val processingLossMode: String? = null, + val notes: String = "", + val inputs: List = emptyList(), + val outputs: List = emptyList(), + val inlineError: String? = null, +) + +data class LotTransformationInputFormState( + val selectedLotId: String? = null, + val lotQuery: String = "", + val qtyUsed: String = "", + val notes: String = "", +) + +data class LotTransformationOutputFormState( + val selectedGradeId: String? = null, + val selectedWarehouseId: String? = null, + val selectedWarehouseLocationId: String? = null, + val qtyProduced: String = "", + val notes: String = "", +) + +data class ReceiptsUiState( + val isLoading: Boolean = false, + val isSaving: Boolean = false, + val isGeneratingLots: Boolean = false, + val receipts: List = emptyList(), + val purchases: List = emptyList(), + val warehouses: List = emptyList(), + val screen: ReceiptsScreen = ReceiptsScreen.List, + val selectedReceiptId: String? = null, + val selectedReceipt: ReceiptDetail? = null, + val selectedPurchaseId: String? = null, + val receiptDate: String = "", + val notes: String = "", + val lines: List = emptyList(), + val inlineError: String? = null, +) + +data class ReceiptLineFormState( + val purchaseLineId: String, + val gradeId: String? = null, + val gradeName: String = "-", + val qtyOrdered: String = "0", + val qtyReceived: String = "0", + val qtyAccepted: String = "0", + val qtyRejected: String = "0", + val unitId: String, + val unitCode: String, + val unitCost: String = "0", + val warehouseId: String = "", + val warehouseLocationId: String = "", + val notes: String = "", +) + +enum class WashingScreen { + List, + Create, + Edit, + Complete, +} + +enum class ReceiptsScreen { + List, + Create, + Detail, +} + +enum class LotTransformationsScreen { + List, + Create, + Detail, +} + +enum class PurchasesScreen { + List, + Create, + Edit, + Detail, +} + +class MainViewModel(application: Application) : AndroidViewModel(application) { + private val repository = MobileRepository(application) + + var identity by mutableStateOf("") + private set + var password by mutableStateOf("") + private set + var uiState by mutableStateOf(MainUiState()) + private set + + init { + restoreSession() + } + + fun onIdentityChanged(value: String) { + identity = value + } + + fun onPasswordChanged(value: String) { + password = value + } + + fun login() { + if (identity.isBlank() || password.isBlank()) { + uiState = uiState.copy(errorMessage = "Email/username dan kata sandi wajib diisi.") + return + } + + viewModelScope.launch { + uiState = uiState.copy(isSubmitting = true, errorMessage = null) + try { + repository.login(identity.trim(), password) + loadDashboard(trigger = LoadTrigger.Login) + } catch (error: Throwable) { + uiState = uiState.copy( + isSubmitting = false, + isAuthenticated = false, + errorMessage = error.toReadableMessage(), + ) + } + } + } + + fun refreshDashboard() { + if (uiState.isRefreshing || uiState.isCheckingSession || uiState.isSubmitting) return + viewModelScope.launch { + loadDashboard(trigger = LoadTrigger.Refresh) + } + } + + fun logout() { + repository.logout() + password = "" + uiState = MainUiState( + isCheckingSession = false, + currentModule = "dashboard", + ) + } + + fun clearError() { + uiState = uiState.copy(errorMessage = null) + } + + fun selectModule(module: String) { + val dashboard = uiState.dashboard ?: return + val allowed = module == "dashboard" || dashboard.modules.contains(module) + if (allowed) { + uiState = uiState.copy(currentModule = module) + if (module == "lots" && uiState.lotsState.lots.isEmpty() && !uiState.lotsState.isLoading) { + refreshLots() + } + if (module == "sales_regular" && uiState.salesRegularState.items.isEmpty() && !uiState.salesRegularState.isLoading) { + refreshRegularSales() + } + if (module == "sales_jit" && uiState.salesJitState.items.isEmpty() && !uiState.salesJitState.isLoading) { + refreshJitSales() + } + if (module == "consignments" && uiState.consignmentsState.items.isEmpty() && !uiState.consignmentsState.isLoading) { + refreshConsignments() + } + if (module == "fund_requests" && uiState.fundRequestsState.items.isEmpty() && !uiState.fundRequestsState.isLoading) { + refreshFundRequests() + } + if (module == "purchases" && uiState.purchasesState.items.isEmpty() && !uiState.purchasesState.isLoading) { + refreshPurchases() + } + if (module == "purchase_analyses" && uiState.purchaseAnalysesState.items.isEmpty() && !uiState.purchaseAnalysesState.isLoading) { + refreshPurchaseAnalyses() + } + if (module == "purchase_realizations" && uiState.purchaseRealizationsState.items.isEmpty() && !uiState.purchaseRealizationsState.isLoading) { + refreshPurchaseRealizations() + } + if (module == "lot_transformations" && uiState.lotTransformationsState.items.isEmpty() && !uiState.lotTransformationsState.isLoading) { + refreshLotTransformations() + } + if (module == "stock_adjustments" && uiState.stockAdjustmentsState.items.isEmpty() && !uiState.stockAdjustmentsState.isLoading) { + refreshStockAdjustments() + } + if (module == "washing" && uiState.washingState.washings.isEmpty() && !uiState.washingState.isLoading) { + refreshWashing() + } + if (module == "receipts" && uiState.receiptsState.receipts.isEmpty() && !uiState.receiptsState.isLoading) { + refreshReceipts() + } + } + } + + fun refreshFundRequests() { + if (uiState.fundRequestsState.isLoading) return + viewModelScope.launch { + val previousState = uiState.fundRequestsState + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val bootstrap = repository.loadFundRequestsBootstrap() + val items = repository.loadFundRequests() + val defaultAgent = uiState.fundRequestsState.selectedAgentId ?: bootstrap.agents.firstOrNull()?.id + val defaultAgentBank = bootstrap.agents + .find { it.id == defaultAgent } + ?.bankAccounts + ?.firstOrNull() + ?.id + val defaultCompanyBank = uiState.fundRequestsState.selectedCompanyBankAccountId + ?: bootstrap.companyBankAccounts.firstOrNull()?.id + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy( + isLoading = false, + items = items, + agents = bootstrap.agents, + companyBankAccounts = bootstrap.companyBankAccounts, + currencyCode = bootstrap.currencyCode, + selectedAgentId = defaultAgent, + selectedAgentBankAccountId = uiState.fundRequestsState.selectedAgentBankAccountId ?: defaultAgentBank, + selectedCompanyBankAccountId = defaultCompanyBank, + transferredAt = uiState.fundRequestsState.transferredAt.ifBlank { currentIsoDateTimeLocal() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + fundRequestsState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshConsignments() { + if (uiState.consignmentsState.isLoading) return + viewModelScope.launch { + val previousState = uiState.consignmentsState + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val bootstrap = repository.loadConsignmentsBootstrap() + val items = repository.loadConsignments() + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + isLoading = false, + items = items, + salesOptions = bootstrap.sales, + buyerOptions = bootstrap.buyers, + lotOptions = bootstrap.lots, + closeDate = uiState.consignmentsState.closeDate.ifBlank { todayIsoDate() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + consignmentsState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshJitSales() { + if (uiState.salesJitState.isLoading) return + viewModelScope.launch { + val previousState = uiState.salesJitState + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val items = repository.loadJitSales() + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + isLoading = false, + items = items, + closeDate = uiState.salesJitState.closeDate.ifBlank { todayIsoDate() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + salesJitState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshRegularSales() { + if (uiState.salesRegularState.isLoading) return + viewModelScope.launch { + val previousState = uiState.salesRegularState + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val items = repository.loadRegularSales() + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + isLoading = false, + items = items, + closeDate = uiState.salesRegularState.closeDate.ifBlank { todayIsoDate() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + salesRegularState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshLotTransformations() { + if (uiState.lotTransformationsState.isLoading) return + viewModelScope.launch { + val previousState = uiState.lotTransformationsState + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val dashboard = uiState.dashboard + val lots = if (uiState.lotsState.lots.isNotEmpty()) uiState.lotsState.lots else repository.loadLots() + val items = repository.loadLotTransformations() + uiState = uiState.copy( + lotsState = uiState.lotsState.copy(lots = lots), + lotTransformationsState = uiState.lotTransformationsState.copy( + isLoading = false, + items = items, + selectableLots = lots.filter { it.availableQty > 0 && it.status == "ACTIVE" }, + transformationTypes = dashboard?.transformationTypes ?: defaultTransformationTypes(), + remainderModes = dashboard?.remainderModes ?: defaultRemainderModes(), + processingLossModes = dashboard?.processingLossModes ?: defaultProcessingLossModes(), + grades = dashboard?.grades.orEmpty(), + warehouses = dashboard?.warehouses.orEmpty(), + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + lotTransformationsState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshPurchases() { + if (uiState.purchasesState.isLoading) return + viewModelScope.launch { + val previousState = uiState.purchasesState + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val dashboard = uiState.dashboard + val employees = repository.loadEmployees().filter { it.code != null } + val units = repository.loadUnits() + val items = repository.loadPurchases() + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isLoading = false, + items = items, + employees = employees, + units = units, + grades = dashboard?.grades.orEmpty(), + warehouses = dashboard?.warehouses.orEmpty(), + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchasesState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshPurchaseAnalyses() { + if (uiState.purchaseAnalysesState.isLoading) return + viewModelScope.launch { + val previousState = uiState.purchaseAnalysesState + uiState = uiState.copy( + purchaseAnalysesState = uiState.purchaseAnalysesState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val items = repository.loadPurchaseAnalyses() + uiState = uiState.copy( + purchaseAnalysesState = uiState.purchaseAnalysesState.copy( + isLoading = false, + items = items, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchaseAnalysesState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshPurchaseRealizations() { + if (uiState.purchaseRealizationsState.isLoading) return + viewModelScope.launch { + val previousState = uiState.purchaseRealizationsState + uiState = uiState.copy( + purchaseRealizationsState = uiState.purchaseRealizationsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val items = repository.loadPurchaseRealizations() + uiState = uiState.copy( + purchaseRealizationsState = uiState.purchaseRealizationsState.copy( + isLoading = false, + items = items, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchaseRealizationsState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshStockAdjustments() { + if (uiState.stockAdjustmentsState.isLoading) return + viewModelScope.launch { + val previousState = uiState.stockAdjustmentsState + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val lots = if (uiState.lotsState.lots.isNotEmpty()) uiState.lotsState.lots else repository.loadLots() + val reasons = repository.loadAdjustmentReasons() + .filter { it.status == null || it.status.equals("ACTIVE", ignoreCase = true) } + val items = repository.loadStockAdjustments() + uiState = uiState.copy( + lotsState = uiState.lotsState.copy(lots = lots), + stockAdjustmentsState = uiState.stockAdjustmentsState.copy( + isLoading = false, + items = items, + reasons = reasons, + selectableLots = lots.filter { it.availableQty > 0 && it.status == "ACTIVE" }, + adjustmentDate = uiState.stockAdjustmentsState.adjustmentDate.ifBlank { todayIsoDate() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + stockAdjustmentsState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshReceipts() { + if (uiState.receiptsState.isLoading) return + viewModelScope.launch { + val previousState = uiState.receiptsState + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val bootstrap = repository.loadReceiptsBootstrap() + val receipts = repository.loadReceipts() + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isLoading = false, + receipts = receipts, + purchases = bootstrap.purchases, + warehouses = bootstrap.warehouses, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + receiptsState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshWashing() { + if (uiState.washingState.isLoading) return + viewModelScope.launch { + val previousState = uiState.washingState + uiState = uiState.copy( + washingState = uiState.washingState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val bootstrap = repository.loadWashingBootstrap() + val washings = repository.loadWashings() + val lots = if (uiState.lotsState.lots.isNotEmpty()) uiState.lotsState.lots else repository.loadLots() + val inProgressLotIds = washings.filter { it.status == "IN_PROGRESS" }.map { it.lot.id }.toSet() + val editingLotId = previousState.selectedWashingId?.let { selectedId -> + washings.find { it.id == selectedId }?.lot?.id + } + val selectableLots = lots.filter { + it.status == "ACTIVE" && + it.availableQty > 0 && + (!inProgressLotIds.contains(it.id) || editingLotId == it.id) + } + + uiState = uiState.copy( + lotsState = uiState.lotsState.copy(lots = lots), + washingState = uiState.washingState.copy( + isLoading = false, + washings = washings, + washingPlaces = bootstrap.washingPlaces, + grades = bootstrap.grades, + warehouses = bootstrap.warehouses, + selectableLots = selectableLots, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + washingState = previousState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun refreshLots() { + if (uiState.lotsState.isLoading) return + viewModelScope.launch { + val previousLots = uiState.lotsState.lots + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isLoading = true, + inlineError = null, + ), + ) + try { + val lots = repository.loadLots() + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isLoading = false, + lots = lots, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isLoading = false, + lots = previousLots, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun updateLotQuery(value: String) { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy(query = value), + ) + } + + fun updateScanInput(value: String) { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy(scanInput = value), + ) + } + + fun openLotDetail(lotId: String) { + viewModelScope.launch { + val cached = uiState.lotsState.lotDetail?.takeIf { it.lot.id == lotId } + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + selectedLotId = lotId, + isLoadingDetail = cached == null, + lotDetail = cached, + inlineError = null, + ), + ) + if (cached != null) return@launch + + try { + val detail = repository.loadLotDetail(lotId) + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isLoadingDetail = false, + lotDetail = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isLoadingDetail = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun closeLotDetail() { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + selectedLotId = null, + isLoadingDetail = false, + inlineError = null, + ), + ) + } + + fun scanLot() { + val code = uiState.lotsState.scanInput.trim() + performLotScan(code) + } + + fun scanLotCode(code: String) { + performLotScan(code.trim()) + } + + private fun performLotScan(code: String) { + if (code.isBlank()) { + uiState = uiState.copy(errorMessage = "Masukkan kode lot untuk pencarian.") + return + } + + viewModelScope.launch { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isScanning = true, + inlineError = null, + ), + ) + try { + val result = repository.scanLot(code) + uiState = uiState.copy( + currentModule = "lots", + lotsState = uiState.lotsState.copy( + isScanning = false, + scanInput = "", + selectedLotId = result.payload.lot.id, + lotDetail = result.payload.toLotDetailData(), + recentScans = (listOf(result) + uiState.lotsState.recentScans) + .distinctBy { it.payload.lot.id } + .take(10), + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + lotsState = uiState.lotsState.copy( + isScanning = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun openRecentScan(result: LotScanResult) { + uiState = uiState.copy( + currentModule = "lots", + lotsState = uiState.lotsState.copy( + selectedLotId = result.payload.lot.id, + lotDetail = result.payload.toLotDetailData(), + inlineError = null, + ), + ) + } + + fun openRegularSaleDetail(id: String) { + viewModelScope.launch { + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + selectedSaleId = id, + selectedSale = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadRegularSaleDetail(id) + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + isLoading = false, + selectedSale = detail, + closeDate = detail.closeDate ?: todayIsoDate(), + closeLines = detail.lines.map { it.toCloseFormState() }, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun closeRegularSaleDetail() { + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + selectedSaleId = null, + selectedSale = null, + isClosing = false, + closeDate = todayIsoDate(), + closeLines = emptyList(), + inlineError = null, + ), + ) + } + + fun updateRegularSaleCloseDate(value: String) { + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy(closeDate = value), + ) + } + + private fun updateRegularSaleCloseLine( + index: Int, + transform: (RegularSaleCloseLineFormState) -> RegularSaleCloseLineFormState, + ) { + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + closeLines = uiState.salesRegularState.closeLines.mapIndexed { lineIndex, line -> + if (lineIndex == index) transform(line) else line + }, + ), + ) + } + + fun updateRegularSaleQtySold(index: Int, value: String) { + updateRegularSaleCloseLine(index) { it.copy(qtyActualSold = value) } + } + + fun updateRegularSaleQtyReturned(index: Int, value: String) { + updateRegularSaleCloseLine(index) { it.copy(qtyReturned = value) } + } + + fun updateRegularSalePriceActual(index: Int, value: String) { + updateRegularSaleCloseLine(index) { it.copy(sellingPriceActual = value) } + } + + fun closeRegularSale() { + val saleId = uiState.salesRegularState.selectedSaleId ?: return + val state = uiState.salesRegularState + if (state.closeDate.isBlank() || state.closeLines.isEmpty()) { + uiState = uiState.copy( + salesRegularState = state.copy(inlineError = "Tanggal close dan seluruh line penjualan wajib diisi."), + ) + return + } + + val payloadLines = state.closeLines.mapNotNull { line -> + val qtyActualSold = line.qtyActualSold.toDoubleOrNull() + val qtyReturned = line.qtyReturned.toDoubleOrNull() + val sellingPriceActual = line.sellingPriceActual.toDoubleOrNull() + if (qtyActualSold == null || qtyReturned == null || sellingPriceActual == null) { + null + } else { + RegularSaleCloseLinePayload( + lineId = line.lineId, + qtyActualSold = qtyActualSold, + qtyReturned = qtyReturned, + sellingPriceActual = sellingPriceActual, + ) + } + } + + if (payloadLines.size != state.closeLines.size) { + uiState = uiState.copy( + salesRegularState = state.copy(inlineError = "Semua input close penjualan harus numerik dan lengkap."), + ) + return + } + + if (payloadLines.any { it.qtyActualSold + it.qtyReturned > state.closeLines.first { form -> form.lineId == it.lineId }.qtyPlanned }) { + uiState = uiState.copy( + salesRegularState = state.copy(inlineError = "Qty sold + returned tidak boleh melebihi qty planned."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy( + salesRegularState = state.copy( + isClosing = true, + inlineError = null, + ), + ) + try { + val detail = repository.closeRegularSale( + saleId, + RegularSaleClosePayload( + closeDate = state.closeDate, + lines = payloadLines, + ), + ) + refreshRegularSales() + refreshLots() + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + isClosing = false, + selectedSale = detail, + closeDate = detail.closeDate ?: state.closeDate, + closeLines = detail.lines.map { it.toCloseFormState() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + salesRegularState = uiState.salesRegularState.copy( + isClosing = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun openJitSaleDetail(id: String) { + viewModelScope.launch { + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + selectedSaleId = id, + selectedSale = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadJitSaleDetail(id) + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + isLoading = false, + selectedSale = detail, + closeDate = detail.closeDate ?: todayIsoDate(), + closeLines = detail.lines.map { it.toCloseFormState() }, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun closeJitSaleDetail() { + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + selectedSaleId = null, + selectedSale = null, + isClosing = false, + closeDate = todayIsoDate(), + closeLines = emptyList(), + inlineError = null, + ), + ) + } + + fun updateJitSaleCloseDate(value: String) { + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy(closeDate = value), + ) + } + + fun updateJitSalePriceActual(index: Int, value: String) { + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + closeLines = uiState.salesJitState.closeLines.mapIndexed { lineIndex, line -> + if (lineIndex == index) line.copy(sellingPriceActual = value) else line + }, + ), + ) + } + + fun closeJitSale() { + val saleId = uiState.salesJitState.selectedSaleId ?: return + val state = uiState.salesJitState + if (state.closeDate.isBlank() || state.closeLines.isEmpty()) { + uiState = uiState.copy( + salesJitState = state.copy(inlineError = "Tanggal close dan seluruh line penjualan wajib diisi."), + ) + return + } + + val payloadLines = state.closeLines.mapNotNull { line -> + val sellingPriceActual = line.sellingPriceActual.toDoubleOrNull() + if (sellingPriceActual == null) null else JitSaleCloseLinePayload( + lineId = line.lineId, + sellingPriceActual = sellingPriceActual, + ) + } + + if (payloadLines.size != state.closeLines.size) { + uiState = uiState.copy( + salesJitState = state.copy(inlineError = "Harga jual aktual semua line harus numerik dan lengkap."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy( + salesJitState = state.copy( + isClosing = true, + inlineError = null, + ), + ) + try { + val detail = repository.closeJitSale( + saleId, + JitSaleClosePayload( + closeDate = state.closeDate, + lines = payloadLines, + ), + ) + refreshJitSales() + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + isClosing = false, + selectedSale = detail, + closeDate = detail.closeDate ?: state.closeDate, + closeLines = detail.lines.map { it.toCloseFormState() }, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + salesJitState = uiState.salesJitState.copy( + isClosing = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun openConsignmentDetail(id: String) { + viewModelScope.launch { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + selectedConsignmentId = id, + selectedConsignment = null, + selectedLineId = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadConsignmentDetail(id) + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + isLoading = false, + selectedConsignment = detail, + closeDate = todayIsoDate(), + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun closeConsignmentDetail() { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + selectedConsignmentId = null, + selectedConsignment = null, + selectedLineId = null, + isClosing = false, + inlineError = null, + ), + ) + } + + fun selectConsignmentLine(lineId: String?) { + val line = uiState.consignmentsState.selectedConsignment?.lines?.find { it.id == lineId } + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + selectedLineId = lineId, + closeDate = line?.closeDate ?: todayIsoDate(), + sellingPrice = line?.sellingPrice?.let(::formatQuantityInput) ?: "", + qtySold = line?.qtySold?.let(::formatQuantityInput) ?: "", + qtyReturned = line?.qtyReturned?.let(::formatQuantityInput) ?: "", + salesCommission = line?.salesCommission?.let(::formatQuantityInput) ?: "", + inlineError = null, + ), + ) + } + + fun updateConsignmentCloseDate(value: String) { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy(closeDate = value), + ) + } + + fun updateConsignmentSellingPrice(value: String) { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy(sellingPrice = value), + ) + } + + fun updateConsignmentQtySold(value: String) { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy(qtySold = value), + ) + } + + fun updateConsignmentQtyReturned(value: String) { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy(qtyReturned = value), + ) + } + + fun updateConsignmentSalesCommission(value: String) { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy(salesCommission = value), + ) + } + + fun closeConsignmentLine() { + val state = uiState.consignmentsState + val lineId = state.selectedLineId ?: return + val selectedLine = state.selectedConsignment?.lines?.find { it.id == lineId } ?: return + val sellingPrice = state.sellingPrice.toDoubleOrNull() + val qtySold = state.qtySold.toDoubleOrNull() + val qtyReturned = state.qtyReturned.toDoubleOrNull() + val salesCommission = state.salesCommission.toDoubleOrNull() + + if (state.closeDate.isBlank() || sellingPrice == null || qtySold == null || qtyReturned == null || salesCommission == null) { + uiState = uiState.copy( + consignmentsState = state.copy(inlineError = "Tanggal close, harga jual, qty sold, qty returned, dan komisi sales wajib diisi."), + ) + return + } + + if (qtySold == 0.0 && qtyReturned == 0.0) { + uiState = uiState.copy( + consignmentsState = state.copy(inlineError = "Isi qty sold atau qty returned untuk menutup item."), + ) + return + } + + if (qtySold + qtyReturned > selectedLine.qtyConsigned) { + uiState = uiState.copy( + consignmentsState = state.copy(inlineError = "Qty sold + returned tidak boleh melebihi qty consigned."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy( + consignmentsState = state.copy( + isClosing = true, + inlineError = null, + ), + ) + try { + repository.closeConsignmentLine( + lineId = lineId, + payload = ConsignmentCloseLinePayload( + closeDate = state.closeDate, + sellingPrice = sellingPrice, + qtySold = qtySold, + qtyReturned = qtyReturned, + salesCommission = salesCommission, + ), + ) + val consignmentId = state.selectedConsignmentId ?: return@launch + val detail = repository.loadConsignmentDetail(consignmentId) + refreshConsignments() + refreshLots() + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + isClosing = false, + selectedConsignment = detail, + selectedLineId = null, + sellingPrice = "", + qtySold = "", + qtyReturned = "", + salesCommission = "", + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + consignmentsState = uiState.consignmentsState.copy( + isClosing = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun updateFundRequestTransferType(value: String) { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy(transferType = value, inlineError = null), + ) + } + + fun updateFundRequestReferenceNo(value: String) { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy(referenceNo = value, inlineError = null), + ) + } + + fun selectFundRequestAgent(agentId: String) { + val agent = uiState.fundRequestsState.agents.find { it.id == agentId } + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy( + selectedAgentId = agentId, + selectedAgentBankAccountId = agent?.bankAccounts?.firstOrNull()?.id, + inlineError = null, + ), + ) + } + + fun selectFundRequestAgentBank(accountId: String) { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy(selectedAgentBankAccountId = accountId, inlineError = null), + ) + } + + fun selectFundRequestCompanyBank(accountId: String) { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy(selectedCompanyBankAccountId = accountId, inlineError = null), + ) + } + + fun updateFundRequestAmount(value: String) { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy(amount = value, inlineError = null), + ) + } + + fun updateFundRequestTransferredAt(value: String) { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy(transferredAt = value, inlineError = null), + ) + } + + fun saveFundRequest() { + val state = uiState.fundRequestsState + if ( + state.referenceNo.isBlank() || + state.selectedAgentId.isNullOrBlank() || + state.selectedAgentBankAccountId.isNullOrBlank() || + state.selectedCompanyBankAccountId.isNullOrBlank() || + state.amount.toDoubleOrNull() == null || + state.amount.toDouble() <= 0 || + state.transferredAt.isBlank() + ) { + uiState = uiState.copy( + fundRequestsState = state.copy(inlineError = "Tipe transfer, referensi, agen, rekening, nominal, dan waktu transfer wajib diisi."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy( + fundRequestsState = state.copy( + isSaving = true, + inlineError = null, + ), + ) + try { + repository.createFundRequest( + transferType = state.transferType, + referenceNo = state.referenceNo, + agentId = state.selectedAgentId, + agentBankAccountId = state.selectedAgentBankAccountId, + companyBankAccountId = state.selectedCompanyBankAccountId, + amount = state.amount, + transferredAt = state.transferredAt, + ) + refreshFundRequests() + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy( + isSaving = false, + referenceNo = "", + amount = "", + transferredAt = currentIsoDateTimeLocal(), + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + fundRequestsState = uiState.fundRequestsState.copy( + isSaving = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun openPurchaseDetail(id: String) { + viewModelScope.launch { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + screen = PurchasesScreen.Detail, + selectedPurchaseId = id, + selectedPurchase = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadPurchaseDetail(id) + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isLoading = false, + selectedPurchase = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun closePurchaseDetail() { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + screen = PurchasesScreen.List, + selectedPurchaseId = null, + selectedPurchase = null, + isActing = false, + isSaving = false, + inlineError = null, + ), + ) + } + + fun openCreatePurchase() { + if (uiState.purchasesState.employees.isEmpty() || uiState.purchasesState.units.isEmpty()) { + refreshPurchases() + } + val defaultWarehouse = uiState.purchasesState.warehouses.firstOrNull() + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + screen = PurchasesScreen.Create, + selectedPurchaseId = null, + selectedPurchase = null, + purchaseDate = todayIsoDate(), + receivedAt = currentIsoDateTimeLocal(), + selectedEmployeeId = uiState.purchasesState.employees.firstOrNull()?.id, + selectedWarehouseId = defaultWarehouse?.id, + selectedWarehouseLocationId = defaultWarehouse?.locations?.firstOrNull()?.id, + notes = "", + lines = listOf( + PurchaseLineFormState( + selectedUnitId = uiState.purchasesState.units.firstOrNull()?.id, + selectedWarehouseId = defaultWarehouse?.id, + selectedWarehouseLocationId = defaultWarehouse?.locations?.firstOrNull()?.id, + ), + ), + inlineError = null, + ), + ) + } + + fun openEditPurchase() { + val detail = uiState.purchasesState.selectedPurchase ?: return + val defaultWarehouse = uiState.purchasesState.warehouses.firstOrNull() + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + screen = PurchasesScreen.Edit, + purchaseDate = detail.purchaseDate, + receivedAt = detail.receivedAt ?: currentIsoDateTimeLocal(), + selectedEmployeeId = detail.receivedByEmployee?.id ?: uiState.purchasesState.employees.firstOrNull()?.id, + selectedWarehouseId = detail.lines.firstOrNull()?.warehouse?.id ?: defaultWarehouse?.id, + selectedWarehouseLocationId = detail.lines.firstOrNull()?.warehouseLocation?.id ?: defaultWarehouse?.locations?.firstOrNull()?.id, + notes = detail.notes.orEmpty(), + lines = detail.lines.map { line -> + PurchaseLineFormState( + selectedGradeId = line.grade?.id, + qtyOrdered = formatQuantityInput(line.qtyOrdered), + selectedUnitId = line.unit.id, + unitPrice = formatQuantityInput(line.unitPrice), + unitCost = formatQuantityInput(line.unitCost), + selectedWarehouseId = line.warehouse?.id ?: defaultWarehouse?.id, + selectedWarehouseLocationId = line.warehouseLocation?.id ?: defaultWarehouse?.locations?.firstOrNull()?.id, + notes = line.notes.orEmpty(), + ) + }.ifEmpty { + listOf( + PurchaseLineFormState( + selectedUnitId = uiState.purchasesState.units.firstOrNull()?.id, + selectedWarehouseId = defaultWarehouse?.id, + selectedWarehouseLocationId = defaultWarehouse?.locations?.firstOrNull()?.id, + ), + ) + }, + inlineError = null, + ), + ) + } + + fun closePurchaseEditor() { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + screen = if (uiState.purchasesState.selectedPurchase != null) PurchasesScreen.Detail else PurchasesScreen.List, + isSaving = false, + inlineError = null, + ), + ) + } + + fun updatePurchaseDate(value: String) { + uiState = uiState.copy(purchasesState = uiState.purchasesState.copy(purchaseDate = value, inlineError = null)) + } + + fun updatePurchaseReceivedAt(value: String) { + uiState = uiState.copy(purchasesState = uiState.purchasesState.copy(receivedAt = value, inlineError = null)) + } + + fun selectPurchaseEmployee(id: String) { + uiState = uiState.copy(purchasesState = uiState.purchasesState.copy(selectedEmployeeId = id, inlineError = null)) + } + + fun selectPurchaseWarehouse(id: String) { + val warehouse = uiState.purchasesState.warehouses.find { it.id == id } + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + selectedWarehouseId = id, + selectedWarehouseLocationId = warehouse?.locations?.firstOrNull()?.id, + lines = uiState.purchasesState.lines.map { line -> + line.copy( + selectedWarehouseId = id, + selectedWarehouseLocationId = warehouse?.locations?.firstOrNull()?.id, + ) + }, + inlineError = null, + ), + ) + } + + fun selectPurchaseWarehouseLocation(id: String?) { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + selectedWarehouseLocationId = id, + lines = uiState.purchasesState.lines.map { it.copy(selectedWarehouseLocationId = id) }, + inlineError = null, + ), + ) + } + + fun updatePurchaseNotes(value: String) { + uiState = uiState.copy(purchasesState = uiState.purchasesState.copy(notes = value, inlineError = null)) + } + + fun addPurchaseLine() { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + lines = uiState.purchasesState.lines + PurchaseLineFormState( + selectedUnitId = uiState.purchasesState.units.firstOrNull()?.id, + selectedWarehouseId = uiState.purchasesState.selectedWarehouseId, + selectedWarehouseLocationId = uiState.purchasesState.selectedWarehouseLocationId, + ), + inlineError = null, + ), + ) + } + + fun removePurchaseLine(index: Int) { + if (uiState.purchasesState.lines.size <= 1) return + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + lines = uiState.purchasesState.lines.filterIndexed { currentIndex, _ -> currentIndex != index }, + inlineError = null, + ), + ) + } + + fun selectPurchaseLineGrade(index: Int, id: String?) { + updatePurchaseLine(index) { it.copy(selectedGradeId = id) } + } + + fun updatePurchaseLineQty(index: Int, value: String) { + updatePurchaseLine(index) { it.copy(qtyOrdered = value) } + } + + fun selectPurchaseLineUnit(index: Int, id: String) { + updatePurchaseLine(index) { it.copy(selectedUnitId = id) } + } + + fun updatePurchaseLineUnitPrice(index: Int, value: String) { + updatePurchaseLine(index) { it.copy(unitPrice = value, unitCost = if (it.unitCost.isBlank()) value else it.unitCost) } + } + + fun updatePurchaseLineUnitCost(index: Int, value: String) { + updatePurchaseLine(index) { it.copy(unitCost = value) } + } + + fun updatePurchaseLineNotes(index: Int, value: String) { + updatePurchaseLine(index) { it.copy(notes = value) } + } + + fun savePurchase() { + val state = uiState.purchasesState + if (state.purchaseDate.isBlank() || state.receivedAt.isBlank() || state.selectedEmployeeId.isNullOrBlank() || state.lines.isEmpty()) { + uiState = uiState.copy( + purchasesState = state.copy(inlineError = "Tanggal beli, waktu diterima, penerima, dan minimal satu line wajib diisi."), + ) + return + } + + val lines = state.lines.mapNotNull { line -> + val qty = line.qtyOrdered.toDoubleOrNull() + val unitPrice = line.unitPrice.toDoubleOrNull() + val unitCost = line.unitCost.toDoubleOrNull() + val unitId = line.selectedUnitId + if (qty == null || qty <= 0 || unitPrice == null || unitPrice < 0 || unitCost == null || unitCost < 0 || unitId.isNullOrBlank()) { + null + } else { + PurchaseCreateLinePayload( + gradeId = line.selectedGradeId, + qtyOrdered = qty, + qtyReceived = qty, + qtyAccepted = qty, + qtyRejected = 0.0, + unitId = unitId, + unitPrice = unitPrice, + unitCost = unitCost, + classificationStatus = "FINAL", + warehouseId = line.selectedWarehouseId ?: state.selectedWarehouseId, + warehouseLocationId = line.selectedWarehouseLocationId ?: state.selectedWarehouseLocationId, + notes = line.notes.ifBlank { null }, + ) + } + } + + if (lines.size != state.lines.size) { + uiState = uiState.copy( + purchasesState = state.copy(inlineError = "Semua line pembelian wajib lengkap dan numerik."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy(purchasesState = state.copy(isSaving = true, inlineError = null)) + try { + val payload = PurchaseCreatePayload( + receivedByEmployeeId = state.selectedEmployeeId, + receivedAt = state.receivedAt, + purchaseDate = state.purchaseDate, + warehouseId = state.selectedWarehouseId, + warehouseLocationId = state.selectedWarehouseLocationId, + notes = state.notes.ifBlank { null }, + lines = lines, + ) + val detail = if (state.screen == PurchasesScreen.Edit && !state.selectedPurchaseId.isNullOrBlank()) { + repository.updatePurchase(state.selectedPurchaseId, payload) + } else { + repository.createPurchase(payload) + } + refreshPurchases() + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isSaving = false, + screen = PurchasesScreen.Detail, + selectedPurchaseId = detail.id, + selectedPurchase = detail, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isSaving = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun submitPurchase() { + val purchaseId = uiState.purchasesState.selectedPurchaseId ?: return + viewModelScope.launch { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isActing = true, + inlineError = null, + ), + ) + try { + val detail = repository.submitPurchase(purchaseId) + refreshPurchases() + refreshLots() + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isActing = false, + selectedPurchase = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isActing = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun cancelPurchase() { + val purchaseId = uiState.purchasesState.selectedPurchaseId ?: return + viewModelScope.launch { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isActing = true, + inlineError = null, + ), + ) + try { + repository.cancelPurchase(purchaseId) + val detail = repository.loadPurchaseDetail(purchaseId) + refreshPurchases() + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isActing = false, + selectedPurchase = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + isActing = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun openPurchaseAnalysisDetail(purchaseId: String) { + viewModelScope.launch { + uiState = uiState.copy( + purchaseAnalysesState = uiState.purchaseAnalysesState.copy( + selectedPurchaseId = purchaseId, + selectedDetail = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadPurchaseAnalysisDetail(purchaseId) + uiState = uiState.copy( + purchaseAnalysesState = uiState.purchaseAnalysesState.copy( + isLoading = false, + selectedDetail = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchaseAnalysesState = uiState.purchaseAnalysesState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun closePurchaseAnalysisDetail() { + uiState = uiState.copy( + purchaseAnalysesState = uiState.purchaseAnalysesState.copy( + selectedPurchaseId = null, + selectedDetail = null, + inlineError = null, + ), + ) + } + + fun openPurchaseRealizationDetail(purchaseId: String) { + viewModelScope.launch { + uiState = uiState.copy( + purchaseRealizationsState = uiState.purchaseRealizationsState.copy( + selectedPurchaseId = purchaseId, + selectedDetail = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadPurchaseRealizationDetail(purchaseId) + uiState = uiState.copy( + purchaseRealizationsState = uiState.purchaseRealizationsState.copy( + isLoading = false, + selectedDetail = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + purchaseRealizationsState = uiState.purchaseRealizationsState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun closePurchaseRealizationDetail() { + uiState = uiState.copy( + purchaseRealizationsState = uiState.purchaseRealizationsState.copy( + selectedPurchaseId = null, + selectedDetail = null, + inlineError = null, + ), + ) + } + + fun openLotTransformationDetail(id: String) { + viewModelScope.launch { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + screen = LotTransformationsScreen.Detail, + selectedTransformationId = id, + selectedTransformation = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadLotTransformationDetail(id) + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + isLoading = false, + selectedTransformation = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun closeLotTransformationDetail() { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + screen = LotTransformationsScreen.List, + selectedTransformationId = null, + selectedTransformation = null, + inlineError = null, + ), + ) + } + + fun openCreateLotTransformation() { + if (uiState.lotTransformationsState.selectableLots.isEmpty()) { + refreshLotTransformations() + } + val defaultWarehouse = uiState.lotTransformationsState.warehouses.firstOrNull() + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + screen = LotTransformationsScreen.Create, + selectedTransformationId = null, + selectedTransformation = null, + transformationType = "MIX", + transformationDate = todayIsoDate(), + remainderMode = uiState.lotTransformationsState.remainderModes.firstOrNull()?.code, + processingLossMode = uiState.lotTransformationsState.processingLossModes.firstOrNull()?.code, + notes = "", + inputs = listOf( + LotTransformationInputFormState(), + LotTransformationInputFormState(), + ), + outputs = listOf( + LotTransformationOutputFormState( + selectedWarehouseId = defaultWarehouse?.id, + selectedWarehouseLocationId = defaultWarehouse?.locations?.firstOrNull()?.id, + ), + ), + inlineError = null, + ), + ) + } + + fun closeLotTransformationScreen() { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + screen = LotTransformationsScreen.List, + selectedTransformationId = null, + selectedTransformation = null, + isSaving = false, + inlineError = null, + ), + ) + } + + fun updateLotTransformationType(value: String) { + val inputs = if (value == "REGRADE") { + listOf(uiState.lotTransformationsState.inputs.firstOrNull() ?: LotTransformationInputFormState()) + } else { + uiState.lotTransformationsState.inputs.ifEmpty { + listOf(LotTransformationInputFormState(), LotTransformationInputFormState()) + }.let { existing -> + if (existing.size >= 2) existing else existing + LotTransformationInputFormState() + } + } + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + transformationType = value, + inputs = inputs, + inlineError = null, + ), + ) + } + + fun updateLotTransformationDate(value: String) { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + transformationDate = value, + inlineError = null, + ), + ) + } + + fun updateLotTransformationRemainderMode(value: String?) { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + remainderMode = value, + inlineError = null, + ), + ) + } + + fun updateLotTransformationProcessingLossMode(value: String?) { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + processingLossMode = value, + inlineError = null, + ), + ) + } + + fun updateLotTransformationNotes(value: String) { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + notes = value, + inlineError = null, + ), + ) + } + + fun addLotTransformationInput() { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + inputs = uiState.lotTransformationsState.inputs + LotTransformationInputFormState(), + inlineError = null, + ), + ) + } + + fun removeLotTransformationInput(index: Int) { + val current = uiState.lotTransformationsState.inputs + if (current.size <= 1 || index !in current.indices) return + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + inputs = current.filterIndexed { currentIndex, _ -> currentIndex != index }, + inlineError = null, + ), + ) + } + + fun updateLotTransformationInputQuery(index: Int, value: String) { + updateLotTransformationInput(index) { + it.copy( + selectedLotId = null, + lotQuery = value, + ) + } + } + + fun selectLotTransformationInputLot(index: Int, lotId: String) { + val lot = uiState.lotTransformationsState.selectableLots.find { it.id == lotId } ?: return + updateLotTransformationInput(index) { + it.copy( + selectedLotId = lot.id, + lotQuery = lot.lotCode, + ) + } + } + + fun updateLotTransformationInputQty(index: Int, value: String) { + updateLotTransformationInput(index) { it.copy(qtyUsed = value) } + } + + fun updateLotTransformationInputNotes(index: Int, value: String) { + updateLotTransformationInput(index) { it.copy(notes = value) } + } + + fun clearLotTransformationInputLot(index: Int) { + updateLotTransformationInput(index) { + it.copy(selectedLotId = null, lotQuery = "") + } + } + + fun addLotTransformationOutput() { + val defaultWarehouse = uiState.lotTransformationsState.warehouses.firstOrNull() + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + outputs = uiState.lotTransformationsState.outputs + LotTransformationOutputFormState( + selectedWarehouseId = defaultWarehouse?.id, + selectedWarehouseLocationId = defaultWarehouse?.locations?.firstOrNull()?.id, + ), + inlineError = null, + ), + ) + } + + fun removeLotTransformationOutput(index: Int) { + val current = uiState.lotTransformationsState.outputs + if (current.size <= 1 || index !in current.indices) return + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + outputs = current.filterIndexed { currentIndex, _ -> currentIndex != index }, + inlineError = null, + ), + ) + } + + fun selectLotTransformationOutputGrade(index: Int, gradeId: String) { + updateLotTransformationOutput(index) { it.copy(selectedGradeId = gradeId) } + } + + fun selectLotTransformationOutputWarehouse(index: Int, warehouseId: String) { + val warehouse = uiState.lotTransformationsState.warehouses.find { it.id == warehouseId } + updateLotTransformationOutput(index) { + it.copy( + selectedWarehouseId = warehouseId, + selectedWarehouseLocationId = warehouse?.locations?.firstOrNull()?.id, + ) + } + } + + fun selectLotTransformationOutputLocation(index: Int, locationId: String?) { + updateLotTransformationOutput(index) { it.copy(selectedWarehouseLocationId = locationId) } + } + + fun updateLotTransformationOutputQty(index: Int, value: String) { + updateLotTransformationOutput(index) { it.copy(qtyProduced = value) } + } + + fun updateLotTransformationOutputNotes(index: Int, value: String) { + updateLotTransformationOutput(index) { it.copy(notes = value) } + } + + fun saveLotTransformation() { + val state = uiState.lotTransformationsState + if (state.transformationDate.isBlank()) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Tanggal transformasi wajib diisi."), + ) + return + } + + val inputs = state.inputs.mapNotNull { input -> + val lot = state.selectableLots.find { it.id == input.selectedLotId } + val qtyUsed = input.qtyUsed.toDoubleOrNull() + if (lot == null || qtyUsed == null || qtyUsed <= 0) { + null + } else { + LotTransformationCreateInputPayload( + sourceLotCode = lot.lotCode, + qtyUsed = qtyUsed, + notes = input.notes.ifBlank { null }, + ) + } + } + + val outputs = state.outputs.mapNotNull { output -> + val qtyProduced = output.qtyProduced.toDoubleOrNull() + val warehouseId = output.selectedWarehouseId + val gradeId = output.selectedGradeId + if (qtyProduced == null || qtyProduced <= 0 || warehouseId.isNullOrBlank() || gradeId.isNullOrBlank()) { + null + } else { + LotTransformationCreateOutputPayload( + gradeId = gradeId, + warehouseId = warehouseId, + warehouseLocationId = output.selectedWarehouseLocationId, + qtyProduced = qtyProduced, + notes = output.notes.ifBlank { null }, + ) + } + } + + if (inputs.size != state.inputs.size || outputs.size != state.outputs.size) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Semua input dan output transformasi harus lengkap serta numerik."), + ) + return + } + + if (state.transformationType == "REGRADE" && inputs.size != 1) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Regrade harus memakai tepat satu lot sumber."), + ) + return + } + + if (state.transformationType == "MIX" && inputs.size < 2) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Mix minimal memakai dua lot sumber."), + ) + return + } + + if (inputs.map { it.sourceLotCode.uppercase() }.distinct().size != inputs.size) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Lot sumber tidak boleh duplikat."), + ) + return + } + + val totalInput = inputs.sumOf { it.qtyUsed } + val totalOutput = outputs.sumOf { it.qtyProduced } + if (totalOutput > totalInput) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Total output tidak boleh melebihi total input."), + ) + return + } + + if (state.transformationType == "REGRADE") { + val selectedLot = state.selectableLots.find { it.id == state.inputs.firstOrNull()?.selectedLotId } + val qtyUsed = inputs.firstOrNull()?.qtyUsed ?: 0.0 + val remainderQty = ((selectedLot?.availableQty ?: 0.0) - qtyUsed) + if (remainderQty > 0 && state.remainderMode.isNullOrBlank()) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Pilih penanganan sisa lot sumber untuk regrade."), + ) + return + } + if (totalInput > totalOutput && state.processingLossMode.isNullOrBlank()) { + uiState = uiState.copy( + lotTransformationsState = state.copy(inlineError = "Pilih penanganan selisih hasil regrade."), + ) + return + } + } + + viewModelScope.launch { + uiState = uiState.copy( + lotTransformationsState = state.copy( + isSaving = true, + inlineError = null, + ), + ) + try { + val detail = repository.createLotTransformation( + LotTransformationCreatePayload( + transformationType = state.transformationType, + transformationDate = state.transformationDate, + remainderMode = state.remainderMode, + processingLossMode = state.processingLossMode, + notes = state.notes.ifBlank { null }, + inputs = inputs, + outputs = outputs, + ), + ) + refreshLotTransformations() + refreshLots() + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + isSaving = false, + screen = LotTransformationsScreen.Detail, + selectedTransformationId = detail.id, + selectedTransformation = detail, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + isSaving = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun selectStockAdjustmentLot(id: String) { + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy( + selectedLotId = id, + inlineError = null, + ), + ) + } + + fun selectStockAdjustmentReason(id: String) { + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy( + selectedReasonId = id, + inlineError = null, + ), + ) + } + + fun updateStockAdjustmentDate(value: String) { + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy(adjustmentDate = value), + ) + } + + fun updateStockAdjustmentQty(value: String) { + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy(qtyChange = value), + ) + } + + fun updateStockAdjustmentNotes(value: String) { + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy(notes = value), + ) + } + + fun saveStockAdjustment() { + val state = uiState.stockAdjustmentsState + val lotId = state.selectedLotId + val reasonId = state.selectedReasonId + val qtyChange = state.qtyChange.toDoubleOrNull() + if (lotId.isNullOrBlank() || reasonId.isNullOrBlank() || state.adjustmentDate.isBlank() || qtyChange == null || qtyChange == 0.0) { + uiState = uiState.copy( + stockAdjustmentsState = state.copy(inlineError = "Lot, alasan, tanggal, dan qty adjustment wajib diisi."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy( + stockAdjustmentsState = state.copy( + isSaving = true, + inlineError = null, + ), + ) + try { + repository.createStockAdjustment( + StockAdjustmentCreatePayload( + lotId = lotId, + adjustmentReasonId = reasonId, + adjustmentDate = state.adjustmentDate, + qtyChange = qtyChange, + notes = state.notes.ifBlank { null }, + ), + ) + refreshStockAdjustments() + refreshLots() + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy( + isSaving = false, + selectedLotId = null, + selectedReasonId = null, + adjustmentDate = todayIsoDate(), + qtyChange = "", + notes = "", + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + stockAdjustmentsState = uiState.stockAdjustmentsState.copy( + isSaving = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun openCreateWashing() { + if (uiState.washingState.selectableLots.isEmpty()) { + refreshWashing() + } + uiState = uiState.copy( + washingState = uiState.washingState.copy( + screen = WashingScreen.Create, + selectedWashingId = null, + selectedLotId = null, + selectedWashingPlaceId = uiState.washingState.washingPlaces.firstOrNull()?.id, + washingCost = "", + durationHours = "24", + inlineError = null, + ), + ) + } + + fun openEditWashing(id: String) { + val item = uiState.washingState.washings.find { it.id == id } ?: return + uiState = uiState.copy( + washingState = uiState.washingState.copy( + screen = WashingScreen.Edit, + selectedWashingId = id, + selectedLotId = item.lot.id, + selectedWashingPlaceId = item.washingPlace.id, + washingCost = item.washingCost.toInt().toString(), + durationHours = item.durationHours.toString(), + inlineError = null, + ), + ) + } + + fun openCompleteWashing(id: String) { + val item = uiState.washingState.washings.find { it.id == id } ?: return + val defaultWarehouse = uiState.washingState.warehouses.firstOrNull() + uiState = uiState.copy( + washingState = uiState.washingState.copy( + screen = WashingScreen.Complete, + selectedWashingId = id, + afterQty = formatQuantityInput(item.afterQty ?: item.beforeQty), + selectedGradeId = null, + selectedWarehouseId = defaultWarehouse?.id, + selectedWarehouseLocationId = defaultWarehouse?.locations?.firstOrNull()?.id, + inlineError = null, + ), + ) + } + + fun closeWashingScreen() { + uiState = uiState.copy( + washingState = uiState.washingState.copy( + screen = WashingScreen.List, + selectedWashingId = null, + inlineError = null, + isSaving = false, + isCompleting = false, + ), + ) + } + + fun openCreateReceipt() { + if (uiState.receiptsState.purchases.isEmpty()) { + refreshReceipts() + } + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + screen = ReceiptsScreen.Create, + selectedReceiptId = null, + selectedReceipt = null, + selectedPurchaseId = null, + receiptDate = todayIsoDate(), + notes = "", + lines = emptyList(), + inlineError = null, + ), + ) + } + + fun closeReceiptScreen() { + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + screen = ReceiptsScreen.List, + selectedReceiptId = null, + selectedReceipt = null, + isSaving = false, + isGeneratingLots = false, + inlineError = null, + ), + ) + } + + fun selectReceiptPurchase(id: String) { + val purchase = uiState.receiptsState.purchases.find { it.id == id } + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + selectedPurchaseId = id, + lines = purchase?.lines?.map { it.toFormState() } ?: emptyList(), + inlineError = null, + ), + ) + } + + fun updateReceiptDate(value: String) { + uiState = uiState.copy(receiptsState = uiState.receiptsState.copy(receiptDate = value)) + } + + fun updateReceiptNotes(value: String) { + uiState = uiState.copy(receiptsState = uiState.receiptsState.copy(notes = value)) + } + + fun updateReceiptLine(index: Int, transform: (ReceiptLineFormState) -> ReceiptLineFormState) { + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + lines = uiState.receiptsState.lines.mapIndexed { lineIndex, line -> + if (lineIndex == index) transform(line) else line + }, + ), + ) + } + + fun updateReceiptLineQtyReceived(index: Int, value: String) { + updateReceiptLine(index) { line -> + line.copy( + qtyReceived = value, + qtyAccepted = value, + qtyRejected = if (value.isBlank()) line.qtyRejected else "0", + ) + } + } + + fun updateReceiptLineQtyAccepted(index: Int, value: String) { + updateReceiptLine(index) { it.copy(qtyAccepted = value) } + } + + fun updateReceiptLineQtyRejected(index: Int, value: String) { + updateReceiptLine(index) { it.copy(qtyRejected = value) } + } + + fun updateReceiptLineUnitCost(index: Int, value: String) { + updateReceiptLine(index) { it.copy(unitCost = value) } + } + + fun updateReceiptLineWarehouse(index: Int, warehouseId: String) { + val warehouse = uiState.receiptsState.warehouses.find { it.id == warehouseId } + updateReceiptLine(index) { + it.copy( + warehouseId = warehouseId, + warehouseLocationId = warehouse?.locations?.firstOrNull()?.id.orEmpty(), + ) + } + } + + fun updateReceiptLineLocation(index: Int, locationId: String) { + updateReceiptLine(index) { it.copy(warehouseLocationId = locationId) } + } + + fun updateReceiptLineNotes(index: Int, value: String) { + updateReceiptLine(index) { it.copy(notes = value) } + } + + fun openReceiptDetail(id: String) { + viewModelScope.launch { + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + screen = ReceiptsScreen.Detail, + selectedReceiptId = id, + selectedReceipt = null, + isLoading = true, + inlineError = null, + ), + ) + try { + val detail = repository.loadReceiptDetail(id) + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isLoading = false, + selectedReceipt = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isLoading = false, + inlineError = error.toReadableMessage(), + ), + errorMessage = error.toReadableMessage(), + ) + } + } + } + } + + fun saveReceipt() { + val state = uiState.receiptsState + if (state.selectedPurchaseId.isNullOrBlank() || state.receiptDate.isBlank() || state.lines.isEmpty()) { + uiState = uiState.copy( + receiptsState = state.copy(inlineError = "Purchase, tanggal receipt, dan minimal satu line wajib diisi."), + ) + return + } + + val lines = state.lines.mapNotNull { line -> + val qtyReceived = line.qtyReceived.toDoubleOrNull() + val qtyAccepted = line.qtyAccepted.toDoubleOrNull() + val qtyRejected = line.qtyRejected.toDoubleOrNull() + val unitCost = line.unitCost.toDoubleOrNull() + if ( + qtyReceived == null || + qtyAccepted == null || + qtyRejected == null || + unitCost == null || + line.warehouseId.isBlank() + ) { + null + } else { + ReceiptCreateLinePayload( + purchaseLineId = line.purchaseLineId, + gradeId = line.gradeId, + qtyReceived = qtyReceived, + qtyAccepted = qtyAccepted, + qtyRejected = qtyRejected, + unitId = line.unitId, + unitCost = unitCost, + warehouseId = line.warehouseId, + warehouseLocationId = line.warehouseLocationId.ifBlank { null }, + notes = line.notes.ifBlank { null }, + ) + } + } + + if (lines.size != state.lines.size) { + uiState = uiState.copy( + receiptsState = state.copy(inlineError = "Semua line receipt harus lengkap dan numerik."), + ) + return + } + + if (lines.any { it.qtyAccepted + it.qtyRejected > it.qtyReceived }) { + uiState = uiState.copy( + receiptsState = state.copy(inlineError = "Qty accepted + rejected tidak boleh melebihi qty received."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy(receiptsState = state.copy(isSaving = true, inlineError = null)) + try { + val detail = repository.createReceipt( + ReceiptCreatePayload( + purchaseId = state.selectedPurchaseId, + receiptDate = state.receiptDate, + notes = state.notes.ifBlank { null }, + lines = lines, + ), + ) + refreshReceipts() + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isSaving = false, + screen = ReceiptsScreen.Detail, + selectedReceiptId = detail.id, + selectedReceipt = detail, + inlineError = null, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isSaving = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun generateReceiptLots() { + val receiptId = uiState.receiptsState.selectedReceiptId ?: return + viewModelScope.launch { + uiState = uiState.copy(receiptsState = uiState.receiptsState.copy(isGeneratingLots = true, inlineError = null)) + try { + val detail = repository.generateReceiptLots(receiptId) + refreshReceipts() + refreshLots() + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isGeneratingLots = false, + selectedReceipt = detail, + ), + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + receiptsState = uiState.receiptsState.copy( + isGeneratingLots = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun selectWashingLot(id: String) { + uiState = uiState.copy(washingState = uiState.washingState.copy(selectedLotId = id)) + } + + fun selectWashingPlace(id: String) { + uiState = uiState.copy(washingState = uiState.washingState.copy(selectedWashingPlaceId = id)) + } + + fun updateWashingCost(value: String) { + uiState = uiState.copy(washingState = uiState.washingState.copy(washingCost = value)) + } + + fun updateWashingDuration(value: String) { + uiState = uiState.copy(washingState = uiState.washingState.copy(durationHours = value)) + } + + fun updateAfterQty(value: String) { + uiState = uiState.copy(washingState = uiState.washingState.copy(afterQty = value)) + } + + fun selectCompleteGrade(id: String?) { + uiState = uiState.copy(washingState = uiState.washingState.copy(selectedGradeId = id)) + } + + fun selectCompleteWarehouse(id: String) { + val warehouse = uiState.washingState.warehouses.find { it.id == id } + uiState = uiState.copy( + washingState = uiState.washingState.copy( + selectedWarehouseId = id, + selectedWarehouseLocationId = warehouse?.locations?.firstOrNull()?.id, + ), + ) + } + + fun selectCompleteWarehouseLocation(id: String?) { + uiState = uiState.copy(washingState = uiState.washingState.copy(selectedWarehouseLocationId = id)) + } + + fun saveWashing() { + val state = uiState.washingState + val lotId = state.selectedLotId + val placeId = state.selectedWashingPlaceId + val cost = state.washingCost.toDoubleOrNull() + val duration = state.durationHours.toIntOrNull() + if (lotId.isNullOrBlank() || placeId.isNullOrBlank() || cost == null || duration == null) { + uiState = uiState.copy( + washingState = uiState.washingState.copy(inlineError = "Lot, tempat cuci, biaya, dan durasi wajib diisi."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy(washingState = uiState.washingState.copy(isSaving = true, inlineError = null)) + try { + val payload = WashingCreatePayload( + lotId = lotId, + washingPlaceId = placeId, + washingCost = cost, + durationHours = duration, + ) + if (state.screen == WashingScreen.Edit && !state.selectedWashingId.isNullOrBlank()) { + repository.updateWashing(state.selectedWashingId, payload) + } else { + repository.createWashing(payload) + } + closeWashingScreen() + refreshWashing() + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + washingState = uiState.washingState.copy( + isSaving = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + fun completeWashing() { + val state = uiState.washingState + val washingId = state.selectedWashingId + val afterQty = state.afterQty.toDoubleOrNull() + val warehouseId = state.selectedWarehouseId + if (washingId.isNullOrBlank() || afterQty == null || warehouseId.isNullOrBlank()) { + uiState = uiState.copy( + washingState = uiState.washingState.copy(inlineError = "After qty dan gudang tujuan wajib diisi."), + ) + return + } + + viewModelScope.launch { + uiState = uiState.copy(washingState = uiState.washingState.copy(isCompleting = true, inlineError = null)) + try { + repository.completeWashing( + id = washingId, + payload = CompleteWashingPayload( + afterQty = afterQty, + gradeId = state.selectedGradeId, + warehouseId = warehouseId, + warehouseLocationId = state.selectedWarehouseLocationId, + ), + ) + closeWashingScreen() + refreshWashing() + refreshLots() + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + if (shouldLogout) { + handleUnauthorized(error) + } else { + uiState = uiState.copy( + washingState = uiState.washingState.copy( + isCompleting = false, + inlineError = error.toReadableMessage(), + ), + ) + } + } + } + } + + private fun updateLotTransformationInput( + index: Int, + transform: (LotTransformationInputFormState) -> LotTransformationInputFormState, + ) { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + inputs = uiState.lotTransformationsState.inputs.mapIndexed { currentIndex, item -> + if (currentIndex == index) transform(item) else item + }, + inlineError = null, + ), + ) + } + + private fun updateLotTransformationOutput( + index: Int, + transform: (LotTransformationOutputFormState) -> LotTransformationOutputFormState, + ) { + uiState = uiState.copy( + lotTransformationsState = uiState.lotTransformationsState.copy( + outputs = uiState.lotTransformationsState.outputs.mapIndexed { currentIndex, item -> + if (currentIndex == index) transform(item) else item + }, + inlineError = null, + ), + ) + } + + private fun updatePurchaseLine( + index: Int, + transform: (PurchaseLineFormState) -> PurchaseLineFormState, + ) { + uiState = uiState.copy( + purchasesState = uiState.purchasesState.copy( + lines = uiState.purchasesState.lines.mapIndexed { currentIndex, line -> + if (currentIndex == index) transform(line) else line + }, + inlineError = null, + ), + ) + } + + private fun restoreSession() { + viewModelScope.launch { + if (repository.sessionToken().isNullOrBlank()) { + uiState = MainUiState(isCheckingSession = false) + } else { + loadDashboard(trigger = LoadTrigger.SessionRestore) + } + } + } + + private suspend fun loadDashboard(trigger: LoadTrigger) { + val previousDashboard = uiState.dashboard + uiState = uiState.copy( + isCheckingSession = previousDashboard == null, + isSubmitting = trigger == LoadTrigger.Login, + isRefreshing = trigger == LoadTrigger.Refresh && previousDashboard != null, + errorMessage = null, + ) + try { + val dashboard = repository.loadDashboardBundle() + val nextModule = uiState.currentModule.takeIf { + it == "dashboard" || dashboard.modules.contains(it) + } ?: "dashboard" + + uiState = MainUiState( + isCheckingSession = false, + isSubmitting = false, + isRefreshing = false, + isAuthenticated = true, + dashboard = dashboard, + currentModule = nextModule, + lotsState = uiState.lotsState, + salesRegularState = uiState.salesRegularState, + salesJitState = uiState.salesJitState, + consignmentsState = uiState.consignmentsState, + fundRequestsState = uiState.fundRequestsState, + purchasesState = uiState.purchasesState, + purchaseAnalysesState = uiState.purchaseAnalysesState, + purchaseRealizationsState = uiState.purchaseRealizationsState, + lotTransformationsState = uiState.lotTransformationsState, + stockAdjustmentsState = uiState.stockAdjustmentsState, + washingState = uiState.washingState, + receiptsState = uiState.receiptsState, + ) + } catch (error: Throwable) { + val shouldLogout = error is HttpException && (error.code() == 401 || error.code() == 403) + + if (previousDashboard != null && !shouldLogout) { + uiState = uiState.copy( + isCheckingSession = false, + isSubmitting = false, + isRefreshing = false, + errorMessage = error.toReadableMessage(), + ) + } else { + handleUnauthorized(error) + } + } + } + + private fun handleUnauthorized(error: Throwable) { + repository.logout() + password = "" + uiState = MainUiState( + isCheckingSession = false, + isSubmitting = false, + isRefreshing = false, + isAuthenticated = false, + currentModule = "dashboard", + errorMessage = error.toReadableMessage(), + ) + } + + private enum class LoadTrigger { + SessionRestore, + Login, + Refresh, + } +} + +private fun LotScanResult.payloadToState(): LotDetailData = payload.toLotDetailData() + +private fun id.abelbirdnest.mobile.data.LotScanPayload.toLotDetailData(): LotDetailData { + return LotDetailData( + summaryCard = summaryCard, + lot = lot, + procurement = procurement, + mobileActions = mobileActions, + ) +} + +private fun formatQuantityInput(value: Double): String { + return if (value % 1.0 == 0.0) value.roundToInt().toString() else value.toString() +} + +private fun ReceiptPurchaseLineOption.toFormState(): ReceiptLineFormState { + return ReceiptLineFormState( + purchaseLineId = purchaseLineId, + gradeId = grade?.id, + gradeName = grade?.name ?: "-", + qtyOrdered = formatQuantityInput(qtyOrdered), + qtyReceived = formatQuantityInput(qtyReceived), + qtyAccepted = formatQuantityInput(qtyAccepted), + qtyRejected = formatQuantityInput(qtyRejected), + unitId = unit.id, + unitCode = unit.code, + unitCost = formatQuantityInput(unitCost), + warehouseId = warehouseId.orEmpty(), + warehouseLocationId = warehouseLocationId.orEmpty(), + notes = "", + ) +} + +private fun RegularSaleLineDetail.toCloseFormState(): RegularSaleCloseLineFormState { + return RegularSaleCloseLineFormState( + lineId = id, + lotCode = lotCode, + qtyPlanned = qtyPlanned, + qtyActualSold = formatQuantityInput(qtyActualSold ?: qtyPlanned), + qtyReturned = formatQuantityInput(qtyReturned ?: 0.0), + sellingPriceActual = formatQuantityInput(sellingPriceActual ?: sellingPricePlanned), + ) +} + +private fun JitSaleLineDetail.toCloseFormState(): JitSaleCloseLineFormState { + return JitSaleCloseLineFormState( + lineId = id, + gradeName = grade.name, + qtyPlanned = qtyPlanned, + sellingPriceActual = formatQuantityInput(sellingPriceActual ?: sellingPricePlanned), + ) +} + +private fun todayIsoDate(): String = java.time.LocalDate.now().toString() + +private fun currentIsoDateTimeLocal(): String = java.time.LocalDateTime.now().withSecond(0).withNano(0).toString() + +private fun defaultTransformationTypes(): List = listOf( + CodeLabelOption(code = "MIX", label = "Mixing"), + CodeLabelOption(code = "REGRADE", label = "Regrade"), +) + +private fun defaultRemainderModes(): List = listOf( + CodeLabelOption(code = "KEEP_SOURCE_GRADE", label = "Simpan sisa di lot sumber"), + CodeLabelOption(code = "SHRINKAGE", label = "Catat sebagai shrinkage"), +) + +private fun defaultProcessingLossModes(): List = listOf( + CodeLabelOption(code = "SHRINKAGE", label = "Catat sebagai shrinkage"), +) + +private fun Throwable.toReadableMessage(): String { + return when (this) { + is HttpException -> "Login atau pengambilan data gagal (${code()})." + is IOException -> "Tidak bisa terhubung ke server. Periksa koneksi internet." + else -> message ?: "Terjadi kesalahan yang tidak terduga." + } +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Color.kt b/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Color.kt new file mode 100644 index 0000000..cfddda9 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Color.kt @@ -0,0 +1,21 @@ +package id.abelbirdnest.mobile.ui.theme + +import androidx.compose.ui.graphics.Color + +val Background = Color(0xFFF8FAFA) +val Surface = Color(0xFFF8FAFA) +val SurfaceContainerLowest = Color(0xFFFFFFFF) +val SurfaceContainerHigh = Color(0xFFE6E8E9) +val SurfaceContainer = Color(0xFFECEEEE) +val OnSurface = Color(0xFF191C1D) +val OnSurfaceVariant = Color(0xFF3F484A) +val Outline = Color(0xFF6F797A) +val OutlineVariant = Color(0xFFBFC8CA) +val Primary = Color(0xFF00454C) +val PrimaryContainer = Color(0xFF0D5E67) +val OnPrimary = Color(0xFFFFFFFF) +val OnPrimaryContainer = Color(0xFF92D5DF) +val Secondary = Color(0xFF4E6073) +val SecondaryContainer = Color(0xFFCFE2F9) +val Tertiary = Color(0xFF60320F) +val Error = Color(0xFFBA1A1A) diff --git a/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Theme.kt b/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Theme.kt new file mode 100644 index 0000000..d80ed8d --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Theme.kt @@ -0,0 +1,87 @@ +package id.abelbirdnest.mobile.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +private val AppColorScheme: ColorScheme = lightColorScheme( + primary = Primary, + onPrimary = OnPrimary, + primaryContainer = PrimaryContainer, + onPrimaryContainer = OnPrimaryContainer, + secondary = Secondary, + secondaryContainer = SecondaryContainer, + tertiary = Tertiary, + error = Error, + background = Background, + surface = Surface, + surfaceContainer = SurfaceContainer, + surfaceContainerHigh = SurfaceContainerHigh, + surfaceContainerLowest = SurfaceContainerLowest, + onSurface = OnSurface, + onSurfaceVariant = OnSurfaceVariant, + outline = Outline, + outlineVariant = OutlineVariant, +) + +private val AppTypography = Typography( + headlineMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 32.sp, + ), + headlineSmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 30.sp, + ), + titleMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 22.sp, + ), + bodyMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + ), + bodySmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 13.sp, + lineHeight = 18.sp, + ), + labelSmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.6.sp, + ), + displaySmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 34.sp, + lineHeight = 40.sp, + ), +) + +@Composable +fun AbelbirdnestTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = AppColorScheme, + typography = AppTypography, + content = content, + ) +} diff --git a/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Type.kt b/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Type.kt new file mode 100644 index 0000000..7809044 --- /dev/null +++ b/app/src/main/java/id/abelbirdnest/mobile/ui/theme/Type.kt @@ -0,0 +1,3 @@ +package id.abelbirdnest.mobile.ui.theme + +// Typography is defined in Theme.kt. diff --git a/app/src/main/res/drawable/logo_abelbirdnest.png b/app/src/main/res/drawable/logo_abelbirdnest.png new file mode 100755 index 0000000000000000000000000000000000000000..1746601985245969f5ff4e8e2584cf2d9e42552f GIT binary patch literal 123393 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

FE<%ERL+Jugnbyq~;Uvk)b7v9?yc` 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 m-_UGAHK+gl~f(0StBKtokmr9#O%{Qm%_UujVQ literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..edd8958 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Abelbirdnest Stock + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1b4481a --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..fb45c1c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 +org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..74a4ead --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..b9bb139 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aa5f10b --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/logo_abelbirdnest.png b/logo_abelbirdnest.png new file mode 100755 index 0000000000000000000000000000000000000000..1746601985245969f5ff4e8e2584cf2d9e42552f GIT binary patch literal 123393 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

FE<%ERL+Jugnbyq~;Uvk)b7v9?yc` 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 m-_UGAHK+gl~f(0StBKtokmr9#O%{Qm%_UujVQ literal 0 HcmV?d00001 diff --git a/mobile-api-blueprint.md b/mobile-api-blueprint.md new file mode 100644 index 0000000..dbdc856 --- /dev/null +++ b/mobile-api-blueprint.md @@ -0,0 +1,268 @@ +# Mobile API Blueprint + +Dokumen ini merangkum: +- endpoint mobile per role +- layar minimum yang dibutuhkan aplikasi mobile +- batas scope mobile yang sengaja dipertahankan agar tetap praktis + +## Prinsip + +- Mobile memakai auth yang sama dengan web. +- Login lewat `POST /api/v1/auth/login`, lalu kirim `Authorization: Bearer `. +- Semua endpoint mobile berada di prefix `/api/v1/mobile`. +- Mobile fokus ke operasi cepat, scan, input lapangan, monitoring, dan closing ringan. +- Fitur admin berat seperti `users`, `settings`, `audit-trail`, dan master data lengkap tetap web-only. + +## Role Mobile + +Role yang didukung di mobile: +- `WAREHOUSE` +- `QC` +- `SALES` +- `PURCHASING` +- `OWNER` + +Role yang tidak menjadi target mobile utama: +- `ADMIN` +- `SYSTEM_ADMIN` + +## Bootstrap Umum + +Semua role mobile memakai: +- `GET /api/v1/mobile/bootstrap` +- `GET /api/v1/mobile/dashboard?locale=id|en` + +`/mobile/bootstrap` mengembalikan: +- user session +- daftar modul yang boleh diakses role tersebut +- summary ringkas operasional +- grade aktif +- gudang aktif dan lokasi aktif +- master transformation mode + +## Matriks Endpoint Per Role + +### Warehouse + +Tujuan: +- scan lot +- submit purchase yang otomatis membuat receipt dan lot +- buat penyesuaian stok +- kirim / selesaikan washing + +Endpoint: +- `GET /api/v1/mobile/bootstrap` +- `GET /api/v1/mobile/dashboard` +- `GET /api/v1/mobile/lots` +- `GET /api/v1/mobile/lots/:id` +- `GET /api/v1/mobile/lots/scan?code=...` +- `GET /api/v1/mobile/purchases` +- `GET /api/v1/mobile/purchases/:id` +- `POST /api/v1/mobile/purchases/:id/submit` +- `GET /api/v1/mobile/stock-adjustments/bootstrap` +- `GET /api/v1/mobile/stock-adjustments` +- `POST /api/v1/mobile/stock-adjustments` +- `GET /api/v1/mobile/washing/bootstrap` +- `GET /api/v1/mobile/washing` +- `POST /api/v1/mobile/washing` +- `PUT /api/v1/mobile/washing/:id` +- `POST /api/v1/mobile/washing/:id/complete` + +### QC + +Tujuan: +- scan lot +- lihat detail lineage lot +- ubah grade / mixing +- bantu washing completion +- buat penyesuaian stok terkait QC + +Endpoint: +- `GET /api/v1/mobile/bootstrap` +- `GET /api/v1/mobile/dashboard` +- `GET /api/v1/mobile/lots` +- `GET /api/v1/mobile/lots/:id` +- `GET /api/v1/mobile/lots/scan?code=...` +- `GET /api/v1/mobile/washing/bootstrap` +- `GET /api/v1/mobile/washing` +- `PUT /api/v1/mobile/washing/:id` +- `POST /api/v1/mobile/washing/:id/complete` +- `GET /api/v1/mobile/lot-transformations` +- `POST /api/v1/mobile/lot-transformations` +- `GET /api/v1/mobile/lot-transformations/:id` +- `GET /api/v1/mobile/stock-adjustments/bootstrap` +- `GET /api/v1/mobile/stock-adjustments` +- `POST /api/v1/mobile/stock-adjustments` + +### Sales + +Tujuan: +- lihat stok jual +- buat penjualan reguler +- buat penjualan JIT +- buat titip jual +- tutup transaksi + +Endpoint: +- `GET /api/v1/mobile/bootstrap` +- `GET /api/v1/mobile/dashboard` +- `GET /api/v1/mobile/lots` +- `GET /api/v1/mobile/lots/:id` +- `GET /api/v1/mobile/lots/scan?code=...` +- `GET /api/v1/mobile/sales-regular/bootstrap` +- `GET /api/v1/mobile/sales-regular` +- `POST /api/v1/mobile/sales-regular` +- `GET /api/v1/mobile/sales-regular/:id` +- `POST /api/v1/mobile/sales-regular/:id/close` +- `GET /api/v1/mobile/sales-jit/bootstrap` +- `GET /api/v1/mobile/sales-jit` +- `POST /api/v1/mobile/sales-jit` +- `GET /api/v1/mobile/sales-jit/:id` +- `POST /api/v1/mobile/sales-jit/:id/close` +- `GET /api/v1/mobile/consignments/bootstrap` +- `GET /api/v1/mobile/consignments` +- `POST /api/v1/mobile/consignments` +- `GET /api/v1/mobile/consignments/:id` +- `POST /api/v1/mobile/consignments/lines/:lineId/close` + +### Purchasing + +Tujuan: +- buat draft pembelian +- edit / submit pembelian +- buat permintaan dana +- monitor analisis dan realisasi pembelian + +Endpoint: +- `GET /api/v1/mobile/bootstrap` +- `GET /api/v1/mobile/dashboard` +- `GET /api/v1/mobile/purchases` +- `POST /api/v1/mobile/purchases` +- `GET /api/v1/mobile/purchases/:id` +- `PUT /api/v1/mobile/purchases/:id` +- `POST /api/v1/mobile/purchases/:id/submit` +- `POST /api/v1/mobile/purchases/:id/cancel` +- `GET /api/v1/mobile/fund-requests/bootstrap` +- `GET /api/v1/mobile/fund-requests` +- `POST /api/v1/mobile/fund-requests` +- `GET /api/v1/mobile/purchase-analyses` +- `GET /api/v1/mobile/purchase-analyses/:purchaseId` +- `GET /api/v1/mobile/purchase-realizations` +- `GET /api/v1/mobile/purchase-realizations/:purchaseId` + +### Owner + +Tujuan: +- monitoring dashboard +- lihat analisis pembelian +- lihat realisasi pembelian +- lihat status transaksi penting + +Endpoint minimum: +- `GET /api/v1/mobile/bootstrap` +- `GET /api/v1/mobile/dashboard` +- `GET /api/v1/mobile/purchases` +- `GET /api/v1/mobile/purchases/:id` +- `GET /api/v1/mobile/fund-requests` +- `GET /api/v1/mobile/purchase-analyses` +- `GET /api/v1/mobile/purchase-analyses/:purchaseId` +- `GET /api/v1/mobile/purchase-realizations` +- `GET /api/v1/mobile/purchase-realizations/:purchaseId` +- `GET /api/v1/mobile/sales-regular` +- `GET /api/v1/mobile/sales-jit` +- `GET /api/v1/mobile/consignments` +- `GET /api/v1/mobile/washing` + +## Layar Minimum Per Role + +### Warehouse + +Layar minimum: +1. Login +2. Dashboard ringkas +3. Scan lot +4. Detail lot +5. Daftar pembelian siap submit +6. Submit pembelian yang otomatis membuat receipt + lot +7. Penyesuaian stok +8. Daftar washing +9. Buat washing +10. Selesaikan washing + +### QC + +Layar minimum: +1. Login +2. Dashboard ringkas +3. Scan lot +4. Detail lot dan turunan lot +5. Daftar transformasi +6. Buat mixing / ubah grade +7. Daftar washing +8. Selesaikan washing +9. Penyesuaian stok QC + +### Sales + +Layar minimum: +1. Login +2. Dashboard ringkas +3. Stok siap jual +4. Scan lot +5. Buat penjualan reguler +6. Detail dan tutup penjualan reguler +7. Buat penjualan JIT +8. Detail dan tutup penjualan JIT +9. Buat titip jual +10. Detail dan tutup item titip jual + +### Purchasing + +Layar minimum: +1. Login +2. Dashboard ringkas +3. Daftar pembelian +4. Form draft pembelian +5. Detail pembelian +6. Submit pembelian +7. Daftar permintaan dana +8. Form permintaan dana +9. Daftar analisis pembelian +10. Daftar realisasi pembelian + +### Owner + +Layar minimum: +1. Login +2. Dashboard ringkas +3. Daftar pembelian +4. Detail pembelian +5. Daftar analisis pembelian +6. Detail analisis pembelian +7. Daftar realisasi pembelian +8. Detail realisasi pembelian +9. Daftar transaksi keluar +10. Daftar washing + +## Scope Yang Sengaja Tidak Dibawa ke Mobile + +- manajemen user +- pengaturan sistem +- audit trail penuh +- master data lengkap +- office buyout +- print label / dokumen +- laporan web yang kompleks + +## Urutan Implementasi UI Mobile yang Disarankan + +1. Warehouse +2. QC +3. Sales +4. Purchasing +5. Owner + +Alasannya: +- Warehouse dan QC paling sering butuh scan dan aksi lapangan cepat +- Sales di urutan berikutnya karena butuh transaksi tapi tidak banyak input master +- Purchasing dan Owner lebih banyak monitoring dan persetujuan ringan diff --git a/postman/abelbirdnest-mobile-api.postman_collection.json b/postman/abelbirdnest-mobile-api.postman_collection.json new file mode 100644 index 0000000..5a6023f --- /dev/null +++ b/postman/abelbirdnest-mobile-api.postman_collection.json @@ -0,0 +1,569 @@ +{ + "info": { + "name": "AbelBirdnest Mobile Operations API", + "description": "Collection Postman untuk integrasi mobile Warehouse, QC, Sales, Purchasing, dan Owner. Gunakan Login terlebih dahulu untuk mengisi {{sessionToken}}.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { "key": "baseUrl", "value": "http://localhost:3000/api/v1" }, + { "key": "sessionToken", "value": "" }, + { "key": "purchaseId", "value": "" }, + { "key": "receiptId", "value": "" }, + { "key": "lotId", "value": "" }, + { "key": "washingId", "value": "" }, + { "key": "regularSaleId", "value": "" }, + { "key": "jitSaleId", "value": "" }, + { "key": "consignmentId", "value": "" }, + { "key": "consignmentLineId", "value": "" }, + { "key": "purchaseAnalysisId", "value": "" }, + { "key": "purchaseRealizationId", "value": "" } + ], + "auth": { + "type": "bearer", + "bearer": [ + { "key": "token", "value": "{{sessionToken}}", "type": "string" } + ] + }, + "item": [ + { + "name": "1. Auth", + "item": [ + { + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "if (pm.response.code === 200) {", + " const json = pm.response.json();", + " const token = json?.data?.session_token;", + " const role = json?.data?.user?.role;", + " if (token) pm.collectionVariables.set('sessionToken', token);", + " if (role) pm.collectionVariables.set('userRole', role);", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"identity\": \"admin\",\n \"password\": \"admin123\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": ["{{baseUrl}}"], + "path": ["auth", "login"] + } + } + }, + { + "name": "Session Me", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/auth/me", + "host": ["{{baseUrl}}"], + "path": ["auth", "me"] + } + } + } + ] + }, + { + "name": "2. Bootstrap & Dashboard", + "item": [ + { + "name": "Mobile Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "bootstrap"] + } + } + }, + { + "name": "Mobile Dashboard", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/dashboard?locale=id", + "host": ["{{baseUrl}}"], + "path": ["mobile", "dashboard"], + "query": [ + { "key": "locale", "value": "id" } + ] + } + } + } + ] + }, + { + "name": "3. Warehouse & QC", + "item": [ + { + "name": "Lot List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/lots", + "host": ["{{baseUrl}}"], + "path": ["mobile", "lots"] + } + } + }, + { + "name": "Lot Scan", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/lots/scan?code=LOT-EXAMPLE-001", + "host": ["{{baseUrl}}"], + "path": ["mobile", "lots", "scan"], + "query": [ + { "key": "code", "value": "LOT-EXAMPLE-001" } + ] + } + } + }, + { + "name": "Lot Detail", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/lots/{{lotId}}", + "host": ["{{baseUrl}}"], + "path": ["mobile", "lots", "{{lotId}}"] + } + } + }, + { + "name": "Receipt Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/receipts/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "receipts", "bootstrap"] + } + } + }, + { + "name": "Receipt List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/receipts", + "host": ["{{baseUrl}}"], + "path": ["mobile", "receipts"] + } + } + }, + { + "name": "Create Receipt", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"purchase_id\": \"{{purchaseId}}\",\n \"receipt_date\": \"2026-05-16\",\n \"notes\": \"Receipt dari mobile\",\n \"lines\": []\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/receipts", + "host": ["{{baseUrl}}"], + "path": ["mobile", "receipts"] + } + } + }, + { + "name": "Generate Lots From Receipt", + "request": { + "method": "POST", + "url": { + "raw": "{{baseUrl}}/mobile/receipts/{{receiptId}}/generate-lots", + "host": ["{{baseUrl}}"], + "path": ["mobile", "receipts", "{{receiptId}}", "generate-lots"] + } + } + }, + { + "name": "Stock Adjustment Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/stock-adjustments/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "stock-adjustments", "bootstrap"] + } + } + }, + { + "name": "Create Stock Adjustment", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"lot_id\": \"{{lotId}}\",\n \"adjustment_reason_id\": \"\",\n \"adjustment_date\": \"2026-05-16\",\n \"qty_change\": -1,\n \"notes\": \"Mobile adjustment\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/stock-adjustments", + "host": ["{{baseUrl}}"], + "path": ["mobile", "stock-adjustments"] + } + } + }, + { + "name": "Washing Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/washing/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "washing", "bootstrap"] + } + } + }, + { + "name": "Washing List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/washing", + "host": ["{{baseUrl}}"], + "path": ["mobile", "washing"] + } + } + }, + { + "name": "Create Washing", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"lot_id\": \"{{lotId}}\",\n \"washing_place_id\": \"\",\n \"washing_cost\": 10000,\n \"duration_hours\": 24\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/washing", + "host": ["{{baseUrl}}"], + "path": ["mobile", "washing"] + } + } + }, + { + "name": "Complete Washing", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"after_qty\": 1,\n \"grade_id\": \"\",\n \"warehouse_id\": \"\",\n \"warehouse_location_id\": \"\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/washing/{{washingId}}/complete", + "host": ["{{baseUrl}}"], + "path": ["mobile", "washing", "{{washingId}}", "complete"] + } + } + }, + { + "name": "Transformation List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/lot-transformations", + "host": ["{{baseUrl}}"], + "path": ["mobile", "lot-transformations"] + } + } + }, + { + "name": "Create Lot Transformation", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"transformation_type\": \"MIX\",\n \"transformation_date\": \"2026-05-16\",\n \"remainder_mode\": \"KEEP_SOURCE_GRADE\",\n \"processing_loss_mode\": \"SHRINKAGE\",\n \"notes\": \"Mobile transformation\",\n \"inputs\": [\n {\n \"source_lot_code\": \"LOT-EXAMPLE-001\",\n \"qty_used\": 1\n }\n ],\n \"outputs\": [\n {\n \"grade_id\": \"\",\n \"warehouse_id\": \"\",\n \"warehouse_location_id\": \"\",\n \"qty_produced\": 1\n }\n ]\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/lot-transformations", + "host": ["{{baseUrl}}"], + "path": ["mobile", "lot-transformations"] + } + } + } + ] + }, + { + "name": "4. Sales", + "item": [ + { + "name": "Regular Sales Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/sales-regular/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-regular", "bootstrap"] + } + } + }, + { + "name": "Regular Sales List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/sales-regular", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-regular"] + } + } + }, + { + "name": "Regular Sale Detail", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/sales-regular/{{regularSaleId}}", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-regular", "{{regularSaleId}}"] + } + } + }, + { + "name": "Close Regular Sale", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"close_date\": \"2026-05-16\",\n \"lines\": []\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/sales-regular/{{regularSaleId}}/close", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-regular", "{{regularSaleId}}", "close"] + } + } + }, + { + "name": "JIT Sales Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/sales-jit/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-jit", "bootstrap"] + } + } + }, + { + "name": "JIT Sales List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/sales-jit", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-jit"] + } + } + }, + { + "name": "Close JIT Sale", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"close_date\": \"2026-05-16\",\n \"lines\": []\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/sales-jit/{{jitSaleId}}/close", + "host": ["{{baseUrl}}"], + "path": ["mobile", "sales-jit", "{{jitSaleId}}", "close"] + } + } + }, + { + "name": "Consignments Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/consignments/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "consignments", "bootstrap"] + } + } + }, + { + "name": "Consignments List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/consignments", + "host": ["{{baseUrl}}"], + "path": ["mobile", "consignments"] + } + } + }, + { + "name": "Consignment Detail", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/consignments/{{consignmentId}}", + "host": ["{{baseUrl}}"], + "path": ["mobile", "consignments", "{{consignmentId}}"] + } + } + }, + { + "name": "Close Consignment Line", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "body": { + "mode": "raw", + "raw": "{\n \"close_date\": \"2026-05-16\",\n \"qty_sold\": 1,\n \"qty_returned\": 0,\n \"selling_price\": 100000,\n \"sales_commission\": 0\n}" + }, + "url": { + "raw": "{{baseUrl}}/mobile/consignments/lines/{{consignmentLineId}}/close", + "host": ["{{baseUrl}}"], + "path": ["mobile", "consignments", "lines", "{{consignmentLineId}}", "close"] + } + } + } + ] + }, + { + "name": "5. Purchasing & Owner", + "item": [ + { + "name": "Purchases List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/purchases", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchases"] + } + } + }, + { + "name": "Purchase Detail", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/purchases/{{purchaseId}}", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchases", "{{purchaseId}}"] + } + } + }, + { + "name": "Submit Purchase", + "request": { + "method": "POST", + "url": { + "raw": "{{baseUrl}}/mobile/purchases/{{purchaseId}}/submit", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchases", "{{purchaseId}}", "submit"] + } + } + }, + { + "name": "Fund Requests Bootstrap", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/fund-requests/bootstrap", + "host": ["{{baseUrl}}"], + "path": ["mobile", "fund-requests", "bootstrap"] + } + } + }, + { + "name": "Fund Requests List", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/fund-requests", + "host": ["{{baseUrl}}"], + "path": ["mobile", "fund-requests"] + } + } + }, + { + "name": "Purchase Analyses", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/purchase-analyses", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchase-analyses"] + } + } + }, + { + "name": "Purchase Analysis Detail", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/purchase-analyses/{{purchaseAnalysisId}}", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchase-analyses", "{{purchaseAnalysisId}}"] + } + } + }, + { + "name": "Purchase Realizations", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/purchase-realizations", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchase-realizations"] + } + } + }, + { + "name": "Purchase Realization Detail", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/mobile/purchase-realizations/{{purchaseRealizationId}}", + "host": ["{{baseUrl}}"], + "path": ["mobile", "purchase-realizations", "{{purchaseRealizationId}}"] + } + } + } + ] + } + ] +} diff --git a/postman/abelbirdnest-mobile-local.postman_environment.json b/postman/abelbirdnest-mobile-local.postman_environment.json new file mode 100644 index 0000000..5abc9e7 --- /dev/null +++ b/postman/abelbirdnest-mobile-local.postman_environment.json @@ -0,0 +1,93 @@ +{ + "id": "e5db42ce-7b3a-4d72-8df7-abelbirdnest-mobile-local", + "name": "AbelBirdnest Mobile Local", + "values": [ + { + "key": "baseUrl", + "value": "https://abelbirdnest.id/api/v1", + "type": "text", + "enabled": true + }, + { + "key": "sessionToken", + "value": "", + "type": "text", + "enabled": true + }, + { + "key": "defaultRedirectTo", + "value": "", + "type": "text", + "enabled": true + }, + { + "key": "userRole", + "value": "", + "type": "text", + "enabled": true + }, + { + "key": "scanCode", + "value": "LOT-260501-MIX-001", + "type": "text", + "enabled": true + }, + { + "key": "lotId", + "value": "1", + "type": "text", + "enabled": true + }, + { + "key": "transformationId", + "value": "1", + "type": "text", + "enabled": true + }, + { + "key": "itemTypeId", + "value": "1", + "type": "text", + "enabled": true + }, + { + "key": "itemGradeId", + "value": "1", + "type": "text", + "enabled": true + }, + { + "key": "targetItemGradeId", + "value": "2", + "type": "text", + "enabled": true + }, + { + "key": "warehouseId", + "value": "1", + "type": "text", + "enabled": true + }, + { + "key": "warehouseLocationId", + "value": "1", + "type": "text", + "enabled": true + }, + { + "key": "sourceLotCode1", + "value": "LOT-260501-MIX-001", + "type": "text", + "enabled": true + }, + { + "key": "sourceLotCode2", + "value": "LOT-260501-MIX-002", + "type": "text", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2026-05-02T06:05:00+07:00", + "_postman_exported_using": "Codex GPT-5" +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..f2283bb --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Abelbirdnest Stock" +include(":app")