Compare commits
186 Commits
main
...
emv_latest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f958dfe572 | ||
|
|
83047648b4 | ||
|
|
1d132c481f | ||
|
|
c4f49bc440 | ||
|
|
70f2913989 | ||
|
|
43c903c9ee | ||
|
|
23a4ebfb64 | ||
|
|
962bfbc6c5 | ||
|
|
2093fb214a | ||
|
|
2323562270 | ||
|
|
c45c8adf65 | ||
|
|
2ca1b5eee0 | ||
|
|
bb9db73182 | ||
|
|
59153170bf | ||
|
|
726310a779 | ||
|
|
209279b847 | ||
|
|
7ccf393793 | ||
|
|
81920acc8c | ||
|
|
8223b4877f | ||
|
|
752693c8f3 | ||
|
|
d011b46345 | ||
|
|
5bb07df9e3 | ||
|
|
f0799455cb | ||
|
|
66c969873e | ||
|
|
46bf866de7 | ||
| f8066d50fb | |||
| 145b3ea898 | |||
|
|
92a24851f1 | ||
|
|
f1a2707190 | ||
|
|
706a48d1d1 | ||
|
|
188158a494 | ||
| f544ea7547 | |||
| 4ae46e9e99 | |||
|
|
4884152faa | ||
|
|
beb6ddcba8 | ||
|
|
b2e8e13a81 | ||
|
|
511c3e0128 | ||
|
|
40c392cc01 | ||
|
|
86f3e099db | ||
|
|
f4d9dfaf7b | ||
|
|
b3cb166f1a | ||
| ee91e6f787 | |||
|
|
6f61532741 | ||
|
|
e0baafb84d | ||
|
|
c3faee2eb0 | ||
|
|
f08702125c | ||
|
|
81e383798f | ||
|
|
80594be6f9 | ||
|
|
48513f8da9 | ||
|
|
838a40ee4f | ||
| 836eb07b3b | |||
|
|
26dabebb19 | ||
|
|
38bc26d821 | ||
|
|
887af67ae7 | ||
|
|
782dde479a | ||
|
|
48add55380 | ||
|
|
97d8d2abd9 | ||
|
|
714e7d9820 | ||
|
|
665bf1d5df | ||
|
|
f7510c9bad | ||
|
|
25153bb819 | ||
|
|
4c869ea6a8 | ||
|
|
d4a0d458e3 | ||
|
|
95abf5ba0d | ||
| 1021025f5c | |||
|
|
73195ebc62 | ||
|
|
2f53577f08 | ||
|
|
0ecfa8f06e | ||
|
|
4c23e53983 | ||
|
|
af9a5487b1 | ||
|
|
46ef4521cc | ||
|
|
2937a747c8 | ||
|
|
d32b233d9e | ||
|
|
f3ee2aef4c | ||
|
|
7ebc3a50d6 | ||
|
|
948aba0a8a | ||
|
|
bb054c1b86 | ||
| a6377fb16d | |||
| 6322b68e7d | |||
|
|
74f3d3586b | ||
|
|
070c158985 | ||
|
|
ac33bbd61f | ||
|
|
90a3f1f744 | ||
| db6cfa3eb6 | |||
| 1a7e4c2020 | |||
| f827e75b1e | |||
|
|
6af4b79702 | ||
|
|
fce4284b96 | ||
|
|
fc3d599689 | ||
|
|
c66a1159c5 | ||
|
|
5071f57ed7 | ||
|
|
cd1239e595 | ||
|
|
5283bae462 | ||
|
|
b7c04719c7 | ||
| 6998a270b9 | |||
| 430a8be526 | |||
| 01209196af | |||
| a565aa3f9b | |||
| 48e58722c9 | |||
| 5c18853179 | |||
| cc6821e208 | |||
| f42ecdf631 | |||
|
|
ded84bfa8e | ||
| 349d1eed2c | |||
|
|
d67e27593b | ||
|
|
f173c974d0 | ||
|
|
13aed803ae | ||
|
|
4fb6556d1b | ||
|
|
b9be038d68 | ||
|
|
a286a8258c | ||
|
|
211b092c2d | ||
| 142716caa4 | |||
| 3b7354fc3f | |||
|
|
efad1b4e14 | ||
|
|
669efd87c0 | ||
|
|
60840f0f76 | ||
|
|
edba9da0bd | ||
|
|
fd88dd6dea | ||
| f070ad1c10 | |||
|
|
b69dfeb292 | ||
|
|
c32c93338e | ||
|
|
36b9d83a40 | ||
|
|
3a63c65e8f | ||
|
|
582b08cead | ||
|
|
d745815f74 | ||
|
|
211e633517 | ||
| b073e62a20 | |||
| 5d51f53440 | |||
| 4fefae3646 | |||
|
|
7361078369 | ||
|
|
800eeadd22 | ||
|
|
18851cf7a1 | ||
|
|
8b3fb9a1a5 | ||
|
|
6dc180e791 | ||
|
|
86998e1fe2 | ||
| 8e6fde9856 | |||
| 68dd5eebd7 | |||
| 759cd20209 | |||
|
|
3301cb15e0 | ||
| 9a87493945 | |||
|
|
c1c208187b | ||
|
|
40ee7c600f | ||
|
|
8671d459cf | ||
| 000c48bf6b | |||
|
|
f95208aa9d | ||
|
|
4aebd825d2 | ||
| f80c7c5d1b | |||
|
|
5e2cfa3821 | ||
|
|
0ed4113366 | ||
|
|
6ddf0dda6c | ||
|
|
ba24c60058 | ||
|
|
feb6ea449f | ||
|
|
030eb6f836 | ||
|
|
af917520b6 | ||
|
|
767456b29a | ||
|
|
5c6c8f78c5 | ||
|
|
0f0bd49709 | ||
|
|
dbc5424aab | ||
|
|
675e398aaf | ||
|
|
57dcd13c34 | ||
|
|
72ecfbe4e7 | ||
|
|
0f435192ee | ||
|
|
fe6c5ad15e | ||
| 10f5bcf355 | |||
| e985b37aa2 | |||
|
|
f4fc2fa730 | ||
|
|
ff2858d484 | ||
|
|
9d9ef95140 | ||
|
|
79ab7c0bae | ||
|
|
283d2d64ce | ||
|
|
fb4c832476 | ||
|
|
5bbda074a8 | ||
|
|
ee2f9df351 | ||
|
|
35600bda29 | ||
|
|
44c0fa20af | ||
|
|
04549321fb | ||
|
|
42bd592c3e | ||
|
|
5904b4f69a | ||
|
|
bdd252cda9 | ||
|
|
9602d7cf81 | ||
|
|
0abf7bc6c9 | ||
|
|
bcd5634941 | ||
|
|
b5e2ec8e01 | ||
|
|
d44163b601 | ||
|
|
37a1a2e38d | ||
|
|
d3a6ddbc37 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -14,3 +14,6 @@
|
||||
.cxx
|
||||
local.properties
|
||||
/.idea
|
||||
/sunmiui-lib
|
||||
/paysdk-lib
|
||||
/app/uat
|
||||
|
||||
3
.idea/.gitignore
vendored
3
.idea/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-05-07T16:25:07.233027800Z">
|
||||
<DropdownSelection timestamp="2026-09-15T06:58:26.019801800Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=PB0219AQ60954" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=PB13216J53652" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
96
CLAUDE.md
Normal file
96
CLAUDE.md
Normal file
@ -0,0 +1,96 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```powershell
|
||||
# Build debug APK
|
||||
./gradlew assembleDebug
|
||||
|
||||
# Build release APK
|
||||
./gradlew assembleRelease
|
||||
|
||||
# Run unit tests
|
||||
./gradlew test
|
||||
|
||||
# Run instrumented tests (requires connected device/emulator)
|
||||
./gradlew connectedAndroidTest
|
||||
|
||||
# Run a single unit test class
|
||||
./gradlew :app:test --tests "com.mob.utsmyammer.ExampleUnitTest"
|
||||
|
||||
# Clean build
|
||||
./gradlew clean assembleDebug
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
**MOBPOS** is a mobile Point-of-Sale system for Myanmar, targeting Sunmi hardware devices (card readers, PIN pads, thermal printers). It processes EMV card transactions (sale, void, refund, settlement) and QR payments (MMQR, Wave Pay).
|
||||
|
||||
### Single-Activity Compose App
|
||||
|
||||
`MainActivity` hosts the entire app. Navigation is handled by Compose Navigation (`AppNavGraph.kt`) with typed routes defined in a sealed `Routes` object.
|
||||
|
||||
### MVVM with Shared State
|
||||
|
||||
- Each screen has a dedicated `ViewModel` (Hilt-injected)
|
||||
- `SharedViewModel` in `viewmodel/` is the central state holder for cross-screen transaction data (card info, amount, transaction type, approval codes)
|
||||
- Hardware interaction is abstracted into `CardReaderViewModel` and `EmvTransactionProcessViewModel`
|
||||
- State is a mix of `LiveData` (older screens) and `StateFlow` (newer screens like card reading)
|
||||
|
||||
### Screen Structure Pattern
|
||||
|
||||
Every feature follows: `ui/<feature>/` containing:
|
||||
- `*Screen.kt` — Composable UI
|
||||
- `*ViewModel.kt` — business logic and state
|
||||
|
||||
### Key Transaction Flow
|
||||
|
||||
Dashboard → InputAmount → CardWaiting → ProcessingCard → SendingToHost → TransactionResult → PrintReceipt
|
||||
|
||||
### Local Modules
|
||||
|
||||
The app depends on several local library modules (in `settings.gradle.kts`):
|
||||
- `:baselib` — Room DB, Retrofit, Hilt base setup
|
||||
- `:paylibs` / `:paysdk-lib` — Payment SDK wrappers
|
||||
- `:ecr` / `:ecr-service-lib` — Electronic Cash Register integration
|
||||
- `:mpulib` / `:mpu-lib` — Mini POS Unit hardware
|
||||
- `:sunmiui-lib` — Sunmi device UI components
|
||||
- `:qrgen-lib` — QR code generation
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **UI:** Jetpack Compose + Material 3, Compose Navigation
|
||||
- **DI:** Hilt 2.59.2 with KSP
|
||||
- **Async:** RxJava 3 + RxAndroid (primary async pattern)
|
||||
- **Network:** Retrofit 3 + OkHttp 4 + GSON
|
||||
- **DB:** Room 2.4.3 with RxJava 3 support
|
||||
- **Hardware:** Sunmi SDK (card reader, PIN pad, printer APIs)
|
||||
|
||||
### Package Layout
|
||||
|
||||
```
|
||||
com.mob.utsmyammer/
|
||||
├── config/ # Constants, SunmiPayManager
|
||||
├── model/ # Data models (CardTransactionType, ProcessCode, TransactionStatus)
|
||||
│ └── ecr/ # ECR-specific models
|
||||
├── ui/ # All Compose screens (one subdir per feature)
|
||||
│ ├── components/ # Shared reusables (NumericEntryScreen, SquareButton, AppBar)
|
||||
│ ├── navigation/ # AppNavGraph, Routes
|
||||
│ └── theme/ # Compose theme and colors
|
||||
├── utils/ # Utility functions
|
||||
└── viewmodel/ # Shared ViewModels (SharedViewModel, CardReaderViewModel, etc.)
|
||||
```
|
||||
|
||||
## Hardware Integration Notes
|
||||
|
||||
Sunmi device permissions are declared in `AndroidManifest.xml` (LED, MSR, ICC, PINPAD, PRINTER). When modifying card reading or PIN entry flows, be aware that `CardReaderViewModel` and the Sunmi ECR service handle hardware callbacks via RxJava subjects. Hardware callbacks arrive on background threads — always observe on the main thread before updating UI state.
|
||||
|
||||
## Build Configuration
|
||||
|
||||
- `compileSdk 36`, `minSdk 24`, `targetSdk 36`
|
||||
- Java 11 toolchain, Kotlin 2.2.10
|
||||
- Compose BOM 2026.02.01
|
||||
- ProGuard/minification is **disabled** for all build types
|
||||
- Application ID: `com.mob.ustmm`
|
||||
@ -1,6 +1,8 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.google.dagger.hilt.android")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -12,8 +14,9 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.mob.ustmm"
|
||||
minSdk = 24
|
||||
applicationId = "com.mob.utsmyanmar"
|
||||
minSdk = 23
|
||||
//noinspection OldTargetApi
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
@ -21,6 +24,33 @@ android {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
flavorDimensions += "env"
|
||||
|
||||
productFlavors {
|
||||
create("sit"){
|
||||
dimension = "env"
|
||||
applicationIdSuffix = ".sit"
|
||||
versionNameSuffix = "-SIT"
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
buildConfigField("String", "ENV", "\"SIT\"")
|
||||
resValue("string", "app_name", "MOB-SIT")
|
||||
}
|
||||
create("uat"){
|
||||
dimension = "env"
|
||||
applicationIdSuffix = ".uat"
|
||||
versionNameSuffix = "-UAT"
|
||||
versionCode = 19
|
||||
versionName = "3.7"
|
||||
buildConfigField("String", "ENV", "\"UAT\"")
|
||||
resValue("string", "app_name", "MOB-UAT")
|
||||
}
|
||||
create("production"){
|
||||
dimension = "env"
|
||||
buildConfigField("String", "ENV", "\"PROD\"")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
@ -29,6 +59,14 @@ android {
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
isMinifyEnabled = false
|
||||
isDebuggable = true
|
||||
// proguardFiles(
|
||||
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
// "proguard-rules.pro"
|
||||
// )
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
@ -36,6 +74,8 @@ android {
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
resValues = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,10 +84,13 @@ dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
testImplementation(libs.junit)
|
||||
@ -57,4 +100,31 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
implementation(libs.hilt.android.v2592)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.converter.gson)
|
||||
// Core icons (usually included with material library)
|
||||
implementation( libs.androidx.compose.material.icons.core)
|
||||
// Extended icons (full set of icons)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
// splash screen
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
//image lib
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
|
||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
||||
// local libs
|
||||
implementation(project(":baselib"))
|
||||
implementation(project(":mpulib"))
|
||||
implementation(project(":paylibs"))
|
||||
implementation(project(":paysdk-lib"))
|
||||
implementation(project(":qrgen-lib"))
|
||||
implementation(project(":sunmiui-lib"))
|
||||
implementation(project(":ecr"))
|
||||
implementation(project(":xpay"))
|
||||
implementation(project(":cmhl"))
|
||||
|
||||
implementation("se.warting.signature:signature-pad:0.2.0")
|
||||
}
|
||||
36
app/changelog.md
Normal file
36
app/changelog.md
Normal file
@ -0,0 +1,36 @@
|
||||
## Change Logs For UAT
|
||||
|
||||
### version 3.3
|
||||
* config auto update after transaction
|
||||
* signature removed on void
|
||||
* "Enter Password" color to LegacyRed
|
||||
* Amount and Card Number show in signature pad
|
||||
* fixed recent transaction only working for top 3
|
||||
* routed to dashboard after printing receipt
|
||||
* receipt printing animation re-scale
|
||||
|
||||
### version 3.4
|
||||
* enabled screen sleep
|
||||
* auto-settlement
|
||||
* show expiry date as "`**`/`**`"
|
||||
* align the customer name, in paper slip
|
||||
* settlement fixed in both UI and system
|
||||
* amount, date, time fixed in Void Preview
|
||||
* show reversal status in trans records
|
||||
* error_slip_print enable/disable in tms
|
||||
* auto_settlement enable/disable in tms
|
||||
|
||||
### version 3.5
|
||||
* "always with you" in mob receipt logo
|
||||
* "..." full width in both P2 and P3 preview screens
|
||||
* JCB logo added in Card Capture Screen
|
||||
* "-" in void, refund and cash adv amounts
|
||||
* disable sale button if amount is 0 or 0.00
|
||||
* changed date formats to dd/mm/yy to dd/mm/yyyy
|
||||
* changed keyboard design in reprint any transactions
|
||||
|
||||
### version 3.6
|
||||
* qr for user manual
|
||||
|
||||
### version 3.7
|
||||
* auto-settlement for both P2 and P3
|
||||
@ -2,8 +2,40 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="com.sunmi.perm.LED" />
|
||||
<uses-permission android:name="com.sunmi.perm.MSR" />
|
||||
<uses-permission android:name="com.sunmi.perm.ICC" />
|
||||
<uses-permission android:name="com.sunmi.perm.PINPAD" />
|
||||
<uses-permission android:name="com.sunmi.perm.SECURITY" />
|
||||
<uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
|
||||
<application
|
||||
android:name="com.mob.utsmyanmar.MyApplication"
|
||||
android:allowBackup="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@ -11,14 +43,19 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MOBPOS">
|
||||
<receiver android:name=".auto_settlement.AlarmReceiver" android:exported="false"/>
|
||||
<receiver android:name=".auto_settlement.BootReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service android:name=".auto_settlement.AutoSettleService" android:exported="false" android:foregroundServiceType="dataSync" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name="com.mob.utsmyanmar.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.MOBPOS">
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
43
app/src/main/java/com/mob/utsmyanmar/AGENTS.md
Normal file
43
app/src/main/java/com/mob/utsmyanmar/AGENTS.md
Normal file
@ -0,0 +1,43 @@
|
||||
## Working agreements
|
||||
|
||||
- use Scaffold and AppBar() in every main Screen
|
||||
- use MVVM design pattern
|
||||
- separate UiState and ViewModel for screen for better performance and smooth
|
||||
|
||||
### Re-usable Screens
|
||||
- PasswordInput.kt
|
||||
- InputAmountScreen.kt
|
||||
-
|
||||
|
||||
### Ponytail, lazy senior dev mode
|
||||
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
|
||||
|
||||
Before writing any code, stop at the first rung that holds:
|
||||
|
||||
Does this need to be built at all? (YAGNI)
|
||||
Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||
Does the standard library already do this? Use it.
|
||||
Does a native platform feature cover it? Use it.
|
||||
Does an already-installed dependency solve it? Use it.
|
||||
Can this be one line? Make it one line.
|
||||
Only then: write the minimum code that works.
|
||||
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||
|
||||
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||
|
||||
Rules:
|
||||
|
||||
No abstractions that weren't explicitly requested.
|
||||
No new dependency if it can be avoided.
|
||||
No boilerplate nobody asked for.
|
||||
Deletion over addition. Boring over clever. Fewest files possible.
|
||||
Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||
Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
Pick the edge-case-correct option when two stdlib approaches are the same size, lazy means less code, not the flimsier algorithm.
|
||||
Mark intentional simplifications with a ponytail: comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
|
||||
(Yes, this file also applies to agents working on the ponytail repo itself. Especially to them.)
|
||||
|
||||
### Don't Do This
|
||||
- don't run ./grdlew
|
||||
@ -1,22 +1,122 @@
|
||||
package com.mob.utsmyanmar
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlarmManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.mob.utsmyanmar.auto_settlement.SettlementScheduler
|
||||
import com.mob.utsmyanmar.ui.navigation.AppNavGraph
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var autoSettlementRequested by mutableStateOf(false)
|
||||
private var exactAlarmSettingsOpened = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
autoSettlementRequested = intent?.getBooleanExtra("AUTO_SETTLEMENT", false) == true
|
||||
requestNotificationPermission()
|
||||
scheduleSettlementAlarm()
|
||||
enableEdgeToEdge()
|
||||
hideSystemBars()
|
||||
// window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setContent {
|
||||
MOBPOSTheme {
|
||||
val navController = rememberNavController()
|
||||
AppNavGraph(navController = navController)
|
||||
AppNavGraph(
|
||||
navController = navController,
|
||||
autoSettlementRequested = autoSettlementRequested,
|
||||
onAutoSettlementHandled = { autoSettlementRequested = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
if (intent.getBooleanExtra("AUTO_SETTLEMENT", false)) {
|
||||
autoSettlementRequested = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
scheduleSettlementAlarm()
|
||||
}
|
||||
|
||||
private fun requestNotificationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissions(
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleSettlementAlarm() {
|
||||
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
|
||||
SettlementScheduler.cancel(this)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val alarmManager = getSystemService(AlarmManager::class.java)
|
||||
if (!alarmManager.canScheduleExactAlarms()) {
|
||||
if (!exactAlarmSettingsOpened) {
|
||||
exactAlarmSettingsOpened = true
|
||||
startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
|
||||
Uri.parse("package:$packageName")
|
||||
)
|
||||
)
|
||||
}
|
||||
SettlementScheduler.scheduleFallback(this)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
exactAlarmSettingsOpened = false
|
||||
SettlementScheduler.schedule(this)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1003
|
||||
}
|
||||
|
||||
private fun hideSystemBars() {
|
||||
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
hideSystemBars()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
app/src/main/java/com/mob/utsmyanmar/MyApplication.kt
Normal file
18
app/src/main/java/com/mob/utsmyanmar/MyApplication.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package com.mob.utsmyanmar
|
||||
|
||||
|
||||
import com.mob.utsmyanmar.utils.AppContextHolder
|
||||
import com.utsmyanmar.baselib.BaseApplication
|
||||
import com.utsmyanmar.paylibs.network.ISOSocket
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class MyApplication : BaseApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
AppContextHolder.init(this)
|
||||
ISOSocket.getInstance().initContext(this)
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.mob.utsmyanmar.auto_settlement
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AlarmReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val appContext = context ?: return
|
||||
|
||||
// Exact alarms are one-shot, so keep the next daily alarm alive before
|
||||
// starting the foreground service.
|
||||
if (!SettlementScheduler.schedule(appContext)) {
|
||||
LogUtil.e(AlarmReceiver::class.java.simpleName, "Unable to reschedule settlement alarm")
|
||||
}
|
||||
|
||||
val systemParams = SystemParamsOperation.getInstance()
|
||||
if (!systemParams.isAutoSettlement() || !systemParams.settlementStatus) {
|
||||
LogUtil.d(AlarmReceiver::class.java.simpleName, "Auto settlement is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
val serviceIntent = Intent(appContext, AutoSettleService::class.java)
|
||||
|
||||
LogUtil.d(AlarmReceiver::class.java.simpleName, "Alert service")
|
||||
|
||||
ContextCompat.startForegroundService(appContext, serviceIntent)
|
||||
|
||||
LogUtil.d(AlarmReceiver::class.java.simpleName, "Foreground service")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
package com.mob.utsmyanmar.auto_settlement
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.media.AudioAttributes
|
||||
import android.media.Ringtone
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.mob.utsmyanmar.MainActivity
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AutoSettleService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val FOREGROUND_CHANNEL_ID = "auto_settlement_foreground_channel"
|
||||
private const val ALERT_CHANNEL_ID = "auto_settlement_alert_channel"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
private const val ALERT_NOTIFICATION_ID = 1002
|
||||
private const val ALERT_SOUND_DURATION_MS = 3_000L
|
||||
}
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var alertRingtone: Ringtone? = null
|
||||
private val stopServiceRunnable = Runnable {
|
||||
alertRingtone?.stop()
|
||||
alertRingtone = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
stopForeground(true)
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
LogUtil.d(AutoSettleService::class.java.simpleName, "auto settle service called")
|
||||
startForegroundNotification()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val systemParams = SystemParamsOperation.getInstance()
|
||||
if (!systemParams.isAutoSettlement() || !systemParams.settlementStatus) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
showSettlementAlert()
|
||||
playAlertSound()
|
||||
launchSettlement()
|
||||
mainHandler.removeCallbacks(stopServiceRunnable)
|
||||
mainHandler.postDelayed(stopServiceRunnable, ALERT_SOUND_DURATION_MS)
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
mainHandler.removeCallbacks(stopServiceRunnable)
|
||||
alertRingtone?.stop()
|
||||
alertRingtone = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
private fun startForegroundNotification() {
|
||||
LogUtil.d(AutoSettleService::class.java.simpleName, "foreground noti called")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
FOREGROUND_CHANNEL_ID,
|
||||
"AUTO SETTLEMENT",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
channel.description = "Auto Settlement service status"
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.logo_mob)
|
||||
.setContentTitle("Auto Settlement")
|
||||
.setContentText("Preparing settlement reminder...")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun showSettlementAlert() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
ALERT_CHANNEL_ID,
|
||||
"AUTO SETTLEMENT ALERT",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Daily settlement reminder"
|
||||
enableVibration(true)
|
||||
setSound(
|
||||
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
getSystemService(NotificationManager::class.java)?.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
ALERT_NOTIFICATION_ID,
|
||||
settlementIntent(),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, ALERT_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.logo_mob)
|
||||
.setContentTitle("Settlement Reminder")
|
||||
.setContentText("Daily settlement is due. Tap to open settlement.")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
||||
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
|
||||
.setVibrate(longArrayOf(0, 250, 150, 250))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
getSystemService(NotificationManager::class.java)
|
||||
?.notify(ALERT_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun playAlertSound() {
|
||||
alertRingtone?.stop()
|
||||
alertRingtone = RingtoneManager.getRingtone(
|
||||
this,
|
||||
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||
)?.apply {
|
||||
audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build()
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSettlement() {
|
||||
runCatching {
|
||||
startActivity(settlementIntent())
|
||||
}.onFailure {
|
||||
LogUtil.e(
|
||||
AutoSettleService::class.java.simpleName,
|
||||
"Unable to open settlement screen: ${it.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun settlementIntent(): Intent = Intent(this, MainActivity::class.java).apply {
|
||||
addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
)
|
||||
putExtra("AUTO_SETTLEMENT", true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package com.mob.utsmyanmar.auto_settlement
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||
val appContext = context ?: return
|
||||
|
||||
LogUtil.d(BootReceiver::class.java.simpleName, "Boot completed, rescheduling settlement alarm")
|
||||
SettlementScheduler.schedule(appContext)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.mob.utsmyanmar.auto_settlement
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import java.util.Calendar
|
||||
|
||||
object SettlementScheduler {
|
||||
private const val FALLBACK_ALERT_TIME = "23:30:00"
|
||||
private const val REQUEST_CODE = 100
|
||||
|
||||
fun schedule(context: Context): Boolean {
|
||||
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
|
||||
cancel(context)
|
||||
return false
|
||||
}
|
||||
|
||||
val alarmManager =
|
||||
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
!alarmManager.canScheduleExactAlarms()
|
||||
) {
|
||||
return scheduleFallback(context)
|
||||
}
|
||||
|
||||
val calendar = nextAlarmCalendar() ?: return false
|
||||
val pendingIntent = alarmPendingIntent(context)
|
||||
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun scheduleFallback(context: Context): Boolean {
|
||||
if (!SystemParamsOperation.getInstance().isAutoSettlement()) {
|
||||
cancel(context)
|
||||
return false
|
||||
}
|
||||
|
||||
val alarmManager =
|
||||
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
val calendar = nextAlarmCalendar() ?: return false
|
||||
val pendingIntent = alarmPendingIntent(context)
|
||||
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.timeInMillis,
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun nextAlarmCalendar(): Calendar? {
|
||||
val configuredTime = parseTime(
|
||||
SystemParamsOperation.getInstance().clearBatchTime
|
||||
// "15:45:00"
|
||||
) ?: parseTime(FALLBACK_ALERT_TIME) ?: return null
|
||||
|
||||
return Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, configuredTime.first)
|
||||
set(Calendar.MINUTE, configuredTime.second)
|
||||
set(Calendar.SECOND, configuredTime.third)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
|
||||
if (before(Calendar.getInstance())) {
|
||||
add(Calendar.DAY_OF_MONTH, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alarmPendingIntent(context: Context): PendingIntent {
|
||||
val intent = Intent(context, AlarmReceiver::class.java)
|
||||
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
REQUEST_CODE,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseTime(value: String): Triple<Int, Int, Int>? {
|
||||
val parts = value.trim().split(":")
|
||||
if (parts.size !in 2..3) return null
|
||||
|
||||
val hour = parts[0].toIntOrNull() ?: return null
|
||||
val minute = parts[1].toIntOrNull() ?: return null
|
||||
val second = if (parts.size == 3) parts[2].toIntOrNull() ?: return null else 0
|
||||
|
||||
if (hour !in 0..23 || minute !in 0..59 || second !in 0..59) return null
|
||||
return Triple(hour, minute, second)
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ServiceCast")
|
||||
fun cancel(context: Context) {
|
||||
|
||||
val alarmManager =
|
||||
context.getSystemService(Context.ALARM_SERVICE)
|
||||
as AlarmManager
|
||||
|
||||
|
||||
val intent = Intent(
|
||||
context,
|
||||
AlarmReceiver::class.java
|
||||
)
|
||||
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
100,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
|
||||
alarmManager.cancel(pendingIntent)
|
||||
}
|
||||
}
|
||||
13
app/src/main/java/com/mob/utsmyanmar/config/Constants.kt
Normal file
13
app/src/main/java/com/mob/utsmyanmar/config/Constants.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package com.mob.utsmyanmar.config
|
||||
|
||||
import com.mob.utsmyanmar.R
|
||||
|
||||
object Constants {
|
||||
const val WALLET = "WALLET"
|
||||
const val MPU_CARD_SCHEME = "MPU"
|
||||
const val TIMEOUT = 30
|
||||
const val PROCESSING_TIMEOUT = 180
|
||||
const val PIN_PAD_TIMEOUT = 60
|
||||
const val REVERSAL = "REVERSAL"
|
||||
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.mob.utsmyanmar.config
|
||||
|
||||
import com.sunmi.pay.hardware.aidl.security.SecurityOpt
|
||||
import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2
|
||||
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2
|
||||
import com.sunmi.pay.hardware.aidlv2.readcard.ReadCardOptV2
|
||||
import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2
|
||||
import com.sunmi.pay.hardware.aidlv2.system.BasicOptV2
|
||||
import com.utsmyanmar.baselib.BaseApplication
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SunmiPayManager @Inject constructor() {
|
||||
private val app: BaseApplication?
|
||||
get() = BaseApplication.getInstance()
|
||||
|
||||
val pinPadOptV2: PinPadOptV2?
|
||||
get() = app?.mPinPadOptV2
|
||||
|
||||
val securityOptV2: SecurityOptV2?
|
||||
get() = app?.mSecurityOptV2
|
||||
|
||||
val readCardOptV2: ReadCardOptV2?
|
||||
get() = app?.mReadCardOptV2
|
||||
|
||||
val emvOptV2: EMVOptV2?
|
||||
get() = app?.mEMVOptV2
|
||||
|
||||
val basicOptV2: BasicOptV2?
|
||||
get() = BaseApplication.basicOptV2
|
||||
|
||||
val securityOpt: SecurityOpt?
|
||||
get() = app?.securityOpt
|
||||
|
||||
fun isReady(): Boolean {
|
||||
return pinPadOptV2 != null &&
|
||||
securityOptV2 != null &&
|
||||
readCardOptV2 != null &&
|
||||
emvOptV2 != null &&
|
||||
basicOptV2 != null
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.mob.utsmyanmar.model
|
||||
|
||||
enum class CardTransactionType {
|
||||
MPU, EMV, MAG, FALLBACK, MOCK
|
||||
}
|
||||
12
app/src/main/java/com/mob/utsmyanmar/model/PinPadStatus.kt
Normal file
12
app/src/main/java/com/mob/utsmyanmar/model/PinPadStatus.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package com.mob.utsmyanmar.model
|
||||
|
||||
enum class PinPadStatus {
|
||||
ON_CONFIRM,
|
||||
ON_CANCEL ,
|
||||
ON_EMPTY,
|
||||
ON_ERROR,
|
||||
ON_TIMEOUT,
|
||||
ON_NEXT_SCREEN,
|
||||
ON_CARD_REMOVED,
|
||||
ON_ERROR_DUKPT,
|
||||
}
|
||||
39
app/src/main/java/com/mob/utsmyanmar/model/ProcessCode.kt
Normal file
39
app/src/main/java/com/mob/utsmyanmar/model/ProcessCode.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.mob.utsmyanmar.model
|
||||
|
||||
object ProcessCode {
|
||||
const val BALANCE_INQUIRY = "31"
|
||||
|
||||
const val SALE_PURCHASE = "00"
|
||||
|
||||
const val SALE_VOID = "02"
|
||||
|
||||
const val PRE_AUTH_SALE = "30"
|
||||
|
||||
const val PRE_AUTH_VOID = "30"
|
||||
|
||||
const val PRE_AUTH_COMPLETE = "30"
|
||||
|
||||
const val PRE_AUTH_COMPLETE_VOID = "30"
|
||||
|
||||
const val CASH_ADVANCE = "01" // 01-MPU 17-TTIP
|
||||
|
||||
const val FUND_TRANSFER = "61"
|
||||
|
||||
const val PIN_CHANGE = "70"
|
||||
|
||||
const val SETTLEMENT = "92"
|
||||
|
||||
const val SIGN_ON = "95"
|
||||
|
||||
const val CASH_DEPOSIT = "21"
|
||||
|
||||
const val REFUND = "20"
|
||||
|
||||
const val SMART = "00"
|
||||
|
||||
const val SAVING = "10"
|
||||
|
||||
const val CURRENT = "20"
|
||||
|
||||
const val TO_ACCOUNT = "00"
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.mob.utsmyanmar.model
|
||||
|
||||
enum class SettlementType {
|
||||
NORMAL,
|
||||
CUT_OVER
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.mob.utsmyanmar.model
|
||||
|
||||
enum class TransResultStatus {
|
||||
CLICK_CONFIRM,
|
||||
SUCCESS,
|
||||
FAIL,
|
||||
OFFLINE_SUCCESS,
|
||||
OFFLINE_FAILURE,
|
||||
SECONDARY,
|
||||
REVERSAL_PROCESS,
|
||||
REVERSAL_PREPARE,
|
||||
REVERSAL_SECONDARY,
|
||||
REVERSAL_THIRD,
|
||||
REVERSAL_FAIL,
|
||||
REVERSAL_SUCCESS,
|
||||
BEFORE_REVERSAL,
|
||||
PIN_PAD_CANCEL,
|
||||
PIN_PAD_CONFIRM,
|
||||
PIN_PAD_ERROR,
|
||||
PIN_MISMATCH,
|
||||
PIN_MISMATCH_END,
|
||||
REMOVED_CARD,
|
||||
EMV_ERROR,
|
||||
ERROR,
|
||||
RETRY_AGAIN,
|
||||
NEXT_SCREEN,
|
||||
EMPTY_PIN,
|
||||
NETWORK_ERROR
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.mob.utsmyanmar.model
|
||||
|
||||
enum class TransactionStatus {
|
||||
ON_SUCCESS,
|
||||
ON_REVERSAL,
|
||||
ON_BATCH_UPLOAD,
|
||||
ON_FAIL,
|
||||
ON_ERROR,
|
||||
ON_SECONDARY,
|
||||
ON_DONE
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.mob.utsmyanmar.model.ecr
|
||||
|
||||
enum class ECRResultStatus {
|
||||
USER_CANCEL,
|
||||
TIME_OUT,
|
||||
RESPONSE_RECEIVED
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.mob.utsmyanmar.model.sirius
|
||||
|
||||
import com.utsmyanmar.baselib.network.model.sirius.SiriusHost
|
||||
import com.utsmyanmar.baselib.network.model.sirius.SiriusMerchant
|
||||
import com.utsmyanmar.baselib.network.model.sirius.SiriusProperty
|
||||
|
||||
data class SiriusResponse (
|
||||
var serial: String,
|
||||
var ecrKey: String,
|
||||
var address: String,
|
||||
var merchant : SiriusMerchant,
|
||||
var hosts : List<SiriusHost>,
|
||||
var properties: List<SiriusProperty>
|
||||
)
|
||||
@ -0,0 +1,6 @@
|
||||
package com.mob.utsmyanmar.model.sirius
|
||||
|
||||
enum class TMSUpdate {
|
||||
UPDATE,
|
||||
CHECK
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.mob.utsmyanmar.model.sirius
|
||||
|
||||
data class TMSValidity(
|
||||
var status: ValidityStatus? = null,
|
||||
var message: String? = null
|
||||
)
|
||||
@ -0,0 +1,6 @@
|
||||
package com.mob.utsmyanmar.model.sirius
|
||||
|
||||
enum class ValidityStatus {
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
@ -1,362 +0,0 @@
|
||||
package com.mob.utsmyanmar.ui.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.mob.utsmyanmar.ui.theme.Primary
|
||||
import com.mob.utsmyanmar.ui.theme.White
|
||||
import com.mob.utsmyanmar.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AmountScreen(
|
||||
action: String,
|
||||
onBackClick: () -> Unit,
|
||||
onCancelClick: () -> Unit = {},
|
||||
onNextClick: (String) -> Unit = {}
|
||||
) {
|
||||
|
||||
var amount by remember {
|
||||
mutableStateOf("0")
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = action.uppercase(),
|
||||
color = White,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBackClick) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_left_arrow),
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = White
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Primary
|
||||
)
|
||||
)
|
||||
},
|
||||
containerColor = White
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.background(White)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
AmountBox(
|
||||
amount = formatTypedAmount(amount),
|
||||
height = 134.dp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Keypad(
|
||||
buttonHeight = 74.dp,
|
||||
buttonSpacing = 12.dp,
|
||||
onKeyClick = { key ->
|
||||
amount = handleAmountInput(amount, key)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
|
||||
Button(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(68.dp),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = White,
|
||||
contentColor = Primary
|
||||
),
|
||||
border = ButtonDefaults.outlinedButtonBorder
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onNextClick(normalizeAmount(amount))
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(68.dp),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Primary,
|
||||
contentColor = White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Next",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountBox(
|
||||
amount: String,
|
||||
height: Dp
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.background(
|
||||
color = Primary,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.padding(horizontal = 24.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = amount,
|
||||
color = White,
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(14.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = White,
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
)
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 6.dp
|
||||
)
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = "MMK",
|
||||
color = Primary,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Keypad(
|
||||
buttonHeight: Dp,
|
||||
buttonSpacing: Dp,
|
||||
onKeyClick: (String) -> Unit
|
||||
) {
|
||||
|
||||
val keys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf(".", "0", "DEL")
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(buttonSpacing)
|
||||
) {
|
||||
|
||||
keys.forEach { row ->
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
row.forEach { key ->
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onKeyClick(key)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(buttonHeight),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Primary,
|
||||
contentColor = White
|
||||
),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
|
||||
if (key == "DEL") {
|
||||
|
||||
Text(
|
||||
text = "⌫",
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
} else {
|
||||
|
||||
Text(
|
||||
text = key,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAmountInput(
|
||||
current: String,
|
||||
key: String
|
||||
): String {
|
||||
|
||||
return when (key.uppercase()) {
|
||||
|
||||
"DEL" -> {
|
||||
current
|
||||
.dropLast(1)
|
||||
.ifEmpty { "0" }
|
||||
}
|
||||
|
||||
"." -> {
|
||||
if (current.contains(".")) {
|
||||
current
|
||||
} else {
|
||||
"${current.ifEmpty { "0" }}."
|
||||
}
|
||||
}
|
||||
|
||||
in "0".."9" -> {
|
||||
val sanitizedCurrent = if (current == "0") "" else current
|
||||
|
||||
if (sanitizedCurrent.contains(".")) {
|
||||
val decimal = sanitizedCurrent.substringAfter(".")
|
||||
|
||||
if (decimal.length >= 2) {
|
||||
current
|
||||
} else {
|
||||
sanitizedCurrent + key
|
||||
}
|
||||
} else {
|
||||
val nextValue = sanitizedCurrent + key
|
||||
nextValue.trimStart('0').ifEmpty { "0" }
|
||||
}
|
||||
}
|
||||
|
||||
else -> current
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeAmount(value: String): String {
|
||||
if (value.isBlank()) return "0.00"
|
||||
|
||||
val normalized = if (value.endsWith(".")) {
|
||||
value.dropLast(1)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
|
||||
return when {
|
||||
normalized.isBlank() -> "0.00"
|
||||
normalized.contains(".") -> {
|
||||
val whole = normalized.substringBefore(".").ifEmpty { "0" }
|
||||
val decimal = normalized.substringAfter(".")
|
||||
|
||||
when (decimal.length) {
|
||||
0 -> "$whole.00"
|
||||
1 -> "$whole.${decimal}0"
|
||||
else -> "$whole.${decimal.take(2)}"
|
||||
}
|
||||
}
|
||||
else -> "${normalized}.00"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTypedAmount(value: String): String {
|
||||
if (!value.contains(".")) return "$value.00"
|
||||
|
||||
val whole = value.substringBefore(".").ifEmpty { "0" }
|
||||
val decimal = value.substringAfter(".")
|
||||
|
||||
return when (decimal.length) {
|
||||
0 -> "$whole."
|
||||
1 -> "$whole.${decimal}0"
|
||||
else -> "$whole.${decimal.take(2)}"
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AmountScreenPreview() {
|
||||
|
||||
MOBPOSTheme {
|
||||
|
||||
AmountScreen(
|
||||
action = "Amount",
|
||||
onBackClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.mob.utsmyanmar.ui.cardwaiting
|
||||
|
||||
sealed interface CardWaitingEvent {
|
||||
data object GoManualEntry : CardWaitingEvent
|
||||
data object GoProcessingCard : CardWaitingEvent
|
||||
data object GoTimeout : CardWaitingEvent
|
||||
data object GoMain : CardWaitingEvent
|
||||
data object GoBack : CardWaitingEvent
|
||||
}
|
||||
@ -0,0 +1,469 @@
|
||||
package com.mob.utsmyanmar.ui.cardwaiting
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.rounded.Wifi
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun CardWaitingScreen(
|
||||
viewModel: CardWaitingViewModel,
|
||||
amount: String,
|
||||
onManualEntry: () -> Unit,
|
||||
onProcessingCard: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onMain: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.events.collect { event ->
|
||||
when (event) {
|
||||
CardWaitingEvent.GoManualEntry -> onManualEntry()
|
||||
CardWaitingEvent.GoProcessingCard -> onProcessingCard()
|
||||
CardWaitingEvent.GoMain -> onMain()
|
||||
CardWaitingEvent.GoBack -> onBack()
|
||||
CardWaitingEvent.GoTimeout -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.onScreenResume()
|
||||
}
|
||||
|
||||
DisposableEffect(viewModel) {
|
||||
onDispose {
|
||||
viewModel.onScreenPause()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = uiState.canGoBack) {
|
||||
viewModel.onBackPressed()
|
||||
}
|
||||
|
||||
CardWaitingScreenContent(
|
||||
amount = amount,
|
||||
uiState = uiState,
|
||||
onBackClick = viewModel::onBackPressed,
|
||||
onManualEntryClick = viewModel::onManualEntryClick,
|
||||
onMockClick = viewModel::onMockClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardWaitingScreenContent(
|
||||
amount: String,
|
||||
uiState: CardWaitingUiState,
|
||||
onBackClick: () -> Unit,
|
||||
onManualEntryClick: () -> Unit,
|
||||
onMockClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.IvoryBeige)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(54.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (uiState.canGoBack) {
|
||||
IconButton(
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "AMOUNT TO PAY",
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
|
||||
// Row(verticalAlignment = Alignment.Bottom) {
|
||||
Text(
|
||||
text = amount,
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.displayLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
Text(
|
||||
text = "MMK",
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Medium),
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
// }
|
||||
|
||||
// Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
ContactlessCircle(isLoading = uiState.isLoading)
|
||||
|
||||
// Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = when {
|
||||
uiState.isCardCaptured -> "Card Detected"
|
||||
uiState.isFallback -> "Swipe Your Card"
|
||||
else -> "Tap Your Card"
|
||||
},
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (uiState.isCardCaptured) {
|
||||
"Reader captured card data"
|
||||
} else {
|
||||
"Hold your card near the reader"
|
||||
},
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 9.sp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = Color.Gray.copy(alpha = 0.4f)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "OR",
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = Color.Gray.copy(alpha = 0.4f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
InsertCardRow(onMockClick = onMockClick)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = uiState.alertMessage,
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
ManualEntryAction(onManualEntryClick = onManualEntryClick)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
CardLogoRow()
|
||||
Spacer(modifier = Modifier.height(34.dp))
|
||||
PoweredByMob()
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactlessCircle(isLoading: Boolean) {
|
||||
Box(
|
||||
modifier = Modifier.size(190.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircleBorder(180, 0.1f)
|
||||
CircleBorder(150, 0.20f)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.size(108.dp),
|
||||
shape = CircleShape,
|
||||
color = Color.White,
|
||||
shadowElevation = 8.dp
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
color = Color.LegacyRed,
|
||||
strokeWidth = 3.dp,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Wifi,
|
||||
contentDescription = null,
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier
|
||||
.size(72.dp)
|
||||
.rotate(90f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CircleBorder(
|
||||
size: Int,
|
||||
alpha: Float
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.size(size.dp)
|
||||
.alpha(alpha),
|
||||
shape = CircleShape,
|
||||
border = BorderStroke(1.dp, Color.LegacyRed)
|
||||
) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InsertCardRow(onMockClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.height(44.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_insert_card),
|
||||
contentDescription = null,
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clickable(onClick = onMockClick)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "Insert Your Card",
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
Text(
|
||||
text = "Chip facing up",
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 9.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusPanel(uiState: CardWaitingUiState) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = when {
|
||||
uiState.isCardCaptured -> "Reader status"
|
||||
uiState.isLoading -> "Reader status"
|
||||
else -> "Ready for card"
|
||||
},
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = when {
|
||||
uiState.isCardCaptured -> "Captured"
|
||||
uiState.isLoading -> "Initializing"
|
||||
else -> "Waiting"
|
||||
},
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManualEntryAction(onManualEntryClick: () -> Unit) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.White)
|
||||
.clickable(onClick = onManualEntryClick)
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Manual Entry",
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardLogoRow() {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(3.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(42.dp)
|
||||
.padding(horizontal = 14.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CardLogo(
|
||||
icon = painterResource(R.drawable.logo_mpu),
|
||||
name = "master",
|
||||
)
|
||||
CardLogo(
|
||||
icon = painterResource(R.drawable.logo_visa),
|
||||
name = "visa",
|
||||
)
|
||||
CardLogo(
|
||||
icon = painterResource(R.drawable.logo_master),
|
||||
name = "master",
|
||||
)
|
||||
CardLogo(
|
||||
icon = painterResource(R.drawable.logo_union_pay),
|
||||
name = "union",
|
||||
)
|
||||
CardLogo(
|
||||
icon = painterResource(R.drawable.logo_jcb),
|
||||
name = "jcb",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardLogo(
|
||||
icon: Painter,
|
||||
name: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = Color.White
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = icon,
|
||||
contentDescription = name,
|
||||
modifier = Modifier.height(30.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PoweredByMob() {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "Powered by",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Text(
|
||||
text = "MOB",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 9.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@Composable
|
||||
fun PreviewCardWaitingScreen() {
|
||||
CardWaitingScreenContent(
|
||||
amount = "50,000",
|
||||
uiState = CardWaitingUiState(),
|
||||
onBackClick = {},
|
||||
onManualEntryClick = {},
|
||||
onMockClick = {}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.mob.utsmyanmar.ui.cardwaiting
|
||||
|
||||
data class CardWaitingUiState(
|
||||
val alertMessage: String = "Please insert, tap, or swipe card",
|
||||
val isLoading: Boolean = false,
|
||||
val isFallback: Boolean = false,
|
||||
val canGoBack: Boolean = true,
|
||||
val isCardCaptured: Boolean = false
|
||||
)
|
||||
@ -0,0 +1,333 @@
|
||||
package com.mob.utsmyanmar.ui.cardwaiting
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mob.utsmyanmar.model.CardTransactionType
|
||||
import com.mob.utsmyanmar.model.ecr.ECRResultStatus
|
||||
import com.mob.utsmyanmar.utils.CoreUtils
|
||||
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.sunmi.pay.hardware.aidl.AidlConstants
|
||||
import com.utsmyanmar.checkxread.checkcard.CheckCardResultX
|
||||
import com.utsmyanmar.checkxread.util.CardTypeX
|
||||
import com.utsmyanmar.ecr.ECRHelper
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CardWaitingViewModel(
|
||||
private val cardReadViewModel: CardReaderViewModel,
|
||||
private val sharedViewModel: SharedViewModel
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
fun provideFactory(
|
||||
cardReadViewModel: CardReaderViewModel,
|
||||
sharedViewModel: SharedViewModel
|
||||
): ViewModelProvider.Factory {
|
||||
return object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return CardWaitingViewModel(
|
||||
cardReadViewModel = cardReadViewModel,
|
||||
sharedViewModel = sharedViewModel
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(CardWaitingUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val _events = Channel<CardWaitingEvent>()
|
||||
val events = _events.receiveAsFlow()
|
||||
|
||||
private var retryCounter = 0
|
||||
private var fallbackCounter = 0
|
||||
private var fallbackEnabled = false
|
||||
private var readerInitJob: Job? = null
|
||||
|
||||
fun onScreenResume() {
|
||||
retryCounter = 0
|
||||
fallbackCounter = SystemParamsOperation.getInstance().fallbackCounter
|
||||
fallbackEnabled = SystemParamsOperation.getInstance().fallbackEnabled
|
||||
_uiState.update {
|
||||
it.copy(canGoBack = true)
|
||||
}
|
||||
|
||||
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND) {
|
||||
sharedViewModel.enableCardStatusIcon(false, false, false, false)
|
||||
} else {
|
||||
sharedViewModel.enableCardStatusIcon(true, true, true, false)
|
||||
}
|
||||
|
||||
val isFallback = sharedViewModel.getIsFallback().value == true
|
||||
if (isFallback) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Fallback!\nPlease stripe!",
|
||||
isFallback = true,
|
||||
isCardCaptured = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Please insert, tap, or swipe card",
|
||||
isFallback = false,
|
||||
isLoading = false,
|
||||
isCardCaptured = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
startCardReadWhenReady(isFallback)
|
||||
}
|
||||
|
||||
fun onScreenPause() {
|
||||
readerInitJob?.cancel()
|
||||
stopCardReading()
|
||||
}
|
||||
|
||||
fun onBackPressed() {
|
||||
if (sharedViewModel.isEcr.value == true) {
|
||||
sharedViewModel.isEcr.postValue(false)
|
||||
CoreUtils.getInstance(sharedViewModel).responseRejectMsg("Transaction cancelled")
|
||||
sharedViewModel.isEcrFinished.postValue(true)
|
||||
}
|
||||
|
||||
stopCardReading()
|
||||
|
||||
viewModelScope.launch {
|
||||
_events.send(CardWaitingEvent.GoBack)
|
||||
}
|
||||
}
|
||||
|
||||
fun onManualEntryClick() {
|
||||
stopCardReading()
|
||||
viewModelScope.launch {
|
||||
_events.send(CardWaitingEvent.GoManualEntry)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMockClick() {
|
||||
onScreenPause()
|
||||
cardReadViewModel.setCardTransactionType(CardTransactionType.MOCK)
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Mock card detected.",
|
||||
isLoading = false,
|
||||
isCardCaptured = true
|
||||
)
|
||||
}
|
||||
_events.send(CardWaitingEvent.GoProcessingCard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCardReadWhenReady(isFallback: Boolean) {
|
||||
readerInitJob?.cancel()
|
||||
readerInitJob = viewModelScope.launch {
|
||||
if (cardReadViewModel.isReaderReady()) {
|
||||
setupCardReadProcess(isFallback)
|
||||
return@launch
|
||||
}
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Initializing card reader...",
|
||||
isLoading = true,
|
||||
isCardCaptured = false
|
||||
)
|
||||
}
|
||||
|
||||
repeat(10) {
|
||||
delay(300)
|
||||
if (cardReadViewModel.isReaderReady()) {
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
alertMessage = if (isFallback) {
|
||||
"Fallback!\nPlease stripe!"
|
||||
} else {
|
||||
"Please insert, tap, or swipe card"
|
||||
},
|
||||
isLoading = false,
|
||||
isCardCaptured = false
|
||||
)
|
||||
}
|
||||
setupCardReadProcess(isFallback)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Card reader unavailable.\nPlease wait and try again.",
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCardReadProcess(isFallback: Boolean) {
|
||||
initCheckCard(isFallback)
|
||||
}
|
||||
|
||||
private fun initCheckCard(isFallback: Boolean) {
|
||||
var allType =
|
||||
AidlConstants.CardType.NFC.value or
|
||||
AidlConstants.CardType.IC.value or
|
||||
AidlConstants.CardType.MAGNETIC.value
|
||||
|
||||
if (isFallback) {
|
||||
allType = AidlConstants.CardType.MAGNETIC.value
|
||||
} else if (
|
||||
SystemParamsOperation.getInstance().isMagStripeEnabled &&
|
||||
!SystemParamsOperation.getInstance().isNfcEnabled
|
||||
) {
|
||||
allType =
|
||||
AidlConstants.CardType.IC.value or
|
||||
AidlConstants.CardType.MAGNETIC.value
|
||||
}
|
||||
|
||||
cardReadViewModel.startCheckXProcess(
|
||||
allType,
|
||||
65,
|
||||
object : CheckCardResultX {
|
||||
override fun onSuccess(cardType: CardTypeX, isMPU: Boolean) {
|
||||
Log.d("CardWaitingViewModel", "on success cardType=$cardType and isMpu=$isMPU")
|
||||
when {
|
||||
!isFallback && cardType == CardTypeX.MAG -> {
|
||||
if (SystemParamsOperation.getInstance().isMagStripeEnabled) {
|
||||
cardReadViewModel.setCardTransactionType(CardTransactionType.MAG)
|
||||
} else {
|
||||
_uiState.update {
|
||||
it.copy(alertMessage = "Mag stripe not allowed")
|
||||
}
|
||||
setupCardReadProcess(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isFallback && cardType == CardTypeX.MAG -> {
|
||||
sharedViewModel.setEmvTrans(false)
|
||||
cardReadViewModel.setCardTransactionType(CardTransactionType.FALLBACK)
|
||||
}
|
||||
|
||||
cardType == CardTypeX.IC || cardType == CardTypeX.NFC -> {
|
||||
if (isMPU) {
|
||||
sharedViewModel.setEmvTrans(false)
|
||||
cardReadViewModel.setCardTransactionType(CardTransactionType.MPU)
|
||||
} else {
|
||||
cardReadViewModel.setCardData(cardType.value)
|
||||
cardReadViewModel.setCardTransactionType(CardTransactionType.EMV)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Card detected.",
|
||||
isLoading = false,
|
||||
isCardCaptured = true
|
||||
)
|
||||
}
|
||||
_events.send(CardWaitingEvent.GoProcessingCard)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(code: Int, message: String) {
|
||||
ecrActionCancel("Transaction cancelled")
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = message,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
_events.send(CardWaitingEvent.GoMain)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCommError() {
|
||||
if (fallbackEnabled && retryCounter < fallbackCounter) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Card not detected!\nRemain Attempt - ${fallbackCounter - retryCounter}"
|
||||
)
|
||||
}
|
||||
retryCounter++
|
||||
setupCardReadProcess(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (retryCounter == fallbackCounter) {
|
||||
_uiState.update {
|
||||
it.copy(alertMessage = "Fallback!\nPlease stripe!")
|
||||
}
|
||||
setupCardReadProcess(true)
|
||||
return
|
||||
}
|
||||
|
||||
ecrActionCancel("Transaction cancelled")
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
alertMessage = "Chip not detected!",
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
_events.send(CardWaitingEvent.GoMain)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ecrActionCancel(msg: String) {
|
||||
if (sharedViewModel.isEcr.value != true) {
|
||||
return
|
||||
}
|
||||
|
||||
sharedViewModel.isEcr.postValue(false)
|
||||
sharedViewModel.isEcrFinished.postValue(true)
|
||||
|
||||
if (SystemParamsOperation.getInstance().isCMHLEnabled) {
|
||||
ECRHelper.send(
|
||||
CoreUtils.getInstance(sharedViewModel)
|
||||
.generateCMHLResponse(ECRResultStatus.USER_CANCEL)
|
||||
)
|
||||
|
||||
CoreUtils.getInstance(sharedViewModel).responseACKCMHL()
|
||||
return
|
||||
}
|
||||
|
||||
CoreUtils.getInstance(sharedViewModel).responseRejectMsg(msg)
|
||||
}
|
||||
|
||||
private fun stopCardReading() {
|
||||
sharedViewModel.setIsFallback(false)
|
||||
cardReadViewModel.cancelCheckCard()
|
||||
cardReadViewModel.resetOneTimeFlag()
|
||||
cardReadViewModel.cancelCheckXProcess()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
readerInitJob?.cancel()
|
||||
stopCardReading()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
package com.mob.utsmyanmar.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Backspace
|
||||
import androidx.compose.material.icons.rounded.Backspace
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun NumericEntryScreen(
|
||||
title: String,
|
||||
prompt: String,
|
||||
displayValue: String,
|
||||
supportingText: String,
|
||||
confirmText: String,
|
||||
onBackClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onConfirmClick: () -> Unit,
|
||||
onKeyClick: (String) -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
prefixLabel: String? = null,
|
||||
errorMessage: String? = null,
|
||||
confirmEnabled: Boolean = true,
|
||||
canDelete: Boolean = true,
|
||||
keys: List<List<String>> = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf(".", "0", "00")
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.IvoryBeige)
|
||||
.navigationBarsPadding()
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = prompt,
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
if (!prefixLabel.isNullOrBlank()) {
|
||||
Text(
|
||||
text = prefixLabel,
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = displayValue,
|
||||
style = MaterialTheme.typography.displayLarge.copy(color = Color.LegacyRed)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = supportingText,
|
||||
color = if (errorMessage == null) Color.Gray else Color.Error,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
|
||||
NumericKeypad(
|
||||
keys = keys,
|
||||
onKeyClick = onKeyClick
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onConfirmClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
enabled = confirmEnabled,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = confirmText,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 25.dp, end = 20.dp)
|
||||
.clickable(enabled = canDelete) {
|
||||
onDeleteClick()
|
||||
},
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.Backspace,
|
||||
contentDescription = "Delete",
|
||||
tint = if (canDelete) Color.LegacyRed else Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NumericKeypad(
|
||||
keys: List<List<String>>,
|
||||
onKeyClick: (String) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
keys.forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
row.forEach { key ->
|
||||
KeyPadButton(
|
||||
text = key,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { onKeyClick(key) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyPadButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val enabled = text.isNotBlank()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(66.dp)
|
||||
.shadow(
|
||||
elevation = 2.dp,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
clip = false
|
||||
)
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.clickable(enabled = enabled) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = if (enabled) Color.LegacyRed else Color.White,
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
package com.mob.utsmyanmar.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color as ComposeColor
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun NumericKeypad(
|
||||
modifier: Modifier = Modifier,
|
||||
onKeyClick: (String) -> Unit,
|
||||
keys : List<List<String>>,
|
||||
) {
|
||||
// val keys : List<List<String>> = listOf(
|
||||
// listOf("1", "2", "3"),
|
||||
// listOf("4", "5", "6"),
|
||||
// listOf("7", "8", "9"),
|
||||
// listOf(".", "0", "00")
|
||||
// )
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
keys.forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
row.forEach { key ->
|
||||
KeypadButton(
|
||||
text = key,
|
||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||
onClick = { onKeyClick(key) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeypadButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val enabled = text.isNotBlank()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false)
|
||||
else Modifier
|
||||
)
|
||||
.background(
|
||||
color = if (enabled) Color.White else ComposeColor.Transparent,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.clickable(enabled = enabled) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (enabled) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.LegacyRed,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewNumericKeypad(){
|
||||
val keys : List<List<String>> = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf(".", "0", "00")
|
||||
)
|
||||
NumericKeypad(
|
||||
keys = keys,
|
||||
onKeyClick = {}
|
||||
)
|
||||
}
|
||||
@ -10,11 +10,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.mob.utsmyanmar.ui.theme.Primary
|
||||
import com.mob.utsmyanmar.ui.theme.White
|
||||
@ -66,8 +64,7 @@ fun SquareButton(
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
package com.mob.utsmyanmar.ui.components.appbar
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.ui.theme.White
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppBar(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
actions: @Composable RowScope.() -> Unit = {}
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
color = White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
|
||||
navigationIcon = {
|
||||
if (icon != null && onIconClick != null) {
|
||||
IconButton(
|
||||
onClick = dropUnlessResumed { onIconClick() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = "App bar icon",
|
||||
tint = White
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions = actions,
|
||||
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.LegacyRed
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewAppBar(){
|
||||
AppBar(
|
||||
title = "Title"
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,525 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun AdvertisingArea(
|
||||
modifier: Modifier = Modifier,
|
||||
carouselUrls: List<String> = emptyList(),
|
||||
) {
|
||||
val imageModels: List<Any> = carouselUrls.ifEmpty {
|
||||
listOf(R.drawable.sunmi1, R.drawable.sunmi2, R.drawable.sunmi3)
|
||||
}
|
||||
val pageState = rememberPagerState(pageCount = { imageModels.size })
|
||||
|
||||
LaunchedEffect(pageState) {
|
||||
while (true) {
|
||||
delay(10000.milliseconds)
|
||||
val nextPage = (pageState.currentPage + 1) % imageModels.size
|
||||
pageState.animateScrollToPage(
|
||||
page = nextPage, animationSpec = tween(durationMillis = 700)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(0.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pageState, modifier = Modifier.fillMaxSize()
|
||||
) { page ->
|
||||
AsyncImage(
|
||||
model = imageModels[page],
|
||||
contentDescription = "ads images",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SummaryCard(
|
||||
onClickLastSync: () -> Unit,
|
||||
lastSyncTime: Long?,
|
||||
totalSaleCount: Int,
|
||||
totalSaleAmount: Long,
|
||||
settlementStatusText: String,
|
||||
settlementDateTimeText: String
|
||||
) {
|
||||
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(30_000.milliseconds)
|
||||
now = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
var isRotating by remember { mutableStateOf(false) }
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (isRotating) 360f else 0f,
|
||||
animationSpec = tween(durationMillis = 3000, easing = LinearEasing),
|
||||
label = "sync rotating",
|
||||
finishedListener = { isRotating = false }
|
||||
)
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(4.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
SummaryItem(
|
||||
title = "Total Sale",
|
||||
value = "MMK ${formatAmount(totalSaleAmount)}",
|
||||
subtitle = "$totalSaleCount Transactions",
|
||||
icon = Icons.Default.BarChart,
|
||||
iconBg = Color.CrimsonRed
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp)
|
||||
)
|
||||
|
||||
SummaryItem(
|
||||
title = "Settlement",
|
||||
value = settlementStatusText,
|
||||
subtitle = settlementDateTimeText,
|
||||
icon = Icons.Default.Check,
|
||||
iconBg = Color.CrimsonRed
|
||||
)
|
||||
}
|
||||
|
||||
VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.75f)
|
||||
.padding(start = 18.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Last Sync", style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(38.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.CrimsonRed.copy(alpha = 0.1f))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
if (!isRotating) isRotating = true
|
||||
onClickLastSync()
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Sync,
|
||||
contentDescription = null,
|
||||
tint = Color.CrimsonRed,
|
||||
modifier = Modifier
|
||||
.size(22.dp)
|
||||
.rotate(rotation)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
Text(
|
||||
text = formatRelativeTime(lastSyncTime, now),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SummaryItem(
|
||||
title: String,
|
||||
value: String,
|
||||
subtitle: String,
|
||||
icon: ImageVector,
|
||||
iconBg: androidx.compose.ui.graphics.Color
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(title, style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
value, style = MaterialTheme.typography.titleSmall, color = Color.LegacyRed
|
||||
)
|
||||
Text(subtitle, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
IconCircle(icon, iconBg)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconCircle(
|
||||
icon: ImageVector, color: androidx.compose.ui.graphics.Color
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(38.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MenuPager(
|
||||
items: List<DashboardMenuItem>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val pages = items.chunked(6)
|
||||
val pagerState = rememberPagerState(pageCount = { pages.size })
|
||||
|
||||
Column(modifier = modifier) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { pageIndex ->
|
||||
MenuPage(items = pages[pageIndex])
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(pages.size) { index ->
|
||||
val selected = pagerState.currentPage == index
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(if (selected) 8.dp else 6.dp)
|
||||
.background(
|
||||
color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuPage(items: List<DashboardMenuItem>) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
items.chunked(3).forEach { rowItems ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
rowItems.forEach { item ->
|
||||
MenuCard(
|
||||
title = item.title,
|
||||
icon = item.iconContent,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = item.onClick
|
||||
)
|
||||
}
|
||||
repeat(3 - rowItems.size) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuCard(
|
||||
title: String,
|
||||
icon: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.height(100.dp)
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(5.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(6.dp)
|
||||
) {
|
||||
Column {
|
||||
icon()
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Black,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.recentTransactionsItems(
|
||||
transactions: List<TrnxRecord>,
|
||||
onTransactionClick: (TrnxRecord) -> Unit = {}
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Recent Transactions",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = Color.Gray.copy(alpha = 0.2f)
|
||||
)
|
||||
}
|
||||
if (transactions.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "No recent transactions",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(items = transactions, key = { it.pid }) { record ->
|
||||
TrnxRow(
|
||||
record = record,
|
||||
onClick = { onTransactionClick(record) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrnxRow(
|
||||
record: TrnxRecord,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.White)
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = record.typeLabel.take(1),
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(10.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = record.typeLabel,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = Color.Black
|
||||
)
|
||||
Text(
|
||||
text = record.maskedCard,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = record.amountDisplay,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold),
|
||||
color = if (record.isVoided) Color.Gray else Color.LegacyRed
|
||||
)
|
||||
Text(
|
||||
text = record.dateTime,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Item(
|
||||
onClick: () -> Unit,
|
||||
title: String,
|
||||
subTitle: String,
|
||||
leadingIcon: (@Composable () -> Unit)? = null,
|
||||
trailingIcon: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.Black
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
leadingIcon?.let {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.LegacyRed.copy(alpha = 0.1f))
|
||||
.padding(8.dp)
|
||||
) {
|
||||
it()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Text(text = title)
|
||||
Text(
|
||||
text = subTitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
trailingIcon?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,233 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountBalanceWallet
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
import androidx.compose.material.icons.filled.SwapHoriz
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
|
||||
class DashboardMenuItem(
|
||||
val title: String,
|
||||
val iconContent: @Composable () -> Unit,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
fun buildMenuItems(
|
||||
onNavigateAmount: (String) -> Unit,
|
||||
onNavigateSignOn: () -> Unit,
|
||||
onNavigateSettlement: () -> Unit,
|
||||
onNavigateHistory: () -> Unit,
|
||||
onNavigateAction: (String) -> Unit
|
||||
): List<DashboardMenuItem> {
|
||||
val sp = SystemParamsOperation.getInstance()
|
||||
val settlementEnabled = sp.settlementStatus
|
||||
val voidEnabled = sp.voidStatus
|
||||
val refundEnabled = sp.refundStatus
|
||||
val preAuthEnabled = sp.preAuthStatus
|
||||
val preAuthVoidEnabled = sp.preAuthStatus
|
||||
val preAuthCompleteEnabled = sp.preAuthStatus
|
||||
val preAuthCompleteVoidEnabled = sp.preAuthStatus
|
||||
val cashAdvEnabled = sp.cashAdvanceStatus
|
||||
val mmqrEnabled = sp.mmqrStatus
|
||||
return buildList {
|
||||
|
||||
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Sale",
|
||||
{
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_terminal),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAmount("Sale") }
|
||||
)
|
||||
|
||||
|
||||
if (mmqrEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"MMQR",
|
||||
{
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_mmqr_logo),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(48.dp)
|
||||
)
|
||||
}
|
||||
) { }
|
||||
)
|
||||
}
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"History",
|
||||
{
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_history),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateHistory() }
|
||||
)
|
||||
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Sign On",
|
||||
{
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_sign_on),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateSignOn() }
|
||||
)
|
||||
|
||||
if (settlementEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Settlement",
|
||||
{
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_settlement),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateSettlement() }
|
||||
)
|
||||
}
|
||||
|
||||
if (voidEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Void",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAction("Void") }
|
||||
)
|
||||
}
|
||||
|
||||
if (refundEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Refund",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Replay,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAction("Refund") }
|
||||
)
|
||||
}
|
||||
|
||||
if (preAuthEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Pre-Auth",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAmount("Pre-Auth") }
|
||||
)
|
||||
}
|
||||
|
||||
if (preAuthVoidEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Pre-Auth Void",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.LockOpen,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAction("Pre-Auth Void") }
|
||||
)
|
||||
}
|
||||
|
||||
if (preAuthCompleteEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Pre-Auth Complete",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.CreditCard,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAction("Pre-Auth Complete") }
|
||||
)
|
||||
}
|
||||
|
||||
if (preAuthCompleteVoidEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Pre-Auth Comp Void",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.SwapHoriz,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAction("Pre-Auth Complete Void") }
|
||||
)
|
||||
}
|
||||
|
||||
if (cashAdvEnabled) {
|
||||
add(
|
||||
DashboardMenuItem(
|
||||
"Cash Adv",
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.AccountBalanceWallet,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
) { onNavigateAmount("Cash Adv") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,252 +1,482 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidmads.library.qrgenearator.QRGContents
|
||||
import androidmads.library.qrgenearator.QRGEncoder
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.components.SquareButton
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.mob.utsmyanmar.ui.theme.*
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.mob.utsmyanmar.BuildConfig
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.device_info.DeviceInfoUiState
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.sign_on.EchoTestProcess
|
||||
import com.utsmyanmar.paylibs.sign_on.SignOnListener
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
settlementEnabled: Boolean,
|
||||
wavePayEnabled: Boolean,
|
||||
onAmountClick: (String) -> Unit,
|
||||
onTransactionClick: () -> Unit,
|
||||
onSettlementClick: () -> Unit,
|
||||
onHistoryClick: () -> Unit,
|
||||
onCardClick: () -> Unit,
|
||||
onWavePayClick: () -> Unit
|
||||
fun DashboardScreen2(
|
||||
onNavigateAmount: (String) -> Unit = {},
|
||||
onNavigateSignOn: () -> Unit = {},
|
||||
onNavigateSettlement: () -> Unit = {},
|
||||
onNavigateVersion: () -> Unit = {},
|
||||
onNavigateFunctions: () -> Unit = {},
|
||||
onNavigateHistory: () -> Unit = {},
|
||||
onNavigateAction: (String) -> Unit = {},
|
||||
onNavigateNotifications: () -> Unit = {},
|
||||
dashboardUiState: DashboardUiState = DashboardUiState(),
|
||||
deviceInfo: DeviceInfoUiState = DeviceInfoUiState(),
|
||||
onClickLastSync: () -> Unit = {},
|
||||
onNavigateRecentTransaction: (TrnxRecord) -> Unit = {},
|
||||
) {
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
val mainHandler = remember { Handler(Looper.getMainLooper()) }
|
||||
var showHostActionDialog by remember { mutableStateOf(false) }
|
||||
var activeHostAction by remember { mutableStateOf("Log-On") }
|
||||
var isHostActionRunning by remember { mutableStateOf(false) }
|
||||
var dialogMessage by remember { mutableStateOf("") }
|
||||
var reversalEnabled by remember {
|
||||
mutableStateOf(
|
||||
runCatching { SystemParamsOperation.getInstance().isReversalOn }.getOrDefault(false)
|
||||
)
|
||||
}
|
||||
val isOnline by rememberIsOnline()
|
||||
|
||||
fun confirmationMessage(action: String) = "Do you want to start ${action.lowercase()}?"
|
||||
fun processingMessage(action: String) = "Sending ${action.lowercase()} request to host..."
|
||||
fun successMessage(action: String) = "$action success."
|
||||
fun failureMessage(action: String, resultCode: Int?) =
|
||||
"$action failed. Response code: ${resultCode ?: -1}"
|
||||
|
||||
fun networkFailureMessage(action: String) = "Network error during $action."
|
||||
fun openHostActionDialog(action: String) {
|
||||
activeHostAction = action
|
||||
isHostActionRunning = false
|
||||
dialogMessage = confirmationMessage(action)
|
||||
showHostActionDialog = true
|
||||
}
|
||||
|
||||
if (showHostActionDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
if (!isHostActionRunning) showHostActionDialog = false
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = activeHostAction,
|
||||
color = Color.LegacyRed,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = { Text(text = dialogMessage) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (isHostActionRunning) return@TextButton
|
||||
if (dialogMessage != confirmationMessage(activeHostAction)) {
|
||||
showHostActionDialog = false
|
||||
dialogMessage = confirmationMessage(activeHostAction)
|
||||
return@TextButton
|
||||
}
|
||||
isHostActionRunning = true
|
||||
dialogMessage = processingMessage(activeHostAction)
|
||||
val signOnProcess = EchoTestProcess.getInstance()
|
||||
val request = when (activeHostAction) {
|
||||
"Echo Test" -> signOnProcess.enqueue(false)
|
||||
"Sign-Off" -> signOnProcess.enqueueLogOff()
|
||||
else -> signOnProcess.enqueueLogOn()
|
||||
}
|
||||
request.startSignOn(object : SignOnListener {
|
||||
override fun onSuccessSignOn() {
|
||||
mainHandler.post {
|
||||
isHostActionRunning = false
|
||||
dialogMessage = successMessage(activeHostAction)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailureSignOn(resultCode: Int?) {
|
||||
mainHandler.post {
|
||||
isHostActionRunning = false
|
||||
dialogMessage = failureMessage(activeHostAction, resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNetworkFailSignOn(message: String?) {
|
||||
mainHandler.post {
|
||||
isHostActionRunning = false
|
||||
dialogMessage = message?.takeIf { it.isNotBlank() }
|
||||
?: networkFailureMessage(activeHostAction)
|
||||
}
|
||||
}
|
||||
})
|
||||
}) {
|
||||
Text(
|
||||
text = when {
|
||||
isHostActionRunning -> "Processing"
|
||||
dialogMessage == confirmationMessage(activeHostAction) -> "Start"
|
||||
else -> "Close"
|
||||
},
|
||||
color = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
if (!isHostActionRunning) {
|
||||
TextButton(onClick = {
|
||||
showHostActionDialog = false
|
||||
dialogMessage = confirmationMessage(activeHostAction)
|
||||
}) {
|
||||
Text(text = "Cancel", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
if (dashboardUiState.isSyncing) {
|
||||
Dialog(onDismissRequest = {}) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CircularProgressIndicator(color = Color.LegacyRed)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = dashboardUiState.syncStatusText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Black
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
modifier = Modifier.fillMaxWidth(0.8f),
|
||||
drawerContainerColor = White,
|
||||
drawerShape = RoundedCornerShape(topEnd = 20.dp, bottomEnd = 20.dp)
|
||||
modifier = Modifier.fillMaxWidth(0.78f),
|
||||
drawerContainerColor = Color.White,
|
||||
drawerShape = RoundedCornerShape(topEnd = 24.dp, bottomEnd = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "MOBPOS",
|
||||
modifier = Modifier.padding(start = 24.dp, top = 28.dp, bottom = 8.dp),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = "Dashboard",
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch { drawerState.close() }
|
||||
.background(Color.IvoryBeige)
|
||||
.padding(horizontal = 20.dp, vertical = 28.dp)
|
||||
) {
|
||||
Row {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.logo_mob),
|
||||
contentDescription = "mob logo",
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.padding(16.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
Column {
|
||||
Text(
|
||||
text = "MOB Merchant",
|
||||
color = Color.CrimsonRed,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
text = "Transactions",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch { drawerState.close() }
|
||||
onTransactionClick()
|
||||
}
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
fontSize = 16.sp
|
||||
text = "S/N:${deviceInfo.serialNumber}",
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
text = "History",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch { drawerState.close() }
|
||||
onHistoryClick()
|
||||
}
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
fontSize = 16.sp
|
||||
text = if (isOnline) "Online" else "Offline",
|
||||
color = if (isOnline) Color.Success else Color.Error,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
text = "Settlement",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = settlementEnabled) {
|
||||
scope.launch { drawerState.close() }
|
||||
onSettlementClick()
|
||||
}
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
color = if (settlementEnabled) Black else White,
|
||||
fontSize = 16.sp
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = "App Version : ${BuildConfig.VERSION_NAME}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = "WavePay",
|
||||
text = "Released on : 06-Sep-2026",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = "Connection Settings",
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Item(
|
||||
title = "Sign-Off", subTitle = "Sign off from System",
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
openHostActionDialog("Sign-Off")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_cancel_circle),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {},
|
||||
)
|
||||
Item(
|
||||
title = "Reversal", subTitle = "Enable / Disable Reversal",
|
||||
onClick = {
|
||||
reversalEnabled = !reversalEnabled
|
||||
runCatching {
|
||||
SystemParamsOperation.getInstance().setReversalFlag(reversalEnabled)
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Sync,
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Switch(
|
||||
checked = reversalEnabled,
|
||||
onCheckedChange = { isChecked ->
|
||||
reversalEnabled = isChecked
|
||||
runCatching {
|
||||
SystemParamsOperation.getInstance().setReversalFlag(isChecked)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "System Management",
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Item(
|
||||
title = "Functions", subTitle = "System Function Settings",
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
onNavigateFunctions()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_four_boxes),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
},
|
||||
trailingIcon = {},
|
||||
)
|
||||
Item(
|
||||
title = "Version", subTitle = "View App Version Info",
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
onNavigateVersion()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_version),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
val adUrl = "https://www.mobmyanmar.com/"
|
||||
val qrBitmap = remember {
|
||||
runCatching {
|
||||
val encoder = QRGEncoder(adUrl, null, QRGContents.Type.TEXT, 256)
|
||||
encoder.colorBlack = android.graphics.Color.BLACK
|
||||
encoder.colorWhite = android.graphics.Color.WHITE
|
||||
encoder.bitmap.asImageBitmap()
|
||||
}.getOrNull()
|
||||
}
|
||||
qrBitmap?.let { bitmap ->
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = wavePayEnabled) {
|
||||
scope.launch { drawerState.close() }
|
||||
onWavePayClick()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Image(
|
||||
bitmap = bitmap,
|
||||
contentDescription = "Scan for User Guide",
|
||||
modifier = Modifier.size(150.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Scan for Offers",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
color = if (wavePayEnabled) Black else White,
|
||||
fontSize = 16.sp
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "v${BuildConfig.VERSION_NAME}",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Black.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(text = "Dashboard", color = White, fontWeight = FontWeight.SemiBold)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
drawerState.open()
|
||||
}
|
||||
}
|
||||
) {
|
||||
AppBar(
|
||||
title = "Dashboard",
|
||||
icon = Icons.Default.Menu,
|
||||
onIconClick = { scope.launch { drawerState.open() } },
|
||||
actions = {
|
||||
IconButton(onClick = onNavigateNotifications) {
|
||||
Box {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_menu),
|
||||
contentDescription = "Menu Icon",
|
||||
tint = White
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = "Notifications",
|
||||
tint = Color.White
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(Color.GoldenGlow, CircleShape)
|
||||
.align(Alignment.TopEnd)
|
||||
.offset(x = 2.dp, y = (-2).dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
androidx.compose.foundation.lazy.LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.background(White)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
item {
|
||||
AdvertisingArea(
|
||||
carouselUrls = dashboardUiState.carouselUrls,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.logo_mob),
|
||||
contentDescription = "Mob logo"
|
||||
.height(200.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = Primary,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
item {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SummaryCard(
|
||||
onClickLastSync = onClickLastSync,
|
||||
lastSyncTime = dashboardUiState.lastSyncTime,
|
||||
totalSaleCount = dashboardUiState.totalSaleCount,
|
||||
totalSaleAmount = dashboardUiState.totalSaleAmount,
|
||||
settlementStatusText = dashboardUiState.settlementStatusText,
|
||||
settlementDateTimeText = dashboardUiState.settlementDateTimeText
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SquareButton(
|
||||
title = "Sale",
|
||||
icon = R.drawable.ic_sale,
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = White,
|
||||
contentColor = Primary,
|
||||
iconTint = Primary,
|
||||
border = null,
|
||||
onClick = { onAmountClick("Sale") },
|
||||
)
|
||||
SquareButton(
|
||||
title = "Sign On",
|
||||
icon = R.drawable.ic_menu,
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = White,
|
||||
contentColor = Primary,
|
||||
iconTint = Primary,
|
||||
border = null,
|
||||
onClick = { onAmountClick("Sign On") }
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
item {
|
||||
MenuPager(
|
||||
items = buildMenuItems(
|
||||
onNavigateAmount = onNavigateAmount,
|
||||
onNavigateSignOn = onNavigateSignOn,
|
||||
onNavigateSettlement = onNavigateSettlement,
|
||||
onNavigateHistory = onNavigateHistory,
|
||||
onNavigateAction = onNavigateAction
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SquareButton(
|
||||
title = "Settlement",
|
||||
icon = R.drawable.ic_menu,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = settlementEnabled,
|
||||
containerColor = White,
|
||||
contentColor = Primary,
|
||||
iconTint = Primary,
|
||||
border = null,
|
||||
onClick = onSettlementClick
|
||||
)
|
||||
SquareButton(
|
||||
title = "Others",
|
||||
icon = R.drawable.ic_menu,
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = White,
|
||||
contentColor = Primary,
|
||||
iconTint = Primary,
|
||||
border = null,
|
||||
onClick = onCardClick
|
||||
item { Spacer(Modifier.height(8.dp)) }
|
||||
recentTransactionsItems(
|
||||
transactions = dashboardUiState.recentTransactions,
|
||||
onTransactionClick = onNavigateRecentTransaction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
@Composable
|
||||
private fun DashboardScreenPreview() {
|
||||
MOBPOSTheme {
|
||||
DashboardScreen(
|
||||
settlementEnabled = true,
|
||||
wavePayEnabled = true,
|
||||
onAmountClick = {},
|
||||
onTransactionClick = {},
|
||||
onSettlementClick = {},
|
||||
onHistoryClick = {},
|
||||
onCardClick = {},
|
||||
onWavePayClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
data class TrnxRecord(
|
||||
val pid: Long,
|
||||
val typeLabel: String,
|
||||
val amountDisplay: String,
|
||||
val maskedCard: String,
|
||||
val dateTime: String,
|
||||
val isVoided: Boolean,
|
||||
val isApproved: Boolean
|
||||
)
|
||||
|
||||
data class DashboardUiState(
|
||||
val recentTransactions: List<TrnxRecord> = emptyList(),
|
||||
val lastSyncTime: Long? = null,
|
||||
val carouselUrls: List<String> = emptyList(),
|
||||
val totalSaleCount: Int = 0,
|
||||
val totalSaleAmount: Long = 0L,
|
||||
val settlementStatusText: String = "Pending",
|
||||
val settlementDateTimeText: String = "--/-- --:--",
|
||||
val isSyncing: Boolean = false,
|
||||
val syncStatusText: String = "Connecting to TMS server..."
|
||||
)
|
||||
@ -0,0 +1,42 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
fun formatRelativeTime(timestamp: Long?, now: Long): String {
|
||||
if (timestamp == null) return "Never"
|
||||
val diff = now - timestamp
|
||||
return when {
|
||||
diff < 60_000L -> "Just now"
|
||||
diff < 3_600_000L -> "${diff / 60_000L} mins ago"
|
||||
diff < 86_400_000L -> "${diff / 3_600_000L} hours ago"
|
||||
else -> "${diff / 86_400_000L} days ago"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberIsOnline(): State<Boolean> = run {
|
||||
val context = LocalContext.current
|
||||
val cm = remember(context) {
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
produceState(
|
||||
initialValue = cm.activeNetwork
|
||||
?.let { cm.getNetworkCapabilities(it)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) }
|
||||
?: false
|
||||
) {
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) { value = true }
|
||||
override fun onLost(network: Network) { value = false }
|
||||
}
|
||||
cm.registerDefaultNetworkCallback(callback)
|
||||
awaitDispose { cm.unregisterNetworkCallback(callback) }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.utsmyanmar.baselib.repo.Repository
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.system.SystemDateTime
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.util.Calendar
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
private val repository: Repository,
|
||||
@ApplicationContext private val context: Context
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(DashboardUiState())
|
||||
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
repository.getLastTenTransactions().asFlow().collect { payDetails ->
|
||||
val latestSettlement = payDetails.lastOrNull {
|
||||
it.transactionType == TransactionsType.SETTLEMENT.value
|
||||
}
|
||||
val isCurrentBusinessSettlement = latestSettlement?.isCurrentBusinessSettlement() == true
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
recentTransactions = payDetails
|
||||
.filterNot { it.isSettlementTransaction() }
|
||||
.map { pd -> pd.toRecord() },
|
||||
totalSaleCount = payDetails.totalSaleCount(),
|
||||
totalSaleAmount = payDetails.totalSaleAmount(),
|
||||
settlementStatusText = if (isCurrentBusinessSettlement) {
|
||||
"Completed"
|
||||
} else {
|
||||
"Pending"
|
||||
},
|
||||
settlementDateTimeText = latestSettlement
|
||||
?.let { buildDateTime(it.TradeDate, it.TradeTime) }
|
||||
?: "--/-- --:--"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshLastSyncTime()
|
||||
loadCarouselUrls()
|
||||
}
|
||||
|
||||
fun loadCarouselUrls() {
|
||||
val raw = runCatching {
|
||||
SystemParamsOperation.getInstance().carouselUrls
|
||||
}.getOrDefault("")
|
||||
val urls = raw.split(",").filter { it.isNotBlank() }
|
||||
_uiState.update { it.copy(carouselUrls = urls) }
|
||||
}
|
||||
|
||||
fun refreshLastSyncTime() {
|
||||
val time = context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE)
|
||||
.getLong("last_sync_time", 0L)
|
||||
.takeIf { it > 0L }
|
||||
_uiState.update { it.copy(lastSyncTime = time) }
|
||||
}
|
||||
|
||||
fun startSync() {
|
||||
_uiState.update { it.copy(isSyncing = true, syncStatusText = "Connecting to TMS server...") }
|
||||
}
|
||||
|
||||
fun finishSync(message: String) {
|
||||
_uiState.update { it.copy(syncStatusText = message) }
|
||||
viewModelScope.launch {
|
||||
kotlinx.coroutines.delay(1500)
|
||||
_uiState.update { it.copy(isSyncing = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PayDetail.toRecord(): TrnxRecord {
|
||||
val baseType = transType?.takeIf { it.isNotBlank() }?.let { formatTypeLabel(it) } ?: "Transaction"
|
||||
val type = when {
|
||||
transactionType == TransactionsType.REVERSAL.value && !baseType.equals("Reversal", ignoreCase = true) ->
|
||||
"$baseType (Reversal)"
|
||||
isCanceled -> "Void"
|
||||
else -> baseType
|
||||
}
|
||||
val displayAmount = displayAmount()
|
||||
val amountText = "MMK ${formatAmount(displayAmount)}"
|
||||
val card = CardNo?.takeIf { it.length >= 4 }?.let { "**** ${it.takeLast(4)}" } ?: "----"
|
||||
val dt = buildDateTime(TradeDate, TradeTime)
|
||||
val approved = tradeAnswerCode == "00" || approvalCode?.isNotBlank() == true
|
||||
|
||||
return TrnxRecord(
|
||||
pid = PID ?: 0L,
|
||||
typeLabel = type,
|
||||
amountDisplay = amountText,
|
||||
maskedCard = card,
|
||||
dateTime = dt,
|
||||
isVoided = isCanceled,
|
||||
isApproved = approved
|
||||
)
|
||||
}
|
||||
|
||||
fun formatAmount(amount: Long): String {
|
||||
val scaled = BigDecimal(amount).movePointLeft(2)
|
||||
return DecimalFormat("#,##0.00").format(scaled)
|
||||
}
|
||||
|
||||
private fun PayDetail.displayAmount(): Long {
|
||||
if (amount != 0L) return amount
|
||||
if (transactionType != TransactionsType.SETTLEMENT.value) return 0L
|
||||
|
||||
settleDataObj?.let { data ->
|
||||
return data.saleAmount +
|
||||
data.preAuthCompAmount +
|
||||
data.cashAdvanceAmount -
|
||||
data.refundAmount
|
||||
}
|
||||
|
||||
val parts = settleList.orEmpty().split("-")
|
||||
if (parts.size >= 4) {
|
||||
fun partAmount(raw: String): Long = raw.substringAfter(":", "0").toLongOrNull() ?: 0L
|
||||
val sale = partAmount(parts[0])
|
||||
val cashAdvance = partAmount(parts[1])
|
||||
val refund = partAmount(parts[2])
|
||||
val preAuth = partAmount(parts[3])
|
||||
return sale + cashAdvance + preAuth - refund
|
||||
}
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
private fun List<PayDetail>.totalSaleCount(): Int = count { it.isSaleTransaction() }
|
||||
|
||||
private fun List<PayDetail>.totalSaleAmount(): Long = sumOf {
|
||||
if (it.isSaleTransaction()) it.amount else 0L
|
||||
}
|
||||
|
||||
private fun PayDetail.isSaleTransaction(): Boolean {
|
||||
return transactionType == TransactionsType.SALE.value ||
|
||||
(transactionType == TransactionsType.WAVEPAY.value && qrTransStatus == 1)
|
||||
}
|
||||
|
||||
private fun PayDetail.isSettlementTransaction(): Boolean {
|
||||
return transactionType == TransactionsType.SETTLEMENT.value ||
|
||||
transType?.contains("SETTLEMENT", ignoreCase = true) == true
|
||||
}
|
||||
|
||||
private fun formatTypeLabel(raw: String): String = when {
|
||||
raw.equals("CASH_OUT", ignoreCase = true) -> "CASH_ADV"
|
||||
raw.contains("SALE", ignoreCase = true) -> "Sale"
|
||||
raw.contains("VOID", ignoreCase = true) -> "Void"
|
||||
raw.contains("REFUND", ignoreCase = true) -> "Refund"
|
||||
raw.contains("SETTLEMENT", ignoreCase = true) -> "Settlement"
|
||||
raw.contains("PRE", ignoreCase = true) -> "Pre-Auth"
|
||||
raw.contains("CASH", ignoreCase = true) -> "Cash Adv"
|
||||
raw.contains("WAVE", ignoreCase = true) -> "Wave Pay"
|
||||
else -> raw.lowercase().replaceFirstChar { it.uppercaseChar() }
|
||||
}
|
||||
|
||||
// TradeDate = "MMDD", TradeTime = "HHmmss" (ISO 8583 fields 13 & 12)
|
||||
private fun buildDateTime(date: String?, time: String?): String {
|
||||
val d = date?.padStart(4, '0')?.takeIf { it.length >= 4 } ?: return "--/-- --:--"
|
||||
val t = (time ?: "").padStart(6, '0')
|
||||
return "${d.take(2)}/${d.drop(2).take(2)} ${t.take(2)}:${t.drop(2).take(2)}"
|
||||
}
|
||||
|
||||
private fun PayDetail.isCurrentBusinessSettlement(): Boolean {
|
||||
if (transactionType != TransactionsType.SETTLEMENT.value) return false
|
||||
if (TradeDate != SystemDateTime.getMMDD()) return false
|
||||
|
||||
val now = Calendar.getInstance()
|
||||
val cutoff = Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 23)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
return now.before(cutoff)
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
package com.mob.utsmyanmar.ui.dashboard
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AccountBalanceWallet
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
import androidx.compose.material.icons.filled.SwapHoriz
|
||||
import androidx.compose.material.icons.filled.Undo
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun SeeMoreScreen(
|
||||
onBack: () -> Unit = {},
|
||||
onNavigateAmount: (String) -> Unit = {}
|
||||
) {
|
||||
val items = listOf(
|
||||
SeeMoreItem("Void", Icons.Default.Undo),
|
||||
SeeMoreItem("Refund", Icons.Default.Replay),
|
||||
SeeMoreItem("Pre-Auth", Icons.Default.Lock),
|
||||
SeeMoreItem("Pre-Auth Void", Icons.Default.LockOpen),
|
||||
SeeMoreItem("Pre-Auth Complete", Icons.Default.CreditCard),
|
||||
SeeMoreItem("Pre-Auth Complete Void", Icons.Default.SwapHoriz),
|
||||
SeeMoreItem("Cash Adv", Icons.Default.AccountBalanceWallet)
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "See More",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(items) { item ->
|
||||
MoreMenuCard(
|
||||
title = item.title,
|
||||
icon = item.icon,
|
||||
onClick = { onNavigateAmount(item.title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoreMenuCard(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(132.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(4.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(14.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(Color.CrimsonRed, RoundedCornerShape(14.dp))
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class SeeMoreItem(
|
||||
val title: String,
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
@P2Preview
|
||||
@Composable
|
||||
private fun PreviewSeeMoreScreen() {
|
||||
SeeMoreScreen()
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.mob.utsmyanmar.ui.device_info
|
||||
|
||||
data class DeviceInfoUiState(
|
||||
val hardwareVersion: String = "",
|
||||
val firmwareVersion: String = "",
|
||||
val serialNumber: String = "",
|
||||
val deviceModel: String = "",
|
||||
val finalVersion: String = "",
|
||||
val payHardwareVersion: String = "",
|
||||
val romVersion: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
@ -0,0 +1,79 @@
|
||||
package com.mob.utsmyanmar.ui.device_info
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.sunmi.pay.hardware.aidl.AidlConstants
|
||||
import com.utsmyanmar.baselib.BaseApplication
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
class DeviceInfoViewModel : ViewModel() {
|
||||
var TAG = DeviceInfoViewModel::class.simpleName;
|
||||
private val _uiState = MutableStateFlow(DeviceInfoUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun loadDeviceInfo() {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
|
||||
try {
|
||||
val hardwareVersion = getParams(AidlConstants.SysParam.HARDWARE_VERSION)
|
||||
val firmwareVersion = getParams(AidlConstants.SysParam.FIRMWARE_VERSION)
|
||||
val serialNo = getParams(AidlConstants.SysParam.SN)
|
||||
val deviceModel = getParams(AidlConstants.SysParam.DEVICE_MODEL)
|
||||
val finalVersion = SystemParamsOperation.getInstance().finalVersion ?: ""
|
||||
val payHardwareVersion = getPayHardwareVersion()
|
||||
val romVersion = getRomVersion()
|
||||
|
||||
|
||||
_uiState.value = DeviceInfoUiState(
|
||||
hardwareVersion = hardwareVersion,
|
||||
firmwareVersion = firmwareVersion,
|
||||
serialNumber = serialNo,
|
||||
deviceModel = deviceModel,
|
||||
finalVersion = finalVersion,
|
||||
payHardwareVersion = payHardwareVersion,
|
||||
romVersion = romVersion,
|
||||
isLoading = false
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = e.message ?: "Failed to load device info"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getParams(name: String): String {
|
||||
return try {
|
||||
BaseApplication.getInstance()
|
||||
.basicOptBinder
|
||||
?.getSysParam(name)
|
||||
?: ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPayHardwareVersion():String {
|
||||
return try {
|
||||
BaseApplication.getInstance().applicationContext.packageManager.getPackageInfo(
|
||||
"com.sunmi.pay.hardware_v3",
|
||||
0
|
||||
).versionName ?: "PHV?"
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
"PHV?"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRomVersion():String {
|
||||
return try {
|
||||
android.os.Build.VERSION.RELEASE ?: "UNKNOWN"
|
||||
}catch (e: Exception){
|
||||
LogUtil.d(TAG, "get rom version error " + e)
|
||||
"UNKNOWN"
|
||||
}
|
||||
}
|
||||
}
|
||||
246
app/src/main/java/com/mob/utsmyanmar/ui/disable/DisableScreen.kt
Normal file
246
app/src/main/java/com/mob/utsmyanmar/ui/disable/DisableScreen.kt
Normal file
@ -0,0 +1,246 @@
|
||||
package com.mob.utsmyanmar.ui.disable
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DisableScreen(
|
||||
message: String,
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Terminal Disabled",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.LegacyRed
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(Modifier.height(40.dp))
|
||||
|
||||
// Terminal icon with X badge
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(160.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.LegacyRed.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_terminal),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(90.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(52.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.offset(x = (-8).dp, y = (-8).dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.CrimsonRed),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_cancel_circle),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "This terminal is disabled",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.LegacyRed,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "You won't be able to process any\ntransactions until the terminal is enabled.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
// Info cards
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
// Why card
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(0.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.LegacyRed.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_protection_shield),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Why is this terminal disabled?",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = message.ifBlank {
|
||||
"The terminal may be disabled by an administrator due to security, compliance or configuration changes."
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Help card
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color.GoldenGlow.copy(alpha = 0.35f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(0.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.GoldenGlow.copy(alpha = 0.5f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_alert_triangle),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Need help?",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Contact your system administrator or support team.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = onRetry,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(52.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.LegacyRed)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Retry",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
package com.mob.utsmyanmar.ui.disable
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
package com.mob.utsmyanmar.ui.disable
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
package com.mob.utsmyanmar.ui.error
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val AUTO_DISMISS_DELAY_MS = 3000L
|
||||
|
||||
@Composable
|
||||
fun TransactionErrorScreen(
|
||||
isSuccess: Boolean,
|
||||
message: String,
|
||||
onDone: () -> Unit
|
||||
) {
|
||||
BackHandler(enabled = true) {
|
||||
onDone()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(AUTO_DISMISS_DELAY_MS)
|
||||
onDone()
|
||||
}
|
||||
|
||||
TransactionErrorContent(isSuccess= isSuccess, message = message)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransactionErrorContent(isSuccess: Boolean,message: String) {
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.IvoryBeige)
|
||||
.padding(paddingValues)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(72.dp))
|
||||
|
||||
FailIcon(isSuccess)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Transaction Failed",
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = message.ifBlank { "Something went wrong. Please try again." },
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FailIcon(isSuccess: Boolean) {
|
||||
Box(
|
||||
modifier = Modifier.size(132.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(132.dp)
|
||||
.background(
|
||||
color = if (isSuccess) Color.Success.copy(alpha = 0.12f) else Color.Error.copy(alpha = 0.12f),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.background(
|
||||
color = if (isSuccess) Color.Success else Color.Error,
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if(isSuccess) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = "Success",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = "Failed",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@Composable
|
||||
private fun PreviewTransactionErrorScreen() {
|
||||
TransactionErrorContent(isSuccess = true, message = "Reversal Success")
|
||||
}
|
||||
@ -0,0 +1,236 @@
|
||||
package com.mob.utsmyanmar.ui.functions
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
|
||||
@Composable
|
||||
fun FunctionsScreen(
|
||||
onBack: () -> Unit = {}
|
||||
) {
|
||||
val tmsAddress = SystemParamsOperation.getInstance().tmsAddress
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Settings",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp).verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(text = "General Settings", style = MaterialTheme.typography.titleMedium)
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "App Version",
|
||||
subTitle = "1.0-uat",
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_device_info),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "Host Config",
|
||||
subTitle = "Detail for bound hosts",
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_database_config),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Text(text = "System Configuration", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "Clear Batch",
|
||||
subTitle = "Detail for bound hosts",
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "Clear Reversal",
|
||||
subTitle = "Detail for bound hosts",
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_clear),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "TMS Server Url",
|
||||
subTitle = tmsAddress,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_address_global),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "Download Config",
|
||||
subTitle = "Download terminal config from host",
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(R.drawable.ic_circle_download_arrow),
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = "icon",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FunctionButton(
|
||||
onClick: () -> Unit,
|
||||
title: String,
|
||||
subTitle: String,
|
||||
leadingIcon: (@Composable () -> Unit)? = null,
|
||||
trailingIcon: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
ElevatedButton(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.Black
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
leadingIcon?.invoke()
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)) {
|
||||
Text(text = title)
|
||||
Text(
|
||||
text = subTitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
trailingIcon?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@P3Preview
|
||||
@P2Preview
|
||||
@Composable
|
||||
fun PreviewFunctionsScreen() {
|
||||
FunctionsScreen()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewFunctionButton() {
|
||||
FunctionButton(
|
||||
onClick = {},
|
||||
title = "title",
|
||||
subTitle = "sub-title"
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
|
||||
@Composable
|
||||
fun BatchTotalScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val batchNo = SystemParamsOperation.getInstance().currentBatchNum
|
||||
val records by viewModel.getReviewBatchTransaction(batchNo).observeAsState(emptyList())
|
||||
val totalAmount = records.sumOf { detailReportSignedAmount(it) }
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Batch Total",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
SummaryLine("Batch No", batchNo)
|
||||
SummaryLine("Record Count", records.size.toString())
|
||||
SummaryLine("Total Amount", POSUtil.getInstance().formatAmount(totalAmount))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (records.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("No batch records found.", color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(records, key = { "${it.voucherNo}_${it.PID}" }) { record ->
|
||||
HistoryTransactionCard(record, onClick = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.print.PrintHelper
|
||||
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
|
||||
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import com.utsmyanmar.paylibs.utils.params.Params
|
||||
|
||||
@Composable
|
||||
fun DetailReportScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
hostType: HostType,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val allTransactions by viewModel.getTransactionHistory().observeAsState(emptyList())
|
||||
var isPrinting by remember { mutableStateOf(false) }
|
||||
|
||||
val records = remember(allTransactions, hostType) {
|
||||
filterDetailReportTransactions(allTransactions, hostType)
|
||||
}
|
||||
val totalAmount = remember(records) {
|
||||
records.sumOf(::detailReportSignedAmount)
|
||||
}
|
||||
|
||||
fun printDetailReport() {
|
||||
if (records.isEmpty()) {
|
||||
Toast.makeText(context, "No transactions to print.", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (PrintHelper.getInstance().paperRollStatus() != 1) {
|
||||
Toast.makeText(context, "Paper roll not ready.", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val tradeData = Params.newTrade(false)
|
||||
val payDetail = tradeData.payDetail
|
||||
payDetail.transType = TransactionsType.DETAIL_REPORT.name
|
||||
payDetail.transactionType = TransactionsType.DETAIL_REPORT.value
|
||||
|
||||
isPrinting = true
|
||||
PrintXReceipt.getInstance().printDetailReport(
|
||||
payDetail,
|
||||
ArrayList(records),
|
||||
hostType,
|
||||
object : PrintXStatus {
|
||||
override fun onSuccess() {
|
||||
isPrinting = false
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
isPrinting = false
|
||||
Toast.makeText(context, "Print failed.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Print Detail Report",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
SummaryLine("Host", hostTypeLabel(hostType))
|
||||
SummaryLine("Card Type", hostTypeLabel(hostType))
|
||||
SummaryLine("Transaction Count", records.size.toString())
|
||||
SummaryLine("Total Amount", POSUtil.getInstance().formatAmount(totalAmount))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
if (records.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("No transactions available.", color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(records, key = { "${it.voucherNo}_${it.PID}" }) { record ->
|
||||
HistoryTransactionCard(record, onClick = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Button(
|
||||
onClick = ::printDetailReport,
|
||||
enabled = records.isNotEmpty() && !isPrinting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
if (isPrinting) {
|
||||
CircularProgressIndicator(color = Color.White)
|
||||
} else {
|
||||
Text("Print")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,186 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Backspace
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.utils.convertDateString
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
|
||||
@Composable
|
||||
internal fun HistoryTransactionCard(
|
||||
payDetail: PayDetail,
|
||||
onClick: (() -> Unit)?,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = displayTransactionTitle(payDetail),
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = displayAmountWithSign(payDetail),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text("Trace ${payDetail.voucherNo.ifBlank { "-" }}", style = MaterialTheme.typography.bodySmall)
|
||||
Text("RRN ${payDetail.referNo.ifBlank { "-" }}", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
||||
Text(
|
||||
"${convertDateString(payDetail.transDate)} ${payDetail.transTime}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun HistoryTransactionDetailCard(payDetail: PayDetail) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = displayTransactionTitle(payDetail),
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
SummaryLine("Date", payDetail.tradeDate)
|
||||
SummaryLine("Time", payDetail.tradeTime)
|
||||
SummaryLine("Amount", displayAmountWithSign(payDetail))
|
||||
SummaryLine("Trace No", payDetail.voucherNo)
|
||||
// SummaryLine("Card No", payDetail.cardNo)
|
||||
SummaryLine("Reference No", payDetail.referNo)
|
||||
SummaryLine("Approval Code", payDetail.approvalCode.orEmpty())
|
||||
SummaryLine("Status", displayTransactionStatus(payDetail))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SummaryLine(label: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(label, color = Color.Gray, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
value.ifBlank { "-" },
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SummaryPickerRow(
|
||||
label: String,
|
||||
value: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.titleSmall)
|
||||
Text(value, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun HistoryNumericKeypad(
|
||||
onDigit: (String) -> Unit,
|
||||
onBackspace: () -> Unit,
|
||||
onClear: () -> Unit,
|
||||
) {
|
||||
val rows = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("C", "0", "⌫")
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
rows.forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
row.forEach { key ->
|
||||
Button(
|
||||
onClick = {
|
||||
when (key) {
|
||||
"C" -> onClear()
|
||||
"⌫" -> onBackspace()
|
||||
else -> onDigit(key)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.Black
|
||||
)
|
||||
) {
|
||||
if (key == "⌫") {
|
||||
Icon(Icons.Rounded.Backspace, contentDescription = "Backspace")
|
||||
} else {
|
||||
Text(key, style = MaterialTheme.typography.headlineMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
enum class HistoryHostPurpose {
|
||||
REPRINT_ANY,
|
||||
LAST_SETTLEMENT,
|
||||
DETAIL_REPORT,
|
||||
SUMMARY_REPORT,
|
||||
}
|
||||
|
||||
internal fun prepareReprintPayDetail(payDetail: PayDetail): PayDetail {
|
||||
val suffix = "(REPRINT)"
|
||||
payDetail.transType = if (payDetail.transType.length >= 20) {
|
||||
"${payDetail.transType}\n$suffix"
|
||||
} else {
|
||||
payDetail.transType + suffix
|
||||
}
|
||||
return payDetail
|
||||
}
|
||||
|
||||
internal fun displayTransactionStatus(payDetail: PayDetail): String {
|
||||
if (payDetail.transactionType == TransactionsType.REVERSAL.value) {
|
||||
return if (payDetail.isNeedReversal) "REVERSAL FAILED" else "REVERSAL SUCCESS"
|
||||
}
|
||||
return POSUtil.getInstance().getResponse(payDetail.tradeAnswerCode.orEmpty().ifBlank { "-" })
|
||||
}
|
||||
|
||||
internal fun displayTransactionTitle(payDetail: PayDetail): String {
|
||||
val title = payDetail.transType
|
||||
.ifBlank { "TRANSACTION" }
|
||||
.replace("CASH_OUT", "CASH_ADV", ignoreCase = true)
|
||||
return if (payDetail.transactionType == TransactionsType.REVERSAL.value) {
|
||||
"$title (REVERSAL)"
|
||||
} else {
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isValidForReprint(payDetail: PayDetail): Boolean {
|
||||
if (payDetail.transactionType == TransactionsType.SETTLEMENT.value) return false
|
||||
if (payDetail.transactionType == TransactionsType.REVERSAL.value) return false
|
||||
if (payDetail.transactionType == TransactionsType.WAVEPAY.value && payDetail.qrTransStatus != 1) {
|
||||
return false
|
||||
}
|
||||
if (payDetail.transactionType == TransactionsType.WAVEPAY_REFUND.value && payDetail.qrTransStatus != 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun filterReprintAnyTransactions(
|
||||
transactions: List<PayDetail>,
|
||||
hostType: HostType
|
||||
): List<PayDetail> {
|
||||
return transactions.filter { pay ->
|
||||
if (!isValidForReprint(pay)) return@filter false
|
||||
|
||||
when (hostType) {
|
||||
HostType.MPU ->
|
||||
pay.accountType == "MPU" &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY.value &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value
|
||||
|
||||
HostType.QR ->
|
||||
(pay.transactionType == TransactionsType.WAVEPAY.value ||
|
||||
pay.transactionType == TransactionsType.WAVEPAY_REFUND.value) &&
|
||||
pay.qrTransStatus == 1
|
||||
|
||||
HostType.VISA_MASTER ->
|
||||
pay.accountType != "MPU" &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY.value &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun filterDetailReportTransactions(
|
||||
transactions: List<PayDetail>,
|
||||
hostType: HostType
|
||||
): List<PayDetail> {
|
||||
return transactions.filter { pay ->
|
||||
if (pay.transactionType == TransactionsType.REVERSAL.value) return@filter false
|
||||
if (pay.transactionType == TransactionsType.SETTLEMENT.value) return@filter false
|
||||
|
||||
when (hostType) {
|
||||
HostType.MPU ->
|
||||
pay.accountType == "MPU" &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY.value
|
||||
|
||||
HostType.QR ->
|
||||
(pay.transactionType == TransactionsType.WAVEPAY.value ||
|
||||
pay.transactionType == TransactionsType.WAVEPAY_REFUND.value) &&
|
||||
pay.qrTransStatus == 1
|
||||
|
||||
HostType.VISA_MASTER ->
|
||||
pay.accountType != "MPU" &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY_REFUND.value &&
|
||||
pay.transactionType != TransactionsType.WAVEPAY.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun displayAmountWithSign(payDetail: PayDetail): String {
|
||||
val prefix = if (payDetail.transactionType == TransactionsType.VOID.value ||
|
||||
payDetail.transactionType == TransactionsType.REFUND.value ||
|
||||
payDetail.transactionType == TransactionsType.WAVEPAY_REFUND.value ||
|
||||
payDetail.transactionType == TransactionsType.CASH_OUT.value
|
||||
) "-" else ""
|
||||
return prefix + POSUtil.getInstance().formatAmount(payDetail.amount)
|
||||
}
|
||||
|
||||
internal fun detailReportSignedAmount(payDetail: PayDetail): Long {
|
||||
val isNegative = payDetail.transactionType == TransactionsType.VOID.value ||
|
||||
payDetail.transactionType == TransactionsType.REFUND.value ||
|
||||
payDetail.transactionType == TransactionsType.WAVEPAY_REFUND.value ||
|
||||
payDetail.transactionType == TransactionsType.CASH_OUT.value
|
||||
return if (isNegative) -payDetail.amount else payDetail.amount
|
||||
}
|
||||
|
||||
internal fun hostTypeLabel(hostType: HostType): String = when (hostType) {
|
||||
HostType.QR -> "QR PAY"
|
||||
HostType.MPU -> "MPU"
|
||||
HostType.VISA_MASTER -> "VISA/MASTER"
|
||||
}
|
||||
|
||||
internal fun hostTypeAccountLabel(hostType: HostType): String = when (hostType) {
|
||||
HostType.QR -> "WALLET"
|
||||
HostType.MPU -> "MPU"
|
||||
HostType.VISA_MASTER -> "Visa/Master"
|
||||
}
|
||||
|
||||
internal fun searchReprintAnyTransactions(
|
||||
transactions: List<PayDetail>,
|
||||
query: String
|
||||
): List<PayDetail> {
|
||||
if (query.isBlank()) return transactions
|
||||
return when (query.length) {
|
||||
6 -> transactions.filter { it.voucherNo == query }
|
||||
12 -> transactions.filter { it.referNo == query }
|
||||
else -> transactions.filter {
|
||||
it.voucherNo.contains(query) || it.referNo.contains(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTransDate(value: String, formatter: SimpleDateFormat): Date? {
|
||||
return try {
|
||||
formatter.parse(value)
|
||||
} catch (_: ParseException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.functions.FunctionButton
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun HistoryMenuScreen(
|
||||
onBack: () -> Unit,
|
||||
onReprintLastTransaction: () -> Unit,
|
||||
onReprintAnyTransaction: () -> Unit,
|
||||
onReprintLastSettlement: () -> Unit,
|
||||
onPrintDetailReport: () -> Unit,
|
||||
onPrintSummaryReport: () -> Unit,
|
||||
onReview: () -> Unit,
|
||||
onBatchTotal: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Select Function",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
HistoryMenuButton("Reprint Last Transaction", onReprintLastTransaction)
|
||||
HistoryMenuButton("Reprint Any Transaction", onReprintAnyTransaction)
|
||||
HistoryMenuButton("Reprint Last Settlement Report", onReprintLastSettlement)
|
||||
HistoryMenuButton("Print Detail Report", onPrintDetailReport)
|
||||
HistoryMenuButton("Print Summary Report", onPrintSummaryReport)
|
||||
HistoryMenuButton("Review", onReview)
|
||||
HistoryMenuButton("Batch Total", onBatchTotal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HistoryMenuButton(
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
FunctionButton(
|
||||
onClick = onClick,
|
||||
title = title,
|
||||
subTitle = "",
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.print.PrintHelper
|
||||
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
|
||||
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
|
||||
|
||||
internal fun printSettlementReport(
|
||||
context: Context,
|
||||
payDetail: PayDetail,
|
||||
onSuccess: () -> Unit,
|
||||
onFailure: () -> Unit,
|
||||
) {
|
||||
if (PrintHelper.getInstance().paperRollStatus() != 1) {
|
||||
Toast.makeText(context, "Paper roll not ready.", Toast.LENGTH_SHORT).show()
|
||||
onFailure()
|
||||
return
|
||||
}
|
||||
|
||||
PrintXReceipt.getInstance().printSmileSettlementReport(
|
||||
payDetail,
|
||||
object : PrintXStatus {
|
||||
override fun onSuccess() {
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.PrintStatus
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
import com.utsmyanmar.paylibs.utils.enums.TransMenu
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
|
||||
private sealed interface HistoryDestination {
|
||||
data object Menu : HistoryDestination
|
||||
data class SelectHost(val purpose: HistoryHostPurpose) : HistoryDestination
|
||||
data class ReprintAny(val hostType: HostType) : HistoryDestination
|
||||
data class DetailReport(val hostType: HostType) : HistoryDestination
|
||||
data class SummaryReport(val hostType: HostType) : HistoryDestination
|
||||
data object ReviewTrace : HistoryDestination
|
||||
data class ReviewDetail(val trace: String) : HistoryDestination
|
||||
data object BatchTotal : HistoryDestination
|
||||
data object ReprintSettlement : HistoryDestination
|
||||
data object NotFound : HistoryDestination
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HistoryRoute(
|
||||
sharedViewModel: SharedViewModel,
|
||||
onBack: () -> Unit,
|
||||
onNavigatePrintReceipt: () -> Unit,
|
||||
viewModel: HistoryViewModel = hiltViewModel(),
|
||||
) {
|
||||
var destination by remember { mutableStateOf<HistoryDestination>(HistoryDestination.Menu) }
|
||||
var hostPurpose by remember { mutableStateOf(HistoryHostPurpose.REPRINT_ANY) }
|
||||
|
||||
val voucherNo = SystemParamsOperation.getInstance().lastSuccessTrnx
|
||||
val lastTransactions by viewModel.getLastTransaction(voucherNo).observeAsState(emptyList())
|
||||
|
||||
fun navigateToHostSelection(purpose: HistoryHostPurpose) {
|
||||
hostPurpose = purpose
|
||||
destination = HistoryDestination.SelectHost(purpose)
|
||||
}
|
||||
|
||||
fun handleHostSelected(hostType: HostType) {
|
||||
sharedViewModel.hostType.value = hostType
|
||||
destination = when (hostPurpose) {
|
||||
HistoryHostPurpose.REPRINT_ANY ->
|
||||
HistoryDestination.ReprintAny(hostType)
|
||||
|
||||
HistoryHostPurpose.LAST_SETTLEMENT -> {
|
||||
if (hostType == HostType.MPU) {
|
||||
HistoryDestination.ReprintSettlement
|
||||
} else {
|
||||
HistoryDestination.NotFound
|
||||
}
|
||||
}
|
||||
|
||||
HistoryHostPurpose.DETAIL_REPORT ->
|
||||
HistoryDestination.DetailReport(hostType)
|
||||
|
||||
HistoryHostPurpose.SUMMARY_REPORT ->
|
||||
HistoryDestination.SummaryReport(hostType)
|
||||
}
|
||||
}
|
||||
|
||||
fun reprintTransaction(payDetail: PayDetail) {
|
||||
sharedViewModel.setTransMenu(TransMenu.REPRINT)
|
||||
sharedViewModel.isReprint.value = true
|
||||
sharedViewModel.payDetail.value = payDetail
|
||||
sharedViewModel.setPrintStatus(PrintStatus.FIRST_PRINT)
|
||||
onNavigatePrintReceipt()
|
||||
}
|
||||
|
||||
fun reprintLastTransaction() {
|
||||
sharedViewModel.setTransMenu(TransMenu.REPRINT)
|
||||
if (lastTransactions.isEmpty()) {
|
||||
destination = HistoryDestination.NotFound
|
||||
return
|
||||
}
|
||||
|
||||
val payDetail = lastTransactions.last()
|
||||
if (!isValidForReprint(payDetail)) {
|
||||
destination = HistoryDestination.NotFound
|
||||
return
|
||||
}
|
||||
|
||||
reprintTransaction(prepareReprintPayDetail(payDetail))
|
||||
}
|
||||
|
||||
when (val current = destination) {
|
||||
HistoryDestination.Menu -> HistoryMenuScreen(
|
||||
onBack = onBack,
|
||||
onReprintLastTransaction = ::reprintLastTransaction,
|
||||
onReprintAnyTransaction = {
|
||||
sharedViewModel.setTransMenu(TransMenu.TRANSACTIONS)
|
||||
sharedViewModel.transactionsType.value = null
|
||||
navigateToHostSelection(HistoryHostPurpose.REPRINT_ANY)
|
||||
},
|
||||
onReprintLastSettlement = {
|
||||
sharedViewModel.setTransMenu(TransMenu.LAST_SETTLEMENT)
|
||||
navigateToHostSelection(HistoryHostPurpose.LAST_SETTLEMENT)
|
||||
},
|
||||
onPrintDetailReport = {
|
||||
sharedViewModel.setTransMenu(TransMenu.DETAIL_REPORT)
|
||||
navigateToHostSelection(HistoryHostPurpose.DETAIL_REPORT)
|
||||
},
|
||||
onPrintSummaryReport = {
|
||||
sharedViewModel.setTransMenu(TransMenu.SUMMARY_REPORT)
|
||||
navigateToHostSelection(HistoryHostPurpose.SUMMARY_REPORT)
|
||||
},
|
||||
onReview = {
|
||||
sharedViewModel.setTransMenu(TransMenu.REVIEW)
|
||||
sharedViewModel.transactionsType.value = TransactionsType.REVIEW_TRANS
|
||||
destination = HistoryDestination.ReviewTrace
|
||||
},
|
||||
onBatchTotal = {
|
||||
sharedViewModel.setTransMenu(TransMenu.REVIEW_BATCH)
|
||||
destination = HistoryDestination.BatchTotal
|
||||
}
|
||||
)
|
||||
|
||||
is HistoryDestination.SelectHost -> SelectHostScreen(
|
||||
onBack = { destination = HistoryDestination.Menu },
|
||||
onSelectHost = ::handleHostSelected
|
||||
)
|
||||
|
||||
is HistoryDestination.ReprintAny -> ReprintAnyTransactionScreen(
|
||||
viewModel = viewModel,
|
||||
hostType = current.hostType,
|
||||
onBack = { destination = HistoryDestination.Menu },
|
||||
onReprintTransaction = ::reprintTransaction
|
||||
)
|
||||
|
||||
is HistoryDestination.DetailReport -> DetailReportScreen(
|
||||
viewModel = viewModel,
|
||||
hostType = current.hostType,
|
||||
onBack = { destination = HistoryDestination.Menu }
|
||||
)
|
||||
|
||||
is HistoryDestination.SummaryReport -> SummaryReportScreen(
|
||||
viewModel = viewModel,
|
||||
hostType = current.hostType,
|
||||
onBack = { destination = HistoryDestination.Menu }
|
||||
)
|
||||
|
||||
HistoryDestination.ReviewTrace -> ReviewTraceScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { destination = HistoryDestination.Menu },
|
||||
onNavigateReviewDetail = { trace ->
|
||||
destination = HistoryDestination.ReviewDetail(trace)
|
||||
}
|
||||
)
|
||||
|
||||
is HistoryDestination.ReviewDetail -> ReviewDetailScreen(
|
||||
viewModel = viewModel,
|
||||
trace = current.trace,
|
||||
onBack = { destination = HistoryDestination.ReviewTrace }
|
||||
)
|
||||
|
||||
HistoryDestination.BatchTotal -> BatchTotalScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { destination = HistoryDestination.Menu }
|
||||
)
|
||||
|
||||
HistoryDestination.ReprintSettlement -> ReprintSettlementScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { destination = HistoryDestination.Menu },
|
||||
onDone = onBack
|
||||
)
|
||||
|
||||
HistoryDestination.NotFound -> TransactionNotFoundScreen(
|
||||
onBack = { destination = HistoryDestination.Menu }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.model.SettleData
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
internal fun formatTime(hour: String, minute: String): String {
|
||||
if (hour.isBlank() || minute.isBlank()) return "Select"
|
||||
return "$hour:$minute"
|
||||
}
|
||||
|
||||
internal class SummaryCounter {
|
||||
var count = 0
|
||||
var amount = 0L
|
||||
|
||||
fun inc(value: Long) {
|
||||
count++
|
||||
amount += value
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildSummarySettleData(
|
||||
transactions: List<PayDetail>,
|
||||
hostType: HostType,
|
||||
start: Date,
|
||||
end: Date,
|
||||
): SettleData {
|
||||
val dateTimeFormat = SimpleDateFormat("dd/MM/yy HH:mm", Locale.getDefault())
|
||||
|
||||
val wave = SummaryCounter()
|
||||
val waveRefund = SummaryCounter()
|
||||
val voidTran = SummaryCounter()
|
||||
val sale = SummaryCounter()
|
||||
val ca = SummaryCounter()
|
||||
val preAuth = SummaryCounter()
|
||||
val preAuthVoid = SummaryCounter()
|
||||
val preAuthComp = SummaryCounter()
|
||||
val preAuthCompVoid = SummaryCounter()
|
||||
val refund = SummaryCounter()
|
||||
|
||||
for (pay in transactions) {
|
||||
val currentDate = try {
|
||||
dateTimeFormat.parse("${pay.transDate} ${pay.transTime}")
|
||||
} catch (_: ParseException) {
|
||||
continue
|
||||
} ?: continue
|
||||
|
||||
if (currentDate.before(start) || currentDate.after(end)) continue
|
||||
|
||||
when (hostType) {
|
||||
HostType.QR -> when (pay.transactionType) {
|
||||
TransactionsType.WAVEPAY.value ->
|
||||
if (pay.qrTransStatus == 1) wave.inc(pay.amount)
|
||||
|
||||
TransactionsType.WAVEPAY_REFUND.value ->
|
||||
if (pay.qrTransStatus == 1) waveRefund.inc(pay.amount)
|
||||
}
|
||||
|
||||
HostType.MPU -> if (pay.accountType == "MPU") {
|
||||
accumulateCardSummary(
|
||||
pay, sale, voidTran, ca, preAuth, preAuthVoid, preAuthComp, preAuthCompVoid, refund
|
||||
)
|
||||
}
|
||||
|
||||
HostType.VISA_MASTER -> if (pay.accountType != "MPU") {
|
||||
accumulateCardSummary(
|
||||
pay, sale, voidTran, ca, preAuth, preAuthVoid, preAuthComp, preAuthCompVoid, refund
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SettleData(
|
||||
sale.count, sale.amount,
|
||||
wave.count, wave.amount,
|
||||
voidTran.count, voidTran.amount,
|
||||
ca.count, ca.amount,
|
||||
preAuth.count, preAuth.amount,
|
||||
preAuthVoid.count, preAuthVoid.amount,
|
||||
preAuthComp.count, preAuthComp.amount,
|
||||
preAuthCompVoid.count, preAuthCompVoid.amount,
|
||||
refund.count, refund.amount,
|
||||
waveRefund.count, waveRefund.amount
|
||||
)
|
||||
}
|
||||
|
||||
private fun accumulateCardSummary(
|
||||
pay: PayDetail,
|
||||
sale: SummaryCounter,
|
||||
voidTran: SummaryCounter,
|
||||
ca: SummaryCounter,
|
||||
preAuth: SummaryCounter,
|
||||
preAuthVoid: SummaryCounter,
|
||||
preAuthComp: SummaryCounter,
|
||||
preAuthCompVoid: SummaryCounter,
|
||||
refund: SummaryCounter,
|
||||
) {
|
||||
when (pay.transactionType) {
|
||||
TransactionsType.VOID.value -> voidTran.inc(pay.amount)
|
||||
TransactionsType.SALE.value -> sale.inc(pay.amount)
|
||||
TransactionsType.CASH_OUT.value -> ca.inc(pay.amount)
|
||||
TransactionsType.PRE_AUTH_SALE.value -> preAuth.inc(pay.amount)
|
||||
TransactionsType.PRE_AUTH_VOID.value -> preAuthVoid.inc(pay.amount)
|
||||
TransactionsType.PRE_AUTH_COMPLETE.value -> preAuthComp.inc(pay.amount)
|
||||
TransactionsType.PRE_AUTH_COMPLETE_VOID.value -> preAuthCompVoid.inc(pay.amount)
|
||||
TransactionsType.REFUND.value -> refund.inc(pay.amount)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.utsmyanmar.baselib.repo.Repository
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HistoryViewModel @Inject constructor(
|
||||
private val repository: Repository
|
||||
) : ViewModel() {
|
||||
|
||||
fun getTransactionHistory(): LiveData<List<PayDetail>> =
|
||||
repository.getTransactionHistory()
|
||||
|
||||
fun getLastTransaction(voucherNo: String): LiveData<List<PayDetail>> =
|
||||
repository.getLastTransaction(voucherNo)
|
||||
|
||||
fun getLastSettlement(voucherNo: String): LiveData<List<PayDetail>> =
|
||||
repository.getLastSettlement(voucherNo)
|
||||
|
||||
fun getReviewBatchTransaction(batchNo: String): LiveData<List<PayDetail>> =
|
||||
repository.getReviewBatchTransaction(batchNo)
|
||||
|
||||
fun getSmileSummaryTrans(batchNo: String): LiveData<List<PayDetail>> =
|
||||
repository.getSmileSummaryTrans(batchNo)
|
||||
|
||||
fun searchTransaction(voucherNo: String): LiveData<PayDetail> =
|
||||
repository.searchTransaction(voucherNo)
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.rounded.Backspace
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
|
||||
private val searchKeys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("C", "0", "\u232B")
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReprintAnyTransactionScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
hostType: HostType,
|
||||
onBack: () -> Unit,
|
||||
onReprintTransaction: (PayDetail) -> Unit,
|
||||
) {
|
||||
val allTransactions by viewModel.getTransactionHistory().observeAsState(emptyList())
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
val filtered = remember(allTransactions, hostType, searchQuery) {
|
||||
val base = filterReprintAnyTransactions(allTransactions, hostType)
|
||||
searchReprintAnyTransactions(base, searchQuery.trim())
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = "Reprint Transaction",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = hostTypeLabel(hostType),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
tint = Color.LegacyRed
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.IvoryBeige
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
SearchInputBox(
|
||||
query = searchQuery,
|
||||
isFocused = isFocused,
|
||||
onFocus = { isFocused = true }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "${filtered.size} transaction(s) found",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (filtered.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (allTransactions.isEmpty()) "No transactions available" else "No matching transactions",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(filtered, key = { "${it.voucherNo}_${it.referNo}_${it.PID}" }) { payDetail ->
|
||||
HistoryTransactionCard(payDetail) {
|
||||
isFocused = false
|
||||
onReprintTransaction(prepareReprintPayDetail(payDetail))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isFocused,
|
||||
enter = slideInVertically(initialOffsetY = { it }),
|
||||
exit = slideOutVertically(targetOffsetY = { it })
|
||||
) {
|
||||
NumericKeypad(
|
||||
keys = searchKeys,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(280.dp)
|
||||
.padding(top = 8.dp),
|
||||
onKeyClick = { key ->
|
||||
when (key) {
|
||||
"C" -> searchQuery = ""
|
||||
"\u232B" -> searchQuery = searchQuery.dropLast(1)
|
||||
else -> if (searchQuery.length < 12) searchQuery += key
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchInputBox(
|
||||
query: String,
|
||||
isFocused: Boolean,
|
||||
onFocus: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onFocus),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = if (isFocused) 6.dp else 2.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = if (isFocused) "Trace or RRN" else "Tap to search by Trace or RRN",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (isFocused) Color.LegacyRed else Color.Gray
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = query.ifEmpty { "_" },
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 2.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (query.isNotEmpty()) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
modifier = Modifier
|
||||
.shadow(2.dp, RoundedCornerShape(18.dp))
|
||||
.clickable { }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.Backspace,
|
||||
contentDescription = "Delete",
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@P3Preview
|
||||
@Composable
|
||||
private fun ReprintAnyTransactionScreenPreview() {
|
||||
MOBPOSTheme {
|
||||
Box(modifier = Modifier.height(600.dp)) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "SALE",
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text("Trace 000001", style = MaterialTheme.typography.bodySmall)
|
||||
Text("RRN 123456789012", style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
|
||||
@Composable
|
||||
fun ReprintSettlementScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
onBack: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val voucherNo = SystemParamsOperation.getInstance().currentSerialNum
|
||||
val settlements by viewModel.getLastSettlement(voucherNo).observeAsState(emptyList())
|
||||
var payDetail by remember { mutableStateOf<PayDetail?>(null) }
|
||||
var hasStarted by remember { mutableStateOf(false) }
|
||||
var printFailed by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(settlements) {
|
||||
if (hasStarted) return@LaunchedEffect
|
||||
if (settlements.isEmpty()) {
|
||||
Toast.makeText(context, "No transactions to print.", Toast.LENGTH_SHORT).show()
|
||||
onBack()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
hasStarted = true
|
||||
val detail = prepareReprintPayDetail(settlements.last())
|
||||
payDetail = detail
|
||||
printSettlementReport(context, detail, onSuccess = onDone) {
|
||||
printFailed = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(title = "Printing Settlement Report")
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (printFailed) {
|
||||
Text("Printing settlement report failed.", color = Color.LegacyRed)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
payDetail?.let { detail ->
|
||||
printSettlementReport(context, detail, onSuccess = onDone) {
|
||||
printFailed = true
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Retry")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(onClick = onDone) {
|
||||
Text("Done")
|
||||
}
|
||||
} else {
|
||||
CircularProgressIndicator(color = Color.LegacyRed)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Printing settlement report...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun ReviewDetailScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
trace: String,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val transaction by viewModel.searchTransaction(trace).observeAsState()
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Transaction Info",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
transaction?.let { payDetail ->
|
||||
HistoryTransactionDetailCard(payDetail)
|
||||
} ?: Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Transaction not found.", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
|
||||
@Composable
|
||||
fun ReviewTraceScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
onBack: () -> Unit,
|
||||
onNavigateReviewDetail: (String) -> Unit,
|
||||
) {
|
||||
var traceNumber by remember { mutableStateOf("") }
|
||||
var searchedTrace by remember { mutableStateOf("") }
|
||||
var lastNavigatedTrace by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val transactionSource: LiveData<PayDetail?> = remember(searchedTrace) {
|
||||
if (searchedTrace.isBlank()) {
|
||||
MutableLiveData(null)
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel.searchTransaction(searchedTrace) as LiveData<PayDetail?>
|
||||
}
|
||||
}
|
||||
val transaction by transactionSource.observeAsState()
|
||||
|
||||
LaunchedEffect(transaction?.voucherNo) {
|
||||
val matchedTrace = transaction?.voucherNo ?: return@LaunchedEffect
|
||||
if (lastNavigatedTrace == matchedTrace) return@LaunchedEffect
|
||||
lastNavigatedTrace = matchedTrace
|
||||
onNavigateReviewDetail(matchedTrace)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Review",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (searchedTrace.isNotBlank() && transaction == null) {
|
||||
"No transaction found for trace $searchedTrace."
|
||||
} else {
|
||||
"Input trace number to review transaction."
|
||||
},
|
||||
color = if (searchedTrace.isNotBlank() && transaction == null) {
|
||||
Color.LegacyRed
|
||||
} else {
|
||||
Color.Black
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Text(
|
||||
text = traceNumber.padStart(6, '0').ifEmpty { "000000" },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium.copy(fontSize = 28.sp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
HistoryNumericKeypad(
|
||||
onDigit = { digit ->
|
||||
if (traceNumber.length < 6) traceNumber += digit
|
||||
},
|
||||
onBackspace = {
|
||||
if (traceNumber.isNotEmpty()) {
|
||||
traceNumber = traceNumber.dropLast(1)
|
||||
}
|
||||
},
|
||||
onClear = { traceNumber = "" }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Button(
|
||||
onClick = { searchedTrace = traceNumber.padStart(6, '0') },
|
||||
enabled = traceNumber.isNotBlank(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Search")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
|
||||
@Composable
|
||||
fun SelectHostScreen(
|
||||
onBack: () -> Unit,
|
||||
onSelectHost: (HostType) -> Unit,
|
||||
) {
|
||||
var unavailableMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Select Host",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
HostButton("MPU") { onSelectHost(HostType.MPU) }
|
||||
HostButton("Visa / Master") {
|
||||
if (SystemParamsOperation.getInstance().isEmvEnabled) {
|
||||
onSelectHost(HostType.VISA_MASTER)
|
||||
} else {
|
||||
unavailableMessage = "Currently this menu is unavailable!"
|
||||
}
|
||||
}
|
||||
HostButton("QR Pay") { onSelectHost(HostType.QR) }
|
||||
}
|
||||
}
|
||||
|
||||
unavailableMessage?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { unavailableMessage = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { unavailableMessage = null }) {
|
||||
Text("OK")
|
||||
}
|
||||
},
|
||||
text = { Text(message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HostButton(
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ElevatedButton(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.Black
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
) {
|
||||
Text(label, style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,220 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.utsmyanmar.paylibs.print.PrintHelper
|
||||
import com.utsmyanmar.paylibs.print.printx.PrintXReceipt
|
||||
import com.utsmyanmar.paylibs.print.printx.PrintXStatus
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.enums.HostType
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import com.utsmyanmar.paylibs.utils.params.Params
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun SummaryReportScreen(
|
||||
viewModel: HistoryViewModel,
|
||||
hostType: HostType,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val calendarStart = remember { Calendar.getInstance() }
|
||||
val calendarEnd = remember { Calendar.getInstance() }
|
||||
|
||||
var startDate by remember { mutableStateOf("") }
|
||||
var endDate by remember { mutableStateOf("") }
|
||||
var startHour by remember { mutableStateOf("") }
|
||||
var startMinute by remember { mutableStateOf("") }
|
||||
var endHour by remember { mutableStateOf("") }
|
||||
var endMinute by remember { mutableStateOf("") }
|
||||
var isPrinting by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val allTransactions by viewModel.getSmileSummaryTrans(
|
||||
SystemParamsOperation.getInstance().currentBatchNum
|
||||
).observeAsState(emptyList())
|
||||
|
||||
fun showDatePicker(isStart: Boolean) {
|
||||
val calendar = if (isStart) calendarStart else calendarEnd
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _, year, month, day ->
|
||||
calendar.set(year, month, day)
|
||||
val formatted = SimpleDateFormat("dd/MM/yy", Locale.getDefault())
|
||||
.format(calendar.time)
|
||||
if (isStart) startDate = formatted else endDate = formatted
|
||||
},
|
||||
calendar.get(Calendar.YEAR),
|
||||
calendar.get(Calendar.MONTH),
|
||||
calendar.get(Calendar.DAY_OF_MONTH)
|
||||
).show()
|
||||
}
|
||||
|
||||
fun showTimePicker(isStart: Boolean) {
|
||||
val calendar = if (isStart) calendarStart else calendarEnd
|
||||
TimePickerDialog(
|
||||
context,
|
||||
{ _, hour, minute ->
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour)
|
||||
calendar.set(Calendar.MINUTE, minute)
|
||||
val hr = String.format(Locale.getDefault(), "%02d", hour)
|
||||
val min = String.format(Locale.getDefault(), "%02d", minute)
|
||||
if (isStart) {
|
||||
startHour = hr
|
||||
startMinute = min
|
||||
} else {
|
||||
endHour = hr
|
||||
endMinute = min
|
||||
}
|
||||
},
|
||||
calendar.get(Calendar.HOUR_OF_DAY),
|
||||
calendar.get(Calendar.MINUTE),
|
||||
false
|
||||
).show()
|
||||
}
|
||||
|
||||
fun printSummaryReport() {
|
||||
if (startDate.isBlank() || endDate.isBlank() ||
|
||||
startHour.isBlank() || startMinute.isBlank() ||
|
||||
endHour.isBlank() || endMinute.isBlank()
|
||||
) {
|
||||
errorMessage = "Please select date and time!"
|
||||
return
|
||||
}
|
||||
|
||||
val pattern = "dd/MM/yy HH:mm"
|
||||
val formatter = SimpleDateFormat(pattern, Locale.getDefault())
|
||||
val start: Date
|
||||
val end: Date
|
||||
try {
|
||||
start = formatter.parse("$startDate $startHour:$startMinute") ?: return
|
||||
end = formatter.parse("$endDate $endHour:$endMinute") ?: return
|
||||
} catch (_: ParseException) {
|
||||
errorMessage = "Invalid date or time."
|
||||
return
|
||||
}
|
||||
|
||||
if (start.after(end)) {
|
||||
errorMessage = "Invalid end date!"
|
||||
return
|
||||
}
|
||||
|
||||
val settleData = buildSummarySettleData(allTransactions, hostType, start, end)
|
||||
if (settleData.saleCount + settleData.wavePayCount +
|
||||
settleData.voidSaleCount + settleData.cashAdvanceCount +
|
||||
settleData.preAuthCount + settleData.refundCount + settleData.waveRefundCount == 0
|
||||
) {
|
||||
errorMessage = "No transactions to print."
|
||||
return
|
||||
}
|
||||
|
||||
if (PrintHelper.getInstance().paperRollStatus() != 1) {
|
||||
errorMessage = "Paper roll not ready."
|
||||
return
|
||||
}
|
||||
|
||||
val tradeData = Params.newTrade(false)
|
||||
val payDetail = tradeData.payDetail
|
||||
payDetail.settleDataObj = settleData
|
||||
payDetail.transType = TransactionsType.SUMMARY_REPORT.name
|
||||
payDetail.transactionType = TransactionsType.SUMMARY_REPORT.value
|
||||
payDetail.accountType = hostTypeAccountLabel(hostType)
|
||||
|
||||
isPrinting = true
|
||||
PrintXReceipt.getInstance().printSmileSummaryReport(
|
||||
payDetail,
|
||||
object : PrintXStatus {
|
||||
override fun onSuccess() {
|
||||
isPrinting = false
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
isPrinting = false
|
||||
errorMessage = "Print failed."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Transaction Summary",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
SummaryPickerRow("Start Date", startDate.ifBlank { "Select" }) { showDatePicker(true) }
|
||||
SummaryPickerRow("Start Time", formatTime(startHour, startMinute)) { showTimePicker(true) }
|
||||
SummaryPickerRow("End Date", endDate.ifBlank { "Select" }) { showDatePicker(false) }
|
||||
SummaryPickerRow("End Time", formatTime(endHour, endMinute)) { showTimePicker(false) }
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Button(
|
||||
onClick = ::printSummaryReport,
|
||||
enabled = !isPrinting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(if (isPrinting) "Printing..." else "Print")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorMessage?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { errorMessage = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { errorMessage = null }) {
|
||||
Text("OK")
|
||||
}
|
||||
},
|
||||
text = { Text(message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.mob.utsmyanmar.ui.history_functions
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun TransactionNotFoundScreen(onBack: () -> Unit) {
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Transaction Info",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Transaction not found.",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package com.mob.utsmyanmar.ui.input_amount
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.mob.utsmyanmar.model.ProcessCode
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
|
||||
@Composable
|
||||
fun AmountRoute(
|
||||
action: String,
|
||||
sharedViewModel: SharedViewModel,
|
||||
onBack: () -> Unit,
|
||||
onNavigateCardWaiting: () -> Unit
|
||||
) {
|
||||
InputAmount(
|
||||
title = action.ifBlank { "Amount" },
|
||||
onBackClick = onBack,
|
||||
onChargeClick = { amount ->
|
||||
sharedViewModel.amount.value = amount
|
||||
sharedViewModel.setAmountExist(true)
|
||||
sharedViewModel.setCardDataExist(false)
|
||||
sharedViewModel.setTransMenu(null)
|
||||
val config = action.toTransactionConfig()
|
||||
sharedViewModel.transactionName.value = config.transactionName
|
||||
sharedViewModel.transactionsType.value = config.transactionType
|
||||
sharedViewModel.processCode.value = config.processCode
|
||||
|
||||
onNavigateCardWaiting()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private data class TransactionConfig(
|
||||
val transactionName: String,
|
||||
val transactionType: TransactionsType,
|
||||
val processCode: String
|
||||
)
|
||||
|
||||
private fun String.toTransactionConfig(): TransactionConfig {
|
||||
return when (trim().lowercase()) {
|
||||
"void" -> TransactionConfig(
|
||||
transactionName = "SALE",
|
||||
transactionType = TransactionsType.VOID,
|
||||
processCode = ProcessCode.SALE_VOID + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
"refund" -> TransactionConfig(
|
||||
transactionName = "REFUND",
|
||||
transactionType = TransactionsType.REFUND,
|
||||
processCode = ProcessCode.REFUND + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
"pre-auth" -> TransactionConfig(
|
||||
transactionName = "PRE_AUTH",
|
||||
transactionType = TransactionsType.PRE_AUTH_SALE,
|
||||
processCode = ProcessCode.PRE_AUTH_SALE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
"pre-auth void" -> TransactionConfig(
|
||||
transactionName = "PRE_AUTH",
|
||||
transactionType = TransactionsType.PRE_AUTH_VOID,
|
||||
processCode = ProcessCode.PRE_AUTH_VOID + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
"pre-auth complete" -> TransactionConfig(
|
||||
transactionName = "PRE_AUTH_COMPLETE",
|
||||
transactionType = TransactionsType.PRE_AUTH_COMPLETE,
|
||||
processCode = ProcessCode.PRE_AUTH_COMPLETE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
"pre-auth complete void" -> TransactionConfig(
|
||||
transactionName = "PRE_AUTH_COMPLETE",
|
||||
transactionType = TransactionsType.PRE_AUTH_COMPLETE_VOID,
|
||||
processCode = ProcessCode.PRE_AUTH_COMPLETE_VOID + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
"cash adv" -> TransactionConfig(
|
||||
transactionName = "CASH_OUT",
|
||||
transactionType = TransactionsType.CASH_OUT,
|
||||
processCode = ProcessCode.CASH_ADVANCE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
|
||||
else -> TransactionConfig(
|
||||
transactionName = "SALE",
|
||||
transactionType = TransactionsType.SALE,
|
||||
processCode = ProcessCode.SALE_PURCHASE + ProcessCode.SMART + ProcessCode.TO_ACCOUNT
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,254 @@
|
||||
package com.mob.utsmyanmar.ui.input_amount
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.Backspace
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
private val amountKeys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf(".", "0", "00")
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun InputAmount(
|
||||
title: String = "Amount",
|
||||
onBackClick: () -> Unit = {},
|
||||
onChargeClick: (String) -> Unit = {}
|
||||
) {
|
||||
|
||||
var amount by remember { mutableStateOf("") }
|
||||
val prefixLabel = "MMK"
|
||||
val supportingText = "Enter the amount to continue"
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Amount",
|
||||
icon = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
onIconClick = onBackClick
|
||||
)
|
||||
},
|
||||
containerColor = Color.IvoryBeige
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
//first container
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(2f),
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.clickable(enabled = true) {
|
||||
amount = amount.dropLast(1)
|
||||
},
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Backspace,
|
||||
contentDescription = "Delete",
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Enter Amount",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = formatAmount(amount.ifEmpty { "0" }),
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.displayLarge
|
||||
)
|
||||
|
||||
if (prefixLabel.isNotBlank()) {
|
||||
Text(
|
||||
text = prefixLabel,
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Medium),
|
||||
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Text(
|
||||
text = supportingText,
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(3f),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
NumericKeypad(
|
||||
keys = amountKeys,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onKeyClick = { value ->
|
||||
amount = appendAmountValue(amount, value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
//third container
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onBackClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
val isAmountValid = amount.toDoubleOrNull()?.let { it > 0.0 } == true
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onChargeClick(amount)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
enabled = isAmountValid,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Next",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun appendAmountValue(current: String, value: String): String {
|
||||
if (value == ".") {
|
||||
if (current.contains(".")) return current
|
||||
return if (current.isEmpty()) "0." else "$current."
|
||||
}
|
||||
|
||||
val decimalIndex = current.indexOf('.')
|
||||
return if (decimalIndex >= 0) {
|
||||
val decimalPart = current.substring(decimalIndex + 1)
|
||||
val remainingDecimalDigits = 2 - decimalPart.length
|
||||
if (remainingDecimalDigits <= 0) {
|
||||
current
|
||||
} else {
|
||||
current + value.take(remainingDecimalDigits)
|
||||
}
|
||||
} else {
|
||||
val wholeDigitsCount = current.filter(Char::isDigit).length
|
||||
val remainingWholeDigits = 9 - wholeDigitsCount
|
||||
if (remainingWholeDigits <= 0) {
|
||||
current
|
||||
} else {
|
||||
current + value.take(remainingWholeDigits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatAmount(value: String): String {
|
||||
val normalized = value.ifEmpty { "0" }
|
||||
val wholePart = normalized.substringBefore(".").ifEmpty { "0" }
|
||||
val groupedWholePart = "%,d".format(wholePart.toLongOrNull() ?: 0L)
|
||||
|
||||
if (!normalized.contains(".")) {
|
||||
return "$groupedWholePart.00"
|
||||
}
|
||||
|
||||
val decimalPart = normalized.substringAfter(".", "")
|
||||
return if (normalized.endsWith(".")) {
|
||||
"$groupedWholePart.00"
|
||||
} else {
|
||||
val padded = decimalPart.padEnd(2, '0').take(2)
|
||||
"$groupedWholePart.$padded"
|
||||
}
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@P3Preview
|
||||
@Composable
|
||||
fun PreviewInputAmount() {
|
||||
InputAmount();
|
||||
}
|
||||
@ -0,0 +1,426 @@
|
||||
package com.mob.utsmyanmar.ui.manual_entry
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.rounded.Backspace
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.utils.TransactionUtil
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||
import com.utsmyanmar.checkxread.model.CardDataX
|
||||
import com.utsmyanmar.checkxread.util.CardTypeX
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
|
||||
private const val PAN_RAW_MAX = 19
|
||||
private const val PAN_RAW_MIN = 13
|
||||
private const val EXPIRY_RAW_MAX = 4
|
||||
|
||||
private enum class ManualEntryStep {
|
||||
CARD_NUMBER,
|
||||
EXPIRY
|
||||
}
|
||||
|
||||
private val digitKeys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("", "0", "")
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun InputManualCardRoute(
|
||||
transProcessViewModel: TransProcessViewModel,
|
||||
sharedViewModel: SharedViewModel,
|
||||
pinPadViewModel: PinPadViewModel,
|
||||
onBack: () -> Unit,
|
||||
onNavigatePinPad: () -> Unit
|
||||
) {
|
||||
var step by rememberSaveable { mutableStateOf(ManualEntryStep.CARD_NUMBER) }
|
||||
var pan by rememberSaveable { mutableStateOf("") }
|
||||
var expiry by rememberSaveable { mutableStateOf("") }
|
||||
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
step = ManualEntryStep.CARD_NUMBER
|
||||
pan = ""
|
||||
expiry = ""
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun goBack() {
|
||||
when (step) {
|
||||
ManualEntryStep.EXPIRY -> {
|
||||
step = ManualEntryStep.CARD_NUMBER
|
||||
expiry = ""
|
||||
errorMessage = null
|
||||
}
|
||||
ManualEntryStep.CARD_NUMBER -> onBack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler { goBack() }
|
||||
|
||||
fun confirmCardNumber() {
|
||||
errorMessage = null
|
||||
if (pan.length < PAN_RAW_MIN || pan.length > PAN_RAW_MAX) {
|
||||
errorMessage = "Invalid card number"
|
||||
return
|
||||
}
|
||||
if (!luhnCheck(pan)) {
|
||||
errorMessage = "Invalid card number"
|
||||
return
|
||||
}
|
||||
step = ManualEntryStep.EXPIRY
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun confirmExpiry() {
|
||||
errorMessage = null
|
||||
if (expiry.length != EXPIRY_RAW_MAX) {
|
||||
errorMessage = "Enter MM/YY"
|
||||
return
|
||||
}
|
||||
val month = expiry.take(2).toIntOrNull() ?: 0
|
||||
if (month < 1 || month > 12) {
|
||||
errorMessage = "Invalid month"
|
||||
return
|
||||
}
|
||||
|
||||
val expDate = POSUtil.getInstance().getISOFormatExpDate(expiry)
|
||||
val mpuCardData = CardDataX().apply {
|
||||
setPan(pan)
|
||||
setExp(expDate)
|
||||
setCardHolderName("MANUAL ENTRY")
|
||||
}
|
||||
|
||||
val tradeData = TransactionUtil.initMPUTransaction(mpuCardData, CardTypeX.MANUAL)
|
||||
sharedViewModel.setCardDataExist(true)
|
||||
sharedViewModel.payDetail.value = tradeData.payDetail
|
||||
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
pinPadViewModel.setTradeData(tradeData)
|
||||
onNavigatePinPad()
|
||||
}
|
||||
|
||||
ManualCardEntryScreen(
|
||||
step = step,
|
||||
pan = pan,
|
||||
expiry = expiry,
|
||||
errorMessage = errorMessage,
|
||||
onBackClick = ::goBack,
|
||||
onCancelClick = ::goBack,
|
||||
onConfirmClick = {
|
||||
when (step) {
|
||||
ManualEntryStep.CARD_NUMBER -> confirmCardNumber()
|
||||
ManualEntryStep.EXPIRY -> confirmExpiry()
|
||||
}
|
||||
},
|
||||
onKeyClick = { key ->
|
||||
if (key.isBlank()) return@ManualCardEntryScreen
|
||||
errorMessage = null
|
||||
when (step) {
|
||||
ManualEntryStep.CARD_NUMBER -> {
|
||||
if (pan.length < PAN_RAW_MAX) {
|
||||
pan += key.filter { it.isDigit() }
|
||||
}
|
||||
}
|
||||
ManualEntryStep.EXPIRY -> {
|
||||
if (expiry.length < EXPIRY_RAW_MAX) {
|
||||
expiry += key.filter { it.isDigit() }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDeleteClick = {
|
||||
errorMessage = null
|
||||
when (step) {
|
||||
ManualEntryStep.CARD_NUMBER -> {
|
||||
if (pan.isNotEmpty()) pan = pan.dropLast(1)
|
||||
}
|
||||
ManualEntryStep.EXPIRY -> {
|
||||
if (expiry.isNotEmpty()) expiry = expiry.dropLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManualCardEntryScreen(
|
||||
step: ManualEntryStep,
|
||||
pan: String,
|
||||
expiry: String,
|
||||
errorMessage: String?,
|
||||
onBackClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onConfirmClick: () -> Unit,
|
||||
onKeyClick: (String) -> Unit,
|
||||
onDeleteClick: () -> Unit
|
||||
) {
|
||||
val isCardStep = step == ManualEntryStep.CARD_NUMBER
|
||||
val displayValue = if (isCardStep) formatPanDisplay(pan) else formatExpiryDisplay(expiry)
|
||||
val prompt = if (isCardStep) "CARD NUMBER" else "EXPIRY DATE"
|
||||
val supportingText = when {
|
||||
errorMessage != null -> errorMessage
|
||||
isCardStep -> "Enter the card number to continue"
|
||||
else -> "Enter expiry as MM/YY"
|
||||
}
|
||||
val confirmText = if (isCardStep) "Next" else "Confirm"
|
||||
val confirmEnabled = if (isCardStep) {
|
||||
pan.length >= PAN_RAW_MIN
|
||||
} else {
|
||||
expiry.length == EXPIRY_RAW_MAX
|
||||
}
|
||||
val canDelete = if (isCardStep) pan.isNotEmpty() else expiry.isNotEmpty()
|
||||
val cardScheme = remember(pan) { detectCardScheme(pan) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Manual Entry",
|
||||
icon = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
onIconClick = onBackClick
|
||||
)
|
||||
},
|
||||
containerColor = Color.IvoryBeige
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = prompt,
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = displayValue.ifBlank {
|
||||
if (isCardStep) "---- ---- ---- ----" else "--/--"
|
||||
},
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
letterSpacing = if (isCardStep) 1.sp else 2.sp,
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.clickable(enabled = canDelete, onClick = onDeleteClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.Backspace,
|
||||
contentDescription = "Delete",
|
||||
tint = if (canDelete) Color.LegacyRed else Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = supportingText,
|
||||
color = if (errorMessage == null) Color.Gray else Color.Error,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
if (!isCardStep && pan.length >= 4) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "**** **** **** ${pan.takeLast(4)}" +
|
||||
(cardScheme?.let { " · $it" } ?: ""),
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
|
||||
NumericKeypad(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(280.dp),
|
||||
onKeyClick = onKeyClick,
|
||||
keys = digitKeys
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = if (isCardStep) "Cancel" else "Back",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onConfirmClick,
|
||||
enabled = confirmEnabled,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = confirmText,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPanDisplay(raw: String): String {
|
||||
if (raw.isEmpty()) return ""
|
||||
return raw.chunked(4).joinToString(" ")
|
||||
}
|
||||
|
||||
private fun formatExpiryDisplay(raw: String): String {
|
||||
return when {
|
||||
raw.isEmpty() -> ""
|
||||
raw.length <= 2 -> raw
|
||||
else -> "${raw.take(2)}/${raw.drop(2)}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectCardScheme(pan: String): String? = when {
|
||||
pan.startsWith("4") -> "Visa"
|
||||
pan.length >= 2 && (
|
||||
pan.take(2).toIntOrNull() in 51..55 ||
|
||||
pan.take(4).toIntOrNull() in 2221..2720
|
||||
) -> "Mastercard"
|
||||
pan.length >= 2 && pan.take(2) in listOf("34", "37") -> "Amex"
|
||||
pan.startsWith("62") || pan.startsWith("6") -> "UnionPay / MPU"
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun luhnCheck(pan: String): Boolean {
|
||||
var sum = 0
|
||||
var alt = false
|
||||
for (i in pan.length - 1 downTo 0) {
|
||||
var n = pan[i].digitToIntOrNull() ?: return false
|
||||
if (alt) {
|
||||
n *= 2
|
||||
if (n > 9) n -= 9
|
||||
}
|
||||
sum += n
|
||||
alt = !alt
|
||||
}
|
||||
return sum % 10 == 0
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@Composable
|
||||
private fun PreviewManualCardEntryPan() {
|
||||
ManualCardEntryScreen(
|
||||
step = ManualEntryStep.CARD_NUMBER,
|
||||
pan = "6244310039574344",
|
||||
expiry = "",
|
||||
errorMessage = null,
|
||||
onBackClick = {},
|
||||
onCancelClick = {},
|
||||
onConfirmClick = {},
|
||||
onKeyClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@Composable
|
||||
private fun PreviewManualCardEntryExpiry() {
|
||||
ManualCardEntryScreen(
|
||||
step = ManualEntryStep.EXPIRY,
|
||||
pan = "6244310039574344",
|
||||
expiry = "1228",
|
||||
errorMessage = null,
|
||||
onBackClick = {},
|
||||
onCancelClick = {},
|
||||
onConfirmClick = {},
|
||||
onKeyClick = {},
|
||||
onDeleteClick = {}
|
||||
)
|
||||
}
|
||||
@ -1,34 +1,557 @@
|
||||
package com.mob.utsmyanmar.ui.navigation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.navArgument
|
||||
import com.mob.utsmyanmar.ui.amount.AmountScreen
|
||||
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen
|
||||
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen
|
||||
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel
|
||||
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
|
||||
import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
|
||||
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen
|
||||
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel
|
||||
import com.mob.utsmyanmar.ui.error.TransactionErrorScreen
|
||||
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
|
||||
import com.mob.utsmyanmar.ui.history_functions.HistoryRoute
|
||||
import com.mob.utsmyanmar.ui.history_functions.prepareReprintPayDetail
|
||||
import com.mob.utsmyanmar.ui.input_amount.AmountRoute
|
||||
import com.mob.utsmyanmar.ui.manual_entry.InputManualCardRoute
|
||||
import com.mob.utsmyanmar.ui.password_input.InputPassword
|
||||
import com.mob.utsmyanmar.ui.password_input.PasswordType
|
||||
import com.mob.utsmyanmar.ui.pinpad.PinPadRoute
|
||||
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
|
||||
import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
|
||||
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
|
||||
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
|
||||
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
|
||||
import com.mob.utsmyanmar.ui.refund_rrn.RRNViewModel
|
||||
import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
|
||||
import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen
|
||||
import com.mob.utsmyanmar.ui.sale_void.VoidViewModel
|
||||
import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute
|
||||
import com.mob.utsmyanmar.ui.settlement.SettlementScreen
|
||||
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
||||
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
|
||||
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
|
||||
import com.mob.utsmyanmar.ui.signature_pad.SignaturePad
|
||||
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
|
||||
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel
|
||||
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent
|
||||
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute
|
||||
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
||||
import com.mob.utsmyanmar.ui.version.VersionScreen
|
||||
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||
import com.utsmyanmar.paylibs.model.TradeData
|
||||
import com.utsmyanmar.paylibs.utils.enums.TransMenu
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@SuppressLint("ContextCastToActivity")
|
||||
@Composable
|
||||
fun AppNavGraph(
|
||||
navController: NavHostController
|
||||
navController: NavHostController,
|
||||
autoSettlementRequested: Boolean = false,
|
||||
onAutoSettlementHandled: () -> Unit = {}
|
||||
) {
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = currentBackStackEntry?.destination?.route
|
||||
|
||||
LaunchedEffect(autoSettlementRequested, currentRoute) {
|
||||
if (!autoSettlementRequested ||
|
||||
currentRoute == null ||
|
||||
currentRoute == Routes.TmsSetup.route
|
||||
) return@LaunchedEffect
|
||||
if (currentRoute == Routes.Settlement.route) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
if (currentRoute == Routes.Password.route) {
|
||||
onAutoSettlementHandled()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
navController.navigate(Routes.Settlement.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Routes.Dashboard.route
|
||||
startDestination = Routes.TmsSetup.route
|
||||
) {
|
||||
composable(Routes.Dashboard.route) {
|
||||
DashboardScreen(
|
||||
settlementEnabled = true,
|
||||
wavePayEnabled = true,
|
||||
onAmountClick = { action ->
|
||||
navController.navigate(Routes.Amount.createRoute(action))
|
||||
composable(Routes.TmsSetup.route) {
|
||||
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel()
|
||||
TmsSetupRoute(
|
||||
viewModel = tmsSetupViewModel,
|
||||
onNavigateDashboard = {
|
||||
navController.navigate(
|
||||
if (autoSettlementRequested) Routes.Settlement.route else Routes.Dashboard.route
|
||||
) {
|
||||
popUpTo(Routes.TmsSetup.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onTransactionClick = {},
|
||||
onSettlementClick = {},
|
||||
onHistoryClick = {},
|
||||
onCardClick = {},
|
||||
onWavePayClick = {}
|
||||
onNavigateDisable = {}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Signature.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
SignaturePad(
|
||||
onBack = { navController.popBackStack() },
|
||||
amount = sharedViewModel.payDetail.value?.amount,
|
||||
cardNumber = sharedViewModel.payDetail.value?.cardNo ?: "",
|
||||
onConfirm = { bitmap, signatureBlob ->
|
||||
sharedViewModel.signBitmap = bitmap
|
||||
sharedViewModel.updateSignatureBlob(signatureBlob)
|
||||
navController.navigate(Routes.TransactionResult.route) {
|
||||
popUpTo(Routes.Signature.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Dashboard.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||
val dashboardViewModel: DashboardViewModel = hiltViewModel(activity)
|
||||
val dashboardUiState = dashboardViewModel.uiState.collectAsStateWithLifecycle().value
|
||||
val recentPayDetails by sharedViewModel.getLastTenTransactions().observeAsState(emptyList())
|
||||
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel(activity)
|
||||
val deviceInfoUiState = deviceInfoViewModel.uiState.collectAsStateWithLifecycle().value
|
||||
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity);
|
||||
|
||||
LaunchedEffect(deviceInfoViewModel) {
|
||||
deviceInfoViewModel.loadDeviceInfo()
|
||||
}
|
||||
|
||||
val tmsSetupUiState by tmsSetupViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(tmsSetupViewModel) {
|
||||
tmsSetupViewModel.onSyncCompleted.collect {
|
||||
dashboardViewModel.refreshLastSyncTime()
|
||||
dashboardViewModel.finishSync("Sync completed")
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(tmsSetupUiState.isLoading, tmsSetupUiState.isError) {
|
||||
if (dashboardUiState.isSyncing && !tmsSetupUiState.isLoading) {
|
||||
if (tmsSetupUiState.isError) {
|
||||
dashboardViewModel.finishSync(tmsSetupUiState.statusText.ifBlank { "Download failed!" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(tmsSetupUiState.isTerminalDisabled) {
|
||||
if (tmsSetupUiState.isTerminalDisabled) {
|
||||
tmsSetupViewModel.clearTerminalDisabledState()
|
||||
navController.navigate(Routes.TmsSetup.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transProcessViewModel.resetTransactionStatus()
|
||||
DashboardScreen2(
|
||||
dashboardUiState = dashboardUiState,
|
||||
deviceInfo = deviceInfoUiState,
|
||||
onClickLastSync = {
|
||||
dashboardViewModel.startSync()
|
||||
tmsSetupViewModel.downloadConfigs()
|
||||
},
|
||||
onNavigateRecentTransaction = { record ->
|
||||
recentPayDetails.firstOrNull { it.PID == record.pid }?.let { payDetail ->
|
||||
sharedViewModel.setTransMenu(TransMenu.REPRINT)
|
||||
sharedViewModel.isReprint.value = true
|
||||
sharedViewModel.payDetail.value = prepareReprintPayDetail(payDetail)
|
||||
transResultViewModel.onEvent(
|
||||
TransactionResultEvent.Start,
|
||||
sharedViewModel
|
||||
)
|
||||
navController.navigate(Routes.PrintReceipt.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onNavigateAmount = { action ->
|
||||
when (action) {
|
||||
"Pre-Auth",
|
||||
"Cash Adv",
|
||||
"Sale" -> {
|
||||
navController.navigate(Routes.Amount.createRoute(action)) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
"Pre-Auth",
|
||||
"Cash Adv" -> {
|
||||
navController.navigate(Routes.Amount.createRoute(action)) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.Amount.createRoute(action),
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onNavigateAction = { action ->
|
||||
when (action) {
|
||||
"Void" -> {
|
||||
sharedViewModel.transactionsType.value = TransactionsType.VOID
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.VoidTrace.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
"Pre-Auth",
|
||||
"Cash Adv" -> {
|
||||
sharedViewModel.transactionsType.value = when (action) {
|
||||
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
|
||||
else -> TransactionsType.CASH_OUT
|
||||
}
|
||||
navController.navigate(Routes.Amount.createRoute(action)) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
"Pre-Auth Complete Void" -> {
|
||||
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.VoidTrace.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.Amount.createRoute(action),
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onNavigateSignOn = {
|
||||
navController.navigate(Routes.SignOn.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
|
||||
onNavigateSettlement = {
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.Settlement.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateVersion = {
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.Version.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateFunctions = {
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.Functions.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateHistory = {
|
||||
navController.navigate(Routes.History.route)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.SeeMore.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
SeeMoreScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigateAmount = { action ->
|
||||
when (action) {
|
||||
"Void" -> {
|
||||
sharedViewModel.transactionsType.value = TransactionsType.VOID
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.VoidTrace.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
"Pre-Auth",
|
||||
"Cash Adv" -> {
|
||||
sharedViewModel.transactionsType.value = when (action) {
|
||||
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
|
||||
else -> TransactionsType.CASH_OUT
|
||||
}
|
||||
navController.navigate(Routes.Amount.createRoute(action)) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
"Pre-Auth Complete Void" -> {
|
||||
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.VoidTrace.route,
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
navController.navigate(
|
||||
Routes.Password.createRoute(
|
||||
destination = Routes.Amount.createRoute(action),
|
||||
passwordType = PasswordType.SYSTEM
|
||||
)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Version.route){
|
||||
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel();
|
||||
VersionScreen(
|
||||
onBack = {navController.popBackStack()},
|
||||
deviceInfoViewModel = deviceInfoViewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Routes.Password.route,
|
||||
arguments = listOf(
|
||||
navArgument("destination") {
|
||||
type = NavType.StringType
|
||||
},
|
||||
navArgument("passwordType") {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val destination = backStackEntry.arguments?.getString("destination").orEmpty()
|
||||
val passwordType = backStackEntry.arguments?.getString("passwordType").orEmpty()
|
||||
|
||||
InputPassword(
|
||||
passwordType = passwordType,
|
||||
onBack = { navController.popBackStack() },
|
||||
onPasswordCorrect = {
|
||||
navController.navigate(destination) {
|
||||
popUpTo(Routes.Password.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Functions.route) {
|
||||
FunctionsScreen(
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.VoidTrace.route) {
|
||||
val voidViewModel: VoidViewModel = hiltViewModel()
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
VoidTraceScreen(
|
||||
sharedViewModel = sharedViewModel,
|
||||
voidViewModel = voidViewModel,
|
||||
onNavigateTranDetail = { trace ->
|
||||
navController.navigate(Routes.VoidTranDetail.createRoute(trace)) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Settlement.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val settlementViewMode: SettlementViewModel = hiltViewModel(activity)
|
||||
|
||||
SettlementScreen(
|
||||
sharedViewModel = sharedViewModel,
|
||||
settlementViewMode = settlementViewMode,
|
||||
onBack = {
|
||||
onAutoSettlementHandled()
|
||||
if (!navController.popBackStack(Routes.Dashboard.route, false)) {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
},
|
||||
autoStartSettlement = autoSettlementRequested,
|
||||
onStartSettlement = {
|
||||
sharedViewModel.transactionsType.value = TransactionsType.SETTLEMENT
|
||||
navController.navigate(Routes.Processing.route) {
|
||||
popUpTo(Routes.Settlement.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
if (autoSettlementRequested) {
|
||||
onAutoSettlementHandled()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Routes.VoidTranDetail.route,
|
||||
arguments = listOf(
|
||||
navArgument("trace") {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val voidViewModel: VoidViewModel = hiltViewModel()
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val trace = backStackEntry.arguments?.getString("trace").orEmpty()
|
||||
|
||||
TranDetailPage(
|
||||
voidViewModel = voidViewModel,
|
||||
trace = trace,
|
||||
onBack = { navController.popBackStack() },
|
||||
onProceedVoid = { payDetail ->
|
||||
if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
|
||||
transProcessViewModel.setTransType(TransactionsType.VOID)
|
||||
payDetail.transType = TransactionsType.VOID.name
|
||||
payDetail.transactionType = TransactionsType.VOID.value
|
||||
} else {
|
||||
transProcessViewModel.setTransType(TransactionsType.PRE_AUTH_COMPLETE_VOID)
|
||||
payDetail.transType = TransactionsType.PRE_AUTH_COMPLETE_VOID.name
|
||||
payDetail.transactionType = TransactionsType.PRE_AUTH_COMPLETE_VOID.value
|
||||
}
|
||||
|
||||
val tradeData = TradeData()
|
||||
tradeData.payDetail = payDetail
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
sharedViewModel.payDetail.value = payDetail
|
||||
navController.navigate(Routes.Processing.route) {
|
||||
popUpTo(Routes.VoidTranDetail.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.SignOn.route) {
|
||||
SignOnRoute(
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigateResult = { isSuccess, message ->
|
||||
navController.navigate(Routes.SignOnResult.createRoute(isSuccess, message)) {
|
||||
popUpTo(Routes.SignOn.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Routes.SignOnResult.route,
|
||||
arguments = listOf(
|
||||
navArgument("isSuccess") {
|
||||
type = NavType.BoolType
|
||||
},
|
||||
navArgument("message") {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val isSuccess = backStackEntry.arguments?.getBoolean("isSuccess") ?: false
|
||||
val message = backStackEntry.arguments?.getString("message").orEmpty()
|
||||
|
||||
SignOnResultScreen(
|
||||
isSuccess = isSuccess,
|
||||
message = message,
|
||||
onBack = { navController.popBackStack() },
|
||||
onDone = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onRetry = {
|
||||
navController.navigate(Routes.SignOn.route) {
|
||||
popUpTo(Routes.SignOnResult.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,10 +563,335 @@ fun AppNavGraph(
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
AmountScreen(
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
|
||||
AmountRoute(
|
||||
action = backStackEntry.arguments?.getString("action").orEmpty(),
|
||||
onBackClick = { navController.popBackStack() }
|
||||
sharedViewModel = sharedViewModel,
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigateCardWaiting = {
|
||||
navController.navigate(Routes.CardWaiting.route) {
|
||||
popUpTo(Routes.Amount.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.CardWaiting.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val cardReaderViewModel: CardReaderViewModel = hiltViewModel(activity)
|
||||
val cardWaitingViewModel: CardWaitingViewModel = viewModel(
|
||||
factory = CardWaitingViewModel.provideFactory(
|
||||
cardReadViewModel = cardReaderViewModel,
|
||||
sharedViewModel = sharedViewModel
|
||||
)
|
||||
)
|
||||
|
||||
CardWaitingScreen(
|
||||
viewModel = cardWaitingViewModel,
|
||||
amount = formatAmountForDisplay(sharedViewModel.amount.value),
|
||||
onManualEntry = {
|
||||
navController.navigate(Routes.ManualEntry.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onProcessingCard = {
|
||||
navController.navigate(Routes.ProcessingCard.route) {
|
||||
popUpTo(Routes.CardWaiting.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
onMain = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.ManualEntry.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
|
||||
|
||||
InputManualCardRoute(
|
||||
transProcessViewModel = transProcessViewModel,
|
||||
sharedViewModel = sharedViewModel,
|
||||
pinPadViewModel = pinPadViewModel,
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigatePinPad = {
|
||||
pinPadViewModel.resetSessionState()
|
||||
navController.navigate(Routes.PinPad.route) {
|
||||
popUpTo(Routes.ManualEntry.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.ProcessingCard.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val cardReaderViewModel: CardReaderViewModel = hiltViewModel(activity)
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
|
||||
val emvTransactionViewModel: EmvTransactionProcessViewModel = hiltViewModel(activity)
|
||||
val processingCardViewModel: ProcessingCardViewModel = viewModel(
|
||||
factory = ProcessingCardViewModel.provideFactory(
|
||||
cardReadViewModel = cardReaderViewModel,
|
||||
sharedViewModel = sharedViewModel,
|
||||
transProcessViewModel = transProcessViewModel,
|
||||
pinPadViewModel = pinPadViewModel,
|
||||
emvTransactionViewModel = emvTransactionViewModel
|
||||
)
|
||||
)
|
||||
|
||||
ProcessingCardRoute(
|
||||
viewModel = processingCardViewModel,
|
||||
onNavigatePinPad = {
|
||||
pinPadViewModel.resetSessionState()
|
||||
navController.navigate(Routes.PinPad.route) {
|
||||
popUpTo(Routes.ProcessingCard.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateInputAmount = { navController.popBackStack(Routes.Amount.route, false) },
|
||||
onNavigateProcessing = {},
|
||||
onNavigateEmvTransaction = {},
|
||||
onNavigateError = {},
|
||||
onBack = { navController.popBackStack() },
|
||||
onShowDecline = {}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.PinPad.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val pinPadViewModel: PinPadViewModel = hiltViewModel(activity)
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
|
||||
PinPadRoute(
|
||||
pinPadViewModel = pinPadViewModel,
|
||||
sharedViewModel = sharedViewModel,
|
||||
transProcessViewModel = transProcessViewModel,
|
||||
onNavigateInputRrn = {
|
||||
navController.navigate(Routes.InputRrn.route) {
|
||||
popUpTo(Routes.PinPad.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateProcessing = {
|
||||
navController.navigate(Routes.Processing.route) {
|
||||
popUpTo(Routes.PinPad.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigateMain = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.InputRrn.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val rrnViewModel: RRNViewModel = hiltViewModel(activity)
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
InputRrnRoute(
|
||||
transProcessViewModel = transProcessViewModel,
|
||||
rrnViewModel = rrnViewModel,
|
||||
sharedViewModel = sharedViewModel,
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigateProcessing = {
|
||||
navController.navigate(Routes.Processing.route) {
|
||||
popUpTo(Routes.InputRrn.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.History.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||
HistoryRoute(
|
||||
sharedViewModel = sharedViewModel,
|
||||
onBack = { navController.popBackStack() },
|
||||
onNavigatePrintReceipt = {
|
||||
transResultViewModel.onEvent(
|
||||
TransactionResultEvent.Start,
|
||||
sharedViewModel
|
||||
)
|
||||
navController.navigate(Routes.PrintReceipt.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Processing.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val transProcessViewModel: TransProcessViewModel = hiltViewModel(activity)
|
||||
val settlementViewModel: SettlementViewModel = hiltViewModel(activity)
|
||||
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity)
|
||||
val dashboardViewModel: DashboardViewModel = hiltViewModel(activity)
|
||||
ProcessingRoute(
|
||||
sharedViewModel = sharedViewModel,
|
||||
transProcessViewModel = transProcessViewModel,
|
||||
settlementViewModel = settlementViewModel,
|
||||
onTransactionFinished = {
|
||||
tmsSetupViewModel.downloadConfigs(dashboardViewModel::refreshLastSyncTime)
|
||||
},
|
||||
onNavigateTransactionResult = {
|
||||
navController.navigate(Routes.TransactionResult.route) {
|
||||
popUpTo(Routes.Processing.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateSignature = {
|
||||
navController.navigate(Routes.Signature.route) {
|
||||
popUpTo(Routes.Processing.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigateError = { isSuccess,message ->
|
||||
navController.navigate(Routes.TransactionError.createRoute(isSuccess,message)) {
|
||||
popUpTo(Routes.Processing.route) {
|
||||
inclusive = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Routes.TransactionError.route,
|
||||
arguments = listOf(
|
||||
navArgument("isSuccess") {
|
||||
type = NavType.BoolType
|
||||
},
|
||||
navArgument("message") {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val isSuccess = backStackEntry.arguments?.getBoolean("isSuccess") ?: false
|
||||
val message = backStackEntry.arguments?.getString("message").orEmpty()
|
||||
|
||||
TransactionErrorScreen(
|
||||
isSuccess = isSuccess,
|
||||
message = message,
|
||||
onDone = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.TransactionResult.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||
|
||||
TransactionResultRoute(
|
||||
viewModel = transResultViewModel,
|
||||
sharedViewModel = sharedViewModel,
|
||||
onNavigateMain = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onNavigatePrintReceipt = {
|
||||
navController.navigate(Routes.PrintReceipt.route) {
|
||||
popUpTo(Routes.TransactionResult.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onShowError = {},
|
||||
onShowSuccess = {},
|
||||
onShowPrinterDialog = { message ->
|
||||
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.PrintReceipt.route) {
|
||||
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
|
||||
val transResultViewModel: TransactionResultViewModel = hiltViewModel(activity)
|
||||
PrintReceiptScreen(
|
||||
sharedViewModel = sharedViewModel,
|
||||
transactionResultViewModel = transResultViewModel,
|
||||
onPrint = {
|
||||
transResultViewModel.onEvent(
|
||||
TransactionResultEvent.RetryPrint,
|
||||
sharedViewModel
|
||||
)
|
||||
},
|
||||
onDone = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onPrintFinished = {
|
||||
navController.navigate(Routes.Dashboard.route) {
|
||||
popUpTo(Routes.Dashboard.route) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatAmountForDisplay(amount: String?): String {
|
||||
val raw = amount.orEmpty().trim()
|
||||
if (raw.isBlank()) return "0.00"
|
||||
if (raw.contains(".")) {
|
||||
val whole = raw.substringBefore(".").ifEmpty { "0" }
|
||||
val decimal = raw.substringAfter(".", "")
|
||||
val grouped = "%,d".format(whole.toLongOrNull() ?: 0L)
|
||||
return "$grouped.${decimal.padEnd(2, '0').take(2)}"
|
||||
}
|
||||
val value = raw.toLongOrNull() ?: return raw
|
||||
return "%,d.00".format(value)
|
||||
}
|
||||
|
||||
@ -1,8 +1,51 @@
|
||||
package com.mob.utsmyanmar.ui.navigation
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
sealed class Routes(val route: String) {
|
||||
data object Signature: Routes("signature")
|
||||
data object TmsSetup : Routes("tms_setup")
|
||||
data object Dashboard : Routes("dashboard")
|
||||
data object Amount : Routes("amount/{action}") {
|
||||
fun createRoute(action: String): String = "amount/$action"
|
||||
fun createRoute(action: String): String = "amount/${Uri.encode(action)}"
|
||||
}
|
||||
data object SeeMore : Routes("see_more")
|
||||
data object Settlement : Routes("settlement")
|
||||
data object VoidTrace : Routes("void_trace")
|
||||
data object VoidTranDetail : Routes("void_tran_detail/{trace}") {
|
||||
fun createRoute(trace: String): String = "void_tran_detail/${Uri.encode(trace)}"
|
||||
}
|
||||
data object SignOn : Routes("sign_on")
|
||||
data object SignOnResult : Routes("sign_on_result/{isSuccess}/{message}") {
|
||||
fun createRoute(isSuccess: Boolean, message: String): String {
|
||||
return "sign_on_result/$isSuccess/${Uri.encode(message)}"
|
||||
}
|
||||
}
|
||||
data object CardWaiting : Routes("card_waiting")
|
||||
data object ProcessingCard : Routes("processing_card")
|
||||
data object PinPad : Routes("pin_pad")
|
||||
data object InputRrn : Routes("input_rrn")
|
||||
data object Processing : Routes("processing")
|
||||
data object ManualEntry : Routes("manual_entry")
|
||||
|
||||
|
||||
data object TransactionResult : Routes("transaction_result")
|
||||
data object PrintReceipt : Routes("print_receipt")
|
||||
data object Version : Routes("version")
|
||||
data object Functions : Routes("functions")
|
||||
data object Password : Routes("password/{destination}/{passwordType}") {
|
||||
fun createRoute(destination: String, passwordType: String): String =
|
||||
"password/${Uri.encode(destination)}/$passwordType"
|
||||
}
|
||||
data object NotificationList : Routes("notification_list")
|
||||
data object NotificationDetail : Routes("notification_detail/{notificationId}") {
|
||||
fun createRoute(id: Int): String = "notification_detail/$id"
|
||||
}
|
||||
data object History : Routes("history")
|
||||
data object Disable : Routes("disable/{message}") {
|
||||
fun createRoute(message: String): String = "disable/${Uri.encode(message)}"
|
||||
}
|
||||
data object TransactionError : Routes("transaction_error/{isSuccess}/{message}") {
|
||||
fun createRoute(isSuccess: Boolean, message: String): String = "transaction_error/$isSuccess/${Uri.encode(message)}"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
package com.mob.utsmyanmar.ui.notification
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun NotificationDetailScreen(
|
||||
viewModel: NotificationViewModel,
|
||||
notificationId: Int,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val notification = viewModel.getById(notificationId)
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Notification Detail",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (notification == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Notification not found", color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(4.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(20.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
tint = Color.LegacyRed,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = notification.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
Text(
|
||||
text = notification.timestamp,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = notification.message,
|
||||
color = Color.Black,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
package com.mob.utsmyanmar.ui.notification
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
@Composable
|
||||
fun NotificationListScreen(
|
||||
viewModel: NotificationViewModel,
|
||||
onBack: () -> Unit,
|
||||
onNavigateDetail: (Int) -> Unit
|
||||
) {
|
||||
val notifications by viewModel.notifications.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Notifications",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack,
|
||||
actions = {
|
||||
if (notifications.any { !it.isRead }) {
|
||||
TextButton(onClick = { viewModel.markAllAsRead() }) {
|
||||
Text(text = "Mark all read", color = Color.White, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (notifications.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
Text(text = "No notifications", color = Color.Gray, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(items = notifications, key = { it.id }) { notification ->
|
||||
NotificationItem(
|
||||
notification = notification,
|
||||
onClick = {
|
||||
viewModel.markAsRead(notification.id)
|
||||
onNavigateDetail(notification.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationItem(
|
||||
notification: AppNotification,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(42.dp)
|
||||
.background(
|
||||
color = if (notification.isRead) Color.Gray.copy(alpha = 0.12f)
|
||||
else Color.LegacyRed.copy(alpha = 0.12f),
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
tint = if (notification.isRead) Color.Gray else Color.LegacyRed,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = notification.title,
|
||||
style = if (notification.isRead) MaterialTheme.typography.bodyMedium
|
||||
else MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = if (notification.isRead) Color.Gray else Color.Black,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (!notification.isRead) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(Color.LegacyRed, CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = notification.message,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = notification.timestamp,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.mob.utsmyanmar.ui.notification
|
||||
|
||||
data class AppNotification(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val message: String,
|
||||
val timestamp: String,
|
||||
val isRead: Boolean = false
|
||||
)
|
||||
@ -0,0 +1,37 @@
|
||||
package com.mob.utsmyanmar.ui.notification
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NotificationViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val _notifications = MutableStateFlow(sampleNotifications)
|
||||
val notifications: StateFlow<List<AppNotification>> = _notifications.asStateFlow()
|
||||
|
||||
val unreadCount: Int get() = _notifications.value.count { !it.isRead }
|
||||
|
||||
fun markAsRead(id: Int) {
|
||||
_notifications.value = _notifications.value.map {
|
||||
if (it.id == id) it.copy(isRead = true) else it
|
||||
}
|
||||
}
|
||||
|
||||
fun markAllAsRead() {
|
||||
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
|
||||
}
|
||||
|
||||
fun getById(id: Int): AppNotification? = _notifications.value.find { it.id == id }
|
||||
}
|
||||
|
||||
private val sampleNotifications = listOf(
|
||||
AppNotification(1, "Settlement Reminder", "Your daily settlement is pending. Please settle before end of day.", "Today, 17:30", isRead = false),
|
||||
AppNotification(2, "TMS Update Available", "A new terminal configuration update is ready. Please restart to apply.", "Today, 09:15", isRead = false),
|
||||
AppNotification(3, "Transaction Approved", "Sale of MMK 50,000 was approved successfully. Trace #001234.", "Yesterday, 14:22", isRead = true),
|
||||
AppNotification(4, "Network Warning", "Intermittent network issues detected. Contact your administrator if the problem persists.", "Yesterday, 11:05", isRead = true),
|
||||
AppNotification(5, "Log-On Required", "Your terminal session has expired. Please log on again to continue.", "06/09, 08:00", isRead = true),
|
||||
)
|
||||
@ -0,0 +1,210 @@
|
||||
package com.mob.utsmyanmar.ui.password_input
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.rounded.Backspace
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
|
||||
object PasswordType {
|
||||
const val SYSTEM = "system_password"
|
||||
const val SETTLEMENT = "settlement_password"
|
||||
const val SETTING = "setting_password"
|
||||
}
|
||||
|
||||
private val passwords = mapOf(
|
||||
PasswordType.SYSTEM to "111111",
|
||||
PasswordType.SETTING to "222222",
|
||||
PasswordType.SETTLEMENT to "123456"
|
||||
)
|
||||
|
||||
private val passwordKeys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("", "0", "")
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun InputPassword(
|
||||
passwordType: String,
|
||||
onBack: () -> Unit = {},
|
||||
onPasswordCorrect: () -> Unit = {},
|
||||
viewModel: PasswordInputViewModel = viewModel()
|
||||
) {
|
||||
val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!!
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) { viewModel.reset() }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(title = "Password", icon = Icons.Default.ArrowBackIosNew, onIconClick = onBack)
|
||||
},
|
||||
containerColor = Color.IvoryBeige
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
// Display area
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(2f)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.clickable(enabled = uiState.canDelete, onClick = viewModel::onDelete),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Backspace,
|
||||
contentDescription = "Delete",
|
||||
tint = if (uiState.canDelete) Color.LegacyRed else Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = "Enter Password", color = Color.LegacyRed, fontSize = 18.sp)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
PasswordDots(filledCount = uiState.input.length, totalCount = PASSWORD_LENGTH)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = uiState.errorMessage ?: "",
|
||||
color = Color.LegacyRed,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Keypad — stable lambda: viewModel::onKey never changes between recompositions
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(3f),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
NumericKeypad(
|
||||
keys = passwordKeys,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onKeyClick = viewModel::onKey
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Action buttons
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.weight(1f).height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.validate(expectedPassword, onPasswordCorrect) },
|
||||
modifier = Modifier.weight(1f).height(56.dp),
|
||||
enabled = uiState.isConfirmEnabled,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(text = "Next", fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordDots(filledCount: Int, totalCount: Int) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(totalCount) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.background(
|
||||
color = if (index < filledCount) Color.LegacyRed
|
||||
else Color.Gray.copy(alpha = 0.3f),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@P3Preview
|
||||
@P2Preview
|
||||
@Composable
|
||||
fun PreviewInputPassword() {
|
||||
InputPassword(passwordType = PasswordType.SETTING)
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.mob.utsmyanmar.ui.password_input
|
||||
|
||||
internal const val PASSWORD_LENGTH = 6
|
||||
|
||||
data class PasswordInputUiState(
|
||||
val input: String = "",
|
||||
val errorMessage: String? = null
|
||||
) {
|
||||
val isConfirmEnabled: Boolean get() = input.length == PASSWORD_LENGTH
|
||||
val canDelete: Boolean get() = input.isNotEmpty()
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.mob.utsmyanmar.ui.password_input
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class PasswordInputViewModel : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(PasswordInputUiState())
|
||||
val uiState: StateFlow<PasswordInputUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun onKey(digit: String) {
|
||||
_uiState.update { state ->
|
||||
if (state.input.length < PASSWORD_LENGTH)
|
||||
state.copy(input = state.input + digit, errorMessage = null)
|
||||
else
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
fun onDelete() {
|
||||
_uiState.update { state ->
|
||||
state.copy(input = state.input.dropLast(1), errorMessage = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun validate(expectedPassword: String, onCorrect: () -> Unit) {
|
||||
if (_uiState.value.input == expectedPassword) {
|
||||
onCorrect()
|
||||
} else {
|
||||
_uiState.update { it.copy(errorMessage = "Incorrect password", input = "") }
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_uiState.value = PasswordInputUiState()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package com.mob.utsmyanmar.ui.pinpad
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.mob.utsmyanmar.model.PinPadStatus
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||
import com.utsmyanmar.paylibs.Constant
|
||||
import com.utsmyanmar.paylibs.model.TradeData
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
|
||||
@Composable
|
||||
fun PinPadRoute(
|
||||
pinPadViewModel: PinPadViewModel,
|
||||
sharedViewModel: SharedViewModel,
|
||||
transProcessViewModel: TransProcessViewModel,
|
||||
onNavigateInputRrn: () -> Unit,
|
||||
onNavigateProcessing: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onNavigateMain: () -> Unit,
|
||||
) {
|
||||
val pinText by pinPadViewModel.pinText.collectAsStateWithLifecycle()
|
||||
val alertMsg by pinPadViewModel.alertMsg.collectAsStateWithLifecycle()
|
||||
val pinStatus by pinPadViewModel.pinStatus.collectAsStateWithLifecycle()
|
||||
val canGoBack = sharedViewModel.transactionsType.value != TransactionsType.SALE
|
||||
|
||||
BackHandler(enabled = canGoBack) {
|
||||
onBack()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
val tradeData: TradeData = pinPadViewModel.getTradeData()!!
|
||||
val payDetail = tradeData.getPayDetail()
|
||||
|
||||
val processCode = sharedViewModel.processCode.getValue()
|
||||
val amount = sharedViewModel.amount.getValue()
|
||||
payDetail.setAmount(POSUtil.getInstance().convertAmount(amount))
|
||||
if (!TextUtils.equals(processCode, "")) {
|
||||
payDetail.setProcessCode(processCode)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pinStatus) {
|
||||
when (pinStatus) {
|
||||
PinPadStatus.ON_CONFIRM,
|
||||
PinPadStatus.ON_NEXT_SCREEN -> {
|
||||
val payDetail = pinPadViewModel.getPayDetail()
|
||||
payDetail?.tradeAnswerCode = Constant.ANSWER_CODE_APPROVED
|
||||
sharedViewModel.payDetail.value = payDetail
|
||||
transProcessViewModel.resetTransactionStatus()
|
||||
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND || sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID || sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
|
||||
onNavigateInputRrn()
|
||||
} else {
|
||||
onNavigateProcessing()
|
||||
}
|
||||
}
|
||||
|
||||
PinPadStatus.ON_CANCEL,
|
||||
PinPadStatus.ON_TIMEOUT,
|
||||
PinPadStatus.ON_CARD_REMOVED -> {
|
||||
onBack()
|
||||
}
|
||||
|
||||
PinPadStatus.ON_ERROR -> {
|
||||
onNavigateMain()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
pinPadViewModel.cancelPinPad()
|
||||
}
|
||||
}
|
||||
|
||||
PinPadScreen(
|
||||
pinText = pinText,
|
||||
alertMessage = alertMsg,
|
||||
canGoBack = canGoBack,
|
||||
onBack = onBack,
|
||||
onKeyboardReady = { keyboard ->
|
||||
pinPadViewModel.startPinPadProcess(keyboard)
|
||||
}
|
||||
)
|
||||
}
|
||||
178
app/src/main/java/com/mob/utsmyanmar/ui/pinpad/PinPadScreen.kt
Normal file
178
app/src/main/java/com/mob/utsmyanmar/ui/pinpad/PinPadScreen.kt
Normal file
@ -0,0 +1,178 @@
|
||||
package com.mob.utsmyanmar.ui.pinpad
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.mob.utsmyanmar.R
|
||||
import com.mob.utsmyanmar.ui.theme.Black
|
||||
import com.mob.utsmyanmar.ui.theme.Primary
|
||||
import com.mob.utsmyanmar.ui.theme.White
|
||||
import com.utsmyanmar.baselib.ui.CustomPinPadKeyboard
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PinPadScreen(
|
||||
pinText: String,
|
||||
alertMessage: String?,
|
||||
canGoBack: Boolean,
|
||||
onBack: () -> Unit,
|
||||
onKeyboardReady: (CustomPinPadKeyboard) -> Unit
|
||||
) {
|
||||
var keyboardView by remember { mutableStateOf<CustomPinPadKeyboard?>(null) }
|
||||
var isStarted by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(keyboardView) {
|
||||
val keyboard = keyboardView ?: return@LaunchedEffect
|
||||
if (!isStarted) {
|
||||
isStarted = true
|
||||
onKeyboardReady(keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "PIN ENTRY",
|
||||
color = White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
if (canGoBack) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_left_arrow),
|
||||
contentDescription = "Back",
|
||||
tint = White
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Primary
|
||||
)
|
||||
)
|
||||
},
|
||||
containerColor = White
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.background(White)
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = Primary,
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
.padding(horizontal = 24.dp, vertical = 28.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "Please Input PIN",
|
||||
color = White,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
|
||||
Text(
|
||||
text = if (pinText.isBlank()) "------" else pinText,
|
||||
color = White,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
letterSpacing = 4.sp
|
||||
)
|
||||
|
||||
if (!alertMessage.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = alertMessage,
|
||||
color = White,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = "Enter your PIN on the secured keypad below.",
|
||||
color = Black,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
CustomPinPadKeyboard(context).also {
|
||||
keyboardView = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (canGoBack) {
|
||||
"Cancel on device or use back to exit."
|
||||
} else {
|
||||
"Cancel on device to exit."
|
||||
},
|
||||
color = Black,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,569 @@
|
||||
package com.mob.utsmyanmar.ui.pinpad
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.kizzy.cmhl.utils.ByteUtils
|
||||
import com.mob.utsmyanmar.config.Constants
|
||||
import com.mob.utsmyanmar.config.SunmiPayManager
|
||||
import com.mob.utsmyanmar.model.PinPadStatus
|
||||
import com.sunmi.pay.hardware.aidl.AidlConstants
|
||||
import com.sunmi.pay.hardware.aidlv2.AidlErrorCodeV2
|
||||
import com.sunmi.pay.hardware.aidlv2.bean.PinPadConfigV2
|
||||
import com.sunmi.pay.hardware.aidlv2.bean.PinPadDataV2
|
||||
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadListenerV2
|
||||
import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2
|
||||
import com.utsmyanmar.baselib.ui.CustomPinPadKeyboard
|
||||
import com.utsmyanmar.checkxread.sdk.SunmiSDK
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.model.TradeData
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.ByteUtil
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PinPadViewModel @Inject constructor(
|
||||
private val sunmiPayManager: SunmiPayManager
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "PinPadViewModel"
|
||||
private const val PIN_CONFIRM_SUCCESS_STATUS = 0
|
||||
private const val ON_CONFIRM_CLICK = 2
|
||||
private const val ON_CANCEL_CLICK = 3
|
||||
private const val ON_ERROR_PIN_PAD = 4
|
||||
private const val ON_NUMBER_CLICK = 5
|
||||
private const val ON_EMPTY_PIN_BLOCK = 6
|
||||
private const val ON_TIMEOUT_PIN_PAD = 7
|
||||
private const val ON_ERROR_DUKPT = 8
|
||||
}
|
||||
|
||||
private var mPinPadOptV2: PinPadOptV2? = null
|
||||
private var mPinType = 0
|
||||
private var mWidth = 239
|
||||
private var mHeight = 130
|
||||
private var mInterval = 1
|
||||
private var mKeyboardCoordinate = intArrayOf(0, 661)
|
||||
private var mCancelWidth = 112
|
||||
private var mCancelHeight = 112
|
||||
private var mCancelCoordinate = intArrayOf(0, 48)
|
||||
private var dukptIndex = 0
|
||||
private var tmkIndex = 9
|
||||
private val PIK_INDEX = 11
|
||||
private var tradeData: TradeData? = null
|
||||
private var payDetail: PayDetail? = null
|
||||
private var pan: String = ""
|
||||
|
||||
/*
|
||||
* UI States
|
||||
*/
|
||||
|
||||
private val _pinText = MutableStateFlow("")
|
||||
val pinText = _pinText.asStateFlow()
|
||||
|
||||
private val _alertMsg = MutableStateFlow<String?>(null)
|
||||
val alertMsg = _alertMsg.asStateFlow()
|
||||
|
||||
private val _errorCode = MutableStateFlow<Int?>(null)
|
||||
val errorCode = _errorCode.asStateFlow()
|
||||
|
||||
private val _transType = MutableStateFlow<TransactionsType?>(null)
|
||||
val transType = _transType.asStateFlow()
|
||||
|
||||
private val _pinStatus =
|
||||
MutableStateFlow<PinPadStatus?>(null)
|
||||
|
||||
val pinStatus = _pinStatus.asStateFlow()
|
||||
private var isTerminalStateReached = false
|
||||
|
||||
/*
|
||||
* Trade Data
|
||||
*/
|
||||
|
||||
fun setTradeData(tradeData: TradeData) {
|
||||
this.tradeData = tradeData
|
||||
|
||||
payDetail = tradeData.payDetail
|
||||
|
||||
pan = payDetail?.cardNo ?: ""
|
||||
}
|
||||
|
||||
fun getTradeData(): TradeData? {
|
||||
return tradeData
|
||||
}
|
||||
|
||||
fun setPayDetail(payDetail: PayDetail) {
|
||||
this.payDetail = payDetail
|
||||
|
||||
tradeData = TradeData().apply {
|
||||
this.payDetail = payDetail
|
||||
}
|
||||
}
|
||||
|
||||
fun getPayDetail(): PayDetail? {
|
||||
return payDetail
|
||||
}
|
||||
|
||||
fun setTransType(type: TransactionsType?) {
|
||||
_transType.value = type
|
||||
}
|
||||
|
||||
fun resetSessionState() {
|
||||
_pinText.value = ""
|
||||
_alertMsg.value = null
|
||||
_errorCode.value = null
|
||||
_pinStatus.value = null
|
||||
isTerminalStateReached = false
|
||||
}
|
||||
|
||||
/*
|
||||
* Pin Pad
|
||||
*/
|
||||
|
||||
fun startPinPadProcess(
|
||||
customPinPadKeyboard: CustomPinPadKeyboard
|
||||
) {
|
||||
resetSessionState()
|
||||
|
||||
// dukptIndex = SystemParamsOperation.getInstance().tmkIndex.toInt()
|
||||
if(!sunmiPayManager.isReady()){
|
||||
_alertMsg.value =
|
||||
"Sunmi Pay SDK is not ready"
|
||||
|
||||
_pinStatus.value =
|
||||
PinPadStatus.ON_ERROR
|
||||
|
||||
return
|
||||
}
|
||||
initData()
|
||||
// testInjectPIK()
|
||||
initPinPad(customPinPadKeyboard)
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
|
||||
mPinPadOptV2 = sunmiPayManager.pinPadOptV2
|
||||
|
||||
if (mPinPadOptV2 == null) {
|
||||
Log.d(TAG, "PinPad service are not ready!")
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
val result = mPinPadOptV2?.setAntiExhaustiveProtectionMode(3)
|
||||
|
||||
if ((result ?: -1) >= 0) {
|
||||
LogUtil.d(
|
||||
TAG,
|
||||
"Pin anti exhaustive result:$result"
|
||||
)
|
||||
} else {
|
||||
LogUtil.d(
|
||||
TAG, "Pin Anti Exhaustive failed"
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: RemoteException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelPinPad() {
|
||||
try {
|
||||
if (!isTerminalStateReached) {
|
||||
mPinPadOptV2?.cancelInputPin()
|
||||
Log.d(TAG, "PinPad Canceled")
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Init Pin Pad
|
||||
*/
|
||||
|
||||
private fun initPinPad(
|
||||
customPinPadKeyboard: CustomPinPadKeyboard
|
||||
) {
|
||||
LogUtil.e(TAG, "Init Pin Pad PAN:$pan")
|
||||
|
||||
var timeout = Constants.PIN_PAD_TIMEOUT
|
||||
val pinPadOrder = !SystemParamsOperation.getInstance().isRandomPinPad
|
||||
|
||||
if (SunmiSDK.getInstance().checkCardExist() != 2) {
|
||||
timeout = Constants.TIMEOUT
|
||||
}
|
||||
|
||||
try {
|
||||
val config = PinPadConfigV2().apply {
|
||||
maxInput = 6
|
||||
minInput = 0
|
||||
pinPadType = 1 // custom keyboard
|
||||
algorithmType = 0
|
||||
pinType = mPinType
|
||||
this.timeout = timeout * 1000
|
||||
isOrderNumKey = pinPadOrder
|
||||
keySystem = AidlConstants.Security.SEC_MKSK
|
||||
pinKeyIndex = PIK_INDEX
|
||||
pinblockFormat = AidlConstants.PinBlockFormat.SEC_PIN_BLK_ISO_FMT0
|
||||
this.pan = getPanBytes(this@PinPadViewModel.pan)
|
||||
}
|
||||
|
||||
val result = mPinPadOptV2?.initPinPad(config, mPinPadListener)
|
||||
|
||||
LogUtil.e(TAG, "pinpad result:$result")
|
||||
|
||||
if (result.isNullOrEmpty()) {
|
||||
_alertMsg.value = "PinPad init failed"
|
||||
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||
return
|
||||
}
|
||||
|
||||
getKeyboardCoordinate(result, customPinPadKeyboard)
|
||||
|
||||
_pinText.value = ""
|
||||
customPinPadKeyboard.keepScreenOn = true
|
||||
customPinPadKeyboard.setKeyBoard(result)
|
||||
customPinPadKeyboard.visibility = View.VISIBLE
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
_alertMsg.value = e.message
|
||||
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
private fun testInjectPIK() {
|
||||
try {
|
||||
val securityOptV2 = sunmiPayManager.securityOptV2 ?: return
|
||||
|
||||
val pik = ByteUtil.hexStr2Bytes(
|
||||
"33DD20C9A0B5B861F2914D44BC2AF055"
|
||||
)
|
||||
|
||||
val kcv = ByteUtil.hexStr2Bytes(
|
||||
"28DBDB489D28BC92"
|
||||
)
|
||||
|
||||
val code = securityOptV2.savePlaintextKey(
|
||||
AidlConstants.Security.KEY_TYPE_PIK,
|
||||
pik,
|
||||
kcv,
|
||||
AidlConstants.Security.KEY_ALG_TYPE_3DES,
|
||||
PIK_INDEX
|
||||
)
|
||||
|
||||
LogUtil.e(TAG, "saveTestPIK result:$code")
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Keyboard
|
||||
*/
|
||||
|
||||
private fun getKeyboardCoordinate(
|
||||
keyBoardText: String,
|
||||
customPinPadKeyboard: CustomPinPadKeyboard
|
||||
) {
|
||||
|
||||
customPinPadKeyboard
|
||||
.viewTreeObserver
|
||||
.addOnGlobalLayoutListener(
|
||||
object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
|
||||
customPinPadKeyboard
|
||||
.viewTreeObserver
|
||||
.removeOnGlobalLayoutListener(this)
|
||||
|
||||
val textView: TextView =
|
||||
customPinPadKeyboard.key_0
|
||||
|
||||
textView.getLocationOnScreen(
|
||||
mKeyboardCoordinate
|
||||
)
|
||||
|
||||
mWidth = textView.width
|
||||
mHeight = textView.height
|
||||
|
||||
mInterval = 1
|
||||
|
||||
importPinPadData(keyBoardText)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun importPinPadData(text: String) {
|
||||
|
||||
val pinPadData = PinPadDataV2()
|
||||
|
||||
pinPadData.numX = mKeyboardCoordinate[0]
|
||||
pinPadData.numY = mKeyboardCoordinate[1]
|
||||
|
||||
pinPadData.numW = mWidth
|
||||
pinPadData.numH = mHeight
|
||||
|
||||
pinPadData.lineW = mInterval
|
||||
|
||||
pinPadData.cancelX = mCancelCoordinate[0]
|
||||
pinPadData.cancelY = mCancelCoordinate[1]
|
||||
|
||||
pinPadData.cancelW = mCancelWidth
|
||||
pinPadData.cancelH = mCancelHeight
|
||||
|
||||
pinPadData.lineW = 0
|
||||
|
||||
pinPadData.rows = 5
|
||||
pinPadData.clos = 3
|
||||
|
||||
keyMap(text, pinPadData)
|
||||
|
||||
try {
|
||||
mPinPadOptV2?.importPinPadData(pinPadData)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun keyMap(
|
||||
str: String,
|
||||
data: PinPadDataV2
|
||||
) {
|
||||
|
||||
data.keyMap = ByteArray(64)
|
||||
|
||||
var j = 0
|
||||
|
||||
for (i in 0 until 15) {
|
||||
|
||||
when (i) {
|
||||
|
||||
9, 12 -> {
|
||||
data.keyMap[i] = 0x1B
|
||||
}
|
||||
|
||||
13 -> {
|
||||
data.keyMap[i] = 0x0C
|
||||
}
|
||||
|
||||
11, 14 -> {
|
||||
data.keyMap[i] = 0x0D
|
||||
}
|
||||
|
||||
else -> {
|
||||
data.keyMap[i] =
|
||||
str[j].code.toByte()
|
||||
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Password View
|
||||
*/
|
||||
|
||||
private fun showPasswordView(len: Int) {
|
||||
|
||||
val sb = StringBuilder()
|
||||
|
||||
repeat(len) {
|
||||
sb.append("*")
|
||||
}
|
||||
|
||||
_pinText.value = sb.toString()
|
||||
}
|
||||
|
||||
/*
|
||||
* Handler
|
||||
*/
|
||||
|
||||
private val handler =
|
||||
Handler(Looper.getMainLooper()) { msg ->
|
||||
when (msg.what) {
|
||||
ON_NUMBER_CLICK -> {
|
||||
showPasswordView(msg.arg1)
|
||||
}
|
||||
|
||||
ON_CONFIRM_CLICK -> {
|
||||
LogUtil.d(TAG, "ON CLICK CONFIRM")
|
||||
|
||||
if (
|
||||
transType.value == TransactionsType.PRE_AUTH_COMPLETE ||
|
||||
transType.value == TransactionsType.PRE_AUTH_VOID ||
|
||||
transType.value == TransactionsType.REFUND
|
||||
) {
|
||||
isTerminalStateReached = true
|
||||
_pinStatus.value = PinPadStatus.ON_NEXT_SCREEN
|
||||
} else {
|
||||
isTerminalStateReached = true
|
||||
_pinStatus.value = PinPadStatus.ON_CONFIRM
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ON_CANCEL_CLICK -> {
|
||||
LogUtil.d(TAG, "ON CLICK CANCEL")
|
||||
if (!isTerminalStateReached) {
|
||||
_pinStatus.value = PinPadStatus.ON_CANCEL
|
||||
}
|
||||
}
|
||||
|
||||
ON_ERROR_PIN_PAD -> {
|
||||
LogUtil.d(TAG, "ON ERROR CODE: ${msg.arg1}")
|
||||
_errorCode.value = msg.arg1
|
||||
isTerminalStateReached = true
|
||||
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||
}
|
||||
|
||||
ON_EMPTY_PIN_BLOCK -> {
|
||||
_pinStatus.value = PinPadStatus.ON_EMPTY
|
||||
}
|
||||
|
||||
ON_TIMEOUT_PIN_PAD -> {
|
||||
isTerminalStateReached = true
|
||||
_pinStatus.value = PinPadStatus.ON_TIMEOUT
|
||||
}
|
||||
|
||||
ON_ERROR_DUKPT -> {
|
||||
_alertMsg.value = "Try Again!"
|
||||
isTerminalStateReached = true
|
||||
_pinStatus.value = PinPadStatus.ON_ERROR_DUKPT
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/*
|
||||
* Listener
|
||||
*/
|
||||
|
||||
private val mPinPadListener =
|
||||
object : PinPadListenerV2.Stub() {
|
||||
|
||||
override fun onPinLength(len: Int) {
|
||||
handler.obtainMessage(
|
||||
ON_NUMBER_CLICK,
|
||||
len,
|
||||
0
|
||||
).sendToTarget()
|
||||
}
|
||||
|
||||
override fun onConfirm(
|
||||
status: Int,
|
||||
pinBlock: ByteArray?
|
||||
) {
|
||||
|
||||
LogUtil.e(
|
||||
TAG,
|
||||
"onConfirm status:$status and pinblock :${ByteUtil.bytes2HexStr(pinBlock)}"
|
||||
)
|
||||
if (status != PIN_CONFIRM_SUCCESS_STATUS) {
|
||||
_alertMsg.value = "PinPad confirm failed: $status"
|
||||
_pinStatus.value = PinPadStatus.ON_ERROR
|
||||
return
|
||||
}
|
||||
|
||||
// val isCardValid = SunmiSDK.getInstance().checkCardExist() == 2 ||
|
||||
// payDetail?.cardType == AidlConstants.CardType.MAGNETIC.getValue() ||
|
||||
// payDetail?.cardType == -9
|
||||
//
|
||||
// if (!isCardValid) {
|
||||
// isTerminalStateReached = true
|
||||
// _pinStatus.value = PinPadStatus.ON_CARD_REMOVED
|
||||
// return
|
||||
// }
|
||||
|
||||
if (pinBlock != null) {
|
||||
payDetail?.pinCipher = ByteUtil.bytes2HexStr(pinBlock)
|
||||
} else {
|
||||
payDetail?.pinCipher = ""
|
||||
}
|
||||
|
||||
|
||||
handler.obtainMessage(
|
||||
ON_CONFIRM_CLICK
|
||||
).sendToTarget()
|
||||
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
if (!isTerminalStateReached) {
|
||||
handler.obtainMessage(
|
||||
ON_CANCEL_CLICK
|
||||
).sendToTarget()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(code: Int) {
|
||||
|
||||
val msg =
|
||||
AidlErrorCodeV2
|
||||
.valueOf(code)
|
||||
.msg
|
||||
|
||||
LogUtil.d(
|
||||
TAG,
|
||||
"error code:$code - message :$msg"
|
||||
)
|
||||
|
||||
when (code) {
|
||||
|
||||
-60001 -> {
|
||||
handler.obtainMessage(
|
||||
ON_TIMEOUT_PIN_PAD
|
||||
).sendToTarget()
|
||||
}
|
||||
|
||||
-3025 -> {
|
||||
handler.obtainMessage(
|
||||
ON_ERROR_DUKPT
|
||||
).sendToTarget()
|
||||
}
|
||||
|
||||
else -> {
|
||||
handler.obtainMessage(
|
||||
ON_ERROR_PIN_PAD,
|
||||
code,
|
||||
code,
|
||||
code
|
||||
).sendToTarget()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPanBytes(pan: String): ByteArray {
|
||||
|
||||
LogUtil.d(TAG,"pan : $pan")
|
||||
if (pan.length < 13) {
|
||||
return ByteArray(0)
|
||||
}
|
||||
LogUtil.d(TAG,"pan bytes : ${ByteUtils.byteArrayToHexString(pan
|
||||
.substring(pan.length - 13, pan.length - 1)
|
||||
.toByteArray(StandardCharsets.US_ASCII))}")
|
||||
return pan
|
||||
.substring(pan.length - 13, pan.length - 1)
|
||||
.toByteArray(StandardCharsets.US_ASCII)
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt
Normal file
20
app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package com.mob.utsmyanmar.ui.preview
|
||||
|
||||
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Preview(
|
||||
name = "P2",
|
||||
device = "spec:width=720px,height=1440px,dpi=320",
|
||||
showBackground = true,
|
||||
showSystemUi = true
|
||||
)
|
||||
annotation class P2Preview
|
||||
|
||||
@Preview(
|
||||
name = "P3",
|
||||
device = "spec:width=427dp,height=949dp,dpi=270",
|
||||
showBackground = true,
|
||||
showSystemUi = true
|
||||
)
|
||||
annotation class P3Preview
|
||||
@ -0,0 +1,385 @@
|
||||
package com.mob.utsmyanmar.ui.print_receipt
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.history_functions.displayTransactionStatus
|
||||
import com.mob.utsmyanmar.ui.history_functions.displayTransactionTitle
|
||||
import com.mob.utsmyanmar.ui.preview.P2Preview
|
||||
import com.mob.utsmyanmar.ui.preview.P3Preview
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultState
|
||||
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel
|
||||
import com.mob.utsmyanmar.utils.convertDateString
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.PrintStatus
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private enum class ReceiptCopy(val label: String) {
|
||||
MERCHANT("MERCHANT COPY"),
|
||||
CUSTOMER("CUSTOMER COPY")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrintReceiptScreen(
|
||||
transactionResultViewModel: TransactionResultViewModel,
|
||||
sharedViewModel: SharedViewModel,
|
||||
onPrint: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
onPrintFinished: () -> Unit,
|
||||
) {
|
||||
val state by transactionResultViewModel.state.collectAsStateWithLifecycle()
|
||||
val isReprint = sharedViewModel.isReprint.value ?: false
|
||||
|
||||
PrintReceiptContent(
|
||||
state = state,
|
||||
isReprint = isReprint,
|
||||
onPrint = onPrint,
|
||||
onDone = onDone,
|
||||
onPrintFinished = onPrintFinished,
|
||||
onSetPrintStatus = { sharedViewModel.setPrintStatus(it) },
|
||||
onResetReprint = { sharedViewModel.isReprint.value = false },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrintReceiptContent(
|
||||
state: TransactionResultState,
|
||||
isReprint: Boolean,
|
||||
onPrint: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
onPrintFinished: () -> Unit,
|
||||
onSetPrintStatus: (PrintStatus) -> Unit,
|
||||
onResetReprint: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val receiptOffsetY = remember { Animatable(0f) }
|
||||
val merchant_copy_autoprint = true
|
||||
val customer_copy_autoprint = false
|
||||
var currentCopy by remember { mutableStateOf(ReceiptCopy.MERCHANT) }
|
||||
var nextCopy by remember { mutableStateOf(ReceiptCopy.CUSTOMER) }
|
||||
var isPrinting by remember { mutableStateOf(false) }
|
||||
|
||||
suspend fun animatePrint() {
|
||||
receiptOffsetY.animateTo(
|
||||
targetValue = -1400f,
|
||||
animationSpec = tween(durationMillis = 4000, easing = FastOutSlowInEasing)
|
||||
)
|
||||
// delay(1000.milliseconds)
|
||||
receiptOffsetY.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(durationMillis = 0, easing = FastOutSlowInEasing)
|
||||
)
|
||||
}
|
||||
|
||||
fun doPrint(copy: ReceiptCopy, onFinished: (printFinished: ReceiptCopy) -> Unit) {
|
||||
val actualCopy = if (isReprint && copy == ReceiptCopy.MERCHANT) ReceiptCopy.CUSTOMER else copy
|
||||
currentCopy = actualCopy
|
||||
|
||||
onSetPrintStatus(
|
||||
if (actualCopy == ReceiptCopy.MERCHANT)
|
||||
PrintStatus.FIRST_PRINT
|
||||
else
|
||||
PrintStatus.SECOND_PRINT
|
||||
)
|
||||
onPrint()
|
||||
scope.launch {
|
||||
isPrinting = true
|
||||
animatePrint()
|
||||
isPrinting = false
|
||||
val upcomingCopy = if (isReprint) ReceiptCopy.CUSTOMER
|
||||
else if (actualCopy == ReceiptCopy.MERCHANT) ReceiptCopy.CUSTOMER
|
||||
else ReceiptCopy.CUSTOMER
|
||||
|
||||
currentCopy = upcomingCopy
|
||||
nextCopy = upcomingCopy
|
||||
|
||||
onFinished(actualCopy)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (isReprint) {
|
||||
currentCopy = ReceiptCopy.CUSTOMER
|
||||
nextCopy = ReceiptCopy.CUSTOMER
|
||||
}
|
||||
onResetReprint()
|
||||
when {
|
||||
merchant_copy_autoprint && !isReprint -> doPrint(ReceiptCopy.MERCHANT) { }
|
||||
customer_copy_autoprint -> doPrint(ReceiptCopy.CUSTOMER) { }
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppBar(title = "Receipt")
|
||||
},
|
||||
containerColor = Color.IvoryBeige
|
||||
) { paddingValues ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
|
||||
ReceiptPreview(
|
||||
state = state,
|
||||
copyLabel = currentCopy.label,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.graphicsLayer {
|
||||
translationY = receiptOffsetY.value
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onDone,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
enabled = !isPrinting,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White.copy(alpha = 0.5f),
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
onClick = {
|
||||
doPrint(nextCopy) { printFinished ->
|
||||
if (printFinished == ReceiptCopy.CUSTOMER) {
|
||||
onPrintFinished()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Print",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ReceiptPreview(
|
||||
state: TransactionResultState,
|
||||
copyLabel: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isPreview = LocalInspectionMode.current
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color.Gray,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "MOB POS",
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Transaction Receipt",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = copyLabel,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Color.Gray
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ReceiptDivider()
|
||||
ReceiptRow("Date", convertDateString(state.payDetail?.transDate.toString()))
|
||||
ReceiptRow("Time", state.payDetail?.transTime.toString())
|
||||
ReceiptRow("Merchant Name", state.payDetail?.merchantName.toString())
|
||||
ReceiptRow("TID", state.payDetail?.terminalNo.toString())
|
||||
ReceiptRow("Trace No", state.payDetail?.voucherNo.toString())
|
||||
ReceiptRow("Ref No", state.payDetail?.referNo.toString())
|
||||
ReceiptRow("Approval Code", state.payDetail?.approvalCode.toString())
|
||||
ReceiptRow("Transaction Type", state.payDetail?.let(::displayTransactionTitle).orEmpty())
|
||||
ReceiptRow("Amount", POSUtil.getInstance().getDecimalAmountSeparatorFormat(state.payDetail?.amount?:0).let { amt ->
|
||||
val prefix = POSUtil.getInstance().checkMinusSign(state.payDetail?.transactionType ?: 0)
|
||||
if (prefix.isEmpty() && (state.payDetail?.transactionType ?: 0) == TransactionsType.CASH_OUT.value) "-$amt" else "$prefix$amt"
|
||||
})
|
||||
ReceiptRow("Card Type", state.payDetail?.accountType.toString())
|
||||
ReceiptRow("Card No", state.payDetail?.cardNo.toString())
|
||||
ReceiptRow("Status", if (isPreview) "SUCCESS" else state.payDetail?.let(::displayTransactionStatus).orEmpty())
|
||||
ReceiptDivider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Thank You",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = "Please keep this receipt",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReceiptRow(
|
||||
label: String,
|
||||
value: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReceiptDivider() {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp)
|
||||
.height(2.dp)
|
||||
) {
|
||||
// Convert dp to pixels so it scales perfectly on all screen sizes
|
||||
val dotRadius = 1.5.dp.toPx()
|
||||
val dotSpacing = 6.dp.toPx()
|
||||
|
||||
val totalWidth = size.width
|
||||
var currentX = 0f
|
||||
|
||||
// Loop across the width of the screen and draw individual circles
|
||||
while (currentX < totalWidth) {
|
||||
drawCircle(
|
||||
color = Color.Gray,
|
||||
radius = dotRadius,
|
||||
center = Offset(x = currentX, y = size.height / 2)
|
||||
)
|
||||
// Move X forward by the size of the dot + the space we want
|
||||
currentX += (dotRadius * 2) + dotSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@P2Preview
|
||||
@P3Preview
|
||||
@Composable
|
||||
private fun PrintReceiptScreenPreview() {
|
||||
val samplePayDetail = PayDetail().apply {
|
||||
merchantName = "Sample Merchant"
|
||||
terminalNo = "12345678"
|
||||
voucherNo = "000001"
|
||||
amount = 100000
|
||||
cardNo = "1234********5678"
|
||||
transDate = "25-08-26"
|
||||
transTime = "10:30:00"
|
||||
referNo = "123456789012"
|
||||
transType = "SALE"
|
||||
tradeAnswerCode = "00"
|
||||
approvalCode = "123456"
|
||||
}
|
||||
|
||||
val sampleState = TransactionResultState(
|
||||
payDetail = samplePayDetail
|
||||
)
|
||||
|
||||
MOBPOSTheme {
|
||||
PrintReceiptContent(
|
||||
state = sampleState,
|
||||
isReprint = false,
|
||||
onPrint = {},
|
||||
onDone = {},
|
||||
onPrintFinished = {},
|
||||
onSetPrintStatus = {},
|
||||
onResetReprint = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.mob.utsmyanmar.ui.processing_card
|
||||
|
||||
sealed interface ProcessingCardEvent {
|
||||
data object StartProcess : ProcessingCardEvent
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.mob.utsmyanmar.ui.processing_card
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
||||
@Composable
|
||||
fun ProcessingCardRoute(
|
||||
viewModel: ProcessingCardViewModel,
|
||||
onNavigatePinPad: () -> Unit,
|
||||
onNavigateInputAmount: () -> Unit,
|
||||
onNavigateProcessing: () -> Unit,
|
||||
onNavigateEmvTransaction: () -> Unit,
|
||||
onNavigateError: (String) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onShowDecline: (String) -> Unit
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.onEvent(ProcessingCardEvent.StartProcess)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.uiEvent.collect { event ->
|
||||
when (event) {
|
||||
ProcessingCardUiEvent.NavigateToPinPad -> onNavigatePinPad()
|
||||
ProcessingCardUiEvent.NavigateToInputAmount -> onNavigateInputAmount()
|
||||
ProcessingCardUiEvent.NavigateToProcessing -> onNavigateProcessing()
|
||||
ProcessingCardUiEvent.NavigateToEmvTransaction -> onNavigateEmvTransaction()
|
||||
is ProcessingCardUiEvent.NavigateToError -> onNavigateError(event.message)
|
||||
is ProcessingCardUiEvent.ShowDeclineAndBack -> {
|
||||
onShowDecline(event.message)
|
||||
onBack()
|
||||
}
|
||||
ProcessingCardUiEvent.Back -> onBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProcessingCardScreen(state = state)
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.mob.utsmyanmar.ui.processing_card
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ProcessingCardScreen(
|
||||
state: ProcessingCardState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
Text(text = state.message)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.mob.utsmyanmar.ui.processing_card
|
||||
|
||||
data class ProcessingCardState(
|
||||
val title: String = "Processing Card",
|
||||
val message: String = "Please wait...",
|
||||
val isLoading: Boolean = true
|
||||
)
|
||||
@ -0,0 +1,11 @@
|
||||
package com.mob.utsmyanmar.ui.processing_card
|
||||
|
||||
sealed interface ProcessingCardUiEvent {
|
||||
data object NavigateToPinPad : ProcessingCardUiEvent
|
||||
data object NavigateToInputAmount : ProcessingCardUiEvent
|
||||
data object NavigateToProcessing : ProcessingCardUiEvent
|
||||
data object NavigateToEmvTransaction : ProcessingCardUiEvent
|
||||
data class NavigateToError(val message: String) : ProcessingCardUiEvent
|
||||
data class ShowDeclineAndBack(val message: String) : ProcessingCardUiEvent
|
||||
data object Back : ProcessingCardUiEvent
|
||||
}
|
||||
@ -0,0 +1,284 @@
|
||||
package com.mob.utsmyanmar.ui.processing_card
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mob.utsmyanmar.model.CardTransactionType
|
||||
import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel
|
||||
import com.mob.utsmyanmar.utils.MockData
|
||||
import com.mob.utsmyanmar.utils.TransactionUtil
|
||||
import com.mob.utsmyanmar.viewmodel.CardReaderViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||
import com.utsmyanmar.checkxread.model.CardDataX
|
||||
import com.utsmyanmar.checkxread.readcard.MAGXReadCard
|
||||
import com.utsmyanmar.checkxread.readcard.MPUXReadCard
|
||||
import com.utsmyanmar.checkxread.readcard.ReadCardResultX
|
||||
import com.utsmyanmar.checkxread.util.CardTypeX
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.model.TradeData
|
||||
import com.utsmyanmar.paylibs.model.enums.TransCVM
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
|
||||
import com.utsmyanmar.paylibs.utils.enums.TransMenu
|
||||
import com.utsmyanmar.paylibs.utils.params.Params
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
class ProcessingCardViewModel(
|
||||
private val cardReadViewModel: CardReaderViewModel,
|
||||
private val sharedViewModel: SharedViewModel,
|
||||
private val transProcessViewModel: TransProcessViewModel,
|
||||
private val pinPadViewModel: PinPadViewModel,
|
||||
private val emvTransactionViewModel: EmvTransactionProcessViewModel
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = ProcessingCardViewModel::class.java.simpleName
|
||||
|
||||
fun provideFactory(
|
||||
cardReadViewModel: CardReaderViewModel,
|
||||
sharedViewModel: SharedViewModel,
|
||||
transProcessViewModel: TransProcessViewModel,
|
||||
pinPadViewModel: PinPadViewModel,
|
||||
emvTransactionViewModel: EmvTransactionProcessViewModel
|
||||
): ViewModelProvider.Factory {
|
||||
return object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ProcessingCardViewModel(
|
||||
cardReadViewModel = cardReadViewModel,
|
||||
sharedViewModel = sharedViewModel,
|
||||
transProcessViewModel = transProcessViewModel,
|
||||
pinPadViewModel = pinPadViewModel,
|
||||
emvTransactionViewModel = emvTransactionViewModel
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(ProcessingCardState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private val _uiEvent = Channel<ProcessingCardUiEvent>()
|
||||
val uiEvent = _uiEvent.receiveAsFlow()
|
||||
|
||||
fun onEvent(event: ProcessingCardEvent) {
|
||||
when (event) {
|
||||
ProcessingCardEvent.StartProcess -> {
|
||||
resetEmvTransResult()
|
||||
checkCardTransactionType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetEmvTransResult() {
|
||||
emvTransactionViewModel.resetTransactionStatus()
|
||||
}
|
||||
|
||||
private fun checkCardTransactionType() {
|
||||
when (cardReadViewModel.getCardTransactionType()) {
|
||||
CardTransactionType.MOCK -> mockCardData()
|
||||
CardTransactionType.MPU -> readMPUCard()
|
||||
CardTransactionType.EMV -> handlePreEmvProcess()
|
||||
CardTransactionType.FALLBACK -> readMAGStripe(
|
||||
isFallback = true,
|
||||
isNeedPin = true
|
||||
)
|
||||
|
||||
CardTransactionType.MAG -> readMAGStripe(
|
||||
isFallback = false,
|
||||
isNeedPin = true
|
||||
)
|
||||
|
||||
else -> {
|
||||
Log.d(
|
||||
"ProcessingCardViewModel",
|
||||
"error on CardTransactionType : ${cardReadViewModel.getCardTransactionType()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockCardData() {
|
||||
sharedViewModel.setEmvTrans(false)
|
||||
|
||||
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
pinPadViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
|
||||
val cardDataX = MockData.generateMPUCard();
|
||||
|
||||
val tradeData = TransactionUtil.initMPUTransaction(cardDataX, CardTypeX.IC)
|
||||
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
pinPadViewModel.setTradeData(tradeData)
|
||||
sharedViewModel.setCardDataExist(true)
|
||||
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToPinPad)
|
||||
}
|
||||
private fun readMPUCard() {
|
||||
cardReadViewModel.startReadXProcess(
|
||||
MPUXReadCard.getInstance(),
|
||||
object : ReadCardResultX {
|
||||
override fun onSuccess(cardDataX: CardDataX) {
|
||||
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
pinPadViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
val tradeData = TransactionUtil.initMPUTransaction(cardDataX, CardTypeX.IC)
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
pinPadViewModel.setTradeData(tradeData)
|
||||
|
||||
when {
|
||||
sharedViewModel._transMenu.value == TransMenu.PRE_AUTH_PARTIAL_VOID -> {
|
||||
sendUiEvent(
|
||||
ProcessingCardUiEvent.NavigateToError(
|
||||
"Function not supported"
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.setCardDataExist(true)
|
||||
|
||||
if (sharedViewModel.getAmountExist().value == false) {
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToInputAmount)
|
||||
} else {
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToPinPad)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(code: Int, message: String) {
|
||||
LogUtil.d(TAG, "Failure at $code message: $message")
|
||||
sharedViewModel.setCardDataExist(false)
|
||||
|
||||
sendUiEvent(
|
||||
ProcessingCardUiEvent.ShowDeclineAndBack(
|
||||
"FAILURE :$message"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun readMAGStripe(
|
||||
isFallback: Boolean,
|
||||
isNeedPin: Boolean
|
||||
) {
|
||||
cardReadViewModel.startReadXProcess(
|
||||
MAGXReadCard.getInstance(),
|
||||
object : ReadCardResultX {
|
||||
override fun onSuccess(cardDataX: CardDataX) {
|
||||
sharedViewModel.setEmvTrans(false)
|
||||
|
||||
if (isNeedPin) {
|
||||
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
pinPadViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
|
||||
val tradeData = TransactionUtil.initMagStripeTransaction(cardDataX, isFallback)
|
||||
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
pinPadViewModel.setTradeData(tradeData)
|
||||
} else {
|
||||
transProcessViewModel.setTransType(sharedViewModel.transactionsType.value)
|
||||
|
||||
val tradeData = TransactionUtil.initMagStripeTransaction(cardDataX, isFallback)
|
||||
val processCode = sharedViewModel.processCode.value
|
||||
val amount = sharedViewModel.amount.value
|
||||
val payDetail: PayDetail = tradeData.payDetail
|
||||
|
||||
payDetail.amount = POSUtil.getInstance().convertAmount(amount)
|
||||
|
||||
if (!TextUtils.equals(processCode, "")) {
|
||||
payDetail.processCode = processCode
|
||||
}
|
||||
|
||||
payDetail.transCVM = TransCVM.SIGNATURE
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
}
|
||||
|
||||
when {
|
||||
sharedViewModel.getTransMenu().value == TransMenu.PRE_AUTH_PARTIAL_VOID -> {
|
||||
sharedViewModel.setTransMenu(null)
|
||||
sendUiEvent(
|
||||
ProcessingCardUiEvent.NavigateToError(
|
||||
"Function not supported"
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.setCardDataExist(true)
|
||||
|
||||
if (
|
||||
sharedViewModel.getTransMenu().value != TransMenu.PRE_AUTH_FULL_VOID &&
|
||||
sharedViewModel.getAmountExist().value == false
|
||||
) {
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToInputAmount)
|
||||
} else {
|
||||
if (isNeedPin) {
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToPinPad)
|
||||
} else {
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToProcessing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(code: Int, message: String) {
|
||||
LogUtil.d(TAG, "Failure at $code message: $message")
|
||||
|
||||
sendUiEvent(
|
||||
ProcessingCardUiEvent.ShowDeclineAndBack(
|
||||
"FAILURE :$message"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePreEmvProcess() {
|
||||
emvTransactionViewModel.transType.value =
|
||||
sharedViewModel.transactionsType.value
|
||||
|
||||
if (SystemParamsOperation.getInstance().isEmvEnabled) {
|
||||
prepareEmvTransaction()
|
||||
sendUiEvent(ProcessingCardUiEvent.NavigateToEmvTransaction)
|
||||
} else {
|
||||
sendUiEvent(
|
||||
ProcessingCardUiEvent.NavigateToError(
|
||||
"Please enable EMV"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareEmvTransaction() {
|
||||
sharedViewModel.setEmvTrans(true)
|
||||
|
||||
val cardType = cardReadViewModel.cardTypeData.value ?: return
|
||||
|
||||
val tradeData: TradeData = Params.newTrade(false)
|
||||
val payDetail = tradeData.payDetail
|
||||
|
||||
payDetail.cardType = cardType
|
||||
|
||||
emvTransactionViewModel.setTradeData(tradeData)
|
||||
}
|
||||
|
||||
private fun sendUiEvent(event: ProcessingCardUiEvent) {
|
||||
viewModelScope.launch {
|
||||
_uiEvent.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,407 @@
|
||||
package com.mob.utsmyanmar.ui.refund_rrn
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.model.TradeData
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
private const val RRN_MAX_LENGTH = 12
|
||||
|
||||
@Composable
|
||||
fun InputRrnRoute(
|
||||
transProcessViewModel: TransProcessViewModel,
|
||||
rrnViewModel: RRNViewModel,
|
||||
sharedViewModel: SharedViewModel,
|
||||
onBack: () -> Unit,
|
||||
onNavigateProcessing: () -> Unit
|
||||
) {
|
||||
var rrn by rememberSaveable {
|
||||
mutableStateOf(sharedViewModel.rrNNo.value.orEmpty())
|
||||
}
|
||||
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
rrn = ""
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
val payDetailState by rrnViewModel.payDetailState.collectAsStateWithLifecycle()
|
||||
|
||||
fun processPreAuthComp(payDetail: PayDetail) {
|
||||
val newPay = transProcessViewModel.payDetail
|
||||
newPay?.setReferNo(payDetail.referNo)
|
||||
newPay?.setApprovalCode(payDetail.getApprovalCode())
|
||||
newPay?.setTradeTime(payDetail.getTradeTime())
|
||||
newPay?.setTradeDate(payDetail.getTradeDate())
|
||||
|
||||
val tradeData = TradeData()
|
||||
tradeData.payDetail = newPay
|
||||
transProcessViewModel.setOldTransPayDetail(payDetail)
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
|
||||
rrnViewModel.resetState()
|
||||
onNavigateProcessing()
|
||||
}
|
||||
|
||||
fun processPreAuthVoid(payDetail: PayDetail) {
|
||||
val newPay = transProcessViewModel.payDetail
|
||||
newPay?.setReferNo(payDetail.referNo)
|
||||
|
||||
val tradeData = TradeData()
|
||||
tradeData.payDetail = newPay
|
||||
transProcessViewModel.setOldTransPayDetail(payDetail)
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
|
||||
rrnViewModel.resetState()
|
||||
onNavigateProcessing()
|
||||
}
|
||||
|
||||
LaunchedEffect(payDetailState) {
|
||||
LogUtil.d("RRN", "paydetail state: $payDetailState")
|
||||
when (val state = payDetailState) {
|
||||
is RRNViewModel.PayDetailState.Success -> {
|
||||
sharedViewModel.payDetail.value = state.data
|
||||
sharedViewModel.rrNNo.value = state.data.referNo
|
||||
|
||||
if (sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_COMPLETE) {
|
||||
processPreAuthComp(state.data)
|
||||
} else if (sharedViewModel.transactionsType.value == TransactionsType.PRE_AUTH_VOID) {
|
||||
processPreAuthVoid(state.data)
|
||||
}
|
||||
}
|
||||
|
||||
is RRNViewModel.PayDetailState.Error -> {
|
||||
errorMessage = state.message
|
||||
rrnViewModel.resetState()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
InputRrnContent(
|
||||
rrn = rrn,
|
||||
errorMessage = errorMessage,
|
||||
onRrnChange = { value ->
|
||||
rrn = if (value == "Del") {
|
||||
rrn.dropLast(1)
|
||||
} else {
|
||||
appendRrnValue(rrn, value)
|
||||
}
|
||||
errorMessage = null
|
||||
},
|
||||
onBack = onBack,
|
||||
onNext = {
|
||||
val trimmedRrn = rrn.trim()
|
||||
|
||||
if (POSUtil.getInstance().checkNumberField(trimmedRrn)) {
|
||||
errorMessage = "Invalid RRN"
|
||||
return@InputRrnContent
|
||||
}
|
||||
|
||||
sharedViewModel.rrNNo.value = trimmedRrn
|
||||
|
||||
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND) {
|
||||
val newPay = transProcessViewModel.payDetail
|
||||
newPay?.setReferNo(trimmedRrn)
|
||||
val tradeData = TradeData()
|
||||
tradeData.payDetail = newPay
|
||||
transProcessViewModel.setTradeData(tradeData)
|
||||
rrnViewModel.resetState()
|
||||
onNavigateProcessing()
|
||||
} else {
|
||||
rrnViewModel.searchPayDetail(
|
||||
trimmedRrn,
|
||||
sharedViewModel.payDetail.value?.cardNo ?: ""
|
||||
)
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
if (rrn.isNotEmpty()) {
|
||||
rrn = rrn.dropLast(1)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InputRrnContent(
|
||||
rrn: String,
|
||||
errorMessage: String?,
|
||||
onRrnChange: (String) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onNext: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Input RRN",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Enter RRN",
|
||||
color = Color.Gray,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = rrn.ifEmpty { "0" },
|
||||
color = Color.LegacyRed,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = errorMessage ?: "Enter the retrieval reference number to continue",
|
||||
color = if (errorMessage == null) Color.Gray else Color.Error,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
RrnKeypad(
|
||||
onKeyClick = onRrnChange,
|
||||
canDelete = rrn.isNotEmpty()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 18.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onNext,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
enabled = rrn.isNotBlank(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Next")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Card(
|
||||
// modifier = Modifier
|
||||
// .align(Alignment.TopEnd)
|
||||
// .padding(top = 110.dp, end = 16.dp)
|
||||
// .clickable(enabled = rrn.isNotEmpty()) {
|
||||
// onDelete()
|
||||
// },
|
||||
// shape = RoundedCornerShape(18.dp),
|
||||
// colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
// elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
// ) {
|
||||
// androidx.compose.material3.Icon(
|
||||
// imageVector = Icons.Rounded.Backspace,
|
||||
// contentDescription = "Delete",
|
||||
// tint = if (rrn.isNotEmpty()) Color.LegacyRed else Color.Gray,
|
||||
// modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RrnKeypad(
|
||||
onKeyClick: (String) -> Unit,
|
||||
canDelete: Boolean
|
||||
) {
|
||||
val keys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("Del", "0", "00")
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
keys.forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
row.forEach { key ->
|
||||
RrnKeypadButton(
|
||||
text = key,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = key.isNotBlank() && (key != "Del" || canDelete),
|
||||
onClick = { onKeyClick(key) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RrnKeypadButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(66.dp)
|
||||
.shadow(
|
||||
elevation = 2.dp,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
clip = false
|
||||
)
|
||||
.background(
|
||||
color = Color.White,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.clickable(enabled = enabled) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = if (enabled) Color.LegacyRed else Color.Gray,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendRrnValue(current: String, value: String): String {
|
||||
if (value.isBlank()) {
|
||||
return current
|
||||
}
|
||||
|
||||
val remainingDigits = RRN_MAX_LENGTH - current.length
|
||||
if (remainingDigits <= 0) {
|
||||
return current
|
||||
}
|
||||
|
||||
return current + value.take(remainingDigits)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun InputRrnPreview() {
|
||||
MOBPOSTheme {
|
||||
InputRrnContent(
|
||||
rrn = "123456789012",
|
||||
errorMessage = null,
|
||||
onRrnChange = {},
|
||||
onBack = {},
|
||||
onNext = {},
|
||||
onDelete = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun InputRrnErrorPreview() {
|
||||
MOBPOSTheme {
|
||||
InputRrnContent(
|
||||
rrn = "123",
|
||||
errorMessage = "Invalid RRN",
|
||||
onRrnChange = {},
|
||||
onBack = {},
|
||||
onNext = {},
|
||||
onDelete = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.mob.utsmyanmar.ui.refund_rrn
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.utsmyanmar.baselib.repo.Repository
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.system.SingleLiveEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class RRNViewModel @Inject constructor(
|
||||
private val repository: Repository
|
||||
) : ViewModel() {
|
||||
|
||||
sealed class PayDetailState {
|
||||
object Idle : PayDetailState()
|
||||
object Loading : PayDetailState()
|
||||
data class Success(val data: PayDetail) : PayDetailState()
|
||||
data class Error(val message: String) : PayDetailState()
|
||||
}
|
||||
|
||||
private val _payDetailState = MutableStateFlow<PayDetailState>(PayDetailState.Idle)
|
||||
val payDetailState: StateFlow<PayDetailState> = _payDetailState
|
||||
|
||||
fun searchPayDetail(refNo: String, cardNo: String) {
|
||||
_payDetailState.value = PayDetailState.Loading
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
repository.searchPayDetail(cardNo, refNo)
|
||||
.asFlow()
|
||||
.filterNotNull()
|
||||
.first()
|
||||
.let { result ->
|
||||
_payDetailState.value = PayDetailState.Success(result)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_payDetailState.value = PayDetailState.Error(e.message ?: "Search failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetState() {
|
||||
_payDetailState.value = PayDetailState.Idle
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
package com.mob.utsmyanmar.ui.sale_void
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.utils.convertDateString
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.POSUtil
|
||||
|
||||
@Composable
|
||||
fun TranDetailPage(
|
||||
voidViewModel: VoidViewModel,
|
||||
trace: String,
|
||||
onBack: () -> Unit,
|
||||
onProceedVoid: (PayDetail) -> Unit
|
||||
) {
|
||||
val transaction by voidViewModel.searchTransaction(trace).observeAsState()
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Transaction Detail",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
transaction?.let { payDetail ->
|
||||
TransactionDetailsCard(transaction = payDetail)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Review the transaction before proceeding to void.",
|
||||
color = Color.Gray,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text("Back")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { onProceedVoid(payDetail) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Void")
|
||||
}
|
||||
}
|
||||
} ?: Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Transaction not found.",
|
||||
color = Color.Gray,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransactionDetailsCard(transaction: PayDetail) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Void Sale",
|
||||
color = Color.LegacyRed,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
DetailRow("Date", convertDateString(transaction.transDate))
|
||||
DetailRow("Time", transaction.transTime)
|
||||
DetailRow("Amount", POSUtil.getInstance().formatAmount(transaction.amount))
|
||||
DetailRow("Trace No", transaction.voucherNo)
|
||||
DetailRow("Reference No", transaction.referNo)
|
||||
DetailRow("Approval Code", transaction.approvalCode.orEmpty())
|
||||
// DetailRow("Card No", transaction.cardNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(
|
||||
label: String,
|
||||
value: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = Color.Gray,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(
|
||||
text = value.ifBlank { "-" },
|
||||
color = Color.Black,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,267 @@
|
||||
package com.mob.utsmyanmar.ui.sale_void
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.Backspace
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.mob.utsmyanmar.ui.components.NumericKeypad
|
||||
import com.mob.utsmyanmar.ui.components.appbar.AppBar
|
||||
import com.mob.utsmyanmar.ui.theme.Color
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
import sunmi.sunmiui.utils.LogUtil
|
||||
|
||||
private val traceKeys = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("", "0", "00")
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun VoidTraceScreen(
|
||||
sharedViewModel: SharedViewModel,
|
||||
voidViewModel: VoidViewModel,
|
||||
onNavigateTranDetail: (String) -> Unit,
|
||||
onBack: () -> Unit = {}
|
||||
) {
|
||||
val tag = "VoidTraceScreen"
|
||||
var traceNumber by remember { mutableStateOf("") }
|
||||
var searchedTrace by remember { mutableStateOf("") }
|
||||
var lastNavigatedTrace by remember { mutableStateOf<String?>(null) }
|
||||
val displayTraceNumber = traceNumber.padStart(6, '0').ifEmpty { "000000" }
|
||||
val recentTransactions by voidViewModel.getLastThreeTransactions().observeAsState(emptyList())
|
||||
|
||||
val transactionSource: LiveData<PayDetail?> = remember(searchedTrace) {
|
||||
val transType = if(sharedViewModel.transactionsType.value == TransactionsType.VOID) {
|
||||
TransactionsType.SALE.value
|
||||
} else {
|
||||
TransactionsType.PRE_AUTH_COMPLETE.value;
|
||||
}
|
||||
LogUtil.d("void screen","transaction type : $transType and trace number : $searchedTrace")
|
||||
if (searchedTrace.isBlank()) {
|
||||
MutableLiveData<PayDetail?>(null)
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
voidViewModel.getVoidTrans(
|
||||
transType,
|
||||
searchedTrace
|
||||
) as LiveData<PayDetail?>
|
||||
}
|
||||
}
|
||||
val transaction by transactionSource.observeAsState()
|
||||
val hasSearched = searchedTrace.isNotBlank()
|
||||
val headerMessage = if (hasSearched && transaction == null) {
|
||||
"Warning: no previous sale transaction found for trace $searchedTrace."
|
||||
} else {
|
||||
"Input trace number to find previous sale transaction."
|
||||
}
|
||||
val headerColor = if (hasSearched && transaction == null) {
|
||||
Color.LegacyRed
|
||||
} else {
|
||||
Color.Black
|
||||
}
|
||||
|
||||
LaunchedEffect(searchedTrace) {
|
||||
if (searchedTrace != lastNavigatedTrace) {
|
||||
lastNavigatedTrace = null
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(transaction?.voucherNo) {
|
||||
val matchedTrace = transaction?.voucherNo ?: return@LaunchedEffect
|
||||
if (lastNavigatedTrace == matchedTrace) return@LaunchedEffect
|
||||
lastNavigatedTrace = matchedTrace
|
||||
onNavigateTranDetail(matchedTrace)
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(searchedTrace, transaction) {
|
||||
Log.d(
|
||||
tag,
|
||||
"Recent DB traces: ${
|
||||
recentTransactions.joinToString { detail ->
|
||||
"${detail.voucherNo}:${detail.transType}:${detail.tradeAnswerCode}"
|
||||
}
|
||||
}"
|
||||
)
|
||||
if (searchedTrace.isNotBlank()) {
|
||||
Log.d(
|
||||
tag,
|
||||
"Search trace=$searchedTrace result=${transaction?.voucherNo ?: "NOT_FOUND"}"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.IvoryBeige,
|
||||
topBar = {
|
||||
AppBar(
|
||||
title = "Void",
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
onIconClick = onBack
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(2f)
|
||||
) {
|
||||
Text(
|
||||
text = headerMessage,
|
||||
color = headerColor,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Trace Number",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = displayTraceNumber,
|
||||
color = Color.LegacyRed,
|
||||
style = MaterialTheme.typography.displayLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Enter 6-digit trace number",
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.clickable(enabled = traceNumber.isNotEmpty()) {
|
||||
traceNumber = traceNumber.dropLast(1)
|
||||
},
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
||||
) {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Icons.Rounded.Backspace,
|
||||
contentDescription = "Delete",
|
||||
tint = if (traceNumber.isNotEmpty()) Color.LegacyRed else Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(3f),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
NumericKeypad(
|
||||
keys = traceKeys,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onKeyClick = { value ->
|
||||
if (traceNumber.length < 6) {
|
||||
traceNumber = (traceNumber + value).take(6)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onBack,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color.LegacyRed
|
||||
)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
searchedTrace = traceNumber.padStart(6, '0')
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
enabled = traceNumber.isNotBlank(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LegacyRed,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
|
||||
disabledContentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text("Enter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.mob.utsmyanmar.ui.sale_void
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.utsmyanmar.baselib.repo.Repository
|
||||
import com.utsmyanmar.paylibs.model.PayDetail
|
||||
import com.utsmyanmar.paylibs.system.SingleLiveEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class VoidViewModel @Inject constructor(
|
||||
private val repository: Repository
|
||||
) : ViewModel() {
|
||||
|
||||
val inputTrace = SingleLiveEvent<String>()
|
||||
val lists = SingleLiveEvent<List<PayDetail>>()
|
||||
|
||||
fun getVoidTransactions(transType: Int): LiveData<List<PayDetail>> {
|
||||
return repository.getVoidableTransactions(transType)
|
||||
}
|
||||
|
||||
fun getGenericVoidTransactions(
|
||||
transType: Int,
|
||||
voucherNo: String,
|
||||
isEmv: Boolean
|
||||
): LiveData<PayDetail> {
|
||||
return repository.getGenericVoidTransaction(transType, voucherNo, isEmv)
|
||||
}
|
||||
|
||||
fun getVoidTrans(transType: Int, voucherNo: String): LiveData<PayDetail> {
|
||||
return repository.getVoidTransaction(transType, voucherNo)
|
||||
}
|
||||
|
||||
fun observeVoidTrans(transType: Int) {
|
||||
getVoidTransactions(transType).observeForever(object : Observer<List<PayDetail>> {
|
||||
override fun onChanged(payDetails: List<PayDetail>) {
|
||||
if (lists.value == null) {
|
||||
lists.postValue(payDetails)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun searchTransaction(voucherNo: String): LiveData<PayDetail> {
|
||||
return repository.searchTransaction(voucherNo)
|
||||
}
|
||||
|
||||
fun getLastThreeTransactions(): LiveData<List<PayDetail>> {
|
||||
return repository.getLastThreeTransactions()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.mob.utsmyanmar.ui.sending_to_host
|
||||
|
||||
data class ProcessingState(
|
||||
val title: String = "Processing",
|
||||
)
|
||||
@ -0,0 +1,109 @@
|
||||
package com.mob.utsmyanmar.ui.sending_to_host
|
||||
|
||||
import android.R
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.mob.utsmyanmar.model.TransResultStatus
|
||||
import com.mob.utsmyanmar.model.TransactionStatus
|
||||
import com.mob.utsmyanmar.ui.settlement.SettlementViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.SharedViewModel
|
||||
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
|
||||
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
|
||||
|
||||
private const val MOCK_HOST_DELAY_MS = 2000L
|
||||
|
||||
@Composable
|
||||
fun ProcessingRoute(
|
||||
sharedViewModel: SharedViewModel,
|
||||
transProcessViewModel: TransProcessViewModel,
|
||||
settlementViewModel: SettlementViewModel,
|
||||
onNavigateTransactionResult: () -> Unit,
|
||||
onNavigateSignature: () -> Unit,
|
||||
onNavigateError: (Boolean, String) -> Unit,
|
||||
onTransactionFinished: () -> Unit
|
||||
) {
|
||||
|
||||
val transStatus by transProcessViewModel.transResultStatus.collectAsStateWithLifecycle()
|
||||
|
||||
val state by transProcessViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val settlementStatus by settlementViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
|
||||
LaunchedEffect(Unit) {
|
||||
settlementViewModel.startSettlementProcess()
|
||||
}
|
||||
|
||||
LaunchedEffect(settlementStatus.status) {
|
||||
when(settlementStatus.status) {
|
||||
TransactionStatus.ON_ERROR,
|
||||
TransactionStatus.ON_SUCCESS -> {
|
||||
onTransactionFinished()
|
||||
sharedViewModel.payDetail.value = settlementViewModel.getPayDetail()
|
||||
onNavigateTransactionResult()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
transProcessViewModel.startOnlineProcess()
|
||||
}
|
||||
|
||||
LaunchedEffect(transStatus) {
|
||||
when(transStatus) {
|
||||
TransResultStatus.SUCCESS -> {
|
||||
onTransactionFinished()
|
||||
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
||||
if (requiresSignature(sharedViewModel.transactionsType.value, sharedViewModel.payDetail.value)) {
|
||||
onNavigateSignature()
|
||||
} else {
|
||||
onNavigateTransactionResult()
|
||||
}
|
||||
}
|
||||
|
||||
TransResultStatus.FAIL -> {
|
||||
onTransactionFinished()
|
||||
sharedViewModel.payDetail.value = transProcessViewModel.payDetailResult.value
|
||||
onNavigateTransactionResult()
|
||||
}
|
||||
TransResultStatus.REVERSAL_SUCCESS -> {
|
||||
onTransactionFinished()
|
||||
onNavigateError(true,"Reversal success")
|
||||
}
|
||||
|
||||
TransResultStatus.REVERSAL_FAIL -> {
|
||||
onTransactionFinished()
|
||||
onNavigateError(false,"Reversal failed")
|
||||
}
|
||||
|
||||
TransResultStatus.ERROR -> {
|
||||
onTransactionFinished()
|
||||
onNavigateError(false,
|
||||
transProcessViewModel.errorMessage.value?.takeIf { it.isNotBlank() }
|
||||
?: "Something went wrong. Please try again."
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(sharedViewModel.transactionsType.value == TransactionsType.SETTLEMENT) {
|
||||
ProcessingScreen(settlementStatus.processingState)
|
||||
} else {
|
||||
ProcessingScreen(state)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun requiresSignature(transType: TransactionsType?, payDetail: com.utsmyanmar.paylibs.model.PayDetail?): Boolean {
|
||||
if (transType == TransactionsType.VOID || transType == TransactionsType.PRE_AUTH_COMPLETE_VOID || transType == TransactionsType.PRE_AUTH_VOID) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user