Initial mobile app implementation

This commit is contained in:
2026-05-21 23:36:13 +07:00
commit 6edd98a268
28 changed files with 14407 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.gradle/
.kotlin/
.idea/
build/
app/build/
local.properties
*.iml
.DS_Store

98
CODEX_HANDOFF.md Normal file
View File

@ -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.

86
app/build.gradle.kts Normal file
View File

@ -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")
}

1
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1 @@
# Intentionally empty for now.

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:icon="@drawable/logo_abelbirdnest"
android:label="@string/app_name"
android:roundIcon="@drawable/logo_abelbirdnest"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -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<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AbelbirdnestTheme {
AbelbirdnestApp(viewModel = viewModel)
}
}
}
}

View File

@ -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<LotItem> {
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<AdjustmentReason> {
val token = sessionStore.token() ?: error("Sesi tidak tersedia")
return api.adjustmentReasons("Bearer $token").data
}
suspend fun loadStockAdjustments(): List<StockAdjustmentListItem> {
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<PurchaseListItem> {
val token = sessionStore.token() ?: error("Sesi tidak tersedia")
return api.purchases("Bearer $token").data
}
suspend fun loadUnits(): List<UnitLookup> {
val token = sessionStore.token() ?: error("Sesi tidak tersedia")
return api.units("Bearer $token").data
}
suspend fun loadEmployees(): List<LookupRecord> {
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<PurchaseAnalysisListItem> {
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<PurchaseRealizationListItem> {
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<RegularSaleListItem> {
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<JitSaleListItem> {
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<ConsignmentListItem> {
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<FundRequestListItem> {
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<LotTransformationListItem> {
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<WashingListItem> {
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<ReceiptListItem> {
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()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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<BootstrapData>
@GET("mobile/dashboard")
suspend fun dashboard(
@Header("Authorization") authorization: String,
@Query("locale") locale: String = "id",
): ApiEnvelope<DashboardData>
@GET("mobile/lots")
suspend fun lots(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<LotItem>>
@GET("mobile/lots/{id}")
suspend fun lotDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<LotDetailResponse>
@GET("mobile/lots/scan")
suspend fun scanLot(
@Header("Authorization") authorization: String,
@Query("code") code: String,
): ApiEnvelope<LotScanPayload>
@GET("adjustment-reasons")
suspend fun adjustmentReasons(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<AdjustmentReason>>
@GET("mobile/stock-adjustments")
suspend fun stockAdjustments(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<StockAdjustmentListItem>>
@POST("mobile/stock-adjustments")
suspend fun createStockAdjustment(
@Header("Authorization") authorization: String,
@Body payload: StockAdjustmentCreatePayload,
): ApiEnvelope<StockAdjustmentListItem>
@GET("mobile/purchases")
suspend fun purchases(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<PurchaseListItem>>
@GET("units")
suspend fun units(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<UnitLookup>>
@GET("employees")
suspend fun employees(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<LookupRecord>>
@POST("mobile/purchases")
suspend fun createPurchase(
@Header("Authorization") authorization: String,
@Body payload: PurchaseCreatePayload,
): ApiEnvelope<PurchaseDetail>
@GET("mobile/purchases/{id}")
suspend fun purchaseDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<PurchaseDetail>
@PUT("mobile/purchases/{id}")
suspend fun updatePurchase(
@Header("Authorization") authorization: String,
@Path("id") id: String,
@Body payload: PurchaseCreatePayload,
): ApiEnvelope<PurchaseDetail>
@GET("mobile/purchase-analyses")
suspend fun purchaseAnalyses(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<PurchaseAnalysisListItem>>
@GET("mobile/purchase-analyses/{purchaseId}")
suspend fun purchaseAnalysisDetail(
@Header("Authorization") authorization: String,
@Path("purchaseId") purchaseId: String,
): ApiEnvelope<PurchaseAnalysisDetail>
@GET("mobile/purchase-realizations")
suspend fun purchaseRealizations(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<PurchaseRealizationListItem>>
@GET("mobile/purchase-realizations/{purchaseId}")
suspend fun purchaseRealizationDetail(
@Header("Authorization") authorization: String,
@Path("purchaseId") purchaseId: String,
): ApiEnvelope<PurchaseRealizationDetail>
@GET("mobile/sales-regular")
suspend fun regularSales(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<RegularSaleListItem>>
@GET("mobile/sales-regular/{id}")
suspend fun regularSaleDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<RegularSaleDetail>
@POST("mobile/sales-regular/{id}/close")
suspend fun closeRegularSale(
@Header("Authorization") authorization: String,
@Path("id") id: String,
@Body payload: RegularSaleClosePayload,
): ApiEnvelope<RegularSaleDetail>
@GET("mobile/sales-jit")
suspend fun jitSales(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<JitSaleListItem>>
@GET("mobile/sales-jit/{id}")
suspend fun jitSaleDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<JitSaleDetail>
@POST("mobile/sales-jit/{id}/close")
suspend fun closeJitSale(
@Header("Authorization") authorization: String,
@Path("id") id: String,
@Body payload: JitSaleClosePayload,
): ApiEnvelope<JitSaleDetail>
@GET("mobile/consignments/bootstrap")
suspend fun consignmentsBootstrap(
@Header("Authorization") authorization: String,
): ApiEnvelope<ConsignmentBootstrapData>
@GET("mobile/consignments")
suspend fun consignments(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<ConsignmentListItem>>
@GET("mobile/consignments/{id}")
suspend fun consignmentDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<ConsignmentDetail>
@POST("consignments/lines/{lineId}/close")
suspend fun closeConsignmentLine(
@Header("Authorization") authorization: String,
@Path("lineId") lineId: String,
@Body payload: ConsignmentCloseLinePayload,
): ApiEnvelope<Map<String, Any>>
@GET("mobile/fund-requests/bootstrap")
suspend fun fundRequestsBootstrap(
@Header("Authorization") authorization: String,
): ApiEnvelope<FundRequestsBootstrapData>
@GET("mobile/fund-requests")
suspend fun fundRequests(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<FundRequestListItem>>
@Multipart
@POST("mobile/fund-requests")
suspend fun createFundRequest(
@Header("Authorization") authorization: String,
@PartMap fields: Map<String, @JvmSuppressWildcards RequestBody>,
): ApiEnvelope<FundRequestListItem>
@POST("mobile/purchases/{id}/submit")
suspend fun submitPurchase(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<PurchaseDetail>
@POST("mobile/purchases/{id}/cancel")
suspend fun cancelPurchase(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<Map<String, Any>>
@GET("mobile/lot-transformations")
suspend fun lotTransformations(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<LotTransformationListItem>>
@GET("mobile/lot-transformations/{id}")
suspend fun lotTransformationDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<LotTransformationDetail>
@POST("mobile/lot-transformations")
suspend fun createLotTransformation(
@Header("Authorization") authorization: String,
@Body payload: LotTransformationCreatePayload,
): ApiEnvelope<LotTransformationDetail>
@GET("mobile/washing/bootstrap")
suspend fun washingBootstrap(
@Header("Authorization") authorization: String,
): ApiEnvelope<WashingBootstrapData>
@GET("mobile/washing")
suspend fun washings(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<WashingListItem>>
@Multipart
@POST("mobile/washing")
suspend fun createWashing(
@Header("Authorization") authorization: String,
@Part("payload") payload: RequestBody,
): ApiEnvelope<WashingListItem>
@Multipart
@PUT("mobile/washing/{id}")
suspend fun updateWashing(
@Header("Authorization") authorization: String,
@Path("id") id: String,
@Part("payload") payload: RequestBody,
): ApiEnvelope<WashingListItem>
@POST("mobile/washing/{id}/complete")
suspend fun completeWashing(
@Header("Authorization") authorization: String,
@Path("id") id: String,
@Body payload: CompleteWashingPayload,
): ApiEnvelope<WashingListItem>
@GET("mobile/receipts/bootstrap")
suspend fun receiptsBootstrap(
@Header("Authorization") authorization: String,
): ApiEnvelope<ReceiptBootstrapData>
@GET("mobile/receipts")
suspend fun receipts(
@Header("Authorization") authorization: String,
): ApiEnvelope<List<ReceiptListItem>>
@POST("mobile/receipts")
suspend fun createReceipt(
@Header("Authorization") authorization: String,
@Body payload: ReceiptCreatePayload,
): ApiEnvelope<ReceiptDetail>
@GET("mobile/receipts/{id}")
suspend fun receiptDetail(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<ReceiptDetail>
@POST("mobile/receipts/{id}/generate-lots")
suspend fun generateReceiptLots(
@Header("Authorization") authorization: String,
@Path("id") id: String,
): ApiEnvelope<ReceiptDetail>
}
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)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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,
)
}

View File

@ -0,0 +1,3 @@
package id.abelbirdnest.mobile.ui.theme
// Typography is defined in Theme.kt.

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Abelbirdnest Stock</string>
</resources>

5
build.gradle.kts Normal file
View File

@ -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
}

5
gradle.properties Normal file
View File

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

248
gradlew vendored Executable file
View File

@ -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" "$@"

82
gradlew.bat vendored Normal file
View File

@ -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%

BIN
logo_abelbirdnest.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

268
mobile-api-blueprint.md Normal file
View File

@ -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 <session_token>`.
- 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

View File

@ -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}}"]
}
}
}
]
}
]
}

View File

@ -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"
}

18
settings.gradle.kts Normal file
View File

@ -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")