Initial mobile app implementation
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.gradle/
|
||||
.kotlin/
|
||||
.idea/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
*.iml
|
||||
.DS_Store
|
||||
98
CODEX_HANDOFF.md
Normal file
98
CODEX_HANDOFF.md
Normal 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
86
app/build.gradle.kts
Normal 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
1
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1 @@
|
||||
# Intentionally empty for now.
|
||||
25
app/src/main/AndroidManifest.xml
Normal file
25
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
25
app/src/main/java/id/abelbirdnest/mobile/MainActivity.kt
Normal file
25
app/src/main/java/id/abelbirdnest/mobile/MainActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
1152
app/src/main/java/id/abelbirdnest/mobile/data/Models.kt
Normal file
1152
app/src/main/java/id/abelbirdnest/mobile/data/Models.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
352
app/src/main/java/id/abelbirdnest/mobile/network/ApiService.kt
Normal file
352
app/src/main/java/id/abelbirdnest/mobile/network/ApiService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
7667
app/src/main/java/id/abelbirdnest/mobile/ui/AbelbirdnestApp.kt
Normal file
7667
app/src/main/java/id/abelbirdnest/mobile/ui/AbelbirdnestApp.kt
Normal file
File diff suppressed because it is too large
Load Diff
3231
app/src/main/java/id/abelbirdnest/mobile/ui/MainViewModel.kt
Normal file
3231
app/src/main/java/id/abelbirdnest/mobile/ui/MainViewModel.kt
Normal file
File diff suppressed because it is too large
Load Diff
21
app/src/main/java/id/abelbirdnest/mobile/ui/theme/Color.kt
Normal file
21
app/src/main/java/id/abelbirdnest/mobile/ui/theme/Color.kt
Normal 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)
|
||||
87
app/src/main/java/id/abelbirdnest/mobile/ui/theme/Theme.kt
Normal file
87
app/src/main/java/id/abelbirdnest/mobile/ui/theme/Theme.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
package id.abelbirdnest.mobile.ui.theme
|
||||
|
||||
// Typography is defined in Theme.kt.
|
||||
BIN
app/src/main/res/drawable/logo_abelbirdnest.png
Executable file
BIN
app/src/main/res/drawable/logo_abelbirdnest.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
3
app/src/main/res/values/strings.xml
Normal file
3
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Abelbirdnest Stock</string>
|
||||
</resources>
|
||||
5
build.gradle.kts
Normal file
5
build.gradle.kts
Normal 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
5
gradle.properties
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
9
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
9
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
248
gradlew
vendored
Executable 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
82
gradlew.bat
vendored
Normal 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
BIN
logo_abelbirdnest.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
268
mobile-api-blueprint.md
Normal file
268
mobile-api-blueprint.md
Normal 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
|
||||
569
postman/abelbirdnest-mobile-api.postman_collection.json
Normal file
569
postman/abelbirdnest-mobile-api.postman_collection.json
Normal 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}}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
93
postman/abelbirdnest-mobile-local.postman_environment.json
Normal file
93
postman/abelbirdnest-mobile-local.postman_environment.json
Normal 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
18
settings.gradle.kts
Normal 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")
|
||||
Reference in New Issue
Block a user