Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4afc3a59d4 | ||
|
|
309518f3ca | ||
|
|
c986b69e16 | ||
|
|
9a4f68455d | ||
|
|
dd87f1ca6e | ||
|
|
eae0342ab8 | ||
|
|
c7bc554eca | ||
|
|
647e72aa9b | ||
|
|
89c7d04f7a | ||
|
|
f700881788 | ||
|
|
7ec2d3916a | ||
|
|
6caaf0890e | ||
|
|
9bfb429392 | ||
|
|
317dec1084 | ||
|
|
be3c25bbf5 | ||
|
|
ff65559b4c | ||
|
|
092c0e9a16 | ||
|
|
7875db4c7d | ||
|
|
b32f76c753 | ||
|
|
3fd34becb6 | ||
|
|
a99f7b0572 | ||
|
|
7cb9039a50 | ||
|
|
d7bef7b827 | ||
|
|
1c9022cc5e | ||
|
|
df96f33d5f | ||
|
|
bd23c31ba1 | ||
|
|
0900dc20f9 | ||
|
|
35af9fec8f | ||
|
|
eb0b9c8271 | ||
|
|
8e02e7adeb | ||
|
|
a80bb4dd85 | ||
|
|
92553d96d2 | ||
|
|
c7134fe712 | ||
|
|
a10a51ec10 | ||
|
|
0a19e965a8 | ||
|
|
e331e399bc | ||
|
|
5b8a0d7cf6 | ||
|
|
2e70c16aa3 | ||
|
|
318bfde1fb | ||
|
|
fdf02189fb | ||
|
|
73dee29485 | ||
|
|
a33c0c97bd | ||
|
|
39245a0608 | ||
|
|
f6028ffb80 | ||
|
|
79e1f69325 | ||
|
|
8d2adee5ab | ||
|
|
b0f7381832 | ||
|
|
5af9a94a0c | ||
|
|
ee732e03a3 | ||
|
|
fc21186a53 | ||
|
|
2683b850a2 | ||
|
|
e8b3185b8a | ||
|
|
018da036b1 | ||
|
|
3b1764b75c | ||
|
|
b56c539549 | ||
|
|
42947dce0c | ||
|
|
1d4c4ab071 | ||
|
|
998b3ae89f | ||
|
|
33d79076a6 | ||
|
|
73e2b55c53 | ||
|
|
696e6cb952 | ||
|
|
d4fe3c6cf4 | ||
|
|
c24f409674 | ||
|
|
3d798eb94d | ||
|
|
ab3b53da99 | ||
|
|
2c3255cb64 | ||
|
|
275d6d1fa9 | ||
|
|
588da4a2eb | ||
|
|
53ea3dafa9 | ||
|
|
1720d2007e | ||
|
|
f10f6f5879 | ||
|
|
e8856d0a8b | ||
|
|
2315a32f56 | ||
|
|
402490ee58 | ||
|
|
cfc802dbb5 | ||
|
|
404ba69fba | ||
|
|
8e4a0993c7 | ||
|
|
4d7717edc3 | ||
|
|
5f480d6cb6 | ||
|
|
b1732987ce | ||
|
|
d917f6445c | ||
|
|
d1bf7d18ce | ||
|
|
f2ed393686 | ||
|
|
907d5095d5 | ||
|
|
28a3d623f0 | ||
|
|
2e71cc29b8 | ||
|
|
8f6b5217d6 | ||
|
|
5d9c5ba5cf | ||
|
|
a933bb22f6 | ||
|
|
aed8d9bc1f | ||
|
|
54e0c9ffa5 | ||
|
|
02ae6623d9 | ||
|
|
12e61d2a10 | ||
|
|
c7d75a8522 | ||
|
|
c7b8372dfc |
@ -28,8 +28,8 @@ trigger: always_on
|
||||
依據公司標準規範,實體機台發佈之 APK 與版本名稱需嚴格符合以下欄位對照:
|
||||
|
||||
```
|
||||
來源 _ 安卓號 _ 主板號 _ 機器系列 _ 專案名稱 _ 螢幕大小 _ 次版號 _ 修補版號 _ 環境
|
||||
S _ 12 _ XY _ U _ Chengwai _ 10 _ 08 _ 8 _ R
|
||||
來源 _ 安卓號 _ 主板號 _ 機器系列 _ 螢幕大小 _ 次版號 _ 修補版號 _ 環境
|
||||
S _ 12 _ XY _ U _ 10 _ 08 _ 8 _ R
|
||||
```
|
||||
|
||||
### 欄位解析:
|
||||
@ -37,7 +37,6 @@ S _ 12 _ XY _ U _ Chengwai _ 10 _ 08 _ 8 _ R
|
||||
* **`12` (安卓號)**:Android 系統主版本號。
|
||||
* **`XY` (主板號)**:主板型號規格。
|
||||
* **`U` (機器系列 / 機型規格)**:例如代表 U 系列機型。
|
||||
* **`Chengwai` (專案名稱)**:客戶/銷售場景專案代碼(如 `General`, `Chengwai`, `Cmuh`)。
|
||||
* **`10` (螢幕大小)**:對應 Gradle 中的 `verMajor`。
|
||||
* **`08` (次版號)**:對應 Gradle 中的 `verMinor`(格式化為雙位數)。
|
||||
* **`8` (修補版號)**:對應 Gradle 中的 `verPatch`。
|
||||
@ -55,38 +54,32 @@ S _ 12 _ XY _ U _ Chengwai _ 10 _ 08 _ 8 _ R
|
||||
|
||||
---
|
||||
|
||||
## 4. 雙維度產品風味 (Product Flavors) 與 APK 命名規範
|
||||
## 4. 單維度硬體規格 (Hardware Flavor) 與 APK 命名規範
|
||||
|
||||
本專案採用**雙維度 Flavor** 架構,以完美分離「客戶銷售場景」與「底層硬體規格」,方便後續快速擴充。
|
||||
本專案原本採用雙維度,但目前客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發)動態決定,因此不再使用 flavor 分家。
|
||||
目前僅保留**單一維度 `hardware`**,專門處理底層硬體規格差異。
|
||||
|
||||
### 4.1 雙維度定義 (flavorDimensions)
|
||||
本專案於 `app/build.gradle` 中定義了兩個 Flavor 維度:
|
||||
### 4.1 硬體維度定義 (flavorDimensions)
|
||||
本專案於 `app/build.gradle` 中定義了唯一的 Flavor 維度 `hardware`:
|
||||
|
||||
1. **`project` 維度 (客戶/銷售場景區隔)**:
|
||||
* **`General`** (通用主幹):預設顯示付款方式選單(彈出 `BuyDialog`)。未來新客戶最常用此通用主幹。
|
||||
* **`Chengwai`** (晟崴客製):直接跳出員工卡刷卡畫面(彈出 `MemberCardPickupDialog`),不顯示付款選單。
|
||||
* **`Cmuh`** (中國醫客製):支援 QR Code 掃碼、處方籤醫令離線 AES 解密與解析領藥流程。
|
||||
2. **`hardware` 維度 (硬體與系統規格區隔)**:
|
||||
* **`S12XYU`**:代表 `S` (來源) + `12` (安卓號) + `XY` (主板號) + `U` (機器系列/機型規格)。
|
||||
* **`S9XYU`**:代表中國醫現場使用的 Android 9 / 21.5 吋硬體規格;除非明確指定中國醫 10 吋或 `S12XYU`,中國醫 (`Cmuh`) 相關修正與驗證預設必須使用此硬體 flavor。
|
||||
* **`S12XYU`** (10 吋預設硬體):代表 `S` (來源) + `12` (安卓號) + `XY` (主板號) + `U` (機器系列/機型規格)。
|
||||
* **`S9XYU`** (21.5 吋硬體):代表中國醫現場使用的 Android 9 / 21.5 吋硬體規格。
|
||||
|
||||
### 4.2 Flavor 驗證與編譯指令選擇
|
||||
|
||||
針對特定客戶或現場問題修改後,必須編譯對應的 Gradle variant,不可只用通用 `assembleDebug` 或錯誤硬體 flavor 代替。
|
||||
修改後,必須編譯對應硬體規格的 Gradle variant,不可只用通用 `assembleDebug`:
|
||||
|
||||
* **中國醫現場版 (`Cmuh`) 驗證指令**:`./gradlew :app:assembleCmuhS9XYUDebug --no-daemon`
|
||||
* **晟崴版 (`Chengwai`) 預設驗證指令**:`./gradlew :app:assembleChengwaiS12XYUDebug --no-daemon`
|
||||
* **通用版 (`General`) 預設驗證指令**:`./gradlew :app:assembleGeneralS12XYUDebug --no-daemon`
|
||||
* **10 吋一般機台版 (含晟崴、通用等)**:`./gradlew :app:assembleS12XYUDebug --no-daemon`
|
||||
* **21.5 吋中國醫等大螢幕機台版**:`./gradlew :app:assembleS9XYUDebug --no-daemon`
|
||||
|
||||
### 4.3 實體機台 APK 命名鐵律
|
||||
為了符合實體機台 OTA 平台發佈規範,打包輸出的 APK 檔名必須嚴格連動雙維度,並遵循以下格式:
|
||||
`app-[格式化硬體規格]_[專案名稱]-[版本號]-[BuildType].apk`
|
||||
為了符合實體機台 OTA 平台發佈規範,打包輸出的 APK 檔名必須遵循以下格式:
|
||||
`app-[格式化硬體規格]-[版本號]-[BuildType].apk`
|
||||
|
||||
* **格式化規則**:`S12XYU` 會在 Gradle 打包時被自動展開格式化為 `S_12_XY_U`。
|
||||
* **輸出檔名範例**:
|
||||
* `app-S_12_XY_U_General-10_08_8_R-release.apk`
|
||||
* `app-S_12_XY_U_Chengwai-10_08_8_R-release.apk`
|
||||
* `app-S_12_XY_U_Cmuh-10_08_8_R-release.apk`
|
||||
* `app-S_12_XY_U-10_08_8_R-release.apk`
|
||||
* `app-S_9_XY_U-21_08_8_R-release.apk`
|
||||
* **注意**:此檔名轉換規則已完全由 `app/build.gradle` 頂部 Gradle 變數與 `applicationVariants.all` 自動連動生成,**嚴禁手動修改輸出檔名**。
|
||||
|
||||
---
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
---
|
||||
name: android-deploy
|
||||
description: 用於 TaiwanStar 專案的 Android App 編譯、安裝與自動啟動。當使用者要求「安裝到手機」、「部署 APK」或「更新 App」時,應使用此技能。
|
||||
---
|
||||
|
||||
# Android 部署技能 (TaiwanStar 專案專用)
|
||||
|
||||
此技能定義了在 WSL 環境下編譯 Android App 並透過 Windows ADB 安裝至實體手機的標準流程。
|
||||
|
||||
## 前置要求
|
||||
|
||||
1. **Java 版本**:必須使用 OpenJDK 17。
|
||||
2. **ADB 工具**:使用 Windows 端安裝的 ADB (路徑:`/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe`)。
|
||||
3. **手機連線**:手機必須開啟 USB 調試,且已連線至電腦。
|
||||
|
||||
## 標準部署流程
|
||||
|
||||
執行以下複合指令以完成「編譯 + 安裝 + 自動啟動」:
|
||||
|
||||
```bash
|
||||
# 1. 設定環境變數
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
|
||||
# 2. 執行編譯與安裝 (包含自動開啟 HomeActivity)
|
||||
./gradlew :app:assembleS_12_XY_U_Standard_Debug && \
|
||||
/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe install -r -d -t app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk && \
|
||||
/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity
|
||||
```
|
||||
|
||||
## 注意事項
|
||||
|
||||
- **安裝失敗**:若出現 `INSTALL_FAILED_UPDATE_INCOMPATIBLE`,代表簽名或版本衝突,請先執行 `adb uninstall com.unibuy.smartdevice`。
|
||||
- **ADB 找不到裝置**:請確認手機 USB 模式是否為「檔案傳輸」或「PTP」,並檢查 Windows 端的 ADB server 是否正常執行。
|
||||
- **Mock Mode**:此部署版本已包含 Mock Mode,可在無串口設備的手機上正常運作不崩潰。
|
||||
@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TaiwanStar Android 部署腳本
|
||||
# 自動執行:編譯 -> 安裝 -> 啟動
|
||||
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
|
||||
APK_PATH="app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk"
|
||||
PACKAGE_NAME="com.unibuy.smartdevice"
|
||||
LAUNCHER_ACTIVITY=".HomeActivity"
|
||||
|
||||
echo "🚀 開始編譯 APK..."
|
||||
./gradlew :app:assembleS_12_XY_U_Standard_Debug
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ 編譯成功,正在安裝至手機..."
|
||||
$ADB_PATH install -r -d -t $APK_PATH
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "🎉 安裝成功,正在啟動 App..."
|
||||
$ADB_PATH shell am start -n $PACKAGE_NAME/$LAUNCHER_ACTIVITY
|
||||
else
|
||||
echo "❌ 安裝失敗,請檢查手機連線狀態。"
|
||||
fi
|
||||
else
|
||||
echo "❌ 編譯失敗,請檢查程式碼錯誤。"
|
||||
fi
|
||||
@ -4,13 +4,19 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
|
||||
|
||||
# 編譯與安裝 APK 工作流 (compiler-apk)
|
||||
|
||||
這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk` 時,AI 會自動為您執行以下步驟:
|
||||
這個 Workflow 協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk` 時,AI 會自動執行下列步驟。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),不再用 flavor 分家。**
|
||||
> 打包對象只剩 **硬體規格(hardware flavor)**:`S12XYU`(10 吋)/ `S9XYU`(21.5 吋 / Android 9)。
|
||||
> 同一支 APK 在現場會依雲端 `shopping_mode` 自動切換為 basic(購物車)/ employee_card(員工卡)/ pickup_sheet(取貨單)。
|
||||
|
||||
> [!TIP]
|
||||
> **支援智慧硬體判斷與參數化覆寫:**
|
||||
> * 預設(不帶參數):編譯 **`Chengwai` 晟崴版 (S12XYU)**。
|
||||
> * **智慧判斷**:當編譯 **`中國醫 (Cmuh)`** 時,會自動識別並預設編譯 **`S9XYU` (Android 9 / 21.5 吋)**;編譯 **`General`** 與 **`Chengwai`** 時則預設編譯 **`S12XYU` (10 吋)**。
|
||||
> * **參數手動覆寫**:支援手動指定硬體尺寸。例如 `/compiler-apk Cmuh S12` 可強行編譯中國醫的 10 吋版本;`/compiler-apk General S9` 可強行編譯通用版的大螢幕 21.5 吋版本。
|
||||
> **參數 = 硬體規格:**
|
||||
> * 預設(不帶參數):編譯 **`S12XYU`(10 吋)**。
|
||||
> * `/compiler-apk S9`(或 `S9XYU` / `21.5` / `大螢幕`)→ 編譯 **`S9XYU`(21.5 吋 / Android 9)**。
|
||||
> * `/compiler-apk S12`(或 `S12XYU` / `10`)→ 編譯 **`S12XYU`(10 吋)**。
|
||||
> * 相容舊用語:`中國醫`→S9XYU、`晟崴`/`通用`→S12XYU(僅作硬體對應,流程仍由雲端決定)。
|
||||
|
||||
## 步驟零:自動檢查變更與升級版號
|
||||
在開始編譯之前,AI 助手會自動檢查是否有程式碼變更,以落實實體機台的 OTA 升級規範:
|
||||
@ -19,60 +25,49 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
|
||||
3. 遞增完成後,向使用者呈報最新的版本號資訊,接著繼續執行步驟一。
|
||||
|
||||
## 步驟一:設定環境並編譯 Debug APK
|
||||
使用 Java 17 與 Gradle 編譯對應 Flavor 與硬體規格的 Debug 程式碼。
|
||||
使用 Java 17 與 Gradle 編譯對應硬體規格的 Debug 程式碼。
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
# 終止殘留的 Gradle Daemon 程序,避免多個 Daemon 互搶資源,同時避免誤殺 VS Code Gradle 擴充功能
|
||||
./gradlew --stop 2>/dev/null; pkill -9 -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null; sleep 3
|
||||
# 防呆:先優雅 --stop;僅在仍殘留時才升級終止(先 TERM 再 KILL),避免無差別強殺撞鎖導致 clean 編譯隨機失敗
|
||||
#
|
||||
# ⚠️ 關鍵:pgrep/pkill 的比對字串必須「拆開」拼接,不可在本檔字面寫出完整 class 名。
|
||||
# 否則在背景任務(bash -c "...eval '整段腳本'...")情境下,腳本自己的命令列會含有
|
||||
# 完整的 GradleDaemon class 字串 → pgrep -f 比對到自己 → pkill 把跑腳本的 bash 自殺
|
||||
# → exit 144,編譯根本還沒開始。再加 -v $$ 排除自身 PID 雙重保險。
|
||||
DAEMON_PAT='org.gradle.launcher.daemon.bootstrap''.GradleDaemon' # 字面被拆開,pgrep 不會命中本腳本
|
||||
daemon_alive() { pgrep -f "$DAEMON_PAT" 2>/dev/null | grep -vx "$$" | grep -q .; }
|
||||
./gradlew --stop 2>/dev/null || true
|
||||
sleep 2
|
||||
if daemon_alive; then
|
||||
pkill -TERM -f "$DAEMON_PAT" 2>/dev/null || true
|
||||
sleep 3
|
||||
if daemon_alive; then
|
||||
pkill -9 -f "$DAEMON_PAT" 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
|
||||
# 取得目標 Flavor 參數
|
||||
# 取得目標硬體規格(預設 S12XYU;接受 S9/S12 與相容舊用語)
|
||||
RAW_INPUT="$1"
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
|
||||
TARGET_FLAVOR="General"
|
||||
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
|
||||
TARGET_FLAVOR="Cmuh"
|
||||
else
|
||||
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
|
||||
fi
|
||||
fi
|
||||
|
||||
# 根據 Flavor 動態設定智慧硬體規格預設值
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
case "$LOWER" in
|
||||
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
|
||||
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
|
||||
*) echo "⚠️ 無法識別「$RAW_INPUT」,沿用預設 S12XYU" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
|
||||
RAW_HW_INPUT="$2"
|
||||
if [ -n "$RAW_HW_INPUT" ]; then
|
||||
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 安全防護:檢查是否為支援的客製項目
|
||||
if [ "$TARGET_FLAVOR" != "General" ] && [ "$TARGET_FLAVOR" != "Chengwai" ] && [ "$TARGET_FLAVOR" != "Cmuh" ]; then
|
||||
echo "❌ 不支援的 Flavor: $TARGET_FLAVOR,請使用 General, Chengwai 或 Cmuh。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 開始編譯: ${TARGET_FLAVOR}${TARGET_HARDWARE}Debug"
|
||||
echo "🚀 開始編譯: assemble${TARGET_HARDWARE}Debug"
|
||||
|
||||
# 使用 clean 確保完整重新編譯,避免舊快取遮蔽 import 缺失等錯誤
|
||||
./gradlew clean :app:assemble${TARGET_FLAVOR}${TARGET_HARDWARE}Debug --no-daemon
|
||||
./gradlew clean :app:assemble${TARGET_HARDWARE}Debug --no-daemon
|
||||
```
|
||||
|
||||
## 步驟二:導出 APK 到 outputs 資料夾
|
||||
@ -83,80 +78,44 @@ echo "🚀 開始編譯: ${TARGET_FLAVOR}${TARGET_HARDWARE}Debug"
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
mkdir -p outputs
|
||||
|
||||
# 取得 Flavor 參數
|
||||
RAW_INPUT="$1"
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
|
||||
TARGET_FLAVOR="General"
|
||||
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
|
||||
TARGET_FLAVOR="Cmuh"
|
||||
else
|
||||
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
|
||||
fi
|
||||
fi
|
||||
|
||||
# 根據 Flavor 動態設定智慧硬體規格預設值
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
case "$LOWER" in
|
||||
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
|
||||
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
|
||||
RAW_HW_INPUT="$2"
|
||||
if [ -n "$RAW_HW_INPUT" ]; then
|
||||
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 解析硬體規格來決定輸出檔案名格式 (S9 -> S_9, S12 -> S_12)
|
||||
FORMATTED_HW=""
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
FORMATTED_HW="S_9_XY_U"
|
||||
FORMATTED_HW="S_9_XY_U"; MAJOR=21
|
||||
else
|
||||
FORMATTED_HW="S_12_XY_U"
|
||||
FORMATTED_HW="S_12_XY_U"; MAJOR=10
|
||||
fi
|
||||
|
||||
# 動態尋找編譯產出的最新 APK 檔案
|
||||
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}${TARGET_HARDWARE}/debug/ -name "app-${FORMATTED_HW}_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
|
||||
# 動態尋找編譯產出的最新 APK 檔案(檔名已無 project 段:app-<HW>-<ver>-debug.apk)
|
||||
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_HARDWARE}/debug/ -name "app-${FORMATTED_HW}-*-debug.apk" | head -n 1)
|
||||
|
||||
if [ -n "$APK_SOURCE" ]; then
|
||||
APK_NAME=$(basename "$APK_SOURCE")
|
||||
APK_DEST="outputs/$APK_NAME"
|
||||
cp "$APK_SOURCE" "$APK_DEST"
|
||||
echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST"
|
||||
|
||||
# 提取與計算版本資訊
|
||||
MAJOR=$(grep "def verMajor" app/build.gradle | awk '{print $4}')
|
||||
|
||||
# 若在 build.gradle 內 Major 是由 hardware 動態判定,直接讀取並在 Shell 依據當前規格計算
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
MAJOR=21
|
||||
else
|
||||
MAJOR=10
|
||||
fi
|
||||
|
||||
|
||||
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
|
||||
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
|
||||
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
|
||||
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
|
||||
FLAVOR="${TARGET_FLAVOR}${TARGET_HARDWARE}"
|
||||
|
||||
|
||||
echo "=================================================="
|
||||
echo "📢 後台更新 keyin 參考資料:"
|
||||
echo "--------------------------------------------------"
|
||||
echo "* 產品風味 (Flavor):$FLAVOR"
|
||||
echo "* 硬體規格 (hardware):$TARGET_HARDWARE(${MAJOR} 吋)"
|
||||
echo "* 版本名稱 (versionName):$VERSION_NAME"
|
||||
echo "* 版本代碼 (versionCode):$VERSION_CODE"
|
||||
echo "* APK 檔案名稱:$APK_NAME"
|
||||
echo "* 購買流程:由雲端 shopping_mode 決定(basic / employee_card / pickup_sheet)"
|
||||
echo "* 更新說明:[請在此處附上口語化的一句話更新說明]"
|
||||
echo "=================================================="
|
||||
else
|
||||
@ -172,54 +131,27 @@ fi
|
||||
// turbo
|
||||
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
|
||||
|
||||
# 取得 Flavor 參數
|
||||
RAW_INPUT="$1"
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
|
||||
TARGET_FLAVOR="General"
|
||||
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
|
||||
TARGET_FLAVOR="Cmuh"
|
||||
else
|
||||
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
|
||||
fi
|
||||
fi
|
||||
|
||||
# 根據 Flavor 動態設定智慧硬體規格預設值
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
case "$LOWER" in
|
||||
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
|
||||
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
|
||||
RAW_HW_INPUT="$2"
|
||||
if [ -n "$RAW_HW_INPUT" ]; then
|
||||
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 解析硬體規格來決定輸出檔案名格式 (S9 -> S_9, S12 -> S_12)
|
||||
FORMATTED_HW=""
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
FORMATTED_HW="S_9_XY_U"
|
||||
else
|
||||
FORMATTED_HW="S_12_XY_U"
|
||||
fi
|
||||
|
||||
# 檢查是否有設備連接
|
||||
DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | wc -l)
|
||||
|
||||
if [ "$DEVICE_COUNT" -gt 0 ]; then
|
||||
echo "📱 偵測到 $DEVICE_COUNT 個設備,開始安裝..."
|
||||
APK_TARGET=$(find outputs/ -name "app-${FORMATTED_HW}_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
|
||||
APK_TARGET=$(find outputs/ -name "app-${FORMATTED_HW}-*-debug.apk" | head -n 1)
|
||||
if [ -n "$APK_TARGET" ]; then
|
||||
$ADB_PATH install -r -d -t "/home/mama/projects/TaiwanStar_general_size_s_Chengwai/$APK_TARGET"
|
||||
echo "🚀 安裝完成,正在啟動 App..."
|
||||
|
||||
@ -4,19 +4,26 @@ description: 自動編譯正式發行 Release APK、升級 Patch 版本號、並
|
||||
|
||||
# 生產環境發版打包工作流 (release-apk)
|
||||
|
||||
這個 Workflow 專門為應用程式「生產環境上線/發版」設計。當您準備好將特定客戶的最新功能派送至實體販賣機時,呼叫 `/release-apk` 將自動為您完成嚴格的發行檢查、版號遞增、Release 簽章打包與發版資訊生成。
|
||||
這個 Workflow 專門為應用程式「生產環境上線/發版」設計。呼叫 `/release-apk` 將自動完成嚴格的發行檢查、版號遞增、Release 簽章打包與發版資訊生成。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),不再用 flavor 分家。**
|
||||
> 發版對象只剩 **硬體規格(hardware flavor)**:`S12XYU`(10 吋)/ `S9XYU`(21.5 吋 / Android 9)。
|
||||
> 同一支 APK 上線後,現場機台由雲端 `shopping_mode` 決定購買流程。
|
||||
> **部署鐵律**:發布統一版前,star-cloud 必須先為每台現役機設好 `shopping_mode`
|
||||
> (晟崴=employee_card、中國醫=pickup_sheet),否則預設 basic 會跑錯流程。
|
||||
|
||||
> [!TIP]
|
||||
> **支援參數化指定編譯變體:**
|
||||
> * 預設(不帶參數):打包 **`Chengwai` (晟崴客製正式版)**。
|
||||
> * 您可以呼叫 `/release-apk General`(或 `general`)打包 **`General` (通用正式版)**。
|
||||
> * 您可以呼叫 `/release-apk Cmuh`(或 `cmuh`)打包 **`Cmuh` (中國醫客製正式版)**。
|
||||
> **參數 = 硬體規格:**
|
||||
> * 預設(不帶參數):打包 **`S12XYU`(10 吋)**。
|
||||
> * `/release-apk S9`(或 `S9XYU` / `中國醫`)→ 打包 **`S9XYU`(21.5 吋)**。
|
||||
> * `/release-apk S12`(或 `S12XYU` / `晟崴` / `通用`)→ 打包 **`S12XYU`(10 吋)**。
|
||||
|
||||
---
|
||||
|
||||
## 步驟零:發行檢查與版號自動遞增 (OTA 鐵律)
|
||||
為了確保實體機台能夠順利觸發 OTA 自動下載覆蓋安裝,新編譯的 `versionCode` 必須嚴格大於現場的舊版號:
|
||||
1. AI 助手將讀取 `app/build.gradle` 並自動將 `verPatch`(修補版號)**遞增 1**(例如從 `8` 自動升級為 `9`),確保 versionCode 順利變大。
|
||||
1. AI 助手將讀取 `app/build.gradle` 並自動將 `verPatch`(修補版號)**遞增 1**,確保 versionCode 順利變大。
|
||||
2. 自動在 terminal 呈報即將打包的最新版本號資訊。
|
||||
|
||||
## 步驟一:清理快取並進行正式簽章編譯
|
||||
@ -25,33 +32,37 @@ description: 自動編譯正式發行 Release APK、升級 Patch 版本號、並
|
||||
```bash
|
||||
// turbo
|
||||
# 終止殘留的 Gradle Daemon 程序,避免多個 Daemon 互搶資源,同時避免誤殺 VS Code Gradle 擴充功能
|
||||
./gradlew --stop 2>/dev/null; pkill -9 -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null; sleep 3
|
||||
# 防呆:先優雅 --stop;僅在仍殘留時才升級終止(先 TERM 再 KILL),避免無差別強殺撞鎖導致 clean 編譯隨機失敗
|
||||
./gradlew --stop 2>/dev/null || true
|
||||
sleep 2
|
||||
if pgrep -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" >/dev/null 2>&1; then
|
||||
pkill -TERM -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null || true
|
||||
sleep 3
|
||||
if pgrep -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" >/dev/null 2>&1; then
|
||||
pkill -9 -f "org.gradle.launcher.daemon.bootstrap.GradleDaemon" 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
|
||||
# 取得目標 Flavor 參數(預設為 Chengwai,支援 General, Chengwai, Cmuh)
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
if [ -n "$1" ]; then
|
||||
TARGET_FLAVOR=$(echo "$1" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
|
||||
# 取得目標硬體規格(預設 S12XYU)
|
||||
RAW_INPUT="$1"
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
case "$LOWER" in
|
||||
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
|
||||
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
|
||||
*) echo "⚠️ 無法識別「$RAW_INPUT」,沿用預設 S12XYU" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 安全防護:檢查是否為支援的客製項目
|
||||
if [ "$TARGET_FLAVOR" != "General" ] && [ "$TARGET_FLAVOR" != "Chengwai" ] && [ "$TARGET_FLAVOR" != "Cmuh" ]; then
|
||||
echo "❌ 不支援的 Flavor: $TARGET_FLAVOR,請使用 General, Chengwai 或 Cmuh。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 根據專案 Flavor 判定硬體 Flavor(中國醫 Cmuh 使用 S9XYU,其餘使用 S12XYU)
|
||||
HW_FLAVOR="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
HW_FLAVOR="S9XYU"
|
||||
fi
|
||||
|
||||
echo "📦 [Release] 開始編譯正式版變體: ${TARGET_FLAVOR}${HW_FLAVOR}Release"
|
||||
echo "📦 [Release] 開始編譯正式版變體: assemble${TARGET_HARDWARE}Release"
|
||||
|
||||
# 使用 clean 確保完整重新編譯,避開 incremental 快取問題
|
||||
./gradlew clean :app:assemble${TARGET_FLAVOR}${HW_FLAVOR}Release --no-daemon
|
||||
./gradlew clean :app:assemble${TARGET_HARDWARE}Release --no-daemon
|
||||
```
|
||||
|
||||
## 步驟二:導出正式 APK 並產出後台 Keyin 派送參考資料
|
||||
@ -62,19 +73,24 @@ echo "📦 [Release] 開始編譯正式版變體: ${TARGET_FLAVOR}${HW_FLAVOR}Re
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
mkdir -p outputs
|
||||
|
||||
# 取得 Flavor 參數
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
if [ -n "$1" ]; then
|
||||
TARGET_FLAVOR=$(echo "$1" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
|
||||
RAW_INPUT="$1"
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
LOWER=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
case "$LOWER" in
|
||||
s9|s9xyu|21.5|大螢幕|中國醫|中國醫版|cmuh) TARGET_HARDWARE="S9XYU" ;;
|
||||
s12|s12xyu|10|晟崴|晟崴版|chengwai|通用|通用版|general) TARGET_HARDWARE="S12XYU" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
HW_FLAVOR="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
HW_FLAVOR="S9XYU"
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
FORMATTED_HW="S_9_XY_U"; MAJOR=21
|
||||
else
|
||||
FORMATTED_HW="S_12_XY_U"; MAJOR=10
|
||||
fi
|
||||
|
||||
# 動態尋找編譯產出的最新正式版 APK 檔案
|
||||
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}${HW_FLAVOR}/release/ -name "app-S_*_${TARGET_FLAVOR}-*-release.apk" | head -n 1)
|
||||
# 動態尋找編譯產出的最新正式版 APK 檔案(檔名已無 project 段)
|
||||
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_HARDWARE}/release/ -name "app-${FORMATTED_HW}-*-release.apk" | head -n 1)
|
||||
|
||||
if [ -n "$APK_SOURCE" ]; then
|
||||
APK_NAME=$(basename "$APK_SOURCE")
|
||||
@ -83,31 +99,21 @@ if [ -n "$APK_SOURCE" ]; then
|
||||
echo "=================================================="
|
||||
echo "✅ 正式版 APK 封裝成功,已導出至:$APK_DEST"
|
||||
echo "=================================================="
|
||||
|
||||
# 提取與計算版本資訊
|
||||
|
||||
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
|
||||
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
|
||||
|
||||
# 根據硬體規格決定 Major (S9XYU 為 21 吋,S12XYU 為 10 吋)
|
||||
if [ "$HW_FLAVOR" = "S9XYU" ]; then
|
||||
MAJOR=21
|
||||
else
|
||||
MAJOR=10
|
||||
fi
|
||||
|
||||
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
|
||||
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
|
||||
FLAVOR="${TARGET_FLAVOR}${HW_FLAVOR}"
|
||||
|
||||
|
||||
echo "🚀 實體販賣機 OTA 後台新增版本 Keyin 參考資料:"
|
||||
echo "--------------------------------------------------"
|
||||
echo "1. 專案/客戶風味 (Flavor) : $FLAVOR"
|
||||
echo "1. 硬體規格 (hardware) : $TARGET_HARDWARE(${MAJOR} 吋)"
|
||||
echo "2. 版本代碼 (versionCode) : $VERSION_CODE"
|
||||
echo "3. 版本名稱 (versionName) : $VERSION_NAME"
|
||||
echo "4. 螢幕吋數 (Screen Size) : ${MAJOR} 吋"
|
||||
echo "5. 正式版 APK 檔名 : $APK_NAME"
|
||||
echo "4. 正式版 APK 檔名 : $APK_NAME"
|
||||
echo "5. 購買流程 : 由雲端 shopping_mode 決定(須先於後台設好)"
|
||||
echo "--------------------------------------------------"
|
||||
echo "📢 [溫馨提醒] 請將此 APK 上傳至 OTA 平台,並設定為對應機型的強迫升級。"
|
||||
echo "📢 [溫馨提醒] 請將此 APK 上傳至 OTA 平台(依硬體分版),並確認該機 shopping_mode 已設定。"
|
||||
echo "=================================================="
|
||||
else
|
||||
echo "❌ 找不到編譯產出的正式版 APK 檔案,封裝失敗!"
|
||||
@ -116,14 +122,13 @@ fi
|
||||
```
|
||||
|
||||
## 步驟三:自動化 Git 發版標記引導
|
||||
為了落實公司發版規範第 5 條「必須在主分支合併節點打上 Git Tag,以利日後追溯特定現場機台的版本程式碼」,AI 助手會自動為您計算出對應的版本 Tag 標籤與提示:
|
||||
為了落實公司發版規範第 5 條「必須在主分支合併節點打上 Git Tag,以利日後追溯特定現場機台的版本程式碼」,AI 助手會自動計算出對應的版本 Tag 標籤與提示:
|
||||
|
||||
```bash
|
||||
// turbo
|
||||
MAJOR=$(grep "def verMajor" app/build.gradle | awk '{print $4}')
|
||||
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
|
||||
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
|
||||
TAG_NAME="v${MAJOR}.${MINOR}.${PATCH}"
|
||||
TAG_NAME="v1.${MINOR}.${PATCH}"
|
||||
|
||||
echo "=================================================="
|
||||
echo "🔔 Git 發版追溯標記指南"
|
||||
|
||||
25
.claude/commands/compiler-apk.md
Normal file
25
.claude/commands/compiler-apk.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
description: 編譯 Debug APK、導出 outputs/、偵測到設備則安裝(依硬體規格 S9/S12 打包;購買流程由雲端 shopping_mode 決定)
|
||||
argument-hint: [S9|S12]
|
||||
allowed-tools: Bash, Read, Edit
|
||||
---
|
||||
|
||||
執行本專案的 APK 編譯安裝 workflow,定義於 `.agents/workflows/compiler-apk.md`(單一真相來源)。
|
||||
|
||||
本次參數:「$ARGUMENTS」(硬體規格:S9 / S12;可能為空,預設 S12XYU)
|
||||
|
||||
> 注意:客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),打包對象只剩硬體規格。
|
||||
> 同一支 APK 在現場依雲端 `shopping_mode` 切換 basic / employee_card / pickup_sheet。
|
||||
|
||||
## 執行方式
|
||||
1. 先 Read `.agents/workflows/compiler-apk.md`,理解步驟零~三。
|
||||
2. 步驟零(版號):執行 `git status --porcelain app/`;若有未提交改動且本輪對話尚未升過版,讀 `app/build.gradle` 把 `verPatch` 遞增 1 並回報新版號。
|
||||
3. 步驟一~三:依序執行 workflow 內每個 bash 區塊。**關鍵**:workflow 的 bash 以位置參數 `$1` 取得硬體規格,所以執行每個區塊前,先在區塊最前面注入本次參數:
|
||||
|
||||
set -- $ARGUMENTS
|
||||
|
||||
讓 workflow 內的 `$1` 正確解析(S9/S12,或相容舊用語 中國醫→S9、晟崴/通用→S12)。若本次無參數,workflow 自身預設 S12XYU。
|
||||
4. 環境用 workflow 指定的 Java 17(`JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64`)。
|
||||
5. 編譯/導出屬長任務 → 用 run_in_background,完成再回報。
|
||||
6. 步驟三會偵測 adb 設備:有連設備才安裝並啟動 App;沒設備就保留 APK 結束。
|
||||
7. 最後完整回報 workflow 輸出的「後台 keyin 參考資料」(硬體規格 / versionName / versionCode / APK 檔名)。
|
||||
26
.claude/commands/release-apk.md
Normal file
26
.claude/commands/release-apk.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
description: 正式版 Release APK 打包、升 Patch 版號、產出 OTA keyin 與 Git tag 指引(依硬體 S9/S12;流程由雲端 shopping_mode 決定)
|
||||
argument-hint: [S9|S12]
|
||||
allowed-tools: Bash, Read, Edit
|
||||
---
|
||||
|
||||
執行本專案的正式發版 workflow,定義於 `.agents/workflows/release-apk.md`(單一真相來源)。
|
||||
|
||||
本次參數:「$ARGUMENTS」(硬體規格:S9 / S12;可能為空,預設 S12XYU)
|
||||
|
||||
> 注意:客戶專案(購買流程)已收斂為執行期 `shopping_mode`(雲端 B014 下發),發版對象只剩硬體規格。
|
||||
> 部署鐵律:發布前 star-cloud 必須先為每台現役機設好 `shopping_mode`,否則預設 basic 跑錯流程。
|
||||
|
||||
## 執行方式
|
||||
1. 先 Read `.agents/workflows/release-apk.md`,理解步驟零~三。
|
||||
2. 步驟零(OTA 鐵律):讀 `app/build.gradle` 把 `verPatch` 遞增 1(確保 versionCode 變大可觸發 OTA),回報新版號。
|
||||
3. 步驟一~三:依序執行 workflow 內每個 bash 區塊。**關鍵**:workflow 的 bash 以位置參數 `$1` 取得硬體規格,執行每個區塊前先注入本次參數:
|
||||
|
||||
set -- $ARGUMENTS
|
||||
|
||||
無參數時預設 S12XYU(接受 S9/S12,或相容 中國醫→S9、晟崴/通用→S12)。
|
||||
4. 環境用 Java 17(`JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64`);Release 會跑混淆與 lintVital,較久 → 用 run_in_background。
|
||||
5. 步驟二導出正式簽章 APK 到 `outputs/` 並印出 OTA 後台 keyin 對照資料。
|
||||
6. 步驟三回報 Git 發版 tag 指引。
|
||||
|
||||
> 注意:這是「正式發版」,會升版號並產正式簽章 APK。除非使用者明確指示,不要自行 commit / push / 打 tag,只回報指引。
|
||||
9
.claude/settings.json
Normal file
9
.claude/settings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git switch *)",
|
||||
"Bash(grep -n 'is_spring_slot\\\\|protected $fillable\\\\|protected $casts\\\\|'\\\\''settings'\\\\''' app/Models/Machine/Machine.php)"
|
||||
]
|
||||
}
|
||||
}
|
||||
22
.editorconfig
Normal file
22
.editorconfig
Normal file
@ -0,0 +1,22 @@
|
||||
root = true
|
||||
|
||||
# 全專案預設:UTF-8(不含 BOM)、LF、檔尾換行
|
||||
# 重點:charset = utf-8(非 utf-8-bom)可避免編輯器在 XML 開頭塞入 BOM,
|
||||
# 否則 aapt2 會報「Cannot read field "elmName" because "root" is null」導致打包失敗。
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{xml,java,kt,kts,gradle,pro}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# 二進位資源不套用規則
|
||||
[*.{png,webp,jpg,jpeg,gif,ttf,otf,9.png}]
|
||||
charset = unset
|
||||
end_of_line = unset
|
||||
insert_final_newline = unset
|
||||
trim_trailing_whitespace = unset
|
||||
@ -24,7 +24,8 @@
|
||||
- 禁止直接手改成品 `versionCode`/`versionName` 值。
|
||||
- 版號管理透過 `verMajor`、`verMinor`、`verPatch` 變數。
|
||||
- OTA 發版時,`versionCode` 必須嚴格遞增。
|
||||
- 中國醫現場版 (`Cmuh`) 修正預設以 `:app:assembleCmuhS9XYUDebug` 驗證。
|
||||
- 21.5 吋大螢幕現場版(如中國醫)修正預設以 `:app:assembleS9XYUDebug` 驗證。
|
||||
- 10 吋一般現場版(如晟崴、通用)修正預設以 `:app:assembleS12XYUDebug` 驗證。
|
||||
|
||||
## When Rules Conflict
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@ -8,8 +9,8 @@ plugins {
|
||||
// 螢幕尺寸 (verMajor) 已改為下方依據編譯 Flavor (S12XYU / S9XYU) 自動動態判定,
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMinor = 1
|
||||
def verPatch = 67
|
||||
def verMinor = 2
|
||||
def verPatch = 2
|
||||
|
||||
|
||||
|
||||
@ -29,23 +30,13 @@ android {
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 雙維度 Flavor:project(客戶專案)× hardware(硬體規格)
|
||||
// 單維度 Flavor:hardware(硬體規格 / IPC 世代)
|
||||
// 客戶專案(購買流程)已收斂為執行期 shopping_mode(雲端 B014 下發),不再用 flavor 分家。
|
||||
// hardware 維度決定螢幕尺寸、minSdk(安卓版本)與 COM port(見 ComPort.getFilepath)。
|
||||
// =====================================================================
|
||||
flavorDimensions "project", "hardware"
|
||||
flavorDimensions "hardware"
|
||||
|
||||
productFlavors {
|
||||
General {
|
||||
dimension "project"
|
||||
}
|
||||
|
||||
Chengwai {
|
||||
dimension "project"
|
||||
}
|
||||
|
||||
Cmuh {
|
||||
dimension "project"
|
||||
}
|
||||
|
||||
S12XYU {
|
||||
dimension "hardware"
|
||||
}
|
||||
@ -67,8 +58,7 @@ android {
|
||||
// 自定義打包檔案名稱與動態版本號計算
|
||||
// 遵循公司命名規範:來源_安卓號_主板號_機器系列_專案名稱_螢幕大小_次版號_修補版號_環境
|
||||
applicationVariants.all { variant ->
|
||||
def project = variant.productFlavors[0].name // project dimension
|
||||
def hardware = variant.productFlavors[1].name // hardware dimension
|
||||
def hardware = variant.productFlavors[0].name // hardware dimension(唯一維度)
|
||||
def buildType = variant.buildType.name
|
||||
|
||||
// 根據硬體規格自動動態判定螢幕吋數 (verMajor) 與版號
|
||||
@ -99,8 +89,8 @@ android {
|
||||
def hwFormatted = hardware
|
||||
.replaceAll(/^([A-Z])(\d+)([A-Z]+)([A-Z])$/, '$1_$2_$3_$4')
|
||||
|
||||
// 輸出範例:app-S_12_XY_U_Chengwai-10_08_8_R-release.apk
|
||||
output.outputFileName = "app-${hwFormatted}_${project}-${calculatedVersionName}-${buildType}.apk"
|
||||
// 輸出範例:app-S_12_XY_U-10_08_8_R-release.apk(依硬體分版,購買流程由雲端 shopping_mode 決定)
|
||||
output.outputFileName = "app-${hwFormatted}-${calculatedVersionName}-${buildType}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +101,10 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '11'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
@ -146,4 +140,11 @@ dependencies {
|
||||
implementation 'io.netty:netty-codec-http:4.1.94.Final'
|
||||
implementation 'io.netty:netty-handler:4.1.94.Final'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
|
||||
// Kotlin / MVVM(basic 模式的多品項購物車)
|
||||
implementation libs.kotlin.stdlib
|
||||
implementation libs.lifecycle.viewmodel.ktx
|
||||
implementation libs.lifecycle.livedata.ktx
|
||||
implementation libs.lifecycle.runtime.ktx
|
||||
implementation libs.brvah
|
||||
}
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.ui.dialog.MemberCardPickupDialog;
|
||||
|
||||
/**
|
||||
* Chengwai (晟崴版) 銷售流程處理器。
|
||||
* 點選商品/購物車時直接進入員工卡刷卡取貨 Dialog (MemberCardPickupDialog)。
|
||||
*/
|
||||
public class SaleFlowHandler {
|
||||
|
||||
protected final Context context;
|
||||
protected final SaleFlowCallback callback;
|
||||
protected final HandlerMain handlerMain;
|
||||
|
||||
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
this.context = context;
|
||||
this.handlerMain = handlerMain;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void onSetupUI() {
|
||||
// 晟崴版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onSetupUI(android.view.View rootView) {
|
||||
// 晟崴版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onBuyRequested() {
|
||||
new MemberCardPickupDialog(context, handlerMain).show();
|
||||
}
|
||||
|
||||
public void onBarcodeReceived(String rawData) {
|
||||
// 晟崴版不處理掃碼
|
||||
}
|
||||
|
||||
public void onPickupRequested() {
|
||||
// 晟崴版不處理取貨碼
|
||||
}
|
||||
}
|
||||
@ -1,264 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupCodeBuyBinding;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogPickupCodeBuyListAdpter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class BuyPickupCodeDialog extends DialogAbstract {
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
SET_PRICE(3),
|
||||
RESET_COUNTDOWM(4),
|
||||
RESET_COUNTDOWM_TOAST(5),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getContext(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
if (MyApp.getInstance().getBuyList().size() <= 0) {
|
||||
|
||||
dialogCancel();
|
||||
} else {
|
||||
Option option = optionMap.get(commandCode);
|
||||
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case SET_PRICE:
|
||||
binding.textPrice.setText(String.valueOf(getTotalPrice()));
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
case RESET_COUNTDOWM:
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
case RESET_COUNTDOWM_TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogPickupCodeBuyBinding binding;
|
||||
private RecyclerDialogPickupCodeBuyListAdpter recyclerDialogPickupCodeBuyListAdpter;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
private boolean isChangeDispatch;
|
||||
|
||||
public BuyPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
getLogs().info("open:" + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogPickupCodeBuyBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
isChangeDispatch = true;
|
||||
|
||||
this.recyclerDialogPickupCodeBuyListAdpter = new RecyclerDialogPickupCodeBuyListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
|
||||
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogPickupCodeBuyListAdpter);
|
||||
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(6);
|
||||
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
|
||||
|
||||
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
|
||||
|
||||
this.binding.buttonGetProduct.setOnClickListener(v -> gotoGetProduct());
|
||||
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
binding.textPrice.setText(String.valueOf(getTotalPrice()));
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
|
||||
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
|
||||
totalPrice += buy.getFullPrice();
|
||||
}
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public void gotoGetProduct() {
|
||||
if (isChangeDispatch) {
|
||||
closeDialogCountdown.shutdown();
|
||||
isChangeDispatch = false;
|
||||
changeDialog(DispatchDialog.class);
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
if (buyListSize > 0) {
|
||||
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
if (buyListSize > 0) {
|
||||
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
|
||||
}
|
||||
getTools().dialogClose();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText(getContext().getString(R.string.cancel)+"("+countdown+")");
|
||||
}
|
||||
|
||||
public void gotoDialog() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
if (flowType == 5) {
|
||||
changeCheckout();
|
||||
} else {
|
||||
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
|
||||
if (MyApp.getInstance().getDevSet().isInvoice()) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
} else {
|
||||
if (MyApp.getInstance().getDevSet().isInvoice()) {
|
||||
BuyStructure buyData = MyApp.getInstance().getBuyList().get(0);
|
||||
try {
|
||||
SlotField slotField = SlotField.getSlotField(buyData.getField());
|
||||
if (slotField.isInvoice()) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
}
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void changeCheckout() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
switch (flowType) {
|
||||
case 1:
|
||||
changeDialog(CheckoutNfcCardDialog.class);
|
||||
break;
|
||||
case 2:
|
||||
changeDialog(CheckoutNfcRechargeDialog.class);
|
||||
break;
|
||||
case 3:
|
||||
changeDialog(CheckoutEsunPayDialog.class);
|
||||
break;
|
||||
case 4:
|
||||
changeDialog(CheckoutNfcPhoneDialog.class);
|
||||
break;
|
||||
case 5:
|
||||
changeDialog(CheckoutAccessCodeDialog.class);
|
||||
break;
|
||||
case 9:
|
||||
changeDialog(CashPayDialog.class);
|
||||
break;
|
||||
case 70:
|
||||
changeDialog(CheckoutLinePayDialog.class);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,263 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupCodeBuyBinding;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogPickupCodeBuyListAdpter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class BuyPickupCodeDialog extends DialogAbstract {
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
SET_PRICE(3),
|
||||
RESET_COUNTDOWM(4),
|
||||
RESET_COUNTDOWM_TOAST(5),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getContext(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
if (MyApp.getInstance().getBuyList().size() <= 0) {
|
||||
|
||||
dialogCancel();
|
||||
} else {
|
||||
Option option = optionMap.get(commandCode);
|
||||
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case SET_PRICE:
|
||||
binding.textPrice.setText(String.valueOf(getTotalPrice()));
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
case RESET_COUNTDOWM:
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
case RESET_COUNTDOWM_TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogPickupCodeBuyBinding binding;
|
||||
private RecyclerDialogPickupCodeBuyListAdpter recyclerDialogPickupCodeBuyListAdpter;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
private boolean isChangeDispatch;
|
||||
|
||||
public BuyPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
getLogs().info("open:" + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogPickupCodeBuyBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
isChangeDispatch = true;
|
||||
|
||||
this.recyclerDialogPickupCodeBuyListAdpter = new RecyclerDialogPickupCodeBuyListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
|
||||
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogPickupCodeBuyListAdpter);
|
||||
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(6);
|
||||
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
|
||||
|
||||
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
|
||||
|
||||
this.binding.buttonGetProduct.setOnClickListener(v -> gotoGetProduct());
|
||||
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
binding.textPrice.setText(String.valueOf(getTotalPrice()));
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
|
||||
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
|
||||
totalPrice += buy.getFullPrice();
|
||||
}
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public void gotoGetProduct() {
|
||||
if (isChangeDispatch) {
|
||||
closeDialogCountdown.shutdown();
|
||||
isChangeDispatch = false;
|
||||
changeDialog(DispatchDialog.class);
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
if (buyListSize > 0) {
|
||||
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
if (buyListSize > 0) {
|
||||
recyclerDialogPickupCodeBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
|
||||
}
|
||||
getTools().dialogClose();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText("取消("+countdown+")");
|
||||
}
|
||||
|
||||
public void gotoDialog() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
if (flowType == 5) {
|
||||
changeCheckout();
|
||||
} else {
|
||||
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
|
||||
if (MyApp.getInstance().getDevSet().isInvoice()) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
} else {
|
||||
if (MyApp.getInstance().getDevSet().isInvoice()) {
|
||||
BuyStructure buyData = MyApp.getInstance().getBuyList().get(0);
|
||||
try {
|
||||
SlotField slotField = SlotField.getSlotField(buyData.getField());
|
||||
if (slotField.isInvoice()) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
}
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void changeCheckout() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
switch (flowType) {
|
||||
case 1:
|
||||
changeDialog(CheckoutNfcCardDialog.class);
|
||||
break;
|
||||
case 2:
|
||||
changeDialog(CheckoutNfcRechargeDialog.class);
|
||||
break;
|
||||
case 3:
|
||||
changeDialog(CheckoutEsunPayDialog.class);
|
||||
break;
|
||||
case 4:
|
||||
changeDialog(CheckoutNfcPhoneDialog.class);
|
||||
break;
|
||||
case 5:
|
||||
changeDialog(CheckoutAccessCodeDialog.class);
|
||||
break;
|
||||
case 9:
|
||||
changeDialog(CheckoutCashDialog.class);
|
||||
break;
|
||||
case 70:
|
||||
changeDialog(CheckoutLinePayDialog.class);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,429 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.databinding.DialogAccessCodeBinding;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupCodeFontendBinding;
|
||||
import com.unibuy.smartdevice.devices.DeviceType;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.devices.UsbDev;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.exception.LogsIOException;
|
||||
import com.unibuy.smartdevice.exception.LogsParseException;
|
||||
import com.unibuy.smartdevice.exception.LogsSettingEmptyException;
|
||||
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
|
||||
import com.unibuy.smartdevice.external.HttpAPI;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainScheduler;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
INVOICE_SUCCESSFUL(4),
|
||||
INVOICE_FAILED(5),
|
||||
CHANGE_DISPATCH(6),
|
||||
CHANGE_BUY(7),
|
||||
SET_STATUS_TEXT(8),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getCtx(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
Option option = optionMap.get(commandCode);
|
||||
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case INVOICE_SUCCESSFUL:
|
||||
MyApp.getInstance().getInvoiceData().setCarrier(message);
|
||||
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("carrier:" + MyApp.getInstance().getInvoiceData().getCarrier());
|
||||
break;
|
||||
case INVOICE_FAILED:
|
||||
break;
|
||||
case CHANGE_DISPATCH:
|
||||
reportAccessCodeData();//上傳通行碼資訊到後台系統
|
||||
changeDispatch();
|
||||
break;
|
||||
case CHANGE_BUY:
|
||||
try {
|
||||
SlotStructure slotProduct = MyApp.getInstance().getSlotDataByPid(message);
|
||||
SlotField slotField = SlotField.getSlotField(slotProduct.getField());
|
||||
switch (slotField) {
|
||||
case VMC:
|
||||
if (slotProduct.isLock()) {
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@商品暫停販售");
|
||||
dialogCancel();
|
||||
} else if (slotProduct.getCount() == 0) {
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@商品無存貨");
|
||||
dialogCancel();
|
||||
}
|
||||
|
||||
BuyStructure buyProduct = new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1);
|
||||
MyApp.getInstance().getBuyList().add(buyProduct);
|
||||
changeBuy();
|
||||
|
||||
break;
|
||||
case ELECTRIC:
|
||||
if (slotProduct.isLock())
|
||||
MyApp.getInstance().getSlotDataByPid(message).setLock(false);
|
||||
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@解鎖" + slotProduct.getProduct().getProductName());
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", FontendActivity.Option.RESET_DATA.getOption(), "reset data");
|
||||
dialogCancel();
|
||||
|
||||
break;
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
setStatusText("找不到商品");
|
||||
}
|
||||
break;
|
||||
case SET_STATUS_TEXT:
|
||||
setStatusText(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogPickupCodeFontendBinding binding;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
private boolean isChangeDispatch;
|
||||
private boolean openBarcode;
|
||||
private HttpAPI httpAPI;
|
||||
|
||||
public FontendPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogPickupCodeFontendBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
MyApp.getInstance().initialReportData();
|
||||
|
||||
isChangeDispatch = true;
|
||||
openBarcode = true;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
|
||||
binding.buttonFinish.setOnClickListener(v -> changeOk());
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
binding.buttonR1.setOnClickListener(v -> {
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text.substring(0, text.length()-1));
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.buttonC1.setOnClickListener(v -> {
|
||||
binding.editTextNumber.setText("");
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button10.setOnClickListener(v -> {
|
||||
String btnName = binding.button10.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button11.setOnClickListener(v -> {
|
||||
String btnName = binding.button11.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button12.setOnClickListener(v -> {
|
||||
String btnName = binding.button12.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button13.setOnClickListener(v -> {
|
||||
String btnName = binding.button13.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button14.setOnClickListener(v -> {
|
||||
String btnName = binding.button14.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button15.setOnClickListener(v -> {
|
||||
String btnName = binding.button15.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button16.setOnClickListener(v -> {
|
||||
String btnName = binding.button16.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button17.setOnClickListener(v -> {
|
||||
String btnName = binding.button17.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button18.setOnClickListener(v -> {
|
||||
String btnName = binding.button18.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button19.setOnClickListener(v -> {
|
||||
String btnName = binding.button19.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
|
||||
public ReadBarCodeOnScheduler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(HandlerMain handlerMain) {
|
||||
try {
|
||||
int totalPrice = 0;
|
||||
for (BuyStructure buy : MyApp.getInstance().getBuyList()) {
|
||||
totalPrice += buy.getFullPrice();
|
||||
}
|
||||
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
usbDev.read(buffer, 100);
|
||||
for (int i=0; i<7; i++) {
|
||||
if (isRun() && openBarcode) {
|
||||
int size = usbDev.read(buffer, 5000);
|
||||
if (size > 5) {
|
||||
String barCodeString = new String(buffer, 0, size);
|
||||
getHandlerMain().start(getLogs().getClassName(), "收到條碼:"+ Tools.randomReplace(barCodeString, 0.3)+",送出交易");
|
||||
|
||||
try {
|
||||
// TODO: [新後台改接] 待未來新後台啟用後,此處取貨碼驗證需改串接新的 StarCloudAPI B660 介面
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(barCodeString);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
|
||||
openBarcode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
usbDev.closePort();
|
||||
} catch (LogsUnsupportedOperationException | LogsIOException | LogsEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
|
||||
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
|
||||
totalPrice += buy.getFullPrice();
|
||||
}
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public void changeDispatch() {
|
||||
if (isChangeDispatch) {
|
||||
closeDialogCountdown.shutdown();
|
||||
isChangeDispatch = false;
|
||||
changeDialog(DispatchDialog.class);
|
||||
}
|
||||
}
|
||||
|
||||
public void changeOk() {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void changeBuy() {
|
||||
closeDialogCountdown.shutdown();
|
||||
BuyPickupCodeDialog buyDialog = new BuyPickupCodeDialog(getContext(), getSrcHandlerMain());
|
||||
buyDialog.show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
closeDialogCountdown.shutdown();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText("返回("+countdown+")");
|
||||
}
|
||||
|
||||
public void setStatusText(String message) {
|
||||
binding.textStatus.setText(message);
|
||||
}
|
||||
|
||||
public void reportAccessCodeData() {
|
||||
try {
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setField(MyApp.getInstance().getReportAccessCodeStructure().getField());
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setSlot(MyApp.getInstance().getBuyList().get(0).getSlot());
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setProductId(MyApp.getInstance().getReportAccessCodeStructure().getProductId());
|
||||
httpAPI.reportAccessCode(MyApp.getInstance().getReportAccessCodeStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePickupCodeData() {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
handlerMain.start(getClass().getSimpleName(), CheckoutCashDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,193 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.unibuy.smartdevice.databinding.RecyclerDialogBuyListBinding;
|
||||
import com.unibuy.smartdevice.databinding.RecyclerDialogPickupCodeBuyListBinding;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.exception.Logs;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
|
||||
import com.unibuy.smartdevice.ui.tools.ImageGlide;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<RecyclerDialogPickupCodeBuyListAdpter.ViewHolder> {
|
||||
private Logs logs;
|
||||
private Context context;
|
||||
private final List<BuyStructure> buyList;
|
||||
private HandlerMain handlerMain;
|
||||
|
||||
public RecyclerDialogPickupCodeBuyListAdpter(Context context, List<BuyStructure> buyList, HandlerMain handlerMain) {
|
||||
this.logs = new Logs(this.getClass());
|
||||
this.context = context;
|
||||
this.buyList = buyList;
|
||||
this.handlerMain = handlerMain;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
RecyclerDialogPickupCodeBuyListBinding binding;
|
||||
|
||||
public ViewHolder(@NonNull RecyclerDialogPickupCodeBuyListBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
// 使用 ViewBinding 來綁定佈局
|
||||
RecyclerDialogPickupCodeBuyListBinding binding = RecyclerDialogPickupCodeBuyListBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
|
||||
return new ViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
logs.info("position:" + position);
|
||||
|
||||
BuyStructure buyProduct = buyList.get(position);
|
||||
holder.binding.textPosition.setText(String.valueOf(position + 1));
|
||||
String productID = buyProduct.getProduct().getProductID();
|
||||
if (!productID.isEmpty() && !productID.equals("?")) {
|
||||
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
|
||||
} else {
|
||||
holder.binding.textProductName.setText("點我選擇商品");
|
||||
}
|
||||
// holder.binding.textDelete.setOnClickListener(new textDeleteOnClickListener(position));
|
||||
String urlString = buyProduct.getProduct().getProductImg();
|
||||
try {
|
||||
new ImageGlide(context).fileload(urlString, holder.binding.imageProductPicture);
|
||||
} catch (LogsEmptyException e) {
|
||||
this.logs.warning(e);
|
||||
}
|
||||
int machinePrice = buyProduct.getProduct().getMachinePrice();
|
||||
int sellingPrice = buyProduct.getProduct().getSellingPrice();
|
||||
int memberPrice = buyProduct.getProduct().getMemberPrice();
|
||||
|
||||
int slotUpperLimit = buyProduct.getProduct().getSlotUpperLimit();
|
||||
int count = buyProduct.getCount();
|
||||
|
||||
int price = machinePrice > 0? machinePrice: sellingPrice;
|
||||
|
||||
holder.binding.textPrice.setText("$"+price);
|
||||
holder.binding.textCount.setText("x"+String.valueOf(count));
|
||||
|
||||
logs.info("field:" + buyProduct.getField());
|
||||
}
|
||||
|
||||
public class addition1OnClickListener implements View.OnClickListener {
|
||||
private Context context;
|
||||
private RecyclerDialogBuyListBinding binding;
|
||||
private int position;
|
||||
|
||||
public addition1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
|
||||
this.context = context;
|
||||
this.binding = binding;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BuyStructure buyProduct = buyList.get(position);
|
||||
|
||||
int count = buyProduct.getCount();
|
||||
count++;
|
||||
buyList.get(position).setCount(count);
|
||||
binding.textCount.setText("x"+String.valueOf(count));
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(), "total price");
|
||||
}
|
||||
}
|
||||
|
||||
public class subtraction1OnClickListener implements View.OnClickListener {
|
||||
private Context context;
|
||||
private RecyclerDialogBuyListBinding binding;
|
||||
private int position;
|
||||
public subtraction1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
|
||||
this.context = context;
|
||||
this.binding = binding;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BuyStructure buyProduct = buyList.get(position);
|
||||
|
||||
int count = buyProduct.getCount();
|
||||
count--;
|
||||
if (count <= 0) {
|
||||
buyList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, buyList.size() - position);
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(),"刪除一筆資料");
|
||||
} else {
|
||||
buyList.get(position).setCount(count);
|
||||
binding.textCount.setText("x"+String.valueOf(count));
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(),"total price");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class editCountOnEditorActionListener implements TextView.OnEditorActionListener{
|
||||
private ViewHolder holder;
|
||||
private int position;
|
||||
|
||||
public editCountOnEditorActionListener(ViewHolder holder, int position) {
|
||||
this.holder = holder;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
int count = Integer.valueOf(v.getText().toString());
|
||||
buyList.get(position).setCount(count);
|
||||
notifyItemChanged(position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class textDeleteOnClickListener implements View.OnClickListener{
|
||||
private int position;
|
||||
|
||||
public textDeleteOnClickListener(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
delProductData(position);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return buyList.size();
|
||||
}
|
||||
|
||||
public void addProductData(BuyStructure buyProduct) {
|
||||
buyList.add(buyProduct);
|
||||
notifyItemInserted(buyList.size() -1);
|
||||
}
|
||||
|
||||
public void delProductData(int position) {
|
||||
buyList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
}
|
||||
@ -1,271 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/light_gray"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:background="@color/red"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textVersion"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="20250108ver1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textThreadCount"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="TC:0"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:id="@+id/btnswitchlanTW"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="中"
|
||||
android:visibility="gone"
|
||||
android:textSize="30sp"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnswitchlanEn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="En"
|
||||
android:visibility="gone"
|
||||
android:textSize="30sp"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnswitchlanJa"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:text="日"
|
||||
android:visibility="gone"
|
||||
android:textSize="30sp"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold"
|
||||
/>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textTemperature"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="0 ℃"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textTime"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:text="1911/01/01 01:01:01"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textNetwork"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:text="@string/no_internet"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/relativeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="60dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="@color/black"
|
||||
app:layout_constraintDimensionRatio="18:21"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:show_subtitle_button="true"
|
||||
app:use_artwork="true"
|
||||
app:use_controller="false"
|
||||
app:resize_mode="fit"
|
||||
app:surface_type="texture_view" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageAd"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
</RelativeLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/relativeLayout" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/buttonVmc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/drinksale"
|
||||
app:cornerRadius="12dp"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
app:strokeColor="@color/gray"
|
||||
android:textSize="38sp"
|
||||
app:strokeWidth="2dp"
|
||||
android:visibility="gone"
|
||||
android:layout_margin="4dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/buttonElectric"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/board_games"
|
||||
app:cornerRadius="12dp"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
app:strokeColor="@color/gray"
|
||||
android:textSize="38sp"
|
||||
android:visibility="gone"
|
||||
app:strokeWidth="2dp"
|
||||
android:layout_margin="4dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/buttonOther"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone"
|
||||
android:text="@string/other"
|
||||
app:cornerRadius="12dp"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
app:strokeColor="@color/gray"
|
||||
android:textSize="38sp"
|
||||
app:strokeWidth="2dp"
|
||||
android:layout_margin="4dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/floatButtonPickupCode"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="150dp"
|
||||
android:text="取\n貨\n碼"
|
||||
android:textSize="28sp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="80dp"
|
||||
android:backgroundTint="@color/gray"
|
||||
android:textColor="@color/black"
|
||||
android:alpha="0.8"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/floatButtonShoppingCart"
|
||||
app:layout_constraintEnd_toEndOf="@+id/bottom_navigation"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/floatButtonShoppingCart"
|
||||
android:layout_width="58dp"
|
||||
android:layout_height="51dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="80dp"
|
||||
android:contentDescription="TODO"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/gray"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/fragment_container"
|
||||
app:layout_constraintEnd_toEndOf="@+id/bottom_navigation"
|
||||
app:maxImageSize="60dp"
|
||||
app:srcCompat="@drawable/baseline_add_shopping_cart_24" />
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnTopRight"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="100dp"
|
||||
android:text="進入後台"
|
||||
android:textSize="24sp"
|
||||
android:backgroundTint="@color/red"
|
||||
android:textColor="@android:color/white"
|
||||
android:alpha="1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="60dp"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/linearLayout3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/light_gray">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/light_gray"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:background="@color/red"
|
||||
android:gravity="center"
|
||||
android:text="購物車"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1.15"
|
||||
android:gravity="center|end"
|
||||
android:text="合計金額:"
|
||||
android:textColor="@color/red"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textPrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="0.85"
|
||||
android:gravity="center|start"
|
||||
android:text="0"
|
||||
android:textColor="@color/red"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerDialogBuyList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="750dp"
|
||||
/>
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutMemberTransactions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="340dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/white"
|
||||
android:text="取消(40)"
|
||||
android:textSize="20sp"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonGetProduct"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/white"
|
||||
android:text="取貨"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="20sp"
|
||||
android:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,194 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/light_gray">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/light_gray"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:background="@color/red"
|
||||
android:gravity="center"
|
||||
android:text="取貨碼"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textStatus"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="輸入取貨碼 或 掃描QRCode"
|
||||
android:textColor="@color/red"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearMark1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="20dp"
|
||||
android:background="@color/white"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editTextNumber"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:textSize="20sp"
|
||||
android:hint="取貨碼"
|
||||
android:gravity="center"
|
||||
android:inputType="number" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button10"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="0" />
|
||||
<Button
|
||||
android:id="@+id/button11"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="1" />
|
||||
<Button
|
||||
android:id="@+id/button12"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="2" />
|
||||
<Button
|
||||
android:id="@+id/button13"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="3" />
|
||||
<Button
|
||||
android:id="@+id/button14"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="4" />
|
||||
<Button
|
||||
android:id="@+id/buttonR1"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/light_gray"
|
||||
android:text="←" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button15"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="5" />
|
||||
<Button
|
||||
android:id="@+id/button16"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="6" />
|
||||
<Button
|
||||
android:id="@+id/button17"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="7" />
|
||||
<Button
|
||||
android:id="@+id/button18"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="8" />
|
||||
<Button
|
||||
android:id="@+id/button19"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:text="9" />
|
||||
<Button
|
||||
android:id="@+id/buttonC1"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/light_gray"
|
||||
android:text="C" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/white"
|
||||
android:text="返回(40)"
|
||||
android:textSize="20sp"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonFinish"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="@color/white"
|
||||
android:text="輸入完成"
|
||||
android:textSize="20sp"
|
||||
android:textColor="@color/black" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
tools:context=".ui.devs.vmc.VmcFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerBuy"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/scanbarcodebtn"
|
||||
android:layout_width="400dp"
|
||||
android:layout_height="400dp"
|
||||
android:layout_marginTop="-50dp"
|
||||
android:padding="10dp"
|
||||
android:text="掃描單據\n按鈕"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="70sp"
|
||||
app:strokeColor="@color/gray"
|
||||
app:strokeWidth="5dp" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,109 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="1dp"
|
||||
android:paddingBottom="1dp"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:background="@color/white"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textPosition"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_weight="0.3"
|
||||
android:gravity="center"
|
||||
android:text="0"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="visible" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="0.5"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageProductPicture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/default_product"
|
||||
app:layout_constraintDimensionRatio="16:9" />
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@color/white"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
tools:layout_editor_absoluteY="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textProductName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="商品名稱"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@color/white"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="$0"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:id="@+id/textCount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="x0"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,42 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
|
||||
|
||||
/**
|
||||
* General (通用版) 銷售流程處理器。
|
||||
* 點選商品/購物車時彈出標準付款 Dialog (BuyDialog)。
|
||||
*/
|
||||
public class SaleFlowHandler {
|
||||
|
||||
protected final Context context;
|
||||
protected final SaleFlowCallback callback;
|
||||
protected final HandlerMain handlerMain;
|
||||
|
||||
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
this.context = context;
|
||||
this.handlerMain = handlerMain;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void onSetupUI() {
|
||||
// 通用版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onSetupUI(android.view.View rootView) {
|
||||
// 通用版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onBuyRequested() {
|
||||
new BuyDialog(context, handlerMain).show();
|
||||
}
|
||||
|
||||
public void onBarcodeReceived(String rawData) {
|
||||
// 通用版不處理掃碼
|
||||
}
|
||||
|
||||
public void onPickupRequested() {
|
||||
// 通用版不處理取貨碼
|
||||
}
|
||||
}
|
||||
@ -1,591 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.DialogAccessCodeBinding;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupCodeFontendBinding;
|
||||
import com.unibuy.smartdevice.devices.DeviceType;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.devices.UsbDev;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.exception.LogsIOException;
|
||||
import com.unibuy.smartdevice.exception.LogsParseException;
|
||||
import com.unibuy.smartdevice.exception.LogsSettingEmptyException;
|
||||
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
|
||||
import com.unibuy.smartdevice.external.HttpAPI;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainScheduler;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import com.unibuy.smartdevice.controller.DevController;
|
||||
import com.unibuy.smartdevice.devices.PortTools;
|
||||
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
INVOICE_SUCCESSFUL(4),
|
||||
INVOICE_FAILED(5),
|
||||
CHANGE_DISPATCH(6),
|
||||
CHANGE_BUY(7),
|
||||
SET_STATUS_TEXT(8),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getCtx(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
Option option = optionMap.get(commandCode);
|
||||
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case INVOICE_SUCCESSFUL:
|
||||
MyApp.getInstance().getInvoiceData().setCarrier(message);
|
||||
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("carrier:" + MyApp.getInstance().getInvoiceData().getCarrier());
|
||||
break;
|
||||
case INVOICE_FAILED:
|
||||
break;
|
||||
case CHANGE_DISPATCH:
|
||||
//reportAccessCodeData();//上傳通行碼資訊到後台系統
|
||||
changeDispatch();
|
||||
break;
|
||||
case CHANGE_BUY:
|
||||
try {
|
||||
getLogs().info("CHANGE_BUY triggered with message: " + message);
|
||||
SlotStructure slotProduct = MyApp.getInstance().getSlotDataByPid(message);
|
||||
if (slotProduct == null) {
|
||||
setStatusText(getContext().getString(R.string.product_not_found));
|
||||
return;
|
||||
}
|
||||
|
||||
getLogs().info("Slot found: " + slotProduct.getSlot() + ", Field: " + slotProduct.getField());
|
||||
SlotField slotField = SlotField.getSlotField(slotProduct.getField());
|
||||
switch (slotField) {
|
||||
case VMC:
|
||||
if (slotProduct.isLock()) {
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@"+getContext().getString(R.string.product_sales_suspended));//商品暫停販售
|
||||
dialogCancel();
|
||||
} else if (slotProduct.getCount() == 0) {
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@"+getContext().getString(R.string.product_out_of_stock));//商品無存貨
|
||||
dialogCancel();
|
||||
} else {
|
||||
// 取貨口檢查已在 onCreate 執行,這裡直接進行
|
||||
BuyStructure buyProduct = new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1);
|
||||
MyApp.getInstance().getBuyList().add(buyProduct);
|
||||
changeBuy();
|
||||
}
|
||||
break;
|
||||
case ELECTRIC:
|
||||
if (slotProduct.isLock())
|
||||
MyApp.getInstance().getSlotDataByPid(message).setLock(false);
|
||||
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@"+getContext().getString(R.string.unlock) + slotProduct.getProduct().getProductName());//解鎖
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", FontendActivity.Option.RESET_DATA.getOption(), "reset data");
|
||||
dialogCancel();
|
||||
|
||||
break;
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
setStatusText(getContext().getString(R.string.product_not_found));//找不到商品
|
||||
} catch (Exception e) {
|
||||
getLogs().info("Error in CHANGE_BUY: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
// 發生未知錯誤時,嘗試繼續或顯示錯誤
|
||||
setStatusText("System Error: " + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case SET_STATUS_TEXT:
|
||||
setStatusText(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogPickupCodeFontendBinding binding;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
private boolean isChangeDispatch;
|
||||
private boolean openBarcode;
|
||||
private HttpAPI httpAPI;
|
||||
private boolean haveThings, doorError;
|
||||
|
||||
public FontendPickupCodeDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogPickupCodeFontendBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
MyApp.getInstance().initialReportData();
|
||||
|
||||
isChangeDispatch = true;
|
||||
openBarcode = true;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
|
||||
binding.buttonFinish.setOnClickListener(v -> changeOk());
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
binding.buttonR1.setOnClickListener(v -> {
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
if (text.length() > 0) { // ✅ 避免長度為 0 崩潰
|
||||
binding.editTextNumber.setText(text.substring(0, text.length() - 1));
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
}
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.buttonC1.setOnClickListener(v -> {
|
||||
binding.editTextNumber.setText("");
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button10.setOnClickListener(v -> {
|
||||
String btnName = binding.button10.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button11.setOnClickListener(v -> {
|
||||
String btnName = binding.button11.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button12.setOnClickListener(v -> {
|
||||
String btnName = binding.button12.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button13.setOnClickListener(v -> {
|
||||
String btnName = binding.button13.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button14.setOnClickListener(v -> {
|
||||
String btnName = binding.button14.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button15.setOnClickListener(v -> {
|
||||
String btnName = binding.button15.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button16.setOnClickListener(v -> {
|
||||
String btnName = binding.button16.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button17.setOnClickListener(v -> {
|
||||
String btnName = binding.button17.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button18.setOnClickListener(v -> {
|
||||
String btnName = binding.button18.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.button19.setOnClickListener(v -> {
|
||||
String btnName = binding.button19.getText().toString();
|
||||
String text = binding.editTextNumber.getText().toString();
|
||||
binding.editTextNumber.setText(text+btnName);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
|
||||
|
||||
// 進入頁面時立即檢查取貨口 (使用貨道1作為代表)
|
||||
// callback 為 null,表示檢查通過後不做額外動作,僅在檢查失敗時顯示警告
|
||||
checkSlotHaveProduct(1, 1, null);
|
||||
}
|
||||
|
||||
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
|
||||
public ReadBarCodeOnScheduler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(HandlerMain handlerMain) {
|
||||
try {
|
||||
int totalPrice = 0;
|
||||
for (BuyStructure buy : MyApp.getInstance().getBuyList()) {
|
||||
totalPrice += buy.getFullPrice();
|
||||
}
|
||||
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
usbDev.read(buffer, 100);
|
||||
for (int i=0; i<7; i++) {
|
||||
if (isRun() && openBarcode) {
|
||||
int size = usbDev.read(buffer, 5000);
|
||||
if (size > 5) {
|
||||
String barCodeString = new String(buffer, 0, size);
|
||||
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+":"+ Tools.randomReplace(barCodeString, 0.3)+","+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
|
||||
|
||||
try {
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(barCodeString);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
|
||||
openBarcode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
usbDev.closePort();
|
||||
} catch (LogsUnsupportedOperationException | LogsIOException | LogsEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
|
||||
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
|
||||
totalPrice += buy.getFullPrice();
|
||||
}
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public void changeDispatch() {
|
||||
if (isChangeDispatch) {
|
||||
closeDialogCountdown.shutdown();
|
||||
isChangeDispatch = false;
|
||||
changeDialog(DispatchDialog.class);
|
||||
}
|
||||
}
|
||||
|
||||
public void changeOk() {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
// 長度檢查
|
||||
if (number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
|
||||
return;
|
||||
}
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void changeBuy() {
|
||||
closeDialogCountdown.shutdown();
|
||||
BuyPickupCodeDialog buyDialog = new BuyPickupCodeDialog(getContext(), getSrcHandlerMain());
|
||||
buyDialog.show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
closeDialogCountdown.shutdown();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText(getContext().getString(R.string.return_to)+"("+countdown+")");//返回
|
||||
}
|
||||
|
||||
public void setStatusText(String message) {
|
||||
binding.textStatus.setText(message);
|
||||
}
|
||||
|
||||
public void reportAccessCodeData() {
|
||||
try {
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setField(MyApp.getInstance().getReportAccessCodeStructure().getField());
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setSlot(MyApp.getInstance().getBuyList().get(0).getSlot());
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setProductId(MyApp.getInstance().getReportAccessCodeStructure().getProductId());
|
||||
httpAPI.reportAccessCode(MyApp.getInstance().getReportAccessCodeStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePickupCodeData() {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
handlerMain.start(getClass().getSimpleName(), CashPayDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
|
||||
public void checkSlotHaveProduct(int slotnum, int slotcount, Runnable onSafe) {
|
||||
haveThings = false;
|
||||
doorError = false;
|
||||
|
||||
// 使用 AtomicBoolean 防止重複執行 (Timeout vs Callback)
|
||||
// 預設為 false,只要有一方執行過就設為 true,避免重複觸發
|
||||
java.util.concurrent.atomic.AtomicBoolean isHandled = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
// 設定 3000ms 超時機制:如果機器沒反應,視為正常並繼續
|
||||
// 增加這段是因為如果 Slot 為空,VMC 可能不會回傳任何資料,導致程式卡住
|
||||
// 但如果 onSafe 為 null (代表僅監控不阻塞流程),則不啟動超時,以免太快判定為安全而蓋過隨後回傳的 Error
|
||||
if (onSafe != null) {
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (!isHandled.getAndSet(true)) {
|
||||
getLogs().info("VMC Check Slot Timeout - Approving Safe (Timeout Safeguard)");
|
||||
onSafe.run();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlotPre(slotnum,slotcount));
|
||||
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slotnum));
|
||||
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(getHandlerMain(), onSafe, isHandled));
|
||||
}
|
||||
|
||||
public class DevCheckSlotByVMC extends DevController.ReadBufferOnScheduler {
|
||||
private Runnable onSafe;
|
||||
private java.util.concurrent.atomic.AtomicBoolean isHandled;
|
||||
|
||||
public DevCheckSlotByVMC(HandlerMain handlerMain, Runnable onSafe, java.util.concurrent.atomic.AtomicBoolean isHandled) {
|
||||
super(handlerMain);
|
||||
this.onSafe = onSafe;
|
||||
this.isHandled = isHandled;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
|
||||
// 每次檢查前先重置門狀態旗標
|
||||
doorError = false;
|
||||
for (byte[] buffer: devController.getReadBufferByMax()) {
|
||||
if (buffer[0] == 0x02) {
|
||||
getLogs().info("XinYuan Read:" + PortTools.showHex(buffer));
|
||||
if (buffer[1] == 0x05) {
|
||||
haveThings = true;
|
||||
break;
|
||||
}else if (buffer[1] == 0x06 || buffer[1] == 0x09) {
|
||||
// 0x06:取貨口門沒關上
|
||||
// 0x09:微波爐取貨口門關閉錯誤
|
||||
getLogs().info("檢測到取貨口門未正常關閉,狀態碼=" + String.format("%02X", buffer[1]));
|
||||
doorError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(haveThings){
|
||||
// 防止 Timeout 後又執行這裡,或者重複執行
|
||||
if (isHandled.getAndSet(true)) return;
|
||||
|
||||
// 使用主執行緒顯示 AlertDialog
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
AlertDialog dialog = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.abnormal_reminder)
|
||||
.setMessage(R.string.there_is_something_at_the_pickup_port)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.i_knew, (d, which) -> {
|
||||
d.dismiss();
|
||||
// 如果有東西,可能需要重置或關閉
|
||||
FontendPickupCodeDialog.this.dialogCancel();
|
||||
})
|
||||
.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
// 加入 10 秒後自動觸發點擊確定
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
FontendPickupCodeDialog.this.dialogCancel();
|
||||
}
|
||||
}, 10_000); // 10 秒(10000 毫秒)
|
||||
});
|
||||
} else if (doorError) {
|
||||
if (isHandled.getAndSet(true)) return;
|
||||
|
||||
// 取貨口門未關閉,提示使用者並阻止後續購買流程
|
||||
handlerMain.start("FontendPickupCodeDialog", "取貨口門未正常關閉,請先確認門已關好");
|
||||
// 使用主執行緒顯示 AlertDialog
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
AlertDialog dialog = new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.abnormal_reminder)
|
||||
.setMessage("取貨口門未正常關閉,請先確認門已關好。")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.i_knew, (d, which) -> {
|
||||
d.dismiss();
|
||||
FontendPickupCodeDialog.this.dialogCancel();
|
||||
})
|
||||
.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
// 加入 10 秒後自動觸發點擊確定
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
FontendPickupCodeDialog.this.dialogCancel();
|
||||
}
|
||||
}, 10_000); // 10 秒(10000 毫秒)
|
||||
});
|
||||
} else {
|
||||
// 沒有其他狀況,執行後續動作
|
||||
// 注意:這裡假設 VMC 回傳就代表檢測完成。如果 VMC 會分段回傳,可能需要更嚴謹的判斷。
|
||||
// 但依照原邏輯,只要沒 error 就算 pass
|
||||
if (!isHandled.getAndSet(true)) {
|
||||
if (onSafe != null) {
|
||||
new Handler(Looper.getMainLooper()).post(onSafe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
package com.unibuy.smartdevice.ui.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.RecyclerDialogBuyListBinding;
|
||||
import com.unibuy.smartdevice.databinding.RecyclerDialogPickupCodeBuyListBinding;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.exception.Logs;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
|
||||
import com.unibuy.smartdevice.ui.tools.ImageGlide;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<RecyclerDialogPickupCodeBuyListAdpter.ViewHolder> {
|
||||
private Logs logs;
|
||||
private Context context;
|
||||
private final List<BuyStructure> buyList;
|
||||
private HandlerMain handlerMain;
|
||||
|
||||
public RecyclerDialogPickupCodeBuyListAdpter(Context context, List<BuyStructure> buyList, HandlerMain handlerMain) {
|
||||
this.logs = new Logs(this.getClass());
|
||||
this.context = context;
|
||||
this.buyList = buyList;
|
||||
this.handlerMain = handlerMain;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
RecyclerDialogPickupCodeBuyListBinding binding;
|
||||
|
||||
public ViewHolder(@NonNull RecyclerDialogPickupCodeBuyListBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
// 使用 ViewBinding 來綁定佈局
|
||||
RecyclerDialogPickupCodeBuyListBinding binding = RecyclerDialogPickupCodeBuyListBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
|
||||
return new ViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
logs.info("position:" + position);
|
||||
|
||||
BuyStructure buyProduct = buyList.get(position);
|
||||
holder.binding.textPosition.setText(String.valueOf(position));
|
||||
String productID = buyProduct.getProduct().getProductID();
|
||||
if (!productID.isEmpty() && !productID.equals("?")) {
|
||||
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
|
||||
} else {
|
||||
holder.binding.textProductName.setText(context.getString(R.string.click_select_product));//
|
||||
}
|
||||
// holder.binding.textDelete.setOnClickListener(new textDeleteOnClickListener(position));
|
||||
String urlString = buyProduct.getProduct().getProductImg();
|
||||
try {
|
||||
new ImageGlide(context).fileload(urlString, holder.binding.imageProductPicture);
|
||||
} catch (LogsEmptyException e) {
|
||||
this.logs.warning(e);
|
||||
}
|
||||
int machinePrice = buyProduct.getProduct().getMachinePrice();
|
||||
int sellingPrice = buyProduct.getProduct().getSellingPrice();
|
||||
int memberPrice = buyProduct.getProduct().getMemberPrice();
|
||||
|
||||
int slotUpperLimit = buyProduct.getProduct().getSlotUpperLimit();
|
||||
int count = buyProduct.getCount();
|
||||
|
||||
int price = machinePrice > 0? machinePrice: sellingPrice;
|
||||
|
||||
holder.binding.textPrice.setText("$"+price);
|
||||
holder.binding.textCount.setText("x"+String.valueOf(count));
|
||||
|
||||
logs.info("field:" + buyProduct.getField());
|
||||
}
|
||||
|
||||
public class addition1OnClickListener implements View.OnClickListener {
|
||||
private Context context;
|
||||
private RecyclerDialogBuyListBinding binding;
|
||||
private int position;
|
||||
|
||||
public addition1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
|
||||
this.context = context;
|
||||
this.binding = binding;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BuyStructure buyProduct = buyList.get(position);
|
||||
|
||||
int count = buyProduct.getCount();
|
||||
count++;
|
||||
buyList.get(position).setCount(count);
|
||||
binding.textCount.setText("x"+String.valueOf(count));
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(), "total price");
|
||||
}
|
||||
}
|
||||
|
||||
public class subtraction1OnClickListener implements View.OnClickListener {
|
||||
private Context context;
|
||||
private RecyclerDialogBuyListBinding binding;
|
||||
private int position;
|
||||
public subtraction1OnClickListener(Context context, RecyclerDialogBuyListBinding binding, int position) {
|
||||
this.context = context;
|
||||
this.binding = binding;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
BuyStructure buyProduct = buyList.get(position);
|
||||
|
||||
int count = buyProduct.getCount();
|
||||
count--;
|
||||
if (count <= 0) {
|
||||
buyList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, buyList.size() - position);
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(),context.getString(R.string.delete_a_record));//刪除一筆資料
|
||||
} else {
|
||||
buyList.get(position).setCount(count);
|
||||
binding.textCount.setText("x"+String.valueOf(count));
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_PRICE.getOption(),"total price");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class editCountOnEditorActionListener implements TextView.OnEditorActionListener{
|
||||
private ViewHolder holder;
|
||||
private int position;
|
||||
|
||||
public editCountOnEditorActionListener(ViewHolder holder, int position) {
|
||||
this.holder = holder;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
int count = Integer.valueOf(v.getText().toString());
|
||||
buyList.get(position).setCount(count);
|
||||
notifyItemChanged(position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class textDeleteOnClickListener implements View.OnClickListener{
|
||||
private int position;
|
||||
|
||||
public textDeleteOnClickListener(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
delProductData(position);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return buyList.size();
|
||||
}
|
||||
|
||||
public void addProductData(BuyStructure buyProduct) {
|
||||
buyList.add(buyProduct);
|
||||
notifyItemInserted(buyList.size() -1);
|
||||
}
|
||||
|
||||
public void delProductData(int position) {
|
||||
buyList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
}
|
||||
@ -20,9 +20,9 @@
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/cpu"
|
||||
android:icon="@drawable/starcloud_logo_circle"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/cpu"
|
||||
android:roundIcon="@drawable/starcloud_logo_circle"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SmartDevice"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
|
||||
@ -43,8 +43,8 @@ public abstract class AppCompatActivityAbstract extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// 讀取最後設定語言的設定
|
||||
// LanguageHelper.applySavedLanguage(this);
|
||||
// 讀取最後設定語言的設定(集中在 base,所有 Activity 在 inflate 前都會套用)
|
||||
LanguageHelper.applySavedLanguage(this);
|
||||
|
||||
ctx = setCtx();
|
||||
this.logs = new Logs(setCls());
|
||||
|
||||
@ -6,8 +6,13 @@ public final class AppEntry {
|
||||
private AppEntry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否為「取貨單」購買流程(原 Cmuh 中國醫版)。
|
||||
* 改由雲端 B014 shopping_mode 決定(pickup_sheet),取代舊 project flavor 判定。
|
||||
* 開機時 {@code MyApp.initialSystemSet} 已載入持久化值,故 {@link #initialActivity()} 取得到。
|
||||
*/
|
||||
public static boolean isCmuh() {
|
||||
return "Cmuh".equalsIgnoreCase(BuildConfig.FLAVOR_project);
|
||||
return "pickup_sheet".equals(MyApp.getInstance().getShoppingMode());
|
||||
}
|
||||
|
||||
public static Class<?> initialActivity() {
|
||||
|
||||
@ -88,6 +88,7 @@ public abstract class DialogAbstract extends AlertDialog {
|
||||
// 設定對話框寬度為螢幕的 90%
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
window.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
|
||||
WindowManager.LayoutParams layoutParams = window.getAttributes();
|
||||
// layoutParams.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.9);
|
||||
|
||||
@ -97,11 +98,11 @@ public abstract class DialogAbstract extends AlertDialog {
|
||||
// 設定高度為包裹內容(可調整為 MATCH_PARENT)
|
||||
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
// 設定對話框貼齊底部
|
||||
layoutParams.gravity = Gravity.BOTTOM;
|
||||
// 設定對話框居中顯示
|
||||
layoutParams.gravity = Gravity.CENTER;
|
||||
|
||||
window.setAttributes(layoutParams);
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.HIDDEN_MULTILANGUAGE_BUTTON.getOption(), "hidden button");
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.HIDE_SALE_OVERLAY.getOption(), "hide sale overlay");
|
||||
}
|
||||
|
||||
setCancelable(false);
|
||||
@ -144,6 +145,23 @@ public abstract class DialogAbstract extends AlertDialog {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析當前 flavor 要使用的現金付款 Dialog 類別。
|
||||
* General 使用強化版 CashPayDialogGeneral,其餘 flavor 使用標準 CashPayDialog。
|
||||
* 透過 SaleFlowHandler.cashDialogClass() 由各 flavor 決定,避免共用碼直接相依 flavor 類別。
|
||||
*/
|
||||
protected Class<? extends DialogAbstract> cashDialogClass() {
|
||||
Context c = getCtx();
|
||||
if (c instanceof FontendActivity) {
|
||||
com.unibuy.smartdevice.ui.SaleFlowHandler handler =
|
||||
((FontendActivity) c).getSaleFlowHandler();
|
||||
if (handler != null) {
|
||||
return handler.cashDialogClass();
|
||||
}
|
||||
}
|
||||
return com.unibuy.smartdevice.ui.dialog.CashPayDialog.class;
|
||||
}
|
||||
|
||||
|
||||
// public void changeDialog(Class<? extends DialogAbstract> cls) {
|
||||
// try {
|
||||
@ -202,6 +220,6 @@ public abstract class DialogAbstract extends AlertDialog {
|
||||
public void cancel() {
|
||||
super.cancel();
|
||||
// MyApp.getInstance().setShowDialog(false); // 移至 dismiss 統一處理
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.SHOW_MULTILANGUAGE_BUTTON.getOption(), "show button");
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.SHOW_SALE_OVERLAY.getOption(), "show sale overlay");
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +145,9 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
showLoginDialog(getCtx(), this::updateLoginStatus);
|
||||
} else {
|
||||
MyApp.getInstance().setSuAccess("");
|
||||
// 登出時一併清除身分與登入 Token,避免唯讀判斷殘留
|
||||
MyApp.getInstance().setIdentity("");
|
||||
MyApp.getInstance().setLoginToken("");
|
||||
updateLoginStatus();
|
||||
Toast.makeText(getCtx(), "已登出後台權限", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import com.unibuy.smartdevice.exception.LogsNoSuchElementException;
|
||||
import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
|
||||
import com.unibuy.smartdevice.structure.AccessCodeStructure;
|
||||
import com.unibuy.smartdevice.structure.CashSetStructure;
|
||||
import com.unibuy.smartdevice.structure.HardwareSetStructure;
|
||||
import com.unibuy.smartdevice.structure.CreditCardReceiptStructure;
|
||||
import com.unibuy.smartdevice.structure.FreeGiftStructure;
|
||||
import com.unibuy.smartdevice.structure.MemberVerificationStructure;
|
||||
@ -101,6 +102,7 @@ public class MyApp extends Application {
|
||||
private String cmuhPickupErrorMessage;
|
||||
private Map<String, List<MemoStructure>> memoMap;
|
||||
private CashSetStructure cashSetStructure;
|
||||
private HardwareSetStructure hardwareSetStructure;
|
||||
private AccessCodeStructure accessCodeStructure;
|
||||
private FreeGiftStructure freeGiftStructure;
|
||||
private MemberVerificationStructure memberVerificationStructure;
|
||||
@ -110,6 +112,18 @@ public class MyApp extends Application {
|
||||
private EsunpayStructure esunpay;
|
||||
private MachineStructure machine;
|
||||
private DevSetStructure devSet;
|
||||
// 購買流程模式:basic(多品項購物車)/ employee_card(員工卡)/ pickup_sheet(取貨單),
|
||||
// 由雲端 B014 ShoppingMode 下發,取代舊 project flavor 分家。預設 basic。
|
||||
private String shoppingMode = "basic";
|
||||
// 上傳 log 的寫檔門檻等級。預設 INFO:debug.txt 只留 INFO/WARNING/ERROR/UPDATE,過濾掉 DEBUG/IGNORE 雜訊。
|
||||
// 工程師現場除錯硬體時可設為 DEBUG,讓 hex/協議細節重新寫檔(logcat 一律照印,不受此值影響)。
|
||||
private String logLevel = "INFO";
|
||||
// 機台顯示語系(由雲端 B014 LangSet 下發)。標準標籤(zh-TW/en/...),有序,第 0 個為預設。
|
||||
private java.util.List<String> machineLanguages = new java.util.ArrayList<>();
|
||||
private String defaultLanguage = "";
|
||||
// 商品名稱多語 map(B012 t060v01_i18n),key=商品ID,value=底線代碼→名稱 的 JSONObject。
|
||||
private JSONObject productNameI18nBlob = new JSONObject();
|
||||
private JSONObject productSpecI18nBlob = new JSONObject();
|
||||
private GreenInvoiceStructure greenInvoice;
|
||||
private GreenpaySettingStructure greenpaySetting;
|
||||
private GreenpayMerchantStructure greenpayMerchant;
|
||||
@ -125,6 +139,10 @@ public class MyApp extends Application {
|
||||
|
||||
public boolean isDetectHost;
|
||||
public String suAccess;
|
||||
// 登入身分(B000 回傳 identity):system=系統方(可編輯系統設定並回寫雲端)/ company / staff(唯讀)。
|
||||
private String identity = "";
|
||||
// B000 登入核發的使用者 Token,供系統設定回寫(B016, auth:sanctum)使用;不覆蓋 machine ApiKey。
|
||||
private String loginToken = "";
|
||||
|
||||
// ─── 全域閒置計時器 ───────────────────────────────────────
|
||||
// 記錄使用者最後一次觸碰螢幕的時間 (ms)
|
||||
@ -192,8 +210,29 @@ public class MyApp extends Application {
|
||||
this.comPort2ByNFCPay = ComPort.COM_PORT_S2;
|
||||
}
|
||||
|
||||
this.devSet = new DevSetStructure(true, false, false, true, true,
|
||||
true, true, false, false,true,true,true,true,true);
|
||||
// DevSet 初始預設值改由 res/values/devset_defaults.xml 提供,
|
||||
// 各 flavor 可覆寫(General override 為「購物車 + 現金/刷卡/掃碼/發票」全開)。
|
||||
android.content.res.Resources devSetRes = getResources();
|
||||
this.devSet = new DevSetStructure(
|
||||
devSetRes.getBoolean(R.bool.devset_default_vmc),
|
||||
devSetRes.getBoolean(R.bool.devset_default_electric),
|
||||
devSetRes.getBoolean(R.bool.devset_default_shopping_cart),
|
||||
devSetRes.getBoolean(R.bool.devset_default_invoice),
|
||||
devSetRes.getBoolean(R.bool.devset_default_nfc_pay),
|
||||
devSetRes.getBoolean(R.bool.devset_default_esun_pay),
|
||||
devSetRes.getBoolean(R.bool.devset_default_tap_pay),
|
||||
devSetRes.getBoolean(R.bool.devset_default_cash),
|
||||
devSetRes.getBoolean(R.bool.devset_default_line_pay),
|
||||
devSetRes.getBoolean(R.bool.devset_default_tap_pay_30),
|
||||
devSetRes.getBoolean(R.bool.devset_default_tap_pay_31),
|
||||
devSetRes.getBoolean(R.bool.devset_default_tap_pay_32),
|
||||
devSetRes.getBoolean(R.bool.devset_default_tap_pay_33),
|
||||
devSetRes.getBoolean(R.bool.devset_default_tap_pay_34),
|
||||
devSetRes.getBoolean(R.bool.devset_default_pickup_code),
|
||||
devSetRes.getBoolean(R.bool.devset_default_pass_code));
|
||||
// FunctionSet 母開關/來店禮不在建構子參數,建構後補套 bootstrap 預設(雲端 B014 一到即覆蓋)
|
||||
this.devSet.setPickupModule(devSetRes.getBoolean(R.bool.devset_default_pickup_module));
|
||||
this.devSet.setWelcomeGift(devSetRes.getBoolean(R.bool.devset_default_welcome_gift));
|
||||
this.machine = new MachineStructure("");
|
||||
this.esunpay = new EsunpayStructure("", "", "");
|
||||
this.greenInvoice = new GreenInvoiceStructure("", "", "", "");
|
||||
@ -218,6 +257,8 @@ public class MyApp extends Application {
|
||||
"",
|
||||
"");
|
||||
this.cashSetStructure = new CashSetStructure(true, true, true, true, true, true, true);
|
||||
// HardwareSet(貨道型式)預設全 false,由雲端 B014 下發或系統方主控台設定後覆寫。
|
||||
this.hardwareSetStructure = new HardwareSetStructure(false, false, false, false, false, false);
|
||||
this.accessCodeStructure = new AccessCodeStructure("");
|
||||
this.freeGiftStructure = new FreeGiftStructure("");
|
||||
|
||||
@ -332,11 +373,51 @@ public class MyApp extends Application {
|
||||
MyApp.getInstance().getDevSet().setDevTapPay(jsonObject.getBoolean("DevTapPay"));
|
||||
MyApp.getInstance().getDevSet().setDevCash(jsonObject.getBoolean("DevCash"));
|
||||
MyApp.getInstance().getDevSet().setDevLinePay(jsonObject.getBoolean("DevLinePay"));
|
||||
// TapPay30~34 與取貨碼/通行碼:用 optBoolean 容忍舊紀錄缺 key,避免整段載入中斷
|
||||
MyApp.getInstance().getDevSet().setTapPay30(jsonObject.optBoolean("TapPay30", false));
|
||||
MyApp.getInstance().getDevSet().setTapPay31(jsonObject.optBoolean("TapPay31", false));
|
||||
MyApp.getInstance().getDevSet().setTapPay32(jsonObject.optBoolean("TapPay32", false));
|
||||
MyApp.getInstance().getDevSet().setTapPay33(jsonObject.optBoolean("TapPay33", false));
|
||||
MyApp.getInstance().getDevSet().setTapPay34(jsonObject.optBoolean("TapPay34", false));
|
||||
MyApp.getInstance().getDevSet().setPickupCode(jsonObject.optBoolean("PickupCode", false));
|
||||
MyApp.getInstance().getDevSet().setPassCode(jsonObject.optBoolean("PassCode", false));
|
||||
// B014 新增開關(DevSet 細項 + FunctionSet 模組),optBoolean 容忍舊紀錄缺 key
|
||||
MyApp.getInstance().getDevSet().setDevCreditCard(jsonObject.optBoolean("DevCreditCard", false));
|
||||
MyApp.getInstance().getDevSet().setDevMobilePay(jsonObject.optBoolean("DevMobilePay", false));
|
||||
MyApp.getInstance().getDevSet().setDevCardPay(jsonObject.optBoolean("DevCardPay", false));
|
||||
MyApp.getInstance().getDevSet().setDevScanPay(jsonObject.optBoolean("DevScanPay", false));
|
||||
MyApp.getInstance().getDevSet().setPickupModule(jsonObject.optBoolean("PickupModule", false));
|
||||
MyApp.getInstance().getDevSet().setWelcomeGift(jsonObject.optBoolean("WelcomeGift", false));
|
||||
MyApp.getInstance().getDevSet().setMemberSystem(jsonObject.optBoolean("MemberSystem", false));
|
||||
MyApp.getInstance().getDevSet().setAmbientTemp(jsonObject.optBoolean("AmbientTemp", false));
|
||||
MyApp.getInstance().setShoppingMode(jsonObject.optString("ShoppingMode", "basic"));
|
||||
MyApp.getInstance().setLogLevel(jsonObject.optString("LogLevel", "INFO"));
|
||||
// 機台顯示語系(B014 LangSet)
|
||||
java.util.List<String> langs = new java.util.ArrayList<>();
|
||||
JSONArray langArr = jsonObject.optJSONArray("MachineLanguages");
|
||||
if (langArr != null) {
|
||||
for (int i = 0; i < langArr.length(); i++) {
|
||||
String l = langArr.optString(i, "");
|
||||
if (l != null && !l.isEmpty()) langs.add(l);
|
||||
}
|
||||
}
|
||||
MyApp.getInstance().setMachineLanguages(langs);
|
||||
MyApp.getInstance().setDefaultLanguage(jsonObject.optString("DefaultLanguage", ""));
|
||||
} catch (JSONException e) {
|
||||
new Logs(MyApp.class).warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 商品名稱多語 blob(B012 t060v01_i18n),離線重啟時供 fallback
|
||||
if (memoDao.existOnlyOne("ProductNameI18n")) {
|
||||
MyApp.getInstance().setProductNameI18nBlob(memoDao.getOnlyOne("ProductNameI18n").getData());
|
||||
}
|
||||
|
||||
// 商品規格多語 blob(B012 t060v03_i18n),離線重啟時供 fallback
|
||||
if (memoDao.existOnlyOne("ProductSpecI18n")) {
|
||||
MyApp.getInstance().setProductSpecI18nBlob(memoDao.getOnlyOne("ProductSpecI18n").getData());
|
||||
}
|
||||
|
||||
if (memoDao.existOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName())) {
|
||||
JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()).getData();
|
||||
logs.debug("initialSystemSet:" + jsonObject.toString());
|
||||
@ -352,6 +433,22 @@ public class MyApp extends Application {
|
||||
new Logs(MyApp.class).warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (memoDao.existOnlyOne(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName())) {
|
||||
JSONObject jsonObject = memoDao.getOnlyOne(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName()).getData();
|
||||
logs.debug("initialSystemSet:" + jsonObject.toString());
|
||||
try {
|
||||
HardwareSetStructure h = MyApp.getInstance().getHardwareSetStructure();
|
||||
h.setSpringSlot1_10(jsonObject.optBoolean("SpringSlot1_10", false));
|
||||
h.setSpringSlot11_20(jsonObject.optBoolean("SpringSlot11_20", false));
|
||||
h.setSpringSlot21_30(jsonObject.optBoolean("SpringSlot21_30", false));
|
||||
h.setSpringSlot31_40(jsonObject.optBoolean("SpringSlot31_40", false));
|
||||
h.setSpringSlot41_50(jsonObject.optBoolean("SpringSlot41_50", false));
|
||||
h.setSpringSlot51_60(jsonObject.optBoolean("SpringSlot51_60", false));
|
||||
} catch (Exception e) {
|
||||
new Logs(MyApp.class).warning(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void saveSystemSet(Context context) {
|
||||
MemoDao memoDao = new MemoDao(context);
|
||||
@ -367,6 +464,25 @@ public class MyApp extends Application {
|
||||
systemSetByJsonObject.put("DevTapPay", MyApp.getInstance().getDevSet().isDevTapPay());
|
||||
systemSetByJsonObject.put("DevCash", MyApp.getInstance().getDevSet().isDevCash());
|
||||
systemSetByJsonObject.put("DevLinePay", MyApp.getInstance().getDevSet().isDevLinePay());
|
||||
systemSetByJsonObject.put("TapPay30", MyApp.getInstance().getDevSet().isTapPay30());
|
||||
systemSetByJsonObject.put("TapPay31", MyApp.getInstance().getDevSet().isTapPay31());
|
||||
systemSetByJsonObject.put("TapPay32", MyApp.getInstance().getDevSet().isTapPay32());
|
||||
systemSetByJsonObject.put("TapPay33", MyApp.getInstance().getDevSet().isTapPay33());
|
||||
systemSetByJsonObject.put("TapPay34", MyApp.getInstance().getDevSet().isTapPay34());
|
||||
systemSetByJsonObject.put("PickupCode", MyApp.getInstance().getDevSet().isPickupCode());
|
||||
systemSetByJsonObject.put("PassCode", MyApp.getInstance().getDevSet().isPassCode());
|
||||
systemSetByJsonObject.put("DevCreditCard", MyApp.getInstance().getDevSet().isDevCreditCard());
|
||||
systemSetByJsonObject.put("DevMobilePay", MyApp.getInstance().getDevSet().isDevMobilePay());
|
||||
systemSetByJsonObject.put("DevCardPay", MyApp.getInstance().getDevSet().isDevCardPay());
|
||||
systemSetByJsonObject.put("DevScanPay", MyApp.getInstance().getDevSet().isDevScanPay());
|
||||
systemSetByJsonObject.put("PickupModule", MyApp.getInstance().getDevSet().isPickupModule());
|
||||
systemSetByJsonObject.put("WelcomeGift", MyApp.getInstance().getDevSet().isWelcomeGift());
|
||||
systemSetByJsonObject.put("MemberSystem", MyApp.getInstance().getDevSet().isMemberSystem());
|
||||
systemSetByJsonObject.put("AmbientTemp", MyApp.getInstance().getDevSet().isAmbientTemp());
|
||||
systemSetByJsonObject.put("ShoppingMode", MyApp.getInstance().getShoppingMode());
|
||||
systemSetByJsonObject.put("LogLevel", MyApp.getInstance().getLogLevel());
|
||||
systemSetByJsonObject.put("MachineLanguages", new JSONArray(MyApp.getInstance().getMachineLanguages()));
|
||||
systemSetByJsonObject.put("DefaultLanguage", MyApp.getInstance().getDefaultLanguage());
|
||||
|
||||
JSONObject cashSetByJsonObject = new JSONObject();
|
||||
cashSetByJsonObject.put("BillF1000", MyApp.getInstance().getCashSetStructure().isBillF1000());
|
||||
@ -377,6 +493,14 @@ public class MyApp extends Application {
|
||||
cashSetByJsonObject.put("CoinD5", MyApp.getInstance().getCashSetStructure().isCoinD5());
|
||||
cashSetByJsonObject.put("CoinC1", MyApp.getInstance().getCashSetStructure().isCoinC1());
|
||||
|
||||
JSONObject hardwareSetByJsonObject = new JSONObject();
|
||||
hardwareSetByJsonObject.put("SpringSlot1_10", MyApp.getInstance().getHardwareSetStructure().isSpringSlot1_10());
|
||||
hardwareSetByJsonObject.put("SpringSlot11_20", MyApp.getInstance().getHardwareSetStructure().isSpringSlot11_20());
|
||||
hardwareSetByJsonObject.put("SpringSlot21_30", MyApp.getInstance().getHardwareSetStructure().isSpringSlot21_30());
|
||||
hardwareSetByJsonObject.put("SpringSlot31_40", MyApp.getInstance().getHardwareSetStructure().isSpringSlot31_40());
|
||||
hardwareSetByJsonObject.put("SpringSlot41_50", MyApp.getInstance().getHardwareSetStructure().isSpringSlot41_50());
|
||||
hardwareSetByJsonObject.put("SpringSlot51_60", MyApp.getInstance().getHardwareSetStructure().isSpringSlot51_60());
|
||||
|
||||
memoDao.insertOnlyOne(new MemoStructure(
|
||||
MyApp.getInstance().getDevSet().getClass().getSimpleName(),
|
||||
systemSetByJsonObject
|
||||
@ -394,11 +518,48 @@ public class MyApp extends Application {
|
||||
memoDao.mapUpdate();
|
||||
|
||||
new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getCashSetStructure().getClass().getSimpleName()));
|
||||
|
||||
memoDao.insertOnlyOne(new MemoStructure(
|
||||
MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName(),
|
||||
hardwareSetByJsonObject
|
||||
, new JSONObject()));
|
||||
|
||||
memoDao.mapUpdate();
|
||||
|
||||
new Logs(MyApp.class).info("saveSystemSet count:" + memoDao.getCount(MyApp.getInstance().getHardwareSetStructure().getClass().getSimpleName()));
|
||||
} catch (JSONException e) {
|
||||
new Logs(MyApp.class).warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化商品名稱多語 blob(不動 products 表 schema,存於 MemoDao),供離線重啟 fallback
|
||||
public static void saveProductNameI18n(Context context) {
|
||||
MemoDao memoDao = new MemoDao(context);
|
||||
try {
|
||||
memoDao.insertOnlyOne(new MemoStructure(
|
||||
"ProductNameI18n",
|
||||
MyApp.getInstance().getProductNameI18nBlob(),
|
||||
new JSONObject()));
|
||||
memoDao.mapUpdate();
|
||||
} catch (Exception e) {
|
||||
new Logs(MyApp.class).warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化商品規格多語 blob(不動 products 表 schema,存於 MemoDao),供離線重啟 fallback
|
||||
public static void saveProductSpecI18n(Context context) {
|
||||
MemoDao memoDao = new MemoDao(context);
|
||||
try {
|
||||
memoDao.insertOnlyOne(new MemoStructure(
|
||||
"ProductSpecI18n",
|
||||
MyApp.getInstance().getProductSpecI18nBlob(),
|
||||
new JSONObject()));
|
||||
memoDao.mapUpdate();
|
||||
} catch (Exception e) {
|
||||
new Logs(MyApp.class).warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateLog(Context context) {
|
||||
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(UploadLogOneTimeWorker.class)
|
||||
.setInitialDelay(5, TimeUnit.SECONDS)
|
||||
@ -680,7 +841,7 @@ public class MyApp extends Application {
|
||||
int i = 0;
|
||||
for (SlotStructure slotData: getSlotList(field)) {
|
||||
i += 1;
|
||||
logs.info("AAAAAAAAAAAAAAAAAAAAAAAAAAAA0"+i);
|
||||
logs.debug("slot index:" + i);
|
||||
if (slotData.getProduct().getProductID().equals(String.valueOf(pid))) {
|
||||
if(slotData.getCount() > 0) {
|
||||
return slotData;
|
||||
@ -872,6 +1033,68 @@ public class MyApp extends Application {
|
||||
return devSet;
|
||||
}
|
||||
|
||||
public String getShoppingMode() {
|
||||
return shoppingMode;
|
||||
}
|
||||
|
||||
public void setShoppingMode(String shoppingMode) {
|
||||
this.shoppingMode = (shoppingMode == null || shoppingMode.isEmpty()) ? "basic" : shoppingMode;
|
||||
}
|
||||
|
||||
public String getLogLevel() {
|
||||
return logLevel;
|
||||
}
|
||||
|
||||
public void setLogLevel(String logLevel) {
|
||||
this.logLevel = (logLevel == null || logLevel.isEmpty()) ? "INFO" : logLevel.toUpperCase(java.util.Locale.US);
|
||||
}
|
||||
|
||||
public java.util.List<String> getMachineLanguages() {
|
||||
return machineLanguages;
|
||||
}
|
||||
|
||||
public void setMachineLanguages(java.util.List<String> machineLanguages) {
|
||||
this.machineLanguages = (machineLanguages == null) ? new java.util.ArrayList<>() : machineLanguages;
|
||||
}
|
||||
|
||||
public String getDefaultLanguage() {
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
public void setDefaultLanguage(String defaultLanguage) {
|
||||
this.defaultLanguage = (defaultLanguage == null) ? "" : defaultLanguage;
|
||||
}
|
||||
|
||||
public void setProductNameI18nBlob(JSONObject blob) {
|
||||
this.productNameI18nBlob = (blob == null) ? new JSONObject() : blob;
|
||||
}
|
||||
|
||||
public JSONObject getProductNameI18nBlob() {
|
||||
return productNameI18nBlob;
|
||||
}
|
||||
|
||||
// 取某商品的名稱多語 JSON 字串(底線代碼 key),無則回 null。供 ProductStructure 離線時 fallback。
|
||||
public String getProductNameI18nJson(String productId) {
|
||||
if (productId == null || productNameI18nBlob == null) return null;
|
||||
JSONObject obj = productNameI18nBlob.optJSONObject(productId);
|
||||
return (obj == null) ? null : obj.toString();
|
||||
}
|
||||
|
||||
public void setProductSpecI18nBlob(JSONObject blob) {
|
||||
this.productSpecI18nBlob = (blob == null) ? new JSONObject() : blob;
|
||||
}
|
||||
|
||||
public JSONObject getProductSpecI18nBlob() {
|
||||
return productSpecI18nBlob;
|
||||
}
|
||||
|
||||
// 取某商品的規格多語 JSON 字串(底線代碼 key),無則回 null。供 ProductStructure 離線時 fallback。
|
||||
public String getProductSpecI18nJson(String productId) {
|
||||
if (productId == null || productSpecI18nBlob == null) return null;
|
||||
JSONObject obj = productSpecI18nBlob.optJSONObject(productId);
|
||||
return (obj == null) ? null : obj.toString();
|
||||
}
|
||||
|
||||
|
||||
public ReportAccessCodeStructure getReportAccessCodeStructure() {
|
||||
return reportAccessCodeStructure;
|
||||
@ -885,6 +1108,10 @@ public class MyApp extends Application {
|
||||
return cashSetStructure;
|
||||
}
|
||||
|
||||
public HardwareSetStructure getHardwareSetStructure() {
|
||||
return hardwareSetStructure;
|
||||
}
|
||||
|
||||
public AccessCodeStructure getAccessCodeStructure() {
|
||||
return accessCodeStructure;
|
||||
}
|
||||
@ -943,9 +1170,10 @@ public class MyApp extends Application {
|
||||
if (isShowDialog()) {
|
||||
reason.append("[Dialog Open] ");
|
||||
}
|
||||
if (!getBuyList().isEmpty()) {
|
||||
reason.append("[Cart not empty: ").append(getBuyList().size()).append(" items] ");
|
||||
}
|
||||
// 注意:購物車非空「不可」計入忙碌。否則待機/重啟會與清車死結——
|
||||
// resetIdleTimer 忙碌即不待機並重新計時,但清空購物車的程式就寫在待機那一支,
|
||||
// 購物車有殘留 → 永遠忙碌 → 永遠不待機 → 永遠清不掉。真正「進行中」狀態
|
||||
// 已由 isShowDialog(付款/出貨對話框)與 isNfcPaymentActive(感應支付)涵蓋。
|
||||
if (isNfcPaymentActive()) {
|
||||
reason.append("[NFC Payment Active] ");
|
||||
}
|
||||
@ -965,6 +1193,27 @@ public class MyApp extends Application {
|
||||
this.suAccess = suAccess;
|
||||
}
|
||||
|
||||
public String getIdentity() {
|
||||
return identity == null ? "" : identity;
|
||||
}
|
||||
|
||||
public void setIdentity(String identity) {
|
||||
this.identity = identity == null ? "" : identity;
|
||||
}
|
||||
|
||||
/** 是否為系統方(可編輯系統設定並回寫雲端)。 */
|
||||
public boolean isSystemIdentity() {
|
||||
return "system".equals(getIdentity());
|
||||
}
|
||||
|
||||
public String getLoginToken() {
|
||||
return loginToken == null ? "" : loginToken;
|
||||
}
|
||||
|
||||
public void setLoginToken(String loginToken) {
|
||||
this.loginToken = loginToken == null ? "" : loginToken;
|
||||
}
|
||||
|
||||
public boolean isDetectHost() {
|
||||
return isDetectHost;
|
||||
}
|
||||
|
||||
@ -151,30 +151,17 @@ public class DevNFCPayController extends HandlerMainScheduler {
|
||||
getLogs().info("電文接收:" + readData);
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowRequestInfo(readData);
|
||||
|
||||
while (isRun() && MyApp.getInstance().isShowDialog()) { // ✅ 增加狀態檢查
|
||||
byte[] buffer = comPort2.readListen(30, 200);
|
||||
// 【修復 付款成功卻不出貨】交易結果(含 responseCode)就在這段 404 byte 電文裡,
|
||||
// 直接解析本段即可。先前版本誤改成再用 readListen 等「第二段」電文才解析,
|
||||
// 但刷卡機只送這一段,第二段永遠等不到 → 倒數逾時 → RESPONSE 從未送出 → 不出貨。
|
||||
HashMap<String, String> readMap = devNFCPay.read(readData);
|
||||
String code = readMap.get("code");
|
||||
getLogs().info("交易完成,回傳代碼: " + code);
|
||||
|
||||
if (buffer.length > 0) {
|
||||
getLogs().info("NFC 收到資料: " + PortTools.showHex(buffer));
|
||||
String rxData = new String(buffer, StandardCharsets.UTF_8);
|
||||
|
||||
if (rxData.contains(String.valueOf((char) 0x03))) { // ETX
|
||||
HashMap<String, String> response = devNFCPay.read(rxData);
|
||||
String code = response.get("code");
|
||||
|
||||
if (isRun() && !isShuttingDown && MyApp.getInstance().isShowDialog()) { // ✅ 最後回報前的檢查
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
|
||||
} else {
|
||||
getLogs().info("🚫 控制器已停止或對話框已關閉,捨棄 NFC 回傳結果");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRun() || isShuttingDown || !MyApp.getInstance().isShowDialog()) {
|
||||
getLogs().info("NFC 讀取中斷: 控制器關閉或對話框消失");
|
||||
break;
|
||||
}
|
||||
if (isRun() && !isShuttingDown && MyApp.getInstance().isShowDialog()) { // ✅ 回報前的生命週期檢查
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
|
||||
} else {
|
||||
getLogs().info("🚫 控制器已停止或對話框已關閉,捨棄 NFC 回傳結果");
|
||||
}
|
||||
} catch (LogsParseException | LogsIOException e) {
|
||||
getLogs().warning(e);
|
||||
|
||||
@ -56,7 +56,7 @@ public class DevXinYuanController extends DevController {
|
||||
try {
|
||||
this.rs232Dev = new Rs232Dev(MyApp.getInstance().getComPort1ByXinYuan());
|
||||
devXinYuan.setRun(true);
|
||||
logs.debug("下位機連接");
|
||||
logs.info("下位機連接");
|
||||
} catch (LogsUnsupportedOperationException | LogsIOException e) {
|
||||
// getLogs().error(ErrorCode.DEV_ERROR, "@下位機連接錯誤", handlerMain);
|
||||
getLogs().warning(e);
|
||||
@ -123,9 +123,9 @@ public class DevXinYuanController extends DevController {
|
||||
Byte key = Byte.valueOf(vmcCmd[0]);
|
||||
|
||||
if (!vmcMap.containsKey(key)) {
|
||||
getLogs().info("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
|
||||
getLogs().info("XinYuan vmcData:" + PortTools.showHex(vmcData));
|
||||
getLogs().info("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
|
||||
getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
|
||||
getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData));
|
||||
getLogs().debug("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
|
||||
getLogs().info("XinYuan vmc not found");
|
||||
continue;
|
||||
}
|
||||
@ -133,9 +133,9 @@ public class DevXinYuanController extends DevController {
|
||||
DevXinYuan.VMC vmc = vmcMap.get(key);
|
||||
|
||||
if (vmc != DevXinYuan.VMC.VMC_41_POLL) {
|
||||
getLogs().info("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
|
||||
getLogs().info("XinYuan vmcData:" + PortTools.showHex(vmcData));
|
||||
getLogs().info("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
|
||||
getLogs().debug("XinYuan vmcCmd:" + PortTools.showHex(vmcCmd));
|
||||
getLogs().debug("XinYuan vmcData:" + PortTools.showHex(vmcData));
|
||||
getLogs().debug("XinYuan vmcXor:" + PortTools.showHex(vmcXor));
|
||||
getLogs().info("XinYuan vmc:" + vmc.name());
|
||||
}
|
||||
|
||||
@ -143,12 +143,12 @@ public class DevXinYuanController extends DevController {
|
||||
case VMC_41_POLL:
|
||||
if (getCountBySendBuffer() > 0) {
|
||||
byte[] cmdBytes = getSendBufferByFirst();
|
||||
getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"+bytesToHex(cmdBytes));
|
||||
getLogs().debug("XinYuan cmd hex:" + bytesToHex(cmdBytes));
|
||||
|
||||
if (cmdBytes.length == (4+cmdBytes[3]+1)) {
|
||||
for (int sendCount=1; sendCount <2; sendCount++) {
|
||||
rs232Dev.send(cmdBytes);
|
||||
getLogs().info("XinYuan Send:" + PortTools.showHex(cmdBytes));
|
||||
getLogs().debug("XinYuan Send:" + PortTools.showHex(cmdBytes));
|
||||
|
||||
byte[] vmcBuffer = rs232Dev.readListen(0, 1);
|
||||
if (vmcBuffer.length > 2) {
|
||||
@ -247,7 +247,7 @@ public class DevXinYuanController extends DevController {
|
||||
break;
|
||||
case VMC_52_CHECK_VMC_STATUS:
|
||||
//溫度:vmcData[7]
|
||||
getLogs().info("下位機訊號"+PortTools.showHex(vmcData));
|
||||
getLogs().debug("下位機訊號"+PortTools.showHex(vmcData));
|
||||
if (PortTools.showHex(vmcData).isEmpty()) {
|
||||
getLogs().info("下位機失連,重啟APP");
|
||||
|
||||
|
||||
@ -46,17 +46,16 @@ public enum ComPort {
|
||||
}
|
||||
|
||||
public String getFilepath() {
|
||||
// 下位機接線由硬體 flavor(IPC 世代 / 安卓版本)決定,與購買流程 shopping_mode 正交:
|
||||
// S12XYU(新板,原晟崴 10")→ ttyS7/ttyS6;
|
||||
// S9XYU (Android 9 板,原中國醫/通用 21.5")→ ttyS1/ttyS2。
|
||||
// 單一 flavor 維度(hardware)下,AGP 僅產生 BuildConfig.FLAVOR(值為 S12XYU / S9XYU)。
|
||||
boolean isS12 = "S12XYU".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR);
|
||||
if (this == COM_PORT_S1) {
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
return "/dev/ttyS1";
|
||||
}
|
||||
return "/dev/ttyS7";
|
||||
return isS12 ? "/dev/ttyS7" : "/dev/ttyS1";
|
||||
}
|
||||
if (this == COM_PORT_S2) {
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
return "/dev/ttyS2";
|
||||
}
|
||||
return "/dev/ttyS6";
|
||||
return isS12 ? "/dev/ttyS6" : "/dev/ttyS2";
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
|
||||
@ -267,7 +267,7 @@ public class DevXinYuan {
|
||||
proBytes = PortTools.concatByteArrays(proBytes, XORByte);
|
||||
|
||||
if (isShow) {
|
||||
logs.info("XinYuan ipcCmd:" + cmd.name() + " " + PortTools.showHex(proBytes));
|
||||
logs.debug("XinYuan ipcCmd:" + cmd.name() + " " + PortTools.showHex(proBytes));
|
||||
}
|
||||
|
||||
return proBytes;
|
||||
@ -371,18 +371,18 @@ public class DevXinYuan {
|
||||
}
|
||||
|
||||
public CMD readCMD(byte[] buffer) throws LogsEmptyException {
|
||||
logs.info(PortTools.showHex(buffer));
|
||||
logs.debug(PortTools.showHex(buffer));
|
||||
|
||||
for (CMD cmd : CMD.values()) {
|
||||
if (cmd.getBytes().length >= 2 && buffer.length >= 2) {
|
||||
if (cmd.getBytes()[0] == buffer[0]) {
|
||||
if (cmd.equals(CMD.CMD_FAFB_STX)) {
|
||||
if (cmd.getBytes()[1] == buffer[1]) {
|
||||
logs.info("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return cmd;
|
||||
}
|
||||
} else {
|
||||
logs.info("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug("CMD:" + cmd.name() + " " + PortTools.showHex(cmd.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@ -393,18 +393,18 @@ public class DevXinYuan {
|
||||
}
|
||||
|
||||
public VMC readVMC(byte[] buffer) throws LogsEmptyException {
|
||||
logs.info(PortTools.showHex(buffer));
|
||||
logs.debug(PortTools.showHex(buffer));
|
||||
|
||||
for (VMC vmc : VMC.values()) {
|
||||
if (vmc.getBytes().length >= 2 && buffer.length >= 2) {
|
||||
if (vmc.getBytes()[0] == buffer[0]) {
|
||||
if (vmc.equals(VMC.VMC_FAFB_STX)) {
|
||||
if (vmc.getBytes()[1] == buffer[1]) {
|
||||
logs.info("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return vmc;
|
||||
}
|
||||
} else {
|
||||
logs.info("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug("VMC:" + vmc.name() + " " + PortTools.showHex(vmc.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return vmc;
|
||||
}
|
||||
}
|
||||
@ -416,7 +416,7 @@ public class DevXinYuan {
|
||||
|
||||
public byte readPackNo(byte[] buffer) {
|
||||
byte packNo = buffer[0];
|
||||
logs.info("Read packNo:" + PortTools.showHex(packNo));
|
||||
logs.debug("Read packNo:" + PortTools.showHex(packNo));
|
||||
return packNo;
|
||||
}
|
||||
|
||||
@ -424,7 +424,7 @@ public class DevXinYuan {
|
||||
for (BUY_STATUS_04 buyStatus : BUY_STATUS_04.values()) {
|
||||
if (buyStatus.getBytes().length == 1 && buffer.length > 0) {
|
||||
if (buyStatus.getBytes()[0] == buffer[1]) {
|
||||
logs.info(BUY_STATUS_04.class.getSimpleName()+":" + buyStatus.name() + " " + PortTools.showHex(buyStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug(BUY_STATUS_04.class.getSimpleName()+":" + buyStatus.name() + " " + PortTools.showHex(buyStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return buyStatus;
|
||||
}
|
||||
}
|
||||
@ -437,7 +437,7 @@ public class DevXinYuan {
|
||||
for (SLOT_STATUS_02 slotStatus : SLOT_STATUS_02.values()) {
|
||||
if (slotStatus.getBytes().length == 1 && buffer.length > 0) {
|
||||
if (slotStatus.getBytes()[0] == buffer[1]) {
|
||||
logs.info(SLOT_STATUS_02.class.getSimpleName()+":" + slotStatus.name() + " " + PortTools.showHex(slotStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug(SLOT_STATUS_02.class.getSimpleName()+":" + slotStatus.name() + " " + PortTools.showHex(slotStatus.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return slotStatus;
|
||||
}
|
||||
}
|
||||
@ -451,7 +451,7 @@ public class DevXinYuan {
|
||||
for (GIVE_MONEY_26 giveMoney : GIVE_MONEY_26.values()) {
|
||||
if (giveMoney.getBytes().length == 1 && buffer.length > 0) {
|
||||
if (giveMoney.getBytes()[0] == buffer[1]) {
|
||||
logs.info(GIVE_MONEY_26.class.getSimpleName()+":" + giveMoney.name() + " " + PortTools.showHex(giveMoney.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
logs.debug(GIVE_MONEY_26.class.getSimpleName()+":" + giveMoney.name() + " " + PortTools.showHex(giveMoney.getBytes()) + " buffer:" + PortTools.showHex(new byte[]{buffer[0], buffer[1]}));
|
||||
return giveMoney;
|
||||
}
|
||||
}
|
||||
@ -475,10 +475,10 @@ public class DevXinYuan {
|
||||
byte XORByte = buffer[buffer.length -1];
|
||||
proBytes = PortTools.concatByteArrays(proBytes, cmdLenData);
|
||||
byte XORByte2 = getXOR(proBytes);
|
||||
logs.info("Length:"+length+" XOR1:" + PortTools.showHex(new byte[] {XORByte}) + " XOR2:" + PortTools.showHex(new byte[] {XORByte2}));
|
||||
logs.debug("Length:"+length+" XOR1:" + PortTools.showHex(new byte[] {XORByte}) + " XOR2:" + PortTools.showHex(new byte[] {XORByte2}));
|
||||
if (buffer.length > (2+length)) {
|
||||
byte[] dataBytes = PortTools.catByteArrays(buffer, 2, length);
|
||||
logs.info("data:"+PortTools.showHex(dataBytes));
|
||||
logs.debug("data:"+PortTools.showHex(dataBytes));
|
||||
return dataBytes;
|
||||
} else {
|
||||
throw new LogsEmptyException(logs, "data array length insufficient");
|
||||
|
||||
@ -18,6 +18,10 @@ import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Logs {
|
||||
// 單筆寫檔上限:超過即截斷,避免整包 JSON/payload dump 把 debug.txt 撐爆、淹沒有用事件。
|
||||
// 只影響「寫入 debug.txt」那份;logcat(Log.d/i/...)維持完整不截斷。
|
||||
private static final int MAX_LINE = 1000;
|
||||
|
||||
private String message;
|
||||
private String className;
|
||||
private String methodName;
|
||||
@ -46,6 +50,10 @@ public class Logs {
|
||||
}
|
||||
|
||||
String dateTime = Tools.getTimeStampByNow("yyyy-MM-dd HH:mm:ss.SSS");
|
||||
// 招 1:超長行截斷(止血整包 JSON/payload dump),保留事件本身與原始長度資訊。
|
||||
if (data.length() > MAX_LINE) {
|
||||
data = data.substring(0, MAX_LINE) + " …[truncated " + data.length() + " chars]";
|
||||
}
|
||||
data = dateTime + " " + data;
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(txtDebug, true)) { // try-with-resources 自動 close()
|
||||
@ -61,10 +69,20 @@ public class Logs {
|
||||
return this.className+"."+this.methodName+"("+this.lineNumber+")";
|
||||
}
|
||||
|
||||
// 招 2:是否把 DEBUG/IGNORE 寫進 debug.txt。預設(門檻 INFO)不寫,過濾掉開發雜訊;
|
||||
// 雲端/現場把 LogLevel 設為 DEBUG 時才寫檔。MyApp 尚未就緒時一律不寫,維持乾淨預設。
|
||||
private static boolean writeDebugToFile() {
|
||||
try {
|
||||
return "DEBUG".equals(com.unibuy.smartdevice.MyApp.getInstance().getLogLevel());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void debug(String message) {
|
||||
this.message = message;
|
||||
Log.d(this.className, "DEBUG ["+getClassName()+"]"+this.message);
|
||||
writeFile("DEBUG ["+getClassName()+"]"+this.message);
|
||||
if (writeDebugToFile()) writeFile("DEBUG ["+getClassName()+"]"+this.message);
|
||||
}
|
||||
|
||||
public void info(String message) {
|
||||
@ -87,7 +105,7 @@ public class Logs {
|
||||
public void ignore(String message) {
|
||||
this.message = message;
|
||||
Log.w(this.className, "IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message);
|
||||
writeFile("IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message);
|
||||
if (writeDebugToFile()) writeFile("IGNORE ["+getClassName()+"/"+getInfo()+"]"+this.message);
|
||||
}
|
||||
|
||||
public void warning(Exception e) {
|
||||
|
||||
@ -493,7 +493,6 @@ public class HttpAPI {
|
||||
public void response(int responseCode, String receive, HandlerMain handlerMain) {
|
||||
if (responseCode == 200) {
|
||||
try {
|
||||
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
MyApp.getInstance().getMachine().setMachineID("");
|
||||
|
||||
MyApp.getInstance().getEsunpay().setStoreID("");
|
||||
@ -656,9 +655,7 @@ public class HttpAPI {
|
||||
MyApp.getInstance().getGreenpayMerchant().getAtomeMerchantId()
|
||||
}));
|
||||
|
||||
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa0");
|
||||
SettingActivity.updateProgressUI(10, "下載中...");
|
||||
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa10");
|
||||
settingsDao.close();
|
||||
} catch (JSONException e) {
|
||||
getLogs().warning(e);
|
||||
@ -720,7 +717,7 @@ public class HttpAPI {
|
||||
* t060v06 "https://unibuy.com.tw/Unibuy/dist/img/products/53317506.png",
|
||||
*/
|
||||
|
||||
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAA" + productData);
|
||||
getLogs().debug("productData:" + productData);
|
||||
|
||||
if (productData.has("t060v00") &&
|
||||
productData.has("t060v06") &&
|
||||
@ -783,7 +780,6 @@ public class HttpAPI {
|
||||
productsDao.close();
|
||||
|
||||
SettingActivity.updateProgressUI(10, "下載中...");
|
||||
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa30");
|
||||
|
||||
handlerMain.start(getClass().getSimpleName(), Option.DOWNLOAD_PRODUCT_RESPONSE.getOption(),
|
||||
"成功載入" + MyApp.getInstance().getProductList().size() + "筆商品資料");
|
||||
@ -1053,9 +1049,8 @@ public class HttpAPI {
|
||||
JSONObject receiveByJsonObject = new JSONObject(receive);
|
||||
String workApi = receiveByJsonObject.getString("workApi");
|
||||
int status = receiveByJsonObject.getInt("status");
|
||||
getLogs().info("bbbbbbbbbbbbbbbbbbbbbb status" + status);
|
||||
String message = receiveByJsonObject.getString("message");
|
||||
getLogs().info("bbbbbbbbbbbbbbbbbbbbbb message" + message);
|
||||
getLogs().debug("reportFlow status:" + status + " message:" + message);
|
||||
|
||||
if (status == 0) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.REPORT_FLOW_RESPONSE.getOption(),
|
||||
@ -2142,7 +2137,7 @@ public class HttpAPI {
|
||||
*/
|
||||
jsonObject2.put("data", jsonArray);
|
||||
|
||||
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=" + jsonObject2.toString());
|
||||
getLogs().debug("B009 supplementary:" + jsonObject2.toString());
|
||||
|
||||
formPutRunMain.start(domainNameByUnibuy + "products/supplementary/B009", jsonObject2.toString(), null, null,
|
||||
HttpConnect.ContentType.JSON);
|
||||
@ -2228,7 +2223,7 @@ public class HttpAPI {
|
||||
int slotid = Integer.parseInt(deliveryData.getString("tid"));
|
||||
int slotnum = Integer.parseInt(deliveryData.getString("num"));
|
||||
|
||||
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + slotid + " " + slotnum);
|
||||
getLogs().info("dispatch slotid:" + slotid + " num:" + slotnum);
|
||||
|
||||
slotid = slotid;
|
||||
|
||||
|
||||
@ -174,11 +174,11 @@ public class HttpTapPay {
|
||||
int payType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
GreenpaySettingStructure greenpaySetting = MyApp.getInstance().getGreenpaySetting();
|
||||
String greenpayMerchantId = MyApp.getInstance().getMachine().getMachineID();
|
||||
logs.info("aaaaaaaaaaaaaaaaaaaaaa1"+ MyApp.getInstance().getGreenpayMerchant().getLineMerchantId());
|
||||
logs.info("aaaaaaaaaaaaaaaaaaaaaa2"+ MyApp.getInstance().getGreenpayMerchant().getJkoMerchantId());
|
||||
logs.info("aaaaaaaaaaaaaaaaaaaaaa3"+ MyApp.getInstance().getGreenpayMerchant().getPiMerchantId());
|
||||
logs.info("aaaaaaaaaaaaaaaaaaaaaa4"+ MyApp.getInstance().getGreenpayMerchant().getPsMerchantId());
|
||||
logs.info("aaaaaaaaaaaaaaaaaaaaaa5"+ MyApp.getInstance().getGreenpayMerchant().getEasyMerchantId());
|
||||
logs.debug("merchantId line:" + MyApp.getInstance().getGreenpayMerchant().getLineMerchantId()
|
||||
+ " jko:" + MyApp.getInstance().getGreenpayMerchant().getJkoMerchantId()
|
||||
+ " pi:" + MyApp.getInstance().getGreenpayMerchant().getPiMerchantId()
|
||||
+ " ps:" + MyApp.getInstance().getGreenpayMerchant().getPsMerchantId()
|
||||
+ " easy:" + MyApp.getInstance().getGreenpayMerchant().getEasyMerchantId());
|
||||
switch (payType) {
|
||||
case 30:
|
||||
if (MyApp.getInstance().getGreenpayMerchant() != null && !MyApp.getInstance().getGreenpayMerchant().getLineMerchantId().isEmpty()) {
|
||||
|
||||
@ -15,6 +15,8 @@ import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.database.ProductsDao;
|
||||
import com.unibuy.smartdevice.database.SettingsDao;
|
||||
import com.unibuy.smartdevice.structure.CashSetStructure;
|
||||
import com.unibuy.smartdevice.structure.DevSetStructure;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.ReportSlotListStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
@ -155,6 +157,9 @@ public class StarCloudAPI {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
if (res.optBoolean("success", false)) {
|
||||
MyApp.getInstance().setSuAccess(res.optString("Su_Account", account));
|
||||
// 身分與登入 Token:identity 決定系統設定是否可編輯;loginToken 供回寫 B016 使用。
|
||||
MyApp.getInstance().setIdentity(res.optString("identity", ""));
|
||||
MyApp.getInstance().setLoginToken(res.optString("token", ""));
|
||||
// 移除覆蓋 ApiKey 的邏輯,因為 B000 回傳的可能是 Admin Token,但我們需要保留 Machine Token
|
||||
Log.i(TAG, "Login success for user: " + account);
|
||||
traceLogin("[" + traceId + "] b000_parse_success parseMs="
|
||||
@ -194,6 +199,17 @@ public class StarCloudAPI {
|
||||
* 不需要 ApiKey 即可呼叫,僅需機器序號。
|
||||
*/
|
||||
public void downloadSetting() {
|
||||
downloadSetting(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* B014: 重新下載系統設定。
|
||||
*
|
||||
* @param reconnectMqtt 設定落地後是否立即重連 MQTT。
|
||||
* 綁定 UI 流程傳 true;遠端 update_settings 指令傳 false,
|
||||
* 改由呼叫端在發出 ACK 之後再重連,避免重連把送指令的連線斷掉導致 ACK 發不出去。
|
||||
*/
|
||||
public void downloadSetting(final boolean reconnectMqtt) {
|
||||
final String machineId = MyApp.getInstance().getMachine().getMachineID();
|
||||
if (machineId.isEmpty()) {
|
||||
Log.e(TAG, "Machine ID is empty, cannot bind.");
|
||||
@ -255,7 +271,7 @@ public class StarCloudAPI {
|
||||
if (res.optBoolean("success", false)) {
|
||||
JSONObject config = res.optJSONObject("data");
|
||||
if (config != null) {
|
||||
saveSettings(config);
|
||||
saveSettings(config, reconnectMqtt);
|
||||
handlerMain.start("StarCloudAPI", 200, "綁定成功");
|
||||
} else {
|
||||
handlerMain.start("StarCloudAPI", 500, "設定內容為空,原始資料:" + finalReceive);
|
||||
@ -593,7 +609,389 @@ public class StarCloudAPI {
|
||||
void onFailure(String message);
|
||||
}
|
||||
|
||||
/** 取貨碼驗證回呼:成功時回傳 code_id 與要出貨的貨道 slot_no。 */
|
||||
public interface PickupVerificationCallback {
|
||||
void onSuccess(String codeId, int slotNo);
|
||||
void onFailure(String message);
|
||||
}
|
||||
|
||||
/** 系統設定回寫回呼(B016)。 */
|
||||
public interface SettingsUploadCallback {
|
||||
void onSuccess();
|
||||
void onFailure(String message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 來店禮驗證回呼(B690):成功回傳折扣資訊。
|
||||
* discountType:amount(折抵金額)/ percentage(折扣百分比,discountValue 為折扣趴數,八折=20)。
|
||||
*/
|
||||
public interface WelcomeGiftCallback {
|
||||
void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name);
|
||||
void onFailure(String message);
|
||||
}
|
||||
|
||||
/**
|
||||
* B016:系統方(identity=system)回寫機台系統設定到雲端 star-cloud。
|
||||
* 使用 B000 登入核發的使用者 Token(auth:sanctum),後端再驗一次系統方權限。
|
||||
* @param settings 後端鍵名的開關 JSON(鍵名對照見 SystemSetActivity#buildBackendSettings)。
|
||||
*/
|
||||
public void uploadSystemSettings(final JSONObject settings, final SettingsUploadCallback callback) {
|
||||
final String url = getBaseUrl() + "machine/setting/B016";
|
||||
final String machineId = MyApp.getInstance().getMachine().getMachineID();
|
||||
final String loginToken = MyApp.getInstance().getLoginToken();
|
||||
Log.i(TAG, "Starting B016 (update settings) for machine: " + machineId);
|
||||
|
||||
if (loginToken == null || loginToken.isEmpty()) {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(
|
||||
() -> callback.onFailure("尚未登入或登入逾時,請重新登入後台"));
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int responseCode = -1;
|
||||
String receive = "";
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("machine", machineId);
|
||||
json.put("settings", settings);
|
||||
|
||||
java.net.URL reqUrl = new java.net.URL(url);
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
|
||||
conn.setRequestMethod("PATCH");
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + loginToken);
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(15000);
|
||||
conn.setDoOutput(true);
|
||||
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
|
||||
conn.getOutputStream().flush();
|
||||
|
||||
responseCode = conn.getResponseCode();
|
||||
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
|
||||
? conn.getInputStream() : conn.getErrorStream();
|
||||
if (is != null) {
|
||||
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
receive = sb.toString();
|
||||
}
|
||||
conn.disconnect();
|
||||
Log.d(TAG, "B016 Response (" + responseCode + "): " + receive);
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "B016 Network Error", e);
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(
|
||||
() -> callback.onFailure("連線失敗: " + e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
final int finalCode = responseCode;
|
||||
final String finalReceive = receive;
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
if (finalCode >= 200 && finalCode < 300) {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
if (res.optBoolean("success", false)) {
|
||||
callback.onSuccess();
|
||||
} else {
|
||||
callback.onFailure(res.optString("message", "設定儲存失敗"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("解析錯誤 (" + finalCode + ")");
|
||||
}
|
||||
} else if (finalCode == 403) {
|
||||
callback.onFailure("權限不足:僅系統方可修改系統設定");
|
||||
} else if (finalCode == 401) {
|
||||
callback.onFailure("登入逾時,請重新登入後台");
|
||||
} else if (finalCode == 404) {
|
||||
callback.onFailure("查無此機台");
|
||||
} else {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* B660: 取貨碼驗證(邏輯比照 B680 員工卡,驗證通道不同)。
|
||||
* 成功回傳 data.code_id 與 data.slot_no(要出貨的貨道)。
|
||||
*/
|
||||
public void verifyPickupCode(final String code, final PickupVerificationCallback callback) {
|
||||
final String url = getBaseUrl() + "machine/pickup/verify/B660";
|
||||
Log.i(TAG, "Starting B660 request for code: " + code);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int responseCode = -1;
|
||||
String receive = "";
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("code", code);
|
||||
|
||||
java.net.URL reqUrl = new java.net.URL(url);
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
String machineToken = MyApp.getInstance().getMachine().getApiKey();
|
||||
if (machineToken != null && !machineToken.isEmpty()) {
|
||||
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
|
||||
}
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.setReadTimeout(10000);
|
||||
conn.setDoOutput(true);
|
||||
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
|
||||
conn.getOutputStream().flush();
|
||||
|
||||
responseCode = conn.getResponseCode();
|
||||
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
|
||||
? conn.getInputStream() : conn.getErrorStream();
|
||||
if (is != null) {
|
||||
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
receive = sb.toString();
|
||||
}
|
||||
conn.disconnect();
|
||||
Log.d(TAG, "B660 Response (" + responseCode + "): " + receive);
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "B660 Network Error", e);
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
final int finalCode = responseCode;
|
||||
final String finalReceive = receive;
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
if (finalCode >= 200 && finalCode < 300) {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
if (res.optBoolean("success", false)) {
|
||||
JSONObject data = res.optJSONObject("data");
|
||||
String codeId = data != null ? data.optString("code_id", "") : "";
|
||||
int slotNo = data != null ? data.optInt("slot_no", -1) : -1;
|
||||
callback.onSuccess(codeId, slotNo);
|
||||
} else {
|
||||
callback.onFailure(res.optString("message", "取貨碼驗證失敗"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("解析錯誤: " + finalCode);
|
||||
}
|
||||
} else if (finalCode == 404) {
|
||||
callback.onFailure("查無此取貨碼或已使用");
|
||||
} else if (finalCode == 429) {
|
||||
callback.onFailure("錯誤次數過多,請稍後再試");
|
||||
} else if (finalCode == 401) {
|
||||
callback.onFailure("系統認證失效,請聯繫管理員");
|
||||
} else {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* B670: 通行碼驗證(技師用,邏輯比照 B680,驗證通道不同)。
|
||||
* 成功回傳 data.code_id。
|
||||
*/
|
||||
public void verifyPassCode(final String code, final VerificationCallback callback) {
|
||||
final String url = getBaseUrl() + "machine/passcode/verify/B670";
|
||||
Log.i(TAG, "Starting B670 request for code: " + code);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int responseCode = -1;
|
||||
String receive = "";
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("code", code);
|
||||
|
||||
java.net.URL reqUrl = new java.net.URL(url);
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
String machineToken = MyApp.getInstance().getMachine().getApiKey();
|
||||
if (machineToken != null && !machineToken.isEmpty()) {
|
||||
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
|
||||
}
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.setReadTimeout(10000);
|
||||
conn.setDoOutput(true);
|
||||
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
|
||||
conn.getOutputStream().flush();
|
||||
|
||||
responseCode = conn.getResponseCode();
|
||||
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
|
||||
? conn.getInputStream() : conn.getErrorStream();
|
||||
if (is != null) {
|
||||
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
receive = sb.toString();
|
||||
}
|
||||
conn.disconnect();
|
||||
Log.d(TAG, "B670 Response (" + responseCode + "): " + receive);
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "B670 Network Error", e);
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
final int finalCode = responseCode;
|
||||
final String finalReceive = receive;
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
if (finalCode >= 200 && finalCode < 300) {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
if (res.optBoolean("success", false)) {
|
||||
String codeId = "";
|
||||
if (res.has("data")) {
|
||||
codeId = res.optJSONObject("data").optString("code_id", "");
|
||||
} else {
|
||||
codeId = res.optString("code_id", "");
|
||||
}
|
||||
callback.onSuccess(codeId);
|
||||
} else {
|
||||
callback.onFailure(res.optString("message", "通行碼驗證失敗"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("解析錯誤: " + finalCode);
|
||||
}
|
||||
} else if (finalCode == 404) {
|
||||
callback.onFailure("查無此通行碼或已失效");
|
||||
} else if (finalCode == 429) {
|
||||
callback.onFailure("錯誤次數過多,請稍後再試");
|
||||
} else if (finalCode == 401) {
|
||||
callback.onFailure("系統認證失效,請聯繫管理員");
|
||||
} else {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* B690: 來店禮驗證(邏輯比照 B660/B670,驗證通道不同)。
|
||||
* 成功回傳 data.code_id 與折扣資訊(discount_type/discount_value/discount_label/name)。
|
||||
*/
|
||||
public void verifyWelcomeGift(final String code, final WelcomeGiftCallback callback) {
|
||||
final String url = getBaseUrl() + "machine/welcome-gift/verify/B690";
|
||||
Log.i(TAG, "Starting B690 request for code: " + code);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int responseCode = -1;
|
||||
String receive = "";
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("code", code);
|
||||
|
||||
java.net.URL reqUrl = new java.net.URL(url);
|
||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) reqUrl.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
String machineToken = MyApp.getInstance().getMachine().getApiKey();
|
||||
if (machineToken != null && !machineToken.isEmpty()) {
|
||||
conn.setRequestProperty("Authorization", "Bearer " + machineToken);
|
||||
}
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.setReadTimeout(10000);
|
||||
conn.setDoOutput(true);
|
||||
conn.getOutputStream().write(json.toString().getBytes("UTF-8"));
|
||||
conn.getOutputStream().flush();
|
||||
|
||||
responseCode = conn.getResponseCode();
|
||||
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
|
||||
? conn.getInputStream() : conn.getErrorStream();
|
||||
if (is != null) {
|
||||
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line);
|
||||
receive = sb.toString();
|
||||
}
|
||||
conn.disconnect();
|
||||
Log.d(TAG, "B690 Response (" + responseCode + "): " + receive);
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "B690 Network Error", e);
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> callback.onFailure("連線失敗: " + e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
final int finalCode = responseCode;
|
||||
final String finalReceive = receive;
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
if (finalCode >= 200 && finalCode < 300) {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
if (res.optBoolean("success", false)) {
|
||||
JSONObject data = res.optJSONObject("data");
|
||||
String codeId = data != null ? data.optString("code_id", "") : "";
|
||||
String discountType = data != null ? data.optString("discount_type", "") : "";
|
||||
int discountValue = data != null ? data.optInt("discount_value", 0) : 0;
|
||||
String discountLabel = data != null ? data.optString("discount_label", "") : "";
|
||||
String name = data != null ? data.optString("name", "") : "";
|
||||
callback.onSuccess(codeId, discountType, discountValue, discountLabel, name);
|
||||
} else {
|
||||
callback.onFailure(res.optString("message", "來店禮驗證失敗"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("解析錯誤: " + finalCode);
|
||||
}
|
||||
} else if (finalCode == 404) {
|
||||
callback.onFailure("查無此來店禮或已失效");
|
||||
} else if (finalCode == 429) {
|
||||
callback.onFailure("錯誤次數過多,請稍後再試");
|
||||
} else if (finalCode == 401) {
|
||||
callback.onFailure("系統認證失效,請聯繫管理員");
|
||||
} else if (finalCode == 400) {
|
||||
callback.onFailure("來店禮格式錯誤(需 8 碼)");
|
||||
} else {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
callback.onFailure(res.optString("message", "系統連線錯誤 (" + finalCode + ")"));
|
||||
} catch (Exception e) {
|
||||
callback.onFailure("系統連線錯誤 (" + finalCode + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void saveSettings(JSONObject config) {
|
||||
saveSettings(config, true);
|
||||
}
|
||||
|
||||
private void saveSettings(JSONObject config, boolean reconnectMqtt) {
|
||||
// 使用 optString 避免欄位缺失導致 JSONException
|
||||
|
||||
// 1. 儲存 ApiToken 與 序號
|
||||
@ -623,9 +1021,106 @@ public class StarCloudAPI {
|
||||
MyApp.getInstance().getGreenInvoice().setGreenHashIV(config.optString("t050v36", ""));
|
||||
MyApp.getInstance().getGreenInvoice().setGreenEmail(config.optString("t050v38", ""));
|
||||
|
||||
// 5. 重要:將所有設定同步回資料庫,防止 App 重啟後消失
|
||||
// 5. 解析雲端功能開關(缺漏「欄位」→ false;缺漏「整個物件」→ 不動本地,避免連線異常把現場全關)
|
||||
JSONObject devSet = config.optJSONObject("DevSet");
|
||||
if (devSet != null) {
|
||||
DevSetStructure d = MyApp.getInstance().getDevSet();
|
||||
d.setShoppingCar(devSet.optBoolean("ShoppingCar", false));
|
||||
d.setInvoice(devSet.optBoolean("Invoice", false));
|
||||
d.setDevNFCPay(devSet.optBoolean("DevNFCPay", false));
|
||||
d.setDevEsunPay(devSet.optBoolean("DevEsunPay", false));
|
||||
d.setDevTapPay(devSet.optBoolean("DevTapPay", false));
|
||||
d.setDevCash(devSet.optBoolean("DevCash", false));
|
||||
d.setDevLinePay(devSet.optBoolean("DevLinePay", false));
|
||||
d.setTapPay30(devSet.optBoolean("TapPay30", false));
|
||||
d.setTapPay31(devSet.optBoolean("TapPay31", false));
|
||||
d.setTapPay32(devSet.optBoolean("TapPay32", false));
|
||||
d.setTapPay33(devSet.optBoolean("TapPay33", false));
|
||||
d.setTapPay34(devSet.optBoolean("TapPay34", false));
|
||||
// B014 新增支付細項(刷卡機/掃碼子開關)
|
||||
d.setDevCreditCard(devSet.optBoolean("DevCreditCard", false));
|
||||
d.setDevMobilePay(devSet.optBoolean("DevMobilePay", false));
|
||||
d.setDevCardPay(devSet.optBoolean("DevCardPay", false));
|
||||
d.setDevScanPay(devSet.optBoolean("DevScanPay", false));
|
||||
// VMC/Electic 雲端不下發 → 不碰,保留本地值
|
||||
}
|
||||
JSONObject cashSet = config.optJSONObject("CashSet");
|
||||
if (cashSet != null) {
|
||||
CashSetStructure c = MyApp.getInstance().getCashSetStructure();
|
||||
c.setBillF1000(cashSet.optBoolean("BillF1000", false));
|
||||
c.setBillE500(cashSet.optBoolean("BillE500", false));
|
||||
c.setBillD100(cashSet.optBoolean("BillD100", false));
|
||||
c.setCoinF50(cashSet.optBoolean("CoinF50", false));
|
||||
c.setCoinE10(cashSet.optBoolean("CoinE10", false));
|
||||
c.setCoinD5(cashSet.optBoolean("CoinD5", false));
|
||||
c.setCoinC1(cashSet.optBoolean("CoinC1", false));
|
||||
}
|
||||
// 6. FunctionSet:取貨模組(母)/取貨碼/通行碼/來店禮/會員/環溫
|
||||
// 取貨碼、通行碼從屬取貨模組(PickupModule);來店禮獨立判斷。
|
||||
JSONObject funcSet = config.optJSONObject("FunctionSet");
|
||||
if (funcSet != null) {
|
||||
DevSetStructure d = MyApp.getInstance().getDevSet();
|
||||
d.setPickupModule(funcSet.optBoolean("PickupModule", false));
|
||||
d.setPickupCode(funcSet.optBoolean("PickupCode", false));
|
||||
d.setPassCode(funcSet.optBoolean("PassCode", false));
|
||||
d.setWelcomeGift(funcSet.optBoolean("WelcomeGift", false));
|
||||
d.setMemberSystem(funcSet.optBoolean("MemberSystem", false));
|
||||
d.setAmbientTemp(funcSet.optBoolean("AmbientTemp", false));
|
||||
}
|
||||
// 6c. HardwareSet 貨道型式(缺整個物件→不動本地,比照 DevSet/CashSet 防呆)
|
||||
JSONObject hardwareSet = config.optJSONObject("HardwareSet");
|
||||
if (hardwareSet != null) {
|
||||
com.unibuy.smartdevice.structure.HardwareSetStructure h = MyApp.getInstance().getHardwareSetStructure();
|
||||
h.setSpringSlot1_10(hardwareSet.optBoolean("SpringSlot1_10", false));
|
||||
h.setSpringSlot11_20(hardwareSet.optBoolean("SpringSlot11_20", false));
|
||||
h.setSpringSlot21_30(hardwareSet.optBoolean("SpringSlot21_30", false));
|
||||
h.setSpringSlot31_40(hardwareSet.optBoolean("SpringSlot31_40", false));
|
||||
h.setSpringSlot41_50(hardwareSet.optBoolean("SpringSlot41_50", false));
|
||||
h.setSpringSlot51_60(hardwareSet.optBoolean("SpringSlot51_60", false));
|
||||
}
|
||||
// 6b. ShoppingMode:購買流程模式(basic/employee_card/pickup_sheet),取代舊 project flavor
|
||||
boolean hasShoppingMode = config.has("ShoppingMode");
|
||||
if (hasShoppingMode) {
|
||||
MyApp.getInstance().setShoppingMode(config.optString("ShoppingMode", "basic"));
|
||||
}
|
||||
|
||||
// 6d. LangSet:機台啟用語系(最多 5 種,有序,第 0 個為預設)。後台單向下發,機台僅顯示與切換。
|
||||
JSONObject langSet = config.optJSONObject("LangSet");
|
||||
if (langSet != null) {
|
||||
java.util.List<String> langs = new java.util.ArrayList<>();
|
||||
org.json.JSONArray arr = langSet.optJSONArray("Languages");
|
||||
if (arr != null) {
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
String code = arr.optString(i, "");
|
||||
if (code != null && !code.isEmpty()) {
|
||||
langs.add(com.unibuy.smartdevice.utils.LanguageHelper.canonicalize(code));
|
||||
}
|
||||
}
|
||||
}
|
||||
MyApp.getInstance().setMachineLanguages(langs);
|
||||
String def = langSet.optString("Default", "");
|
||||
String canonicalDef = (def == null || def.isEmpty())
|
||||
? (langs.isEmpty() ? "" : langs.get(0))
|
||||
: com.unibuy.smartdevice.utils.LanguageHelper.canonicalize(def);
|
||||
MyApp.getInstance().setDefaultLanguage(canonicalDef);
|
||||
|
||||
// 首次(使用者尚未手動選過語言)套用機台預設語系
|
||||
android.content.SharedPreferences prefs = handlerMain.getContext()
|
||||
.getSharedPreferences("app_prefs", android.content.Context.MODE_PRIVATE);
|
||||
if (!canonicalDef.isEmpty() && prefs.getString("language", null) == null) {
|
||||
com.unibuy.smartdevice.utils.LanguageHelper.changeLanguage(handlerMain.getContext(), canonicalDef);
|
||||
}
|
||||
}
|
||||
|
||||
if (devSet != null || cashSet != null || funcSet != null || hardwareSet != null || hasShoppingMode || langSet != null) {
|
||||
MyApp.saveSystemSet(handlerMain.getContext());
|
||||
}
|
||||
|
||||
// 7. 重要:將所有設定同步回資料庫,防止 App 重啟後消失
|
||||
persistCurrentSettings();
|
||||
MyApp.getInstance().initMqtt();
|
||||
if (reconnectMqtt) {
|
||||
MyApp.getInstance().initMqtt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -635,7 +1130,12 @@ public class StarCloudAPI {
|
||||
Log.i(TAG, "persistCurrentSettings() - Saving to DB: machineID=" + MyApp.getInstance().getMachine().getMachineID()
|
||||
+ ", token=" + MyApp.getInstance().getMachine().getApiKey());
|
||||
SettingsDao settingsDao = new SettingsDao(handlerMain.getContext());
|
||||
settingsDao.deleteAll();
|
||||
// 只刪除本方法接下來要重寫的這 4 個設定 key,避免 deleteAll() 連坐刪掉廣告(VideoEoxPlayer)等其他持久資料。
|
||||
// 否則「遠端更新設定→重啟」會因廣告列被清空且啟動不會主動抓 B005,導致廣告消失。
|
||||
settingsDao.deleteWhere(MyApp.getInstance().getMachine().getClass().getSimpleName());
|
||||
settingsDao.deleteWhere(MyApp.getInstance().getEsunpay().getClass().getSimpleName());
|
||||
settingsDao.deleteWhere(MyApp.getInstance().getGreenInvoice().getClass().getSimpleName());
|
||||
settingsDao.deleteWhere(MyApp.getInstance().getGreenpaySetting().getClass().getSimpleName());
|
||||
|
||||
settingsDao.insertOne(new SettingStructure(MyApp.getInstance().getMachine().getClass().getSimpleName(),
|
||||
new String[] {
|
||||
@ -672,6 +1172,10 @@ public class StarCloudAPI {
|
||||
|
||||
private void saveProducts(JSONArray products) throws JSONException {
|
||||
MyApp.getInstance().getProductList().clear();
|
||||
// 商品名稱多語 blob:商品ID → t060v01_i18n(底線代碼 key),存於 MemoDao 持久化(不動 products 表)
|
||||
JSONObject nameI18nBlob = new JSONObject();
|
||||
// 商品規格多語 blob:商品ID → t060v03_i18n(純文字含換行)
|
||||
JSONObject specI18nBlob = new JSONObject();
|
||||
for (int i = 0; i < products.length(); i++) {
|
||||
JSONObject p = products.getJSONObject(i);
|
||||
|
||||
@ -688,8 +1192,33 @@ public class StarCloudAPI {
|
||||
p.optString("t060v41", ""), // Material
|
||||
p.optString("t060v01_en", ""),
|
||||
p.optString("t060v01_jp", ""));
|
||||
|
||||
// 新增:商品名稱多語 map(B012 t060v01_i18n),支援後台啟用的所有語系
|
||||
JSONObject nameI18n = p.optJSONObject("t060v01_i18n");
|
||||
if (nameI18n != null) {
|
||||
product.setNameI18nJson(nameI18n.toString());
|
||||
String pid = p.optString("t060v00", "");
|
||||
if (!pid.isEmpty()) {
|
||||
try { nameI18nBlob.put(pid, nameI18n); } catch (JSONException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:商品規格多語 map(B012 t060v03_i18n,純文字含換行)
|
||||
JSONObject specI18n = p.optJSONObject("t060v03_i18n");
|
||||
if (specI18n != null) {
|
||||
product.setSpecI18nJson(specI18n.toString());
|
||||
String pid = p.optString("t060v00", "");
|
||||
if (!pid.isEmpty()) {
|
||||
try { specI18nBlob.put(pid, specI18n); } catch (JSONException ignore) {}
|
||||
}
|
||||
}
|
||||
MyApp.getInstance().getProductList().add(product);
|
||||
}
|
||||
// 持久化商品多語 blob,供離線重啟時 fallback
|
||||
MyApp.getInstance().setProductNameI18nBlob(nameI18nBlob);
|
||||
MyApp.saveProductNameI18n(handlerMain.getContext());
|
||||
MyApp.getInstance().setProductSpecI18nBlob(specI18nBlob);
|
||||
MyApp.saveProductSpecI18n(handlerMain.getContext());
|
||||
|
||||
// 將商品同步至資料庫
|
||||
ProductsDao productsDao = new ProductsDao(handlerMain.getContext());
|
||||
|
||||
@ -470,6 +470,51 @@ public class MqttService extends Service {
|
||||
logs.info("Failed to start remote app update: " + e.getMessage());
|
||||
mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage());
|
||||
}
|
||||
} else if ("update_settings".equals(cmd)) {
|
||||
logs.info("Executing remote settings sync command...");
|
||||
|
||||
// 🚩 忙碌時拒收:交易/購物車/對話框進行中,避免支付開關等設定中途被改動。
|
||||
// 後台 RemoteCommand 約 1 分鐘 timeout,閒置後可重發。
|
||||
String busyReason = MyApp.getInstance().getBusyReason();
|
||||
if (!busyReason.isEmpty()) {
|
||||
logs.info("Remote settings sync rejected: " + busyReason);
|
||||
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + busyReason);
|
||||
} else {
|
||||
// 📢 在主執行緒顯示 Toast 提示使用者
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
|
||||
android.widget.Toast.makeText(getApplicationContext(),
|
||||
"📢 系統設定更新中...", android.widget.Toast.LENGTH_SHORT).show());
|
||||
|
||||
com.unibuy.smartdevice.tools.HandlerMain settingHandler = new com.unibuy.smartdevice.tools.HandlerMain(
|
||||
getApplicationContext(), logs) {
|
||||
@Override
|
||||
protected void execute(Context context, int code, String message) {
|
||||
logs.info("Settings sync event: [" + code + "] " + message);
|
||||
if (code == 200 && "綁定成功".equals(message)) {
|
||||
// 1. 先發 ACK,確保在 MQTT 重連之前送達後台
|
||||
mqttManager.publishCommandAck(cmdId, "success",
|
||||
"Settings sync completed successfully");
|
||||
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
|
||||
android.widget.Toast.makeText(context,
|
||||
"✅ 系統設定更新完成", android.widget.Toast.LENGTH_SHORT).show());
|
||||
|
||||
// 2. 延後重連 MQTT(token 可能已輪換),留時間讓 ACK 先送出
|
||||
new android.os.Handler(android.os.Looper.getMainLooper())
|
||||
.postDelayed(() -> MyApp.getInstance().initMqtt(), 1500);
|
||||
} else if (code >= 400) {
|
||||
mqttManager.publishCommandAck(cmdId, "failed",
|
||||
"Settings sync failed: " + message);
|
||||
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() ->
|
||||
android.widget.Toast.makeText(context,
|
||||
"❌ 系統設定更新失敗: " + message, android.widget.Toast.LENGTH_LONG).show());
|
||||
}
|
||||
}
|
||||
};
|
||||
// 只重抓設定(B014),不串接商品/廣告;reconnectMqtt=false,改由上方 ACK 後再重連
|
||||
new com.unibuy.smartdevice.external.StarCloudAPI(settingHandler, settingHandler).downloadSetting(false);
|
||||
}
|
||||
} else {
|
||||
logs.info("Unknown MQTT command: " + cmd);
|
||||
mqttManager.publishCommandAck(cmdId, "failed", "Unknown command");
|
||||
|
||||
@ -15,10 +15,25 @@ public class DevSetStructure {
|
||||
private boolean isTapPay32;
|
||||
private boolean isTapPay33;
|
||||
private boolean isTapPay34;
|
||||
// 取貨碼/通行碼:改由雲端 B014 FunctionSet 下發控制(預設值來自 flavor devset_defaults.xml)
|
||||
private boolean isPickupCode;
|
||||
private boolean isPassCode;
|
||||
// 以下為 B014 新增開關,預設 false,由雲端下發或主控台(系統方)設定後覆寫。
|
||||
// DevSet 細項:刷卡機/掃碼底下的子開關
|
||||
private boolean isDevCreditCard;
|
||||
private boolean isDevMobilePay;
|
||||
private boolean isDevCardPay;
|
||||
private boolean isDevScanPay;
|
||||
// FunctionSet 細項:取貨模組(主)、來店禮、會員系統、環境溫度監控
|
||||
private boolean isPickupModule;
|
||||
private boolean isWelcomeGift;
|
||||
private boolean isMemberSystem;
|
||||
private boolean isAmbientTemp;
|
||||
|
||||
public DevSetStructure(boolean isVMC, boolean isElectic, boolean isShoppingCar, boolean isInvoice,
|
||||
boolean isDevNFCPay, boolean isDevEsunPay, boolean isDevTapPay, boolean isDevCash, boolean isDevLinePay,
|
||||
boolean isTapPay30,boolean isTapPay31,boolean isTapPay32,boolean isTapPay33, boolean isTapPay34) {
|
||||
boolean isTapPay30,boolean isTapPay31,boolean isTapPay32,boolean isTapPay33, boolean isTapPay34,
|
||||
boolean isPickupCode, boolean isPassCode) {
|
||||
this.isVMC = isVMC;
|
||||
this.isElectic = isElectic;
|
||||
this.isShoppingCar = isShoppingCar;
|
||||
@ -33,6 +48,8 @@ public class DevSetStructure {
|
||||
this.isTapPay32 = isTapPay32;
|
||||
this.isTapPay33 = isTapPay33;
|
||||
this.isTapPay34 = isTapPay34;
|
||||
this.isPickupCode = isPickupCode;
|
||||
this.isPassCode = isPassCode;
|
||||
}
|
||||
|
||||
public boolean isVMC() {
|
||||
@ -124,4 +141,72 @@ public class DevSetStructure {
|
||||
public void setTapPay33(boolean tapPay33) {isTapPay33 = tapPay33;}
|
||||
public void setTapPay34(boolean tapPay34) {isTapPay34 = tapPay34;}
|
||||
|
||||
public boolean isPickupCode() {
|
||||
return isPickupCode;
|
||||
}
|
||||
|
||||
public void setPickupCode(boolean pickupCode) {
|
||||
isPickupCode = pickupCode;
|
||||
}
|
||||
|
||||
public boolean isPassCode() {
|
||||
return isPassCode;
|
||||
}
|
||||
|
||||
public void setPassCode(boolean passCode) {
|
||||
isPassCode = passCode;
|
||||
}
|
||||
|
||||
public boolean isDevCreditCard() { return isDevCreditCard; }
|
||||
public void setDevCreditCard(boolean devCreditCard) { isDevCreditCard = devCreditCard; }
|
||||
|
||||
public boolean isDevMobilePay() { return isDevMobilePay; }
|
||||
public void setDevMobilePay(boolean devMobilePay) { isDevMobilePay = devMobilePay; }
|
||||
|
||||
public boolean isDevCardPay() { return isDevCardPay; }
|
||||
public void setDevCardPay(boolean devCardPay) { isDevCardPay = devCardPay; }
|
||||
|
||||
public boolean isDevScanPay() { return isDevScanPay; }
|
||||
public void setDevScanPay(boolean devScanPay) { isDevScanPay = devScanPay; }
|
||||
|
||||
public boolean isPickupModule() { return isPickupModule; }
|
||||
public void setPickupModule(boolean pickupModule) { isPickupModule = pickupModule; }
|
||||
|
||||
public boolean isWelcomeGift() { return isWelcomeGift; }
|
||||
public void setWelcomeGift(boolean welcomeGift) { isWelcomeGift = welcomeGift; }
|
||||
|
||||
public boolean isMemberSystem() { return isMemberSystem; }
|
||||
public void setMemberSystem(boolean memberSystem) { isMemberSystem = memberSystem; }
|
||||
|
||||
public boolean isAmbientTemp() { return isAmbientTemp; }
|
||||
public void setAmbientTemp(boolean ambientTemp) { isAmbientTemp = ambientTemp; }
|
||||
|
||||
// ── 支付方式「母 && 子」便利判斷 ──────────────────────────────────────────
|
||||
// 各支付實際是否可用 = 母開關開啟 && 子項開啟。集中於此,讓 UI 顯示與入口防呆共用同一份真相,
|
||||
// 避免在多處(BuyDialog 購物車/單品/例外分支、changeCheckout 入口)各自拼湊而漏判。
|
||||
// 刷卡機(母 isDevNFCPay)→ 信用卡 / 電子票證(卡片) / 手機支付
|
||||
// 掃碼(母 isDevScanPay)→ 玉山 EsunPay / TapPay
|
||||
// Line 官方支付、現金:獨立大項,無母開關(自身即為總開關)
|
||||
|
||||
/** flowType 1 信用卡:刷卡機開啟且信用卡子項開啟。 */
|
||||
public boolean canCreditCard() { return isDevNFCPay && isDevCreditCard; }
|
||||
|
||||
/** flowType 2 電子票證(卡片支付):刷卡機開啟且卡片支付子項開啟。 */
|
||||
public boolean canCardPay() { return isDevNFCPay && isDevCardPay; }
|
||||
|
||||
/** flowType 4 手機支付:刷卡機開啟且手機支付子項開啟。 */
|
||||
public boolean canMobilePay() { return isDevNFCPay && isDevMobilePay; }
|
||||
|
||||
/** flowType 3 玉山 EsunPay:掃碼開啟且玉山子項開啟。 */
|
||||
public boolean canEsunPay() { return isDevScanPay && isDevEsunPay; }
|
||||
|
||||
/** flowType 30 TapPay:掃碼開啟且 TapPay 子項開啟。 */
|
||||
public boolean canTapPay() { return isDevScanPay && isDevTapPay; }
|
||||
|
||||
/** flowType 70 Line 官方支付:獨立大項,不從屬掃碼,只看自身開關。 */
|
||||
public boolean canLinePay() { return isDevLinePay; }
|
||||
|
||||
/** flowType 9 現金:獨立大項,只看自身開關。 */
|
||||
public boolean canCash() { return isDevCash; }
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
package com.unibuy.smartdevice.structure;
|
||||
|
||||
/**
|
||||
* HardwareSet:硬體貨道類型設定(B014 HardwareSet)。
|
||||
* 每個欄位代表一段貨道(10 槽一組)的型式:true=彈簧 / false=履帶。
|
||||
* 由雲端 B014 下發,主控台系統方可編輯後回寫雲端。預設 false。
|
||||
*/
|
||||
public class HardwareSetStructure {
|
||||
private boolean springSlot1_10;
|
||||
private boolean springSlot11_20;
|
||||
private boolean springSlot21_30;
|
||||
private boolean springSlot31_40;
|
||||
private boolean springSlot41_50;
|
||||
private boolean springSlot51_60;
|
||||
|
||||
public HardwareSetStructure(boolean springSlot1_10, boolean springSlot11_20, boolean springSlot21_30,
|
||||
boolean springSlot31_40, boolean springSlot41_50, boolean springSlot51_60) {
|
||||
this.springSlot1_10 = springSlot1_10;
|
||||
this.springSlot11_20 = springSlot11_20;
|
||||
this.springSlot21_30 = springSlot21_30;
|
||||
this.springSlot31_40 = springSlot31_40;
|
||||
this.springSlot41_50 = springSlot41_50;
|
||||
this.springSlot51_60 = springSlot51_60;
|
||||
}
|
||||
|
||||
public boolean isSpringSlot1_10() { return springSlot1_10; }
|
||||
public void setSpringSlot1_10(boolean v) { springSlot1_10 = v; }
|
||||
|
||||
public boolean isSpringSlot11_20() { return springSlot11_20; }
|
||||
public void setSpringSlot11_20(boolean v) { springSlot11_20 = v; }
|
||||
|
||||
public boolean isSpringSlot21_30() { return springSlot21_30; }
|
||||
public void setSpringSlot21_30(boolean v) { springSlot21_30 = v; }
|
||||
|
||||
public boolean isSpringSlot31_40() { return springSlot31_40; }
|
||||
public void setSpringSlot31_40(boolean v) { springSlot31_40 = v; }
|
||||
|
||||
public boolean isSpringSlot41_50() { return springSlot41_50; }
|
||||
public void setSpringSlot41_50(boolean v) { springSlot41_50 = v; }
|
||||
|
||||
public boolean isSpringSlot51_60() { return springSlot51_60; }
|
||||
public void setSpringSlot51_60(boolean v) { springSlot51_60 = v; }
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
package com.unibuy.smartdevice.structure;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.utils.LanguageHelper;
|
||||
|
||||
public class ProductStructure {
|
||||
private String productID;
|
||||
private String productImg;
|
||||
@ -115,6 +119,103 @@ public class ProductStructure {
|
||||
this.productNameJp = productNameJp;
|
||||
}
|
||||
|
||||
// B012 t060v01_i18n 原始 JSON 字串(底線代碼 key,如 {"zh_TW":..,"en":..,"ko":..})。
|
||||
// 由 B012 解析時設入;離線從 DB 重新載入商品時此欄為空,改向 MyApp 持久化 blob 取。
|
||||
private String nameI18nJson;
|
||||
|
||||
public String getNameI18nJson() {
|
||||
return nameI18nJson;
|
||||
}
|
||||
|
||||
public void setNameI18nJson(String nameI18nJson) {
|
||||
this.nameI18nJson = nameI18nJson;
|
||||
}
|
||||
|
||||
// 依目前語言取商品名稱。優先順序:
|
||||
// 1) B012 多語 map 對應語系 → 2) map 的 zh_TW → 3) 舊欄位 _en/_jp(向下相容)→ 4) 中文主值。
|
||||
// 所有顯示商品名稱的地方都應呼叫這個方法,避免各自重複判斷導致顯示不一致。
|
||||
public String getLocalizedName(Context context) {
|
||||
String canonical = LanguageHelper.getSavedLanguage(context); // zh-TW / en / ko ...
|
||||
String backendLocale = LanguageHelper.toBackendLocale(canonical); // zh_TW / en / ko ...
|
||||
|
||||
// 1) 商品多語 map:目前語系
|
||||
String fromMap = lookupNameI18n(backendLocale);
|
||||
if (notBlank(fromMap)) return fromMap;
|
||||
|
||||
// 2) 商品多語 map:zh_TW(後台保證的 fallback 主值)
|
||||
if (!"zh_TW".equals(backendLocale)) {
|
||||
String zh = lookupNameI18n("zh_TW");
|
||||
if (notBlank(zh)) return zh;
|
||||
}
|
||||
|
||||
// 3) 舊欄位 _en/_jp(線上舊資料相容;新 map 缺時兜底)
|
||||
String productLang = LanguageHelper.toProductLang(canonical); // en / ja / zh
|
||||
if ("en".equals(productLang) && notBlank(productNameEn)) return productNameEn;
|
||||
if ("ja".equals(productLang) && notBlank(productNameJp)) return productNameJp;
|
||||
|
||||
// 4) 中文主值
|
||||
return productName;
|
||||
}
|
||||
|
||||
// 從 i18n map 取指定後台 locale 的名稱;本物件欄位為空時改向 MyApp 持久化 blob 取(離線重啟)。
|
||||
private String lookupNameI18n(String backendLocale) {
|
||||
String json = nameI18nJson;
|
||||
if (json == null || json.isEmpty()) {
|
||||
json = com.unibuy.smartdevice.MyApp.getInstance().getProductNameI18nJson(productID);
|
||||
}
|
||||
if (json == null || json.isEmpty()) return null;
|
||||
try {
|
||||
return new org.json.JSONObject(json).optString(backendLocale, null);
|
||||
} catch (org.json.JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// B012 t060v03_i18n 原始 JSON 字串(底線代碼 key,如 {"zh_TW":..,"en":..})。
|
||||
// 內容為純文字含換行(\n),顯示時以 setText 原樣呈現(保留換行/空格/縮排)。
|
||||
// 由 B012 解析時設入;離線從 DB 重新載入商品時此欄為空,改向 MyApp 持久化 blob 取。
|
||||
private String specI18nJson;
|
||||
|
||||
public String getSpecI18nJson() {
|
||||
return specI18nJson;
|
||||
}
|
||||
|
||||
public void setSpecI18nJson(String specI18nJson) {
|
||||
this.specI18nJson = specI18nJson;
|
||||
}
|
||||
|
||||
// 依目前語言取商品規格/描述。順序:對應語系 → zh_TW → 空字串(無描述)。
|
||||
public String getLocalizedSpec(Context context) {
|
||||
String canonical = LanguageHelper.getSavedLanguage(context);
|
||||
String backendLocale = LanguageHelper.toBackendLocale(canonical);
|
||||
|
||||
String fromMap = lookupSpecI18n(backendLocale);
|
||||
if (notBlank(fromMap)) return fromMap;
|
||||
|
||||
if (!"zh_TW".equals(backendLocale)) {
|
||||
String zh = lookupSpecI18n("zh_TW");
|
||||
if (notBlank(zh)) return zh;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String lookupSpecI18n(String backendLocale) {
|
||||
String json = specI18nJson;
|
||||
if (json == null || json.isEmpty()) {
|
||||
json = com.unibuy.smartdevice.MyApp.getInstance().getProductSpecI18nJson(productID);
|
||||
}
|
||||
if (json == null || json.isEmpty()) return null;
|
||||
try {
|
||||
return new org.json.JSONObject(json).optString(backendLocale, null);
|
||||
} catch (org.json.JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean notBlank(String s) {
|
||||
return s != null && !s.trim().isEmpty();
|
||||
}
|
||||
|
||||
public int getRealPrice() {
|
||||
int machinePrice = getMachinePrice();
|
||||
int sellingPrice = getSellingPrice();
|
||||
|
||||
@ -15,6 +15,9 @@ public class ReportFlowInfoStructure {
|
||||
int giveChange;
|
||||
int usePoints;
|
||||
String reportFlowId;
|
||||
// MQTT 交易結案流水號:進入付款時生成一次,pending → completed/failed 全程共用同一個,
|
||||
// 讓後台依同一 flow_id 做狀態轉移。與 reportFlowId(取貨碼/通行碼驗證碼、B600 legacy)語意不同,獨立一欄。
|
||||
String mqttFlowId;
|
||||
JSONArray shoppingCartInfoByJsonArray;
|
||||
|
||||
public ReportFlowInfoStructure(String orderId, int flowType, String flowSendInfo, String flowRequestInfo, String productId, int totalPrice, String invoiceInfo, String flowBarCode, int flowStatusType, int giveChange, int usePoints, JSONArray shoppingCartInfoByJsonArray) {
|
||||
@ -136,6 +139,14 @@ public class ReportFlowInfoStructure {
|
||||
this.reportFlowId = reportFlowId;
|
||||
}
|
||||
|
||||
public String getMqttFlowId() {
|
||||
return mqttFlowId;
|
||||
}
|
||||
|
||||
public void setMqttFlowId(String mqttFlowId) {
|
||||
this.mqttFlowId = mqttFlowId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReportFlowInfoStructure{" +
|
||||
|
||||
@ -8,6 +8,9 @@ public class ReportInvoiceInfoStructure {
|
||||
String invoiceDate;
|
||||
String randomNumber;
|
||||
String loveCode;
|
||||
// 綠界 RelateNumber(冪等鍵,= machineID + flow_id)。由 DispatchDialog 開票前產生,
|
||||
// 開票請求與 finalize 上報共用同一值,後台據此查詢/補開避免重複開立。
|
||||
String relateNumber = "";
|
||||
|
||||
public ReportInvoiceInfoStructure(String reportFlowId, String rtnCode, String rtnMsg, String invoiceNo, String invoiceDate, String randomNumber, String loveCode) {
|
||||
this.reportFlowId = reportFlowId;
|
||||
@ -75,6 +78,14 @@ public class ReportInvoiceInfoStructure {
|
||||
this.loveCode = loveCode;
|
||||
}
|
||||
|
||||
public String getRelateNumber() {
|
||||
return relateNumber;
|
||||
}
|
||||
|
||||
public void setRelateNumber(String relateNumber) {
|
||||
this.relateNumber = relateNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReportInvoiceInfoStructure{" +
|
||||
|
||||
@ -10,6 +10,7 @@ public class SlotStructure {
|
||||
private boolean lock;
|
||||
private int secord;
|
||||
private JSONObject otherData;
|
||||
public int cartSelectedCount;// 本地欄位:購物車已選數量(General flavor 多品項購物車使用)
|
||||
|
||||
/*
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
@ -35,6 +35,14 @@ public class TransactionFinalizePayload {
|
||||
/** 每個貨道的出貨結果列表 */
|
||||
private List<DispenseRecord> dispense;
|
||||
|
||||
/**
|
||||
* 電子發票開立結果(選填)。
|
||||
* 機台在進入出貨流程時平行向綠界開立,結果折進此物件隨 finalize 一起上報;
|
||||
* 若開票尚未回應則 status=pending,後台依 relate_number 去綠界查詢/補開。
|
||||
* 無發票需求(未帶發票資訊)時此欄為 null,後台不建立發票。
|
||||
*/
|
||||
private InvoicePayload invoice;
|
||||
|
||||
// =========== Getters & Setters ===========
|
||||
|
||||
public String getAction() {
|
||||
@ -82,6 +90,74 @@ public class TransactionFinalizePayload {
|
||||
this.dispense = dispense;
|
||||
}
|
||||
|
||||
public InvoicePayload getInvoice() {
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public void setInvoice(InvoicePayload invoice) {
|
||||
this.invoice = invoice;
|
||||
}
|
||||
|
||||
// =========== 內部類別:電子發票開立結果 ===========
|
||||
|
||||
public static class InvoicePayload {
|
||||
/** 開立狀態:issued(已開)/ pending(已送出未回應)/ failed(綠界回失敗) */
|
||||
private String status;
|
||||
/** 綠界 RelateNumber(= machineID + flow_id),後台查詢/補開用 */
|
||||
private String relate_number;
|
||||
/** 發票號碼(pending/failed 時為空) */
|
||||
private String invoice_no;
|
||||
/** 綠界回應碼(1=成功) */
|
||||
private String rtn_code;
|
||||
/** 綠界回應訊息 */
|
||||
private String rtn_msg;
|
||||
/** 發票開立日期 */
|
||||
private String invoice_date;
|
||||
/** 隨機碼 */
|
||||
private String random_number;
|
||||
/** 愛心碼(捐贈時) */
|
||||
private String love_code;
|
||||
/** 載具號碼 */
|
||||
private String carrier_id;
|
||||
/** 統一編號(B2B 統編,有打統編時) */
|
||||
private String business_tax_id;
|
||||
/** 發票金額 */
|
||||
private int amount;
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public String getRelate_number() { return relate_number; }
|
||||
public void setRelate_number(String relate_number) { this.relate_number = relate_number; }
|
||||
|
||||
public String getInvoice_no() { return invoice_no; }
|
||||
public void setInvoice_no(String invoice_no) { this.invoice_no = invoice_no; }
|
||||
|
||||
public String getRtn_code() { return rtn_code; }
|
||||
public void setRtn_code(String rtn_code) { this.rtn_code = rtn_code; }
|
||||
|
||||
public String getRtn_msg() { return rtn_msg; }
|
||||
public void setRtn_msg(String rtn_msg) { this.rtn_msg = rtn_msg; }
|
||||
|
||||
public String getInvoice_date() { return invoice_date; }
|
||||
public void setInvoice_date(String invoice_date) { this.invoice_date = invoice_date; }
|
||||
|
||||
public String getRandom_number() { return random_number; }
|
||||
public void setRandom_number(String random_number) { this.random_number = random_number; }
|
||||
|
||||
public String getLove_code() { return love_code; }
|
||||
public void setLove_code(String love_code) { this.love_code = love_code; }
|
||||
|
||||
public String getCarrier_id() { return carrier_id; }
|
||||
public void setCarrier_id(String carrier_id) { this.carrier_id = carrier_id; }
|
||||
|
||||
public String getBusiness_tax_id() { return business_tax_id; }
|
||||
public void setBusiness_tax_id(String business_tax_id) { this.business_tax_id = business_tax_id; }
|
||||
|
||||
public int getAmount() { return amount; }
|
||||
public void setAmount(int amount) { this.amount = amount; }
|
||||
}
|
||||
|
||||
// =========== 內部類別:訂單資訊 ===========
|
||||
|
||||
public static class OrderPayload {
|
||||
@ -115,10 +191,17 @@ public class TransactionFinalizePayload {
|
||||
private int payment_type;
|
||||
|
||||
/**
|
||||
* 支付狀態:1 = 成功
|
||||
* 支付狀態:1 = 成功, 0 = 失敗/未付款
|
||||
*/
|
||||
private int payment_status;
|
||||
|
||||
/**
|
||||
* 交易生命週期狀態(後台 Order.status):
|
||||
* "pending"(進付款待結果)/ "completed"(付款成功)/ "failed"(付款失敗)/ "abandoned"。
|
||||
* 不帶(null)時後台預設 "completed",確保線上 main 機台行為不變。
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 訂單出貨狀態:0 = 全部失敗, 1 = 全部成功, 2 = 部分失敗
|
||||
*/
|
||||
@ -215,6 +298,14 @@ public class TransactionFinalizePayload {
|
||||
this.payment_status = payment_status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getDelivery_status() {
|
||||
return delivery_status;
|
||||
}
|
||||
|
||||
@ -346,8 +346,12 @@ public class Tools {
|
||||
public void setImmOpenByFocusChange(EditText editText) {
|
||||
editText.requestFocus();
|
||||
editText.setSelection(editText.getText().length());
|
||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
|
||||
// 使用 post() 延遲執行,確保 hideSystemBars() 觸發的 Window 重繪完成後才彈出鍵盤
|
||||
// 若直接呼叫 showSoftInput,Window flag 變更會清除 focus,導致鍵盤無法彈出
|
||||
editText.post(() -> {
|
||||
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
|
||||
});
|
||||
}
|
||||
|
||||
public void setImmHideByFocusChange(EditText editText) {
|
||||
|
||||
@ -4,7 +4,9 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.external.mqtt.MqttManager;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.InvoiceStructure;
|
||||
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
|
||||
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;
|
||||
|
||||
@ -45,20 +47,73 @@ public class TransactionFinalizeBuilder {
|
||||
/** 累積的出貨結果清單 */
|
||||
private final List<TransactionFinalizePayload.DispenseRecord> dispenseRecords = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 商品明細快照。在「buyList 仍完整」時擷取(例如進入出貨流程當下);
|
||||
* 之後 build() 會優先採用此快照,不再即時讀 live buyList。
|
||||
* 用以修正掃碼支付(玉山)等較慢流程:結案前 buyList 可能被取消/倒數路徑清空,
|
||||
* 導致 order.items 變成空陣列(出現「無商品紀錄」),但出貨其實成功。
|
||||
*/
|
||||
private List<TransactionFinalizePayload.ItemRecord> itemsSnapshot = null;
|
||||
|
||||
private String codeId = "0";
|
||||
|
||||
/** 交易生命週期狀態(pending/completed/failed/abandoned);預設 completed 維持成功路徑原行為。 */
|
||||
private String status = "completed";
|
||||
|
||||
/** 支付狀態:1=成功、0=失敗/未付款;預設 1 維持成功路徑原行為。 */
|
||||
private int paymentStatus = 1;
|
||||
|
||||
/** 失敗結果碼(如刷卡機 0015);非空時折進 payment_response 方便後台查閱。 */
|
||||
private String resultCode = null;
|
||||
|
||||
// 訂單層級欄位的快照(在「全域 flowInfo 仍正確」時由 captureOrderInfo() 擷取)。
|
||||
// null 代表未快照 → build() 退回讀全域即時值(保留 publishPending/publishFailed 等同步路徑的原行為)。
|
||||
// 出貨是個長流程,期間全域 flowInfo 可能被別的路徑(殘留加購倒數彈出 BuyDialog →
|
||||
// initialReportData())洗回 0;用快照可避免 payment_type/total 在結案當下被誤讀成 0「未知」,
|
||||
// 也避免通行碼(5)的不上報判斷失效。與既有 captureItems()(商品明細快照)一致。
|
||||
private Integer snapFlowType = null;
|
||||
private Integer snapTotalAmount = null;
|
||||
private Integer snapChangeAmount = null;
|
||||
private Integer snapPointsUsed = null;
|
||||
|
||||
public TransactionFinalizeBuilder() {
|
||||
this.orderMachineTime = SDF.format(new Date());
|
||||
// 在進入出貨流程時立即生成本次交易流水號(一次交易只生成一次)
|
||||
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
|
||||
// flow_id 共用「進入付款時生成的 mqttFlowId」,讓 pending → completed/failed 為同一筆,
|
||||
// 後台才能依 flow_id 做狀態轉移。若尚未生成(防禦)則即時生成並回存 flowInfo。
|
||||
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
|
||||
String existing = flowInfo != null ? flowInfo.getMqttFlowId() : null;
|
||||
if (existing != null && !existing.isEmpty()) {
|
||||
this.flowId = existing;
|
||||
} else {
|
||||
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
|
||||
if (flowInfo != null) {
|
||||
flowInfo.setMqttFlowId(this.flowId);
|
||||
}
|
||||
}
|
||||
this.orderNo = this.flowId;
|
||||
Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId);
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public void setPaymentStatus(int paymentStatus) {
|
||||
this.paymentStatus = paymentStatus;
|
||||
}
|
||||
|
||||
public void setResultCode(String resultCode) {
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
public void setFlowId(String flowId) {
|
||||
this.flowId = flowId;
|
||||
}
|
||||
|
||||
public String getFlowId() {
|
||||
return flowId;
|
||||
}
|
||||
|
||||
public void setOrderNo(String orderNo) {
|
||||
this.orderNo = orderNo;
|
||||
}
|
||||
@ -67,6 +122,133 @@ public class TransactionFinalizeBuilder {
|
||||
this.codeId = codeId;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 交易生命週期上報(pending / failed)。只在 basic 購物模式(本專案 general/demo)才送:
|
||||
// - basic:以「進付款時生成的同一 flow_id」上報,後台依 flow_id 做 pending → completed/failed 轉移。
|
||||
// - employee_card(晟崴)/pickup_sheet(中國醫):無實際付款,直接 return 不送,後台資料不受影響。
|
||||
// 用 shopping_mode 判斷(雲端 B014 已逐機台下發),不另設旗標。
|
||||
// 成功(completed)仍由 DispatchDialog 出貨完成後的既有流程上報,不在此處。
|
||||
// =========================================================================
|
||||
|
||||
/** 僅 basic 購物模式才上報生命週期;其餘模式不送。 */
|
||||
private static boolean lifecycleLoggingEnabled() {
|
||||
return "basic".equals(MyApp.getInstance().getShoppingMode());
|
||||
}
|
||||
|
||||
/** 進入付款時上報 pending(進付款一律留底)。非 basic 模式則不送。 */
|
||||
public static void publishPending() {
|
||||
if (!lifecycleLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TransactionFinalizeBuilder b = new TransactionFinalizeBuilder();
|
||||
b.setStatus("pending");
|
||||
b.setPaymentStatus(0);
|
||||
MqttManager.getInstance().publishTransactionFinalize(b.build());
|
||||
Log.i(TAG, "published pending finalize, flow_id=" + b.getFlowId());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "publishPending failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 付款失敗/逾時時上報 failed(救回原本默默消失的失敗單)。非 basic 模式則不送。 */
|
||||
public static void publishFailed(String resultCode) {
|
||||
if (!lifecycleLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
TransactionFinalizeBuilder b = new TransactionFinalizeBuilder();
|
||||
b.setStatus("failed");
|
||||
b.setPaymentStatus(0);
|
||||
b.setResultCode(resultCode);
|
||||
MqttManager.getInstance().publishTransactionFinalize(b.build());
|
||||
Log.i(TAG, "published failed finalize code=" + resultCode + ", flow_id=" + b.getFlowId());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "publishFailed failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 擷取目前 buyList 的商品明細快照。
|
||||
* 應在「buyList 仍完整」時呼叫(例如 DispatchDialog 進入出貨流程當下);
|
||||
* 之後 {@link #build()} 會優先採用此快照,不再即時讀 live buyList,
|
||||
* 避免結案前 buyList 被取消/倒數等其他執行緒清空,導致 order.items 變成空陣列。
|
||||
* buyList 為空時不覆寫既有快照(避免把有效快照洗成空)。
|
||||
*/
|
||||
public void captureItems() {
|
||||
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
|
||||
if (buyList == null || buyList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
this.itemsSnapshot = buildItemRecords(buyList);
|
||||
Log.i(TAG, "captureItems: snapshot " + this.itemsSnapshot.size()
|
||||
+ " item(s), flow_id=" + flowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 擷取訂單層級欄位(flowType / 總金額 / 找零 / 點數)的快照。
|
||||
* 應在「全域 flowInfo 已是本筆最終值」時呼叫(例如 DispatchDialog 進入出貨、且 CMUH→42
|
||||
* 覆寫之後)。之後 {@link #build()} 與「通行碼不上報」判斷皆優先採用此快照,不再即時讀全域,
|
||||
* 避免結案前全域被其他路徑清空導致 payment_type/total 落地成 0「未知」或誤報通行碼。
|
||||
*/
|
||||
public void captureOrderInfo() {
|
||||
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
|
||||
if (flowInfo == null) {
|
||||
return;
|
||||
}
|
||||
this.snapFlowType = flowInfo.getFlowType();
|
||||
this.snapTotalAmount = flowInfo.getTotalPrice();
|
||||
this.snapChangeAmount = flowInfo.getGiveChange();
|
||||
this.snapPointsUsed = flowInfo.getUsePoints();
|
||||
Log.i(TAG, "captureOrderInfo: flowType=" + snapFlowType + ", total=" + snapTotalAmount
|
||||
+ ", flow_id=" + flowId);
|
||||
}
|
||||
|
||||
/** 取得快照的 flowType;尚未快照時退回全域即時值。供「通行碼(5)不上報」判斷使用。 */
|
||||
public int getOrderFlowType() {
|
||||
if (snapFlowType != null) {
|
||||
return snapFlowType;
|
||||
}
|
||||
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
|
||||
return flowInfo != null ? flowInfo.getFlowType() : 0;
|
||||
}
|
||||
|
||||
// 訂單欄位取值:有快照用快照,否則退回全域即時值(build() 共用)。
|
||||
private int effectiveFlowType(ReportFlowInfoStructure flowInfo) {
|
||||
return snapFlowType != null ? snapFlowType : flowInfo.getFlowType();
|
||||
}
|
||||
|
||||
private int effectiveTotalAmount(ReportFlowInfoStructure flowInfo) {
|
||||
return snapTotalAmount != null ? snapTotalAmount : flowInfo.getTotalPrice();
|
||||
}
|
||||
|
||||
private int effectiveChangeAmount(ReportFlowInfoStructure flowInfo) {
|
||||
return snapChangeAmount != null ? snapChangeAmount : flowInfo.getGiveChange();
|
||||
}
|
||||
|
||||
private int effectivePointsUsed(ReportFlowInfoStructure flowInfo) {
|
||||
return snapPointsUsed != null ? snapPointsUsed : flowInfo.getUsePoints();
|
||||
}
|
||||
|
||||
/** 由 buyList 組出 items 明細(captureItems 與 build() fallback 共用)。 */
|
||||
private List<TransactionFinalizePayload.ItemRecord> buildItemRecords(List<BuyStructure> buyList) {
|
||||
List<TransactionFinalizePayload.ItemRecord> items = new ArrayList<>();
|
||||
if (buyList != null) {
|
||||
for (BuyStructure buy : buyList) {
|
||||
int productId = 0;
|
||||
try {
|
||||
productId = Integer.parseInt(buy.getProduct().getProductID());
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Cannot parse productID to int: " + buy.getProduct().getProductID());
|
||||
}
|
||||
int price = buy.getProduct().getRealPrice();
|
||||
int quantity = buy.getCount() > 0 ? buy.getCount() : 1;
|
||||
items.add(new TransactionFinalizePayload.ItemRecord(productId, price, quantity));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入一筆出貨結果記錄(每個貨道出貨後呼叫一次)。
|
||||
@ -123,7 +305,7 @@ public class TransactionFinalizeBuilder {
|
||||
|
||||
|
||||
// --- payment_type:支付類型 (Root 級別) ---
|
||||
payload.setPayment_type(flowInfo.getFlowType());
|
||||
payload.setPayment_type(effectiveFlowType(flowInfo));
|
||||
|
||||
// --- 組裝 order ---
|
||||
TransactionFinalizePayload.OrderPayload order = buildOrderPayload(flowInfo);
|
||||
@ -133,6 +315,26 @@ public class TransactionFinalizeBuilder {
|
||||
// --- 出貨記錄 ---
|
||||
payload.setDispense(new ArrayList<>(dispenseRecords));
|
||||
|
||||
// --- 電子發票(改由後台開立)---
|
||||
// 機台不開票,只把「開立輸入」(載具/愛心碼/統編/金額)帶上去,狀態固定 pending;
|
||||
// 後台建 pending 發票後派 Job 去綠界開立。無發票需求(invoiceInfo 為空)時不帶,後台不建立發票。
|
||||
// 只有付款成功(completed)才建立發票;pending/failed 不開票(避免對未成交的交易開立)。
|
||||
if ("completed".equals(status)
|
||||
&& flowInfo.getInvoiceInfo() != null && !flowInfo.getInvoiceInfo().isEmpty()) {
|
||||
TransactionFinalizePayload.InvoicePayload invoicePayload =
|
||||
new TransactionFinalizePayload.InvoicePayload();
|
||||
invoicePayload.setStatus("pending");
|
||||
invoicePayload.setAmount(effectiveTotalAmount(flowInfo));
|
||||
|
||||
InvoiceStructure invoiceData = MyApp.getInstance().getInvoiceData();
|
||||
if (invoiceData != null) {
|
||||
invoicePayload.setCarrier_id(invoiceData.getCarrier());
|
||||
invoicePayload.setBusiness_tax_id(invoiceData.getCustomerIdentifier());
|
||||
invoicePayload.setLove_code(invoiceData.getLoveCode());
|
||||
}
|
||||
payload.setInvoice(invoicePayload);
|
||||
}
|
||||
|
||||
Log.i(TAG, "TransactionFinalizePayload built: flow_id=" + flowId
|
||||
+ ", order_no=" + order.getOrder_no()
|
||||
+ ", items=" + order.getItems().size()
|
||||
@ -170,24 +372,29 @@ public class TransactionFinalizeBuilder {
|
||||
|
||||
order.setOrder_no(orderNo != null && !orderNo.isEmpty() ? orderNo : flowId);
|
||||
|
||||
// 金額資訊
|
||||
int totalAmount = flowInfo.getTotalPrice();
|
||||
// 金額資訊(優先採快照,未快照時退回全域即時值)
|
||||
int totalAmount = effectiveTotalAmount(flowInfo);
|
||||
order.setOriginal_amount(totalAmount);
|
||||
order.setDiscount_amount(0); // TODO: 如有折扣活動需進一步填入
|
||||
order.setTotal_amount(totalAmount);
|
||||
order.setPay_amount(totalAmount);
|
||||
order.setChange_amount(flowInfo.getGiveChange());
|
||||
order.setPoints_used(flowInfo.getUsePoints());
|
||||
order.setChange_amount(effectiveChangeAmount(flowInfo));
|
||||
order.setPoints_used(effectivePointsUsed(flowInfo));
|
||||
|
||||
// 支付類型:flowType 直接對應 payment_type
|
||||
order.setPayment_type(flowInfo.getFlowType());
|
||||
order.setPayment_status(1); // 走到出貨流程代表支付已成功
|
||||
order.setPayment_type(effectiveFlowType(flowInfo));
|
||||
// 生命週期狀態與支付狀態:成功路徑沿用預設(completed/1);pending/failed 由 setter 覆寫。
|
||||
order.setStatus(status);
|
||||
order.setPayment_status(paymentStatus);
|
||||
|
||||
// 支付流程原始資料
|
||||
order.setPayment_request(
|
||||
flowInfo.getFlowSendInfo() != null ? flowInfo.getFlowSendInfo() : "");
|
||||
order.setPayment_response(
|
||||
flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "");
|
||||
String paymentResponse = flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "";
|
||||
if (resultCode != null && !resultCode.isEmpty()) {
|
||||
paymentResponse = paymentResponse + " | code=" + resultCode;
|
||||
}
|
||||
order.setPayment_response(paymentResponse);
|
||||
|
||||
// 會員資訊
|
||||
order.setMember_barcode(
|
||||
@ -201,20 +408,13 @@ public class TransactionFinalizeBuilder {
|
||||
order.setMachine_time(orderMachineTime);
|
||||
|
||||
// --- 組裝購買商品清單 (items) ---
|
||||
List<TransactionFinalizePayload.ItemRecord> items = new ArrayList<>();
|
||||
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
|
||||
if (buyList != null) {
|
||||
for (BuyStructure buy : buyList) {
|
||||
int productId = 0;
|
||||
try {
|
||||
productId = Integer.parseInt(buy.getProduct().getProductID());
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Cannot parse productID to int: " + buy.getProduct().getProductID());
|
||||
}
|
||||
int price = buy.getProduct().getRealPrice();
|
||||
int quantity = buy.getCount() > 0 ? buy.getCount() : 1;
|
||||
items.add(new TransactionFinalizePayload.ItemRecord(productId, price, quantity));
|
||||
}
|
||||
// 優先採用 captureItems() 在出貨流程開始時擷取的快照;無快照時才即時讀 live buyList。
|
||||
// 快照可避免結案前 buyList 被其他執行緒清空(掃碼支付等慢流程的競態),造成「無商品紀錄」。
|
||||
List<TransactionFinalizePayload.ItemRecord> items;
|
||||
if (itemsSnapshot != null && !itemsSnapshot.isEmpty()) {
|
||||
items = new ArrayList<>(itemsSnapshot);
|
||||
} else {
|
||||
items = buildItemRecords(MyApp.getInstance().getBuyList());
|
||||
}
|
||||
order.setItems(items);
|
||||
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.blankj.utilcode.util.ToastUtils;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.ui.dialog.BuyDialog;
|
||||
import com.unibuy.smartdevice.ui.dialog.FontendPickupCodeDialog;
|
||||
import com.unibuy.v2.UnibuyHelper;
|
||||
import com.unibuy.v2.ui.ShoppingCartFragment;
|
||||
|
||||
/**
|
||||
* basic 模式(原 General 通用版)銷售流程。
|
||||
* 結帳時彈出標準付款 Dialog (BuyDialog);購物車浮鈕則開啟多品項購物車 Fragment。
|
||||
*/
|
||||
public class BasicSaleFlow extends SaleFlowHandler {
|
||||
|
||||
/** 多品項購物車 Fragment(懶建立,重複開啟時沿用同一實例)。 */
|
||||
private ShoppingCartFragment cartFragment;
|
||||
|
||||
public BasicSaleFlow(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
super(context, handlerMain, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuyRequested() {
|
||||
new BuyDialog(context, handlerMain).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 點選 VMC 商品,依雲端 B014 DevSet.ShoppingCar 決定流程:
|
||||
* - 購物車開啟:加入多品項購物車並停留商品列表(不做取貨口檢查,出貨時才檢查)。
|
||||
* - 購物車關閉:單品直購——清空後僅放入這一筆,直接彈出 BuyDialog(商品資料+選付款)。
|
||||
* @return true 表示已消化此次點擊。
|
||||
*/
|
||||
@Override
|
||||
public boolean handleProductSelected(SlotStructure slot) {
|
||||
// 點商品一律先彈出商品詳情頁;詳情頁內依購物車開關呈現
|
||||
// 「數量調整+加入購物車+直接購買」或僅「直接購買」,再走後續加購/單品直購流程。
|
||||
new com.unibuy.smartdevice.ui.dialog.DialogProductDetail(context, handlerMain, slot).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** basic 使用 Luzui 移植的現金強化版(卡鈔/找零保障/自癒),上報接 TaiwanStar 新 MQTT。 */
|
||||
@Override
|
||||
public Class<? extends com.unibuy.smartdevice.DialogAbstract> cashDialogClass() {
|
||||
return com.unibuy.smartdevice.ui.dialog.CashPayDialogGeneral.class;
|
||||
}
|
||||
|
||||
/** 購物車浮鈕:開啟多品項購物車 Fragment。 */
|
||||
@Override
|
||||
public void onShoppingCartRequested() {
|
||||
if (!(context instanceof FontendActivity)) {
|
||||
return;
|
||||
}
|
||||
if (cartFragment == null) {
|
||||
cartFragment = new ShoppingCartFragment(handlerMain);
|
||||
}
|
||||
((FontendActivity) context).openFragment(cartFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPickupRequested() {
|
||||
new FontendPickupCodeDialog(context, handlerMain).show();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.ui.dialog.MemberCardPickupDialog;
|
||||
|
||||
/**
|
||||
* employee_card 模式(原 Chengwai 晟崴版)銷售流程。
|
||||
* 點選商品/購物車時直接進入員工卡刷卡取貨 Dialog (MemberCardPickupDialog, B680)。
|
||||
* 商品點擊不消化、現金走標準 Dialog、無取貨碼入口 —— 皆沿用基底預設。
|
||||
*/
|
||||
public class EmployeeCardSaleFlow extends SaleFlowHandler {
|
||||
|
||||
public EmployeeCardSaleFlow(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
super(context, handlerMain, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuyRequested() {
|
||||
new MemberCardPickupDialog(context, handlerMain).show();
|
||||
}
|
||||
}
|
||||
@ -41,6 +41,7 @@ import com.unibuy.smartdevice.MyApp;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.controller.DevController;
|
||||
@ -92,8 +93,13 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
SET_NETWORK(6),
|
||||
RESET_SLOTNO(49),
|
||||
RESTART_HOST(7),
|
||||
HIDDEN_MULTILANGUAGE_BUTTON(8),
|
||||
SHOW_MULTILANGUAGE_BUTTON(9),
|
||||
// 對話框開啟時隱藏銷售平台浮層(取貨碼/購物車浮鈕、上下系統資訊列),關閉時還原。
|
||||
// 原為僅隱藏語言鈕(HIDDEN/SHOW_MULTILANGUAGE_BUTTON),擴大為整個浮層,避免對話框
|
||||
// (貼底部、非滿版)周圍把這些邊緣元件漏出來。沿用 8/9 代碼不影響既有呼叫。
|
||||
HIDE_SALE_OVERLAY(8),
|
||||
SHOW_SALE_OVERLAY(9),
|
||||
// 純時鐘刷新:每秒只更新機台資訊列時間文字,不做上報/截圖/重啟判斷等重活。
|
||||
CLOCK_TICK(50),
|
||||
;
|
||||
|
||||
private int option;
|
||||
@ -120,6 +126,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
private Fragment vmcFragment;
|
||||
private Fragment electricFragment;
|
||||
private ReportShotTimeByScheduler reportOnScheduler;
|
||||
private ClockTickScheduler clockTickScheduler;
|
||||
private ConnectivityManager connectivityManager;
|
||||
private ConnectivityManager.NetworkCallback networkCallback;
|
||||
|
||||
@ -259,6 +266,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||
String timeByNow = sdf.format(new Date());
|
||||
binding.textTime.setText(timeByNow);
|
||||
// 機台資訊列的時間改由 ClockTickScheduler 每秒更新,這裡不再重複設定。
|
||||
|
||||
String timeByM = new SimpleDateFormat("HH:mm").format(new Date());
|
||||
if (timeByM.equals("23:45")) {
|
||||
@ -330,6 +338,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
binding.textTemperature.setText(getString(R.string.temp) + ":" + tempStr);
|
||||
getLogs().update("Temperature", String.valueOf(DevXinYuanController.getTemperature()), "℃");
|
||||
|
||||
// 同步更新機台資訊列
|
||||
updateMachineInfoName();
|
||||
updateMachineInfoNetwork();
|
||||
binding.textMachineInfoTemperature.setText(getString(R.string.temp) + ":" + tempStr);
|
||||
|
||||
try {
|
||||
// httpAPI.detectHost();
|
||||
} catch (Exception e) {
|
||||
@ -383,6 +396,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
case SET_NETWORK:
|
||||
getLogs().info("網路:" + message);
|
||||
binding.textNetwork.setText(message);
|
||||
updateMachineInfoNetwork();
|
||||
break;
|
||||
case RESET_SLOTNO:
|
||||
new ResetSlotNo(this).start();
|
||||
@ -390,13 +404,21 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
case RESTART_HOST:
|
||||
new RestartHost(this).start();
|
||||
break;
|
||||
case HIDDEN_MULTILANGUAGE_BUTTON:
|
||||
getLogs().info("隱藏多國語按紐");
|
||||
// hiddenMultiLanguageButton();
|
||||
case HIDE_SALE_OVERLAY:
|
||||
getLogs().info("對話框開啟,隱藏銷售平台浮層");
|
||||
refreshSaleOverlay();
|
||||
break;
|
||||
case SHOW_MULTILANGUAGE_BUTTON:
|
||||
getLogs().info("顯示多國語按紐");
|
||||
// showMultiLanguageButton();
|
||||
case SHOW_SALE_OVERLAY:
|
||||
getLogs().info("對話框關閉,重新評估銷售平台浮層");
|
||||
refreshSaleOverlay();
|
||||
break;
|
||||
case CLOCK_TICK:
|
||||
// 輕量時鐘:每秒只更新時間文字,不做其他重活。
|
||||
if (binding != null) {
|
||||
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
.format(new Date());
|
||||
binding.textMachineInfoTime.setText(now);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -421,12 +443,12 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
settingsDao.close();
|
||||
|
||||
// 讀取最後設定語言的設定
|
||||
// LanguageHelper.applySavedLanguage(this);
|
||||
LanguageHelper.applySavedLanguage(this);
|
||||
binding = ActivityFontendBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// 初始化銷售流程處理器
|
||||
saleFlowHandler = new SaleFlowHandler(this, getHandlerMain(), this);
|
||||
saleFlowHandler = SaleFlowHandler.create(this, getHandlerMain(), this);
|
||||
saleFlowHandler.onSetupUI();
|
||||
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
@ -451,6 +473,9 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
public void onUserInteraction() {
|
||||
super.onUserInteraction();
|
||||
MyApp.getInstance().updateLastInteraction();
|
||||
// onUserInteraction 連對話框/彈窗的觸控都會觸發(dispatchTouchEvent 不會),
|
||||
// 故一併重設 5 分鐘待機計時器,避免操作對話框時被誤判閒置踢回待機。
|
||||
resetIdleTimer();
|
||||
}
|
||||
|
||||
public void ReSetAPP() {
|
||||
@ -534,6 +559,17 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
}
|
||||
|
||||
idleRunnable = () -> {
|
||||
// 🚩 以「全域最後互動時間」為準再確認一次是否真的閒置滿 IDLE_TIMEOUT。
|
||||
// 對話框(付款/商品詳情/結帳等)有自己的 Window,操作它時不會觸發本 Activity 的
|
||||
// dispatchTouchEvent,因此 5 分鐘計時器不會被重設;但 onUserInteraction 連對話框觸控
|
||||
// 都會更新 lastInteractionTime。若使用者其實在對話框內操作(尚未真正閒置),這裡就以
|
||||
// 剩餘時間重新計時,避免「明明在操作卻被踢回待機」。
|
||||
long idleMs = MyApp.getInstance().getIdleSeconds() * 1000L;
|
||||
if (idleMs < IDLE_TIMEOUT) {
|
||||
idleHandler.postDelayed(idleRunnable, IDLE_TIMEOUT - idleMs);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🚩 若目前處於主控台/管理員模式,則忽略閒置返回待機頁的觸發,並重新計時
|
||||
if (!MyApp.getInstance().getSuAccess().isEmpty()) {
|
||||
getLogs().info("ℹ️ 偵測到當前處於開發者/主控台模式,忽略閒置返回待機頁的觸發。");
|
||||
@ -548,8 +584,9 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
return;
|
||||
}
|
||||
|
||||
getLogs().info("⚠️ 無操作 3 分鐘,自動返回初始畫面(清空購物車)");
|
||||
getLogs().info("⚠️ 無操作 5 分鐘,自動返回初始畫面(清空購物車)");
|
||||
MyApp.getInstance().getBuyList().clear(); // 🚩 逾時返回首頁時,務必清空購物車,避免機台進入永久忙碌狀態
|
||||
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().clearCart(); // 一併清 v2 多品項購物車,避免取消結帳後走人留下殘單
|
||||
Intent intent = new Intent(FontendActivity.this, AppEntry.initialActivity());
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
@ -631,6 +668,80 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
return -1; // 無法獲取訊號強度
|
||||
}
|
||||
|
||||
/** 取得 App 版本名稱(versionName),取不到時回 "0"。 */
|
||||
private String getAppVersionName() {
|
||||
try {
|
||||
return getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
|
||||
/** 機台資訊列左側:顯示「序號 + 版本號」。 */
|
||||
private void updateMachineInfoName() {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
binding.textMachineInfoName.setText(
|
||||
MyApp.getInstance().getMachine().getMachineID() + " Ver " + getAppVersionName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 依系統/下位機回報的網路狀態,動態更新機台資訊列的網路訊號 icon 與文字。
|
||||
* WiFi 依 RSSI(dBm) 分 1~3 格與滿格;行動網路(SIM) 依 dBm 分 0~4 格;無網路顯示 0 格。
|
||||
*/
|
||||
private void updateMachineInfoNetwork() {
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
int iconRes = R.drawable.signal_cellular_0_bar_24; // 預設:無網路
|
||||
String label = getString(R.string.no_internet);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Network network = connectivityManager.getActiveNetwork();
|
||||
NetworkCapabilities capabilities = (network != null)
|
||||
? connectivityManager.getNetworkCapabilities(network) : null;
|
||||
if (capabilities != null
|
||||
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
int rssi = 0; // 0 代表系統未提供強度
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
rssi = capabilities.getSignalStrength();
|
||||
}
|
||||
iconRes = wifiIconByRssi(rssi);
|
||||
label = getString(R.string.wireless_internet);
|
||||
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
iconRes = cellularIconByDbm(getMobileSignalStrength());
|
||||
label = getString(R.string.mobile_network);
|
||||
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
||||
iconRes = R.drawable.network_wifi_24; // 有線:以滿格示意連線正常
|
||||
label = getString(R.string.wired_internet);
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.textMachineInfoNetwork.setText(label);
|
||||
binding.textMachineInfoNetwork.setCompoundDrawablesRelativeWithIntrinsicBounds(iconRes, 0, 0, 0);
|
||||
}
|
||||
|
||||
/** WiFi RSSI(dBm) → icon;數字越接近 0 越強,rssi==0 視為未提供強度回滿格。 */
|
||||
private int wifiIconByRssi(int rssi) {
|
||||
if (rssi == 0 || rssi >= -55) return R.drawable.network_wifi_24; // 滿格
|
||||
if (rssi >= -66) return R.drawable.network_wifi_3_bar_24; // 等級3
|
||||
if (rssi >= -77) return R.drawable.network_wifi_2_bar_24; // 等級2
|
||||
return R.drawable.network_wifi_1_bar_24; // 等級1
|
||||
}
|
||||
|
||||
/** 行動網路 dBm → icon;數字越接近 0 越強,-1 代表取不到訊號。 */
|
||||
private int cellularIconByDbm(int dbm) {
|
||||
if (dbm == -1) return R.drawable.signal_cellular_0_bar_24; // 無網路
|
||||
if (dbm >= -85) return R.drawable.signal_cellular_4_bar_24; // 滿格
|
||||
if (dbm >= -95) return R.drawable.signal_cellular_3_bar_24; // 等級3
|
||||
if (dbm >= -105) return R.drawable.signal_cellular_2_bar_24; // 等級2
|
||||
if (dbm >= -115) return R.drawable.signal_cellular_1_bar_24; // 等級1
|
||||
return R.drawable.signal_cellular_0_bar_24; // 無網路
|
||||
}
|
||||
|
||||
private void setBottomNavigation() {
|
||||
vmcFragment = new VmcFragment(getHandlerMain());
|
||||
activeFragment = vmcFragment;
|
||||
@ -713,11 +824,21 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
}
|
||||
});
|
||||
|
||||
binding.floatButtonPickupCode.setVisibility(View.GONE);
|
||||
// 前台取貨碼浮鈕:取貨碼從屬「取貨模組」(B014 FunctionSet.PickupModule)。
|
||||
// 取貨模組關閉時,即使 PickupCode=true 也一律隱藏(畫面與流程都不出現)。
|
||||
binding.floatButtonPickupCode.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().isPickupModule()
|
||||
&& MyApp.getInstance().getDevSet().isPickupCode()) ? VISIBLE : View.GONE);
|
||||
|
||||
binding.floatButtonShoppingCart.setOnClickListener(view -> {
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.BUY_DIALOG_OPEN.getOption(),
|
||||
getString(R.string.detect_shopping_cart));
|
||||
// 改由 flavor 的 SaleFlowHandler 決定:General 開多品項購物車 Fragment,
|
||||
// 其他 flavor 維持原行為(onShoppingCartRequested 預設轉呼叫 onBuyRequested)。
|
||||
if (saleFlowHandler != null) {
|
||||
saleFlowHandler.onShoppingCartRequested();
|
||||
} else {
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.BUY_DIALOG_OPEN.getOption(),
|
||||
getString(R.string.detect_shopping_cart));
|
||||
}
|
||||
});
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
|
||||
@ -726,13 +847,36 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
binding.floatButtonShoppingCart.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 觀察購物車內容以更新數量 Badge
|
||||
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().getCartItems().observe(this, cartItems -> {
|
||||
int totalCount = 0;
|
||||
if (cartItems != null) {
|
||||
for (com.unibuy.smartdevice.structure.SlotStructure item : cartItems) {
|
||||
totalCount += item.cartSelectedCount;
|
||||
}
|
||||
}
|
||||
if (totalCount > 0) {
|
||||
binding.textCartBadge.setText(String.valueOf(totalCount));
|
||||
binding.textCartBadge.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.textCartBadge.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
startHeaderReportScheduler();
|
||||
|
||||
// binding.btnswitchlanTW.setOnClickListener(v -> switchLanguage("tw"));
|
||||
//
|
||||
// binding.btnswitchlanEn.setOnClickListener(v -> switchLanguage("en"));
|
||||
//
|
||||
// binding.btnswitchlanJa.setOnClickListener(v -> switchLanguage("ja"));
|
||||
// 語言切換:改為單一入口開啟語言選單(支援 8 語,無法並排)。
|
||||
// 沿用 btnswitchlanTW 當觸發鈕(XML 已改顯示 🌐),隱藏另兩顆舊鈕。
|
||||
binding.btnswitchlanEn.setVisibility(View.GONE);
|
||||
binding.btnswitchlanJa.setVisibility(View.GONE);
|
||||
binding.btnswitchlanTW.setOnClickListener(v -> showLanguagePicker());
|
||||
// 新增:機台資訊列的語言按鈕
|
||||
binding.textMachineInfoLang.setOnClickListener(v -> showLanguagePicker());
|
||||
// 機台資訊列:顯示序號 + 版本號
|
||||
updateMachineInfoName();
|
||||
updateMachineInfoNetwork();
|
||||
String currentLang = LanguageHelper.getSavedLanguage(this);
|
||||
binding.textMachineInfoLangText.setText(LanguageHelper.getDisplayName(currentLang));
|
||||
|
||||
// 🛠️ 隱藏後門:長按「網路狀態」文字 2 秒進入主控台
|
||||
final Handler networkHandler = new Handler();
|
||||
@ -742,7 +886,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
finish();
|
||||
};
|
||||
|
||||
binding.textNetwork.setOnTouchListener((v, event) -> {
|
||||
// 後門已遷至機台資訊列的「序號 + 版本號」元素(舊的 textNetwork 整條系統列已隱藏,不再接收觸控)
|
||||
binding.textMachineInfoName.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case android.view.MotionEvent.ACTION_DOWN:
|
||||
networkHandler.postDelayed(networkRunnable, 2000); // 2秒
|
||||
@ -763,6 +908,29 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
|
||||
}
|
||||
|
||||
// 顯示語言選單:8 種語言以各自文字呈現,選擇後即套用並重繪畫面。
|
||||
private void showLanguagePicker() {
|
||||
// 語系清單以後台 B014 LangSet 下發的機台啟用語系為準;未取得時退回全部支援語系
|
||||
final String[] codes = LanguageHelper.getMachineLanguages();
|
||||
String[] names = new String[codes.length];
|
||||
int current = 0;
|
||||
String saved = LanguageHelper.getSavedLanguage(this);
|
||||
for (int i = 0; i < codes.length; i++) {
|
||||
names[i] = LanguageHelper.getDisplayName(codes[i]);
|
||||
if (codes[i].equals(saved)) {
|
||||
current = i;
|
||||
}
|
||||
}
|
||||
new androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle(R.string.select_language)
|
||||
.setSingleChoiceItems(names, current, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
switchLanguage(codes[which]);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void switchLanguage(String languageCode) {
|
||||
LanguageHelper.changeLanguage(this, languageCode);
|
||||
binding = ActivityFontendBinding.inflate(getLayoutInflater());
|
||||
@ -772,6 +940,15 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
|
||||
setBottomNavigation();
|
||||
startOnSwitchFragment();
|
||||
// 重新綁定語言選單入口(重繪後 binding 已換新)
|
||||
binding.btnswitchlanEn.setVisibility(View.GONE);
|
||||
binding.btnswitchlanJa.setVisibility(View.GONE);
|
||||
binding.btnswitchlanTW.setOnClickListener(v -> showLanguagePicker());
|
||||
binding.textMachineInfoLang.setOnClickListener(v -> showLanguagePicker());
|
||||
updateMachineInfoName();
|
||||
updateMachineInfoNetwork();
|
||||
String currentLang = LanguageHelper.getSavedLanguage(this);
|
||||
binding.textMachineInfoLangText.setText(LanguageHelper.getDisplayName(currentLang));
|
||||
}
|
||||
|
||||
private void startHeaderReportScheduler() {
|
||||
@ -779,6 +956,10 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.REPORT_AND_SHOT_TIME.getOption(), "start");
|
||||
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
|
||||
|
||||
// 時鐘獨立每秒刷新,與上面的 10 秒重活迴圈分開,避免把上報/重啟判斷拉到每秒。
|
||||
clockTickScheduler = new ClockTickScheduler(getHandlerMain());
|
||||
clockTickScheduler.start(0L, 1L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void stopHeaderReportScheduler() {
|
||||
@ -786,18 +967,38 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
reportOnScheduler.shutdown();
|
||||
reportOnScheduler = null;
|
||||
}
|
||||
if (clockTickScheduler != null) {
|
||||
clockTickScheduler.shutdown();
|
||||
clockTickScheduler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void hiddenMultiLanguageButton() {
|
||||
binding.btnswitchlanTW.setVisibility(View.GONE);
|
||||
binding.btnswitchlanEn.setVisibility(View.GONE);
|
||||
binding.btnswitchlanJa.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void showMultiLanguageButton() {
|
||||
binding.btnswitchlanTW.setVisibility(VISIBLE);
|
||||
binding.btnswitchlanEn.setVisibility(VISIBLE);
|
||||
binding.btnswitchlanJa.setVisibility(VISIBLE);
|
||||
/**
|
||||
* 統一控制銷售平台「浮層」在對話框期間的顯示:取貨碼/購物車浮鈕、上方系統列(時間/溫度/網路)、
|
||||
* 下方系統列(版號/語言/執行緒)。對話框(貼底部、非滿版)只蓋住中間,這些邊緣元件若不隱藏會漏出來。
|
||||
* visible=false → 全部隱藏(對話框開啟時)。
|
||||
* visible=true → 還原;浮鈕依各自 B014 gating 重新判斷(不可無腦設 VISIBLE,否則會把已停用的
|
||||
* 取貨碼/購物車漏出來),系統列則直接還原。
|
||||
*/
|
||||
private void applySaleOverlayVisibility(boolean visible) {
|
||||
if (binding == null) return;
|
||||
if (visible) {
|
||||
// 取貨碼浮鈕:從屬取貨模組,PickupModule && PickupCode 才顯示。
|
||||
binding.floatButtonPickupCode.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().isPickupModule()
|
||||
&& MyApp.getInstance().getDevSet().isPickupCode()) ? VISIBLE : View.GONE);
|
||||
// 購物車浮鈕:依購物車開關。
|
||||
binding.floatButtonShoppingCart.setVisibility(
|
||||
MyApp.getInstance().getDevSet().isShoppingCar() ? VISIBLE : View.GONE);
|
||||
// 上/下灰色系統列已停用,銷售時不再顯示(機台資訊改由 machine_info_bar 呈現)。
|
||||
binding.linearLayout5.setVisibility(View.GONE);
|
||||
binding.bottomBar.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.floatButtonPickupCode.setVisibility(View.GONE);
|
||||
binding.floatButtonShoppingCart.setVisibility(View.GONE);
|
||||
binding.linearLayout5.setVisibility(View.GONE);
|
||||
binding.bottomBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private class RestartHost extends HandlerMainScheduler {
|
||||
@ -1087,6 +1288,28 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
}
|
||||
}
|
||||
|
||||
/** 每秒只刷新機台資訊列時間文字的輕量排程,與 10 秒上報迴圈分離。 */
|
||||
public class ClockTickScheduler extends HandlerMainScheduler {
|
||||
public ClockTickScheduler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.CLOCK_TICK.getOption(), "tick");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
|
||||
public class AddProductOnCountdown extends HandlerMainCountdown {
|
||||
public AddProductOnCountdown(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
@ -1117,24 +1340,74 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
|
||||
private void switchFragment(Fragment from, Fragment to) {
|
||||
if (from != to) {
|
||||
// 防呆:若 Activity 已 onSaveInstanceState(非前景),commit 會丟
|
||||
// IllegalStateException。購物車 180 秒倒數 onFinish 在背景觸發返回時即屬此情況。
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
|
||||
getLogs().info("switchFragment skipped: activity not started");
|
||||
return;
|
||||
}
|
||||
|
||||
if (from instanceof FragmentAbstract) {
|
||||
getLogs().info("change pay stop");
|
||||
((FragmentAbstract) from).stopOnSwitchFragment();
|
||||
}
|
||||
|
||||
// 此切換僅為 Fragment 顯示切換(資料在 ViewModel),允許 state loss 不致實害。
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.hide(from)
|
||||
.show(to)
|
||||
.commit();
|
||||
.commitAllowingStateLoss();
|
||||
activeFragment = to;
|
||||
|
||||
if (to instanceof FragmentAbstract) {
|
||||
getLogs().info("change pay start");
|
||||
((FragmentAbstract) to).startOnSwitchFragment();
|
||||
}
|
||||
|
||||
// 浮層連動:購物車(General 多品項)是開在 fragment_container 的 Fragment,不走 DialogAbstract,
|
||||
// 故不會送 HIDE_SALE_OVERLAY。在此依目標 fragment 補上連動(activeFragment 已於上方更新)。
|
||||
refreshSaleOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新評估銷售平台浮層(取貨碼/購物車浮鈕、上下系統列)是否該顯示。
|
||||
* 唯一真相:僅當「處於商品瀏覽頁(VMC/Electric)」且「沒有任何對話框開著」時才顯示,否則隱藏。
|
||||
* 涵蓋兩條會遮住銷售平台的路徑:對話框(DialogAbstract,貼底部非滿版)與購物車 Fragment。
|
||||
*/
|
||||
private void refreshSaleOverlay() {
|
||||
boolean inProductBrowsing = (activeFragment == vmcFragment || activeFragment == electricFragment);
|
||||
boolean anyDialog = MyApp.getInstance().isShowDialog();
|
||||
applySaleOverlayVisibility(inProductBrowsing && !anyDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 將指定 Fragment 加入容器(若尚未加入)並切換顯示。
|
||||
* 供 flavor 端注入專屬 Fragment(如 General 多品項購物車)使用,本身不依賴任何 flavor 類別。
|
||||
*/
|
||||
public void openFragment(Fragment to) {
|
||||
if (to == null) {
|
||||
return;
|
||||
}
|
||||
// 同 switchFragment:背景狀態下 commitNow 亦會丟 IllegalStateException。
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
|
||||
getLogs().info("openFragment skipped: activity not started");
|
||||
return;
|
||||
}
|
||||
if (!to.isAdded()) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(binding.fragmentContainer.getId(), to)
|
||||
.hide(to)
|
||||
.commitNow();
|
||||
}
|
||||
switchFragment(activeFragment, to);
|
||||
}
|
||||
|
||||
/** 切回 VMC 商品列表(供購物車「加購/取消/逾時」返回使用)。 */
|
||||
public void backToVmc() {
|
||||
switchFragment(activeFragment, vmcFragment);
|
||||
}
|
||||
|
||||
public void callbackOnBackPressed() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
int backStackCount = fragmentManager.getBackStackEntryCount();
|
||||
|
||||
@ -36,7 +36,7 @@ import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Cmuh (中國醫版) 銷售流程處理器。
|
||||
* pickup_sheet 模式(原 Cmuh 中國醫版)銷售流程。
|
||||
* 負責:
|
||||
* 1. 綁定「40秒內刷條碼」按鈕 (scanbarcodebtn)
|
||||
* 2. 連接 USB 掃描器背景讀取
|
||||
@ -44,49 +44,37 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
* 4. 庫存核對與自動加入購物車
|
||||
* 5. 顯示領藥與錯誤 Dialog
|
||||
*/
|
||||
public class SaleFlowHandler {
|
||||
public class PickupSheetSaleFlow extends SaleFlowHandler {
|
||||
private static final String ORDC_EMPTY = "ORDC_EMPTY";
|
||||
private static final String MATERIAL_NOT_FOUND = "MATERIAL_NOT_FOUND";
|
||||
private static final String STOCK_EMPTY = "STOCK_EMPTY";
|
||||
private static final String STOCK_NOT_ENOUGH = "STOCK_NOT_ENOUGH";
|
||||
|
||||
protected final Context context;
|
||||
protected final SaleFlowCallback callback;
|
||||
protected final HandlerMain handlerMain;
|
||||
|
||||
private boolean openBarcode = true;
|
||||
private boolean haveValuestate = false;
|
||||
private long lastClickTime = 0;
|
||||
|
||||
public SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
this.context = context;
|
||||
this.handlerMain = handlerMain;
|
||||
this.callback = callback;
|
||||
public PickupSheetSaleFlow(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
super(context, handlerMain, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 Activity 的 onResume() 中會被呼叫,此時尋找並綁定 VmcFragment 中的掃碼按鈕
|
||||
*/
|
||||
public void onSetupUI() {
|
||||
onSetupUI(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupUI(View rootView) {
|
||||
if (context instanceof Activity) {
|
||||
View btnScan = rootView != null ? rootView.findViewById(R.id.scanbarcodebtn) : ((Activity) context).findViewById(R.id.scanbarcodebtn);
|
||||
if (btnScan != null) {
|
||||
btnScan.setOnClickListener(new BtnQrCodeOnClick(handlerMain, context));
|
||||
MyApp.getInstance().getLogs().info("✅ 成功在 Cmuh SaleFlowHandler 綁定掃碼按鈕!");
|
||||
MyApp.getInstance().getLogs().info("✅ 成功在 PickupSheetSaleFlow 綁定掃碼按鈕!");
|
||||
} else {
|
||||
MyApp.getInstance().getLogs().info("❌ 在 Cmuh SaleFlowHandler 找不到掃碼按鈕 R.id.scanbarcodebtn!");
|
||||
MyApp.getInstance().getLogs().info("❌ 在 PickupSheetSaleFlow 找不到掃碼按鈕 R.id.scanbarcodebtn!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onBuyRequested() {
|
||||
// 中國醫版沒有購物車付款,此方法不執行
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBarcodeReceived(String rawData) {
|
||||
// 若是經由其他方式傳入的 Barcode 可在此處直接處理
|
||||
try {
|
||||
@ -99,8 +87,9 @@ public class SaleFlowHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* CMUH 點選取貨按鈕,開啟輸入取貨碼的 Dialog
|
||||
* 點選取貨按鈕,開啟輸入取貨碼的 Dialog(pickup_sheet 走舊 API 驗證,見 FontendPickupCodeDialog)。
|
||||
*/
|
||||
@Override
|
||||
public void onPickupRequested() {
|
||||
new FontendPickupCodeDialog(context, handlerMain).show();
|
||||
}
|
||||
@ -129,7 +118,7 @@ public class SaleFlowHandler {
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
usbDev.read(buffer, 100); // 預讀清除緩衝
|
||||
|
||||
|
||||
for (int i = 0; i < 40; i++) { // 總計約 40 秒
|
||||
if (isRun() && openBarcode) {
|
||||
int size = usbDev.read(buffer, 1000); // 超時為 1 秒
|
||||
@ -216,7 +205,7 @@ public class SaleFlowHandler {
|
||||
if (encryptedBytes.length % 16 != 0) {
|
||||
throw new IllegalArgumentException("錯誤:加密數據長度不是 16 的倍數,可能需要填充");
|
||||
}
|
||||
|
||||
|
||||
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
|
||||
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
@ -231,7 +220,7 @@ public class SaleFlowHandler {
|
||||
|
||||
String decryptedStr = MyApp.getInstance().getDecryptedData();
|
||||
MyApp.getInstance().getLogs().info("decryptedStr=" + decryptedStr);
|
||||
|
||||
|
||||
JSONObject jsonObject = new JSONObject(decryptedStr);
|
||||
if (checkORDCDispatchNumber(decryptedStr)) {
|
||||
return false;
|
||||
@ -290,7 +279,7 @@ public class SaleFlowHandler {
|
||||
JSONObject itemObject = itemsArray.getJSONObject(i);
|
||||
String ordc = itemObject.optString("ORDC", "").trim();
|
||||
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
|
||||
|
||||
|
||||
// 重新初始化這些變數
|
||||
MedicineName = "";
|
||||
MedicineCount = 0;
|
||||
@ -317,10 +306,10 @@ public class SaleFlowHandler {
|
||||
for (int i = 0; i < itemsArray.length(); i++) {
|
||||
JSONObject itemObject = itemsArray.getJSONObject(i);
|
||||
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
|
||||
|
||||
|
||||
if (requestQty <= medicineList.get(i).getMedicineCount()) {
|
||||
haveValuestate = true;
|
||||
|
||||
|
||||
if (requestQty > 1) {
|
||||
class TempSlot {
|
||||
int slotNo;
|
||||
@ -12,6 +12,7 @@ import android.widget.ImageView;
|
||||
import android.widget.SearchView;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.unibuy.smartdevice.AppCompatActivityAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
@ -94,34 +95,44 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
|
||||
if (size > 0) recyclerSearchTextAdpter.notifyItemRangeRemoved(0, size);
|
||||
|
||||
if (message.isEmpty()) {
|
||||
binding.recyclerSearchText.setVisibility(View.GONE);
|
||||
binding.textSearchStatus.setVisibility(View.VISIBLE);
|
||||
binding.textSearchStatus.setText("請輸入您要搜尋的文字");
|
||||
|
||||
for (ProductStructure product: MyApp.getInstance().getProductList()) {
|
||||
searchProductList.add(product);
|
||||
}
|
||||
|
||||
recyclerProductSearchAdpter.notifyItemInserted(searchProductList.size() -1);
|
||||
if (searchProductList.size() > 0) {
|
||||
recyclerProductSearchAdpter.notifyDataSetChanged();
|
||||
}
|
||||
} else {
|
||||
for (ProductStructure product: MyApp.getInstance().getProductList()) {
|
||||
if (product.getProductName().contains(message)) {
|
||||
searchProductList.add(product);
|
||||
String name = product.getProductName();
|
||||
// Log.i("product name", name);
|
||||
int index = name.indexOf(message);
|
||||
if (index != -1 && index < name.length() - 1) {
|
||||
String result = name.substring(index + message.length());
|
||||
|
||||
if (!result.isEmpty()) {
|
||||
String oneText = String.valueOf(result.charAt(0));
|
||||
if (!searchTextList.contains(oneText)) {
|
||||
searchTextList.add(oneText);
|
||||
}
|
||||
}
|
||||
if (!searchTextList.contains(name)) {
|
||||
searchTextList.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchTextList.size() > 2) Collections.sort(searchTextList);
|
||||
recyclerProductSearchAdpter.notifyItemInserted(searchProductList.size() -1);
|
||||
recyclerSearchTextAdpter.notifyItemInserted(searchTextList.size() -1);
|
||||
|
||||
if (searchTextList.isEmpty()) {
|
||||
binding.recyclerSearchText.setVisibility(View.GONE);
|
||||
binding.textSearchStatus.setVisibility(View.VISIBLE);
|
||||
binding.textSearchStatus.setText("查無相關的候選字");
|
||||
} else {
|
||||
binding.recyclerSearchText.setVisibility(View.VISIBLE);
|
||||
binding.textSearchStatus.setVisibility(View.GONE);
|
||||
recyclerSearchTextAdpter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
if (searchProductList.size() > 0) {
|
||||
recyclerProductSearchAdpter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -140,7 +151,7 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
|
||||
|
||||
recyclerProductSearchAdpter = new RecyclerProductSearchAdpter(getCtx(), position, searchProductList);
|
||||
binding.recyclerProductSearch.setAdapter(recyclerProductSearchAdpter);
|
||||
binding.recyclerProductSearch.setLayoutManager(new LinearLayoutManager(getCtx()));
|
||||
binding.recyclerProductSearch.setLayoutManager(new GridLayoutManager(getCtx(), 3));
|
||||
|
||||
recyclerSearchTextAdpter = new RecyclerSearchTextAdpter(binding, searchTextList);
|
||||
binding.recyclerSearchText.setAdapter(recyclerSearchTextAdpter);
|
||||
@ -149,8 +160,30 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
|
||||
// SearchView searchView = binding.searchProductList;
|
||||
// enlargeSearchView(searchView, 32, 20, R.drawable.search); // icon 32dp,文字 20sp,自訂 drawable
|
||||
replaceSearchViewIcon(binding.searchProductList);
|
||||
binding.searchProductList.setOnQueryTextListener(new SearchProductListOnQueryText(getHandlerMain()));
|
||||
binding.searchProductList.setOnQueryTextListener(new SearchProductListOnQueryText(getHandlerMain()) {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
binding.searchProductList.clearFocus();
|
||||
return super.onQueryTextSubmit(query);
|
||||
}
|
||||
});
|
||||
|
||||
// 點擊列表時自動關閉鍵盤
|
||||
binding.recyclerProductSearch.setOnTouchListener((v, event) -> {
|
||||
binding.searchProductList.clearFocus();
|
||||
return false;
|
||||
});
|
||||
binding.recyclerSearchText.setOnTouchListener((v, event) -> {
|
||||
binding.searchProductList.clearFocus();
|
||||
return false;
|
||||
});
|
||||
|
||||
binding.buttonBack.setOnClickListener(v -> finish());
|
||||
|
||||
binding.buttonClear.setOnClickListener(v -> {
|
||||
binding.searchProductList.setQuery("", false);
|
||||
binding.searchProductList.clearFocus();
|
||||
});
|
||||
|
||||
if (MyApp.getInstance().getProductList().isEmpty()) {
|
||||
ProductsDao productsDao = new ProductsDao(getCtx());
|
||||
@ -172,9 +205,6 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
|
||||
private void replaceSearchViewIcon(SearchView searchView) {
|
||||
if (searchView == null) return;
|
||||
|
||||
// 設定 hint
|
||||
searchView.setQueryHint("輸入搜尋關鍵字");
|
||||
|
||||
// 找到內部的 EditText,修改字體大小 & 顏色
|
||||
int searchEditTextId = searchView.getContext()
|
||||
.getResources()
|
||||
@ -194,7 +224,24 @@ public class ProductSearchActivity extends AppCompatActivityAbstract {
|
||||
|
||||
ImageView searchIcon = searchView.findViewById(searchIconId);
|
||||
if (searchIcon != null) {
|
||||
searchIcon.setImageResource(R.drawable.searchopen);
|
||||
searchIcon.setImageDrawable(null);
|
||||
searchIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 隱藏內部的清除 icon
|
||||
int closeIconId = searchView.getContext()
|
||||
.getResources()
|
||||
.getIdentifier("android:id/search_close_btn", null, null);
|
||||
ImageView closeIcon = searchView.findViewById(closeIconId);
|
||||
if (closeIcon != null) {
|
||||
closeIcon.setImageDrawable(null);
|
||||
closeIcon.setEnabled(false);
|
||||
ViewGroup.LayoutParams params = closeIcon.getLayoutParams();
|
||||
if (params != null) {
|
||||
params.width = 0;
|
||||
params.height = 0;
|
||||
closeIcon.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
|
||||
/**
|
||||
* 銷售流程處理器(抽象基底)。
|
||||
*
|
||||
* 三種購買流程改由雲端 B014 {@code shopping_mode} 執行期切換,取代舊 project flavor 分家:
|
||||
* <ul>
|
||||
* <li>{@code basic} → {@link BasicSaleFlow}(多品項購物車,原 General)</li>
|
||||
* <li>{@code employee_card}→ {@link EmployeeCardSaleFlow}(員工卡 B680,原 Chengwai)</li>
|
||||
* <li>{@code pickup_sheet} → {@link PickupSheetSaleFlow}(掃碼醫令自動結帳,原 Cmuh)</li>
|
||||
* </ul>
|
||||
* 由 {@link #create} 依 {@link MyApp#getShoppingMode()} 建立對應策略。
|
||||
*/
|
||||
public abstract class SaleFlowHandler {
|
||||
|
||||
protected final Context context;
|
||||
protected final SaleFlowCallback callback;
|
||||
protected final HandlerMain handlerMain;
|
||||
|
||||
protected SaleFlowHandler(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
this.context = context;
|
||||
this.handlerMain = handlerMain;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/** 依雲端 shopping_mode 建立對應的銷售流程策略(預設 basic)。 */
|
||||
public static SaleFlowHandler create(Context context, HandlerMain handlerMain, SaleFlowCallback callback) {
|
||||
String mode = MyApp.getInstance().getShoppingMode();
|
||||
if ("employee_card".equals(mode)) {
|
||||
return new EmployeeCardSaleFlow(context, handlerMain, callback);
|
||||
}
|
||||
if ("pickup_sheet".equals(mode)) {
|
||||
return new PickupSheetSaleFlow(context, handlerMain, callback);
|
||||
}
|
||||
return new BasicSaleFlow(context, handlerMain, callback);
|
||||
}
|
||||
|
||||
public void onSetupUI() {
|
||||
onSetupUI(null);
|
||||
}
|
||||
|
||||
public void onSetupUI(View rootView) {
|
||||
// 預設無動作
|
||||
}
|
||||
|
||||
public void onBuyRequested() {
|
||||
// 預設無動作
|
||||
}
|
||||
|
||||
public void onShoppingCartRequested() {
|
||||
// 預設:購物車浮鈕等同進入購買流程
|
||||
onBuyRequested();
|
||||
}
|
||||
|
||||
public boolean handleProductSelected(SlotStructure slot) {
|
||||
// 預設:不消化商品點擊,交回 adapter 既有流程
|
||||
return false;
|
||||
}
|
||||
|
||||
public Class<? extends DialogAbstract> cashDialogClass() {
|
||||
return com.unibuy.smartdevice.ui.dialog.CashPayDialog.class;
|
||||
}
|
||||
|
||||
public void onBarcodeReceived(String rawData) {
|
||||
// 預設無動作
|
||||
}
|
||||
|
||||
public void onPickupRequested() {
|
||||
// 預設無動作
|
||||
}
|
||||
}
|
||||
@ -2,30 +2,28 @@ package com.unibuy.smartdevice.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.unibuy.smartdevice.AppCompatActivityAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.database.MemoDao;
|
||||
import com.unibuy.smartdevice.database.SettingsDao;
|
||||
import com.unibuy.smartdevice.databinding.ActivitySettingTestBinding;
|
||||
import com.unibuy.smartdevice.databinding.ActivitySystemSetBinding;
|
||||
import com.unibuy.smartdevice.structure.DevSetStructure;
|
||||
import com.unibuy.smartdevice.structure.MemoStructure;
|
||||
import com.unibuy.smartdevice.external.StarCloudAPI;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.recycler.RecyclerSettingListAdpter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 主控台「系統設定」:呈現雲端 B014 下發的開關設定。
|
||||
* 系統方(identity=system)可編輯並按「儲存」回寫雲端(B016);其他身分唯讀。
|
||||
*/
|
||||
public class SystemSetActivity extends AppCompatActivityAbstract {
|
||||
private ActivitySystemSetBinding binding;
|
||||
private boolean isSystem;
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
@ -49,57 +47,249 @@ public class SystemSetActivity extends AppCompatActivityAbstract {
|
||||
binding = ActivitySystemSetBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
isSystem = MyApp.getInstance().isSystemIdentity();
|
||||
|
||||
applyShoppingModeView();
|
||||
updateCheckBoxes();
|
||||
applyRolePermission();
|
||||
|
||||
binding.buttonBack.setOnClickListener(v -> {
|
||||
updateSystemSet();
|
||||
finish();
|
||||
});
|
||||
|
||||
// IC 卡讀取功能已移除
|
||||
binding.buttonBack.setOnClickListener(v -> finish());
|
||||
binding.buttonSave.setOnClickListener(v -> onSaveClicked());
|
||||
}
|
||||
|
||||
/**
|
||||
* 購物方式(雲端 B014 ShoppingMode,機台唯讀):顯示中文名稱,並只在 basic(基礎版)
|
||||
* 時顯示其下的可設定項;員工卡/取貨單模式整段隱藏(流程與硬體由雲端固定,現場不可調)。
|
||||
*/
|
||||
private void applyShoppingModeView() {
|
||||
String mode = MyApp.getInstance().getShoppingMode();
|
||||
String label;
|
||||
switch (mode == null ? "" : mode) {
|
||||
case "employee_card":
|
||||
label = "員工卡";
|
||||
break;
|
||||
case "pickup_sheet":
|
||||
label = "取貨單";
|
||||
break;
|
||||
case "basic":
|
||||
default:
|
||||
label = "基礎版";
|
||||
break;
|
||||
}
|
||||
binding.textViewShoppingMode.setText("購物方式:" + label);
|
||||
binding.layoutBasicOnlySettings.setVisibility("basic".equals(mode) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/** 將記憶體中的設定值套到各 CheckBox。 */
|
||||
private void updateCheckBoxes() {
|
||||
binding.checkboxVMC.setChecked(MyApp.getInstance().getDevSet().isVMC());
|
||||
binding.checkboxElectic.setChecked(MyApp.getInstance().getDevSet().isElectic());
|
||||
binding.checkboxShoppingCar.setChecked(MyApp.getInstance().getDevSet().isShoppingCar());
|
||||
binding.checkboxInvoice.setChecked(MyApp.getInstance().getDevSet().isInvoice());
|
||||
binding.checkboxDevNFCPay.setChecked(MyApp.getInstance().getDevSet().isDevNFCPay());
|
||||
binding.checkboxDevEsunPay.setChecked(MyApp.getInstance().getDevSet().isDevEsunPay());
|
||||
binding.checkboxDevTapPay.setChecked(MyApp.getInstance().getDevSet().isDevTapPay());
|
||||
binding.checkboxDevCash.setChecked(MyApp.getInstance().getDevSet().isDevCash());
|
||||
binding.checkboxDevLinePay.setChecked(MyApp.getInstance().getDevSet().isDevLinePay());
|
||||
binding.cardDevShoppingCar.setVisibility(MyApp.getInstance().getDevSet().isShoppingCar() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevInvoice.setVisibility(MyApp.getInstance().getDevSet().isInvoice() ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean isNfcPayEnabled = MyApp.getInstance().getDevSet().isDevNFCPay();
|
||||
|
||||
if (isNfcPayEnabled) {
|
||||
binding.textViewNFCStatusHint.setText("啟用中");
|
||||
binding.textViewNFCStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
|
||||
binding.gridLayoutNFCPayOptions.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.textViewNFCStatusHint.setText("功能未啟用");
|
||||
binding.textViewNFCStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
|
||||
binding.gridLayoutNFCPayOptions.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.cardDevCreditCard.setVisibility(MyApp.getInstance().getDevSet().isDevCreditCard() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevMobilePay.setVisibility(MyApp.getInstance().getDevSet().isDevMobilePay() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevCardPay.setVisibility(MyApp.getInstance().getDevSet().isDevCardPay() ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean isScanPayEnabled = MyApp.getInstance().getDevSet().isDevScanPay();
|
||||
binding.checkboxDevScanPay.setChecked(isScanPayEnabled);
|
||||
|
||||
if (isScanPayEnabled) {
|
||||
binding.textViewScanStatusHint.setText("啟用中");
|
||||
binding.textViewScanStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
|
||||
binding.layoutScanPayOptions.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.textViewScanStatusHint.setText("功能未啟用");
|
||||
binding.textViewScanStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
|
||||
binding.layoutScanPayOptions.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
boolean isEsunEnabled = MyApp.getInstance().getDevSet().isDevEsunPay();
|
||||
binding.checkboxDevEsunPay.setChecked(isEsunEnabled);
|
||||
int esunVisibility = isEsunEnabled ? View.VISIBLE : View.GONE;
|
||||
binding.cardDevEsunPay.setVisibility(esunVisibility);
|
||||
binding.cardDevEsunSubTaiwanPay.setVisibility(esunVisibility);
|
||||
|
||||
boolean isTapPayEnabled = MyApp.getInstance().getDevSet().isDevTapPay();
|
||||
binding.checkboxDevTapPay.setChecked(isTapPayEnabled);
|
||||
binding.cardDevTapPay.setVisibility(isTapPayEnabled ? View.VISIBLE : View.GONE);
|
||||
|
||||
binding.checkboxDevTapPay30.setChecked(MyApp.getInstance().getDevSet().isTapPay30());
|
||||
binding.checkboxDevTapPay31.setChecked(MyApp.getInstance().getDevSet().isTapPay31());
|
||||
binding.checkboxDevTapPay32.setChecked(MyApp.getInstance().getDevSet().isTapPay32());
|
||||
binding.checkboxDevTapPay33.setChecked(MyApp.getInstance().getDevSet().isTapPay33());
|
||||
binding.checkboxDevTapPay34.setChecked(MyApp.getInstance().getDevSet().isTapPay34());
|
||||
|
||||
binding.cardDevTapPay30.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay30() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevTapPay31.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay31() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevTapPay32.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay32() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevTapPay33.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay33() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevTapPay34.setVisibility(isTapPayEnabled && MyApp.getInstance().getDevSet().isTapPay34() ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean isLinePayEnabled = MyApp.getInstance().getDevSet().isDevLinePay();
|
||||
binding.checkboxDevLinePay.setChecked(isLinePayEnabled);
|
||||
binding.cardDevLinePay.setVisibility(isLinePayEnabled ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevLineSubLinePay.setVisibility(isLinePayEnabled ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean isCashEnabled = MyApp.getInstance().getDevSet().isDevCash();
|
||||
binding.checkboxDevCash.setChecked(isCashEnabled);
|
||||
|
||||
if (isCashEnabled) {
|
||||
binding.textViewCashStatusHint.setText("啟用中");
|
||||
binding.textViewCashStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
|
||||
binding.layoutCashPayOptions.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.textViewCashStatusHint.setText("功能未啟用");
|
||||
binding.textViewCashStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
|
||||
binding.layoutCashPayOptions.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.checkboxDevBillF.setChecked(MyApp.getInstance().getCashSetStructure().isBillF1000());
|
||||
binding.checkboxDevBillE.setChecked(MyApp.getInstance().getCashSetStructure().isBillE500());
|
||||
binding.checkboxDevBillD.setChecked(MyApp.getInstance().getCashSetStructure().isBillD100());
|
||||
|
||||
binding.checkboxDevCoinF.setChecked(MyApp.getInstance().getCashSetStructure().isCoinF50());
|
||||
binding.checkboxDevCoinE.setChecked(MyApp.getInstance().getCashSetStructure().isCoinE10());
|
||||
binding.checkboxDevCoinD.setChecked(MyApp.getInstance().getCashSetStructure().isCoinD5());
|
||||
binding.checkboxDevCoinC.setChecked(MyApp.getInstance().getCashSetStructure().isCoinC1());
|
||||
|
||||
binding.cardDevBillF.setVisibility(MyApp.getInstance().getCashSetStructure().isBillF1000() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevBillE.setVisibility(MyApp.getInstance().getCashSetStructure().isBillE500() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevBillD.setVisibility(MyApp.getInstance().getCashSetStructure().isBillD100() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevCoinF.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinF50() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevCoinE.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinE10() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevCoinD.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinD5() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevCoinC.setVisibility(MyApp.getInstance().getCashSetStructure().isCoinC1() ? View.VISIBLE : View.GONE);
|
||||
|
||||
boolean isPickupEnabled = MyApp.getInstance().getDevSet().isPickupModule();
|
||||
binding.checkboxPickupModule.setChecked(isPickupEnabled);
|
||||
|
||||
if (isPickupEnabled) {
|
||||
binding.textViewPickupStatusHint.setText("啟用中");
|
||||
binding.textViewPickupStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.success));
|
||||
binding.chipGroupPickup.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.textViewPickupStatusHint.setText("功能未啟用");
|
||||
binding.textViewPickupStatusHint.setTextColor(androidx.core.content.ContextCompat.getColor(getCtx(), com.unibuy.smartdevice.R.color.warning));
|
||||
binding.chipGroupPickup.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.checkboxPickupCode.setChecked(MyApp.getInstance().getDevSet().isPickupCode());
|
||||
binding.checkboxPassCode.setChecked(MyApp.getInstance().getDevSet().isPassCode());
|
||||
|
||||
binding.cardDevPickupCode.setVisibility(isPickupEnabled && MyApp.getInstance().getDevSet().isPickupCode() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevPassCode.setVisibility(isPickupEnabled && MyApp.getInstance().getDevSet().isPassCode() ? View.VISIBLE : View.GONE);
|
||||
|
||||
binding.checkboxWelcomeGift.setChecked(MyApp.getInstance().getDevSet().isWelcomeGift());
|
||||
binding.checkboxMemberSystem.setChecked(MyApp.getInstance().getDevSet().isMemberSystem());
|
||||
binding.checkboxAmbientTemp.setChecked(MyApp.getInstance().getDevSet().isAmbientTemp());
|
||||
|
||||
binding.cardDevWelcomeGift.setVisibility(MyApp.getInstance().getDevSet().isWelcomeGift() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevMemberSystem.setVisibility(MyApp.getInstance().getDevSet().isMemberSystem() ? View.VISIBLE : View.GONE);
|
||||
binding.cardDevAmbientTemp.setVisibility(MyApp.getInstance().getDevSet().isAmbientTemp() ? View.VISIBLE : View.GONE);
|
||||
|
||||
setupSpringSlotRow(binding.rowSpringSlot110, binding.textSpringSlot110, MyApp.getInstance().getHardwareSetStructure().isSpringSlot1_10());
|
||||
setupSpringSlotRow(binding.rowSpringSlot1120, binding.textSpringSlot1120, MyApp.getInstance().getHardwareSetStructure().isSpringSlot11_20());
|
||||
setupSpringSlotRow(binding.rowSpringSlot2130, binding.textSpringSlot2130, MyApp.getInstance().getHardwareSetStructure().isSpringSlot21_30());
|
||||
setupSpringSlotRow(binding.rowSpringSlot3140, binding.textSpringSlot3140, MyApp.getInstance().getHardwareSetStructure().isSpringSlot31_40());
|
||||
setupSpringSlotRow(binding.rowSpringSlot4150, binding.textSpringSlot4150, MyApp.getInstance().getHardwareSetStructure().isSpringSlot41_50());
|
||||
setupSpringSlotRow(binding.rowSpringSlot5160, binding.textSpringSlot5160, MyApp.getInstance().getHardwareSetStructure().isSpringSlot51_60());
|
||||
}
|
||||
|
||||
private void updateSystemSet() {
|
||||
MyApp.getInstance().getDevSet().setVMC(binding.checkboxVMC.isChecked());
|
||||
MyApp.getInstance().getDevSet().setElectic(binding.checkboxElectic.isChecked());
|
||||
private void setupSpringSlotRow(TableRow row, TextView textMode, boolean isChecked) {
|
||||
textMode.setTag(isChecked);
|
||||
textMode.setText(isChecked ? "彈簧模式" : "履帶模式");
|
||||
}
|
||||
|
||||
/** 依登入身分決定可否編輯:系統方僅可編輯硬體貨道並顯示儲存鈕;其餘全唯讀。 */
|
||||
private void applyRolePermission() {
|
||||
binding.buttonSave.setVisibility(View.GONE);
|
||||
refreshChildStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 統一刷新所有 CheckBox 的 enabled 狀態。機台不可編輯系統設定:
|
||||
* - 所有系統設定項(購物車/發票/各支付方式/刷卡機/掃碼/現金總開關與面額/取貨模組/
|
||||
* 來店禮/會員/環溫)→ 一律唯讀,真相在雲端後台,即使系統方也不可改。
|
||||
* - 唯一例外:硬體貨道(彈簧/履帶)→ 僅系統方可改並寫回雲端,因只有現場技師最清楚
|
||||
* 各貨道的實際機械型式。
|
||||
*/
|
||||
private void refreshChildStates() {
|
||||
// 系統設定項:恆唯讀(與身分、主開關都無關)
|
||||
setEnabled(false,
|
||||
binding.checkboxShoppingCar, binding.checkboxInvoice,
|
||||
binding.checkboxDevScanPay, binding.checkboxDevEsunPay, binding.checkboxDevTapPay,
|
||||
binding.checkboxDevLinePay,
|
||||
binding.checkboxDevTapPay30, binding.checkboxDevTapPay31, binding.checkboxDevTapPay32,
|
||||
binding.checkboxDevTapPay33, binding.checkboxDevTapPay34,
|
||||
binding.checkboxDevCash,
|
||||
binding.checkboxDevBillF, binding.checkboxDevBillE, binding.checkboxDevBillD,
|
||||
binding.checkboxDevCoinF, binding.checkboxDevCoinE, binding.checkboxDevCoinD,
|
||||
binding.checkboxDevCoinC,
|
||||
binding.checkboxPickupModule, binding.checkboxPickupCode, binding.checkboxPassCode,
|
||||
binding.checkboxWelcomeGift, binding.checkboxMemberSystem, binding.checkboxAmbientTemp);
|
||||
|
||||
// 硬體貨道已改為全唯讀
|
||||
}
|
||||
|
||||
private void setEnabled(boolean enabled, View... views) {
|
||||
for (View v : views) {
|
||||
v.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
/** 系統方按下儲存:先回寫雲端,成功後才更新本地並關閉。 */
|
||||
private void onSaveClicked() {
|
||||
if (!isSystem) return;
|
||||
binding.buttonSave.setEnabled(false);
|
||||
try {
|
||||
JSONObject settings = buildBackendSettings();
|
||||
new StarCloudAPI(getHandlerMain(), getHandlerMain()).uploadSystemSettings(settings,
|
||||
new StarCloudAPI.SettingsUploadCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
commitToLocal();
|
||||
android.widget.Toast.makeText(getCtx(), "設定已儲存並回寫雲端", android.widget.Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String message) {
|
||||
binding.buttonSave.setEnabled(true);
|
||||
android.widget.Toast.makeText(getCtx(), "儲存失敗:" + message, android.widget.Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} catch (JSONException e) {
|
||||
binding.buttonSave.setEnabled(true);
|
||||
android.widget.Toast.makeText(getCtx(), "設定組裝失敗:" + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/** 將目前 CheckBox 狀態寫回記憶體並持久化(本地)。 */
|
||||
private void commitToLocal() {
|
||||
MyApp.getInstance().getDevSet().setShoppingCar(binding.checkboxShoppingCar.isChecked());
|
||||
MyApp.getInstance().getDevSet().setInvoice(binding.checkboxInvoice.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevNFCPay(binding.checkboxDevNFCPay.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevScanPay(binding.checkboxDevScanPay.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevEsunPay(binding.checkboxDevEsunPay.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevTapPay(binding.checkboxDevTapPay.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevCash(binding.checkboxDevCash.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevLinePay(binding.checkboxDevLinePay.isChecked());
|
||||
MyApp.getInstance().getDevSet().setTapPay30(binding.checkboxDevTapPay30.isChecked());
|
||||
MyApp.getInstance().getDevSet().setTapPay31(binding.checkboxDevTapPay31.isChecked());
|
||||
MyApp.getInstance().getDevSet().setTapPay32(binding.checkboxDevTapPay32.isChecked());
|
||||
MyApp.getInstance().getDevSet().setTapPay33(binding.checkboxDevTapPay33.isChecked());
|
||||
MyApp.getInstance().getDevSet().setTapPay34(binding.checkboxDevTapPay34.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevLinePay(binding.checkboxDevLinePay.isChecked());
|
||||
MyApp.getInstance().getDevSet().setDevCash(binding.checkboxDevCash.isChecked());
|
||||
|
||||
MyApp.getInstance().getCashSetStructure().setBillF1000(binding.checkboxDevBillF.isChecked());
|
||||
MyApp.getInstance().getCashSetStructure().setBillE500(binding.checkboxDevBillE.isChecked());
|
||||
@ -109,6 +299,36 @@ public class SystemSetActivity extends AppCompatActivityAbstract {
|
||||
MyApp.getInstance().getCashSetStructure().setCoinD5(binding.checkboxDevCoinD.isChecked());
|
||||
MyApp.getInstance().getCashSetStructure().setCoinC1(binding.checkboxDevCoinC.isChecked());
|
||||
|
||||
MyApp.getInstance().getDevSet().setPickupModule(binding.checkboxPickupModule.isChecked());
|
||||
MyApp.getInstance().getDevSet().setPickupCode(binding.checkboxPickupCode.isChecked());
|
||||
MyApp.getInstance().getDevSet().setPassCode(binding.checkboxPassCode.isChecked());
|
||||
MyApp.getInstance().getDevSet().setWelcomeGift(binding.checkboxWelcomeGift.isChecked());
|
||||
MyApp.getInstance().getDevSet().setMemberSystem(binding.checkboxMemberSystem.isChecked());
|
||||
MyApp.getInstance().getDevSet().setAmbientTemp(binding.checkboxAmbientTemp.isChecked());
|
||||
|
||||
MyApp.getInstance().getHardwareSetStructure().setSpringSlot1_10((Boolean) binding.textSpringSlot110.getTag());
|
||||
MyApp.getInstance().getHardwareSetStructure().setSpringSlot11_20((Boolean) binding.textSpringSlot1120.getTag());
|
||||
MyApp.getInstance().getHardwareSetStructure().setSpringSlot21_30((Boolean) binding.textSpringSlot2130.getTag());
|
||||
MyApp.getInstance().getHardwareSetStructure().setSpringSlot31_40((Boolean) binding.textSpringSlot3140.getTag());
|
||||
MyApp.getInstance().getHardwareSetStructure().setSpringSlot41_50((Boolean) binding.textSpringSlot4150.getTag());
|
||||
MyApp.getInstance().getHardwareSetStructure().setSpringSlot51_60((Boolean) binding.textSpringSlot5160.getTag());
|
||||
|
||||
MyApp.saveSystemSet(getCtx());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 組出回寫雲端用的 settings JSON。機台僅可編輯硬體貨道,故只送這六個 is_spring_slot_*
|
||||
* 欄位;B016 後端以 array_key_exists 逐欄位守門,未送的欄位保持原值,避免用本地舊值
|
||||
* 覆寫雲端後台可能已更新的其他設定。
|
||||
*/
|
||||
private JSONObject buildBackendSettings() throws JSONException {
|
||||
JSONObject s = new JSONObject();
|
||||
s.put("is_spring_slot_1_10", (Boolean) binding.textSpringSlot110.getTag());
|
||||
s.put("is_spring_slot_11_20", (Boolean) binding.textSpringSlot1120.getTag());
|
||||
s.put("is_spring_slot_21_30", (Boolean) binding.textSpringSlot2130.getTag());
|
||||
s.put("is_spring_slot_31_40", (Boolean) binding.textSpringSlot3140.getTag());
|
||||
s.put("is_spring_slot_41_50", (Boolean) binding.textSpringSlot4150.getTag());
|
||||
s.put("is_spring_slot_51_60", (Boolean) binding.textSpringSlot5160.getTag());
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,21 +79,9 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
} catch (LogsEmptyException e) {
|
||||
this.logs.warning(e);
|
||||
}
|
||||
holder.binding.textSlot.setText(String.format("%03d", slot.getSlot()));
|
||||
// 多國語的判斷
|
||||
String currentLang = LanguageHelper.getSavedLanguage(context);
|
||||
holder.binding.textProductName.setText(slot.getProduct().getProductName());
|
||||
switch (currentLang) {
|
||||
case "tw":
|
||||
holder.binding.textProductName.setText(slot.getProduct().getProductName());
|
||||
break;
|
||||
case "en":
|
||||
holder.binding.textProductName.setText(slot.getProduct().getProductNameEn());
|
||||
break;
|
||||
case "ja":
|
||||
holder.binding.textProductName.setText(slot.getProduct().getProductNameJp());
|
||||
break;
|
||||
}
|
||||
holder.binding.textSlot.setText("貨道" + slot.getSlot());
|
||||
// 多國語的判斷(統一由 ProductStructure 處理,英/日未翻譯自動 fallback 中文)
|
||||
holder.binding.textProductName.setText(slot.getProduct().getLocalizedName(context));
|
||||
|
||||
int machinePrice = slot.getProduct().getMachinePrice();
|
||||
int sellingPrice = slot.getProduct().getSellingPrice();
|
||||
@ -103,19 +91,18 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
boolean isLock = slot.isLock();
|
||||
|
||||
int price = machinePrice > 0 ? machinePrice : sellingPrice;
|
||||
if (price > 0) {
|
||||
holder.binding.textPrice.setVisibility(View.VISIBLE);
|
||||
holder.binding.textPrice.setText("NT$ " + price);
|
||||
} else {
|
||||
holder.binding.textPrice.setVisibility(View.GONE);
|
||||
}
|
||||
holder.binding.textPrice.setVisibility(View.VISIBLE);
|
||||
holder.binding.textPrice.setText("NT$ " + price);
|
||||
|
||||
// 綁定庫存數量顯示
|
||||
holder.binding.textStock.setText("庫存" + count);
|
||||
|
||||
holder.binding.mask.setVisibility(View.GONE);
|
||||
holder.binding.textMask.setText("");
|
||||
|
||||
if (count <= 0) {
|
||||
holder.binding.mask.setVisibility(View.VISIBLE);
|
||||
holder.binding.textMask.setText("補貨中");// 已經售完
|
||||
holder.binding.textMask.setText("售完");
|
||||
}
|
||||
|
||||
if (isLock) {
|
||||
@ -127,8 +114,25 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
holder.binding.imageProductPicture.setFocusable(false);
|
||||
holder.binding.imageProductPicture.setFocusableInTouchMode(false);
|
||||
holder.binding.imageProductPicture.setOnClickListener(new imageProductPictureOnClickListener(position));
|
||||
// 整張卡片皆可點開商品詳情(不限圖片),避免點到名稱/價格區沒反應
|
||||
holder.binding.getRoot().setOnClickListener(new imageProductPictureOnClickListener(position));
|
||||
} else {
|
||||
holder.binding.imageProductPicture.setOnClickListener(null);
|
||||
holder.binding.getRoot().setOnClickListener(null);
|
||||
}
|
||||
|
||||
// 商品卡右下角「+」快速加購:僅購物車功能開啟時顯示;無購物車時隱藏(只能點商品開詳情頁)
|
||||
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
|
||||
if (cartOn && count > 0 && !isLock) {
|
||||
holder.binding.buttonQuickAdd.setVisibility(View.VISIBLE);
|
||||
holder.binding.buttonQuickAdd.setOnClickListener(v -> {
|
||||
if (MyApp.getInstance().isMachineBusy()) return;
|
||||
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
|
||||
com.blankj.utilcode.util.ToastUtils.showShort(R.string.cart_added);
|
||||
});
|
||||
} else {
|
||||
holder.binding.buttonQuickAdd.setVisibility(View.GONE);
|
||||
holder.binding.buttonQuickAdd.setOnClickListener(null);
|
||||
}
|
||||
|
||||
// devRunVMC(0);
|
||||
@ -177,6 +181,16 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
|
||||
// 查詢失敗時不阻擋,讓原流程繼續
|
||||
}
|
||||
|
||||
// flavor 路由:若該 flavor 自行消化此次點擊(如 General 多品項購物車「加入購物車」),
|
||||
// 則直接結束;否則走以下既有流程(晟崴員工卡取貨等),行為不變。
|
||||
if (fragment.getActivity() instanceof FontendActivity) {
|
||||
com.unibuy.smartdevice.ui.SaleFlowHandler handler =
|
||||
((FontendActivity) fragment.getActivity()).getSaleFlowHandler();
|
||||
if (handler != null && handler.handleProductSelected(slotProduct)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 點選時即時檢查出貨口狀態(上鎖防止重複點擊)
|
||||
isCheckingSlot = true;
|
||||
checkSlotHaveProduct(slotProduct.getSlot(), 1, () -> {
|
||||
|
||||
@ -19,10 +19,12 @@ import androidx.viewpager.widget.ViewPager;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.RecyclerVmcSlotListBinding;
|
||||
import com.unibuy.smartdevice.exception.Logs;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.ProductSearchActivity;
|
||||
import com.unibuy.smartdevice.ui.tools.ImageGlide;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -62,7 +64,7 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
SlotStructure slot = slotList.get(position);
|
||||
holder.binding.textSlot.setText(String.valueOf(slot.getSlot()));
|
||||
holder.binding.textSlot.setText("貨道" + slot.getSlot());
|
||||
String productID = slot.getProduct().getProductID();
|
||||
if (!productID.isEmpty() && !productID.equals("?")) {
|
||||
holder.binding.textProductName.setText(slot.getProduct().getProductName());
|
||||
@ -71,17 +73,54 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
|
||||
}
|
||||
holder.binding.textCount.setText(String.valueOf(slot.getCount()));
|
||||
holder.binding.editCount.setText(String.valueOf(slot.getCount()));
|
||||
// numberPassword 預設會遮罩輸入,關閉遮罩以顯示明碼數字
|
||||
holder.binding.editCount.setTransformationMethod(null);
|
||||
|
||||
if (slot.isLock()) {
|
||||
holder.binding.textLock.setText(context.getString(R.string.lock));
|
||||
holder.binding.textLock.setTextColor(Color.BLACK);
|
||||
holder.binding.textLock.setChecked(false);
|
||||
holder.binding.textLockStatus.setText("停售中");
|
||||
holder.binding.textLockStatus.setTextColor(androidx.core.content.ContextCompat.getColor(context, R.color.warning));
|
||||
holder.binding.textLock.setThumbTintList(android.content.res.ColorStateList.valueOf(androidx.core.content.ContextCompat.getColor(context, R.color.warning)));
|
||||
holder.binding.textLock.setTrackTintList(android.content.res.ColorStateList.valueOf(androidx.core.graphics.ColorUtils.setAlphaComponent(androidx.core.content.ContextCompat.getColor(context, R.color.warning), 77)));
|
||||
holder.binding.viewProductMask.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.binding.textLock.setText(context.getString(R.string.unlocked));
|
||||
holder.binding.textLock.setTextColor(Color.GRAY);
|
||||
holder.binding.textLock.setChecked(true);
|
||||
holder.binding.textLockStatus.setText("販售中");
|
||||
holder.binding.textLockStatus.setTextColor(androidx.core.content.ContextCompat.getColor(context, R.color.success));
|
||||
holder.binding.textLock.setThumbTintList(android.content.res.ColorStateList.valueOf(androidx.core.content.ContextCompat.getColor(context, R.color.success)));
|
||||
holder.binding.textLock.setTrackTintList(android.content.res.ColorStateList.valueOf(androidx.core.graphics.ColorUtils.setAlphaComponent(androidx.core.content.ContextCompat.getColor(context, R.color.success), 77)));
|
||||
holder.binding.viewProductMask.setVisibility(View.GONE);
|
||||
}
|
||||
holder.binding.textProductName.setOnClickListener(new selectPositionOnClickListener(position));
|
||||
holder.binding.textCount.setOnClickListener(new textCountOnClickListener(holder.binding, slot.getCount()));
|
||||
holder.binding.textLock.setOnClickListener(new textLockOnClickListener(position));
|
||||
|
||||
String productImg = slot.getProduct().getProductImg();
|
||||
if (productImg != null && !productImg.isEmpty() && !productImg.equals("?")) {
|
||||
try {
|
||||
new ImageGlide(context).fileload(productImg, holder.binding.imageProduct);
|
||||
} catch (LogsEmptyException e) {
|
||||
holder.binding.imageProduct.setImageResource(R.drawable.slot_no_product);
|
||||
}
|
||||
} else {
|
||||
holder.binding.imageProduct.setImageResource(R.drawable.slot_no_product);
|
||||
}
|
||||
|
||||
holder.binding.buttonCountMinus.setOnClickListener(v -> {
|
||||
int current = slot.getCount();
|
||||
if (current > 0) {
|
||||
slot.setCount(current - 1);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.binding.buttonCountPlus.setOnClickListener(v -> {
|
||||
int current = slot.getCount();
|
||||
slot.setCount(current + 1);
|
||||
notifyItemChanged(position);
|
||||
});
|
||||
|
||||
holder.binding.imageProduct.setOnClickListener(new selectPositionOnClickListener(position));
|
||||
holder.binding.textProductName.setOnClickListener(null);
|
||||
holder.binding.textCount.setOnClickListener(new textCountOnClickListener(holder.binding, slot.getCount()));
|
||||
holder.binding.textDelete.setOnClickListener(new textDeleteOnClickListener(position));
|
||||
holder.binding.editCount.setOnEditorActionListener(new editCountOnEditorActionListener(holder.binding, position));
|
||||
holder.binding.editCount.setOnClickListener(new editCountOnClickListener(holder.binding, slot.getCount()));
|
||||
@ -163,7 +202,9 @@ public class RecyclerVmcSlotListAdpter extends RecyclerView.Adapter<RecyclerVmcS
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
binding.textCount.setVisibility(View.GONE);
|
||||
// 改用 INVISIBLE 而非 GONE,避免 buttonCountPlus 的 constraint 因 textCount 折疊
|
||||
// 導致 editCount 可用寬度被壓縮成 0(看不到輸入框、IME 無法接受輸入)
|
||||
binding.textCount.setVisibility(View.INVISIBLE);
|
||||
binding.editCount.setVisibility(View.VISIBLE);
|
||||
if (this.count == 0) {
|
||||
binding.editCount.setText("");
|
||||
|
||||
@ -163,6 +163,11 @@ public class VmcFragment extends FragmentAbstract {
|
||||
binding.recyclerBuy.setAdapter(recyclerVmcBuyListAdpter);
|
||||
binding.recyclerBuy.setLayoutManager(new GridLayoutManager(getContext(), 3));
|
||||
|
||||
// pickup_sheet(原 Cmuh)模式:顯示大掃描單據按鈕、隱藏商品清單;其餘模式相反。
|
||||
boolean isPickupSheet = "pickup_sheet".equals(com.unibuy.smartdevice.MyApp.getInstance().getShoppingMode());
|
||||
binding.layoutScanBarcode.setVisibility(isPickupSheet ? View.VISIBLE : View.GONE);
|
||||
binding.recyclerBuy.setVisibility(isPickupSheet ? View.GONE : View.VISIBLE);
|
||||
|
||||
// for (int i = 0; i < MyApp.getInstance().getSlotList(SlotField.VMC.getField()).size(); i++) {
|
||||
// SlotStructure slot = MyApp.getInstance().getSlotList(SlotField.VMC.getField()).get(i);
|
||||
// if (!slot.getProduct().getProductID().isEmpty() && !slot.getProduct().getProductName().isEmpty() && slot.getProduct().getSellingPrice() > 0) {
|
||||
|
||||
@ -9,7 +9,7 @@ import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.unibuy.smartdevice.AppCompatActivityAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
@ -127,7 +127,7 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
|
||||
|
||||
recyclerVmcSlotListAdpter = new RecyclerVmcSlotListAdpter(this, resultLauncher, MyApp.getInstance().getSlotList(SlotField.VMC.getField()));
|
||||
binding.recyclerSlotList.setAdapter(recyclerVmcSlotListAdpter);
|
||||
binding.recyclerSlotList.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.recyclerSlotList.setLayoutManager(new GridLayoutManager(this, 3));
|
||||
|
||||
// dataShow();
|
||||
|
||||
@ -281,6 +281,9 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
|
||||
}
|
||||
|
||||
private String validateMaterialCodeConsistency() {
|
||||
if (!com.unibuy.smartdevice.AppEntry.isCmuh()) {
|
||||
return "";
|
||||
}
|
||||
for (SlotStructure slot : MyApp.getInstance().getSlotList(SlotField.VMC.getField())) {
|
||||
if (slot == null || slot.getProduct() == null) continue;
|
||||
ProductStructure product = slot.getProduct();
|
||||
|
||||
@ -10,6 +10,7 @@ import com.unibuy.smartdevice.databinding.DialogBuyBinding;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.DevSetStructure;
|
||||
import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
@ -17,7 +18,6 @@ import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import com.unibuy.smartdevice.ui.recycler.RecyclerDialogBuyListAdpter;
|
||||
import com.unibuy.smartdevice.utils.LanguageHelper;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -83,21 +83,7 @@ public class BuyDialog extends DialogAbstract {
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case SET_PRICE:
|
||||
String status = "";
|
||||
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
|
||||
switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
status = "("+context.getString(R.string.discounted)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountAmount()+")";
|
||||
break;
|
||||
case 2:
|
||||
status = "("+context.getString(R.string.discounted1)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountPercentage()+")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
binding.textPrice.setText(getTotalPrice()+status);
|
||||
refreshAmountBar();
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
case RESET_COUNTDOWM:
|
||||
@ -117,7 +103,6 @@ public class BuyDialog extends DialogAbstract {
|
||||
}
|
||||
|
||||
private DialogBuyBinding binding;
|
||||
private RecyclerDialogBuyListAdpter recyclerDialogBuyListAdpter;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
|
||||
public BuyDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
@ -132,35 +117,12 @@ public class BuyDialog extends DialogAbstract {
|
||||
// 讀取最後設定語言的設定
|
||||
// LanguageHelper.applySavedLanguage(context);
|
||||
|
||||
this.recyclerDialogBuyListAdpter = new RecyclerDialogBuyListAdpter(getCtx(), MyApp.getInstance().getBuyList(), getHandlerMain());
|
||||
this.binding.recyclerDialogBuyList.setAdapter(recyclerDialogBuyListAdpter);
|
||||
this.binding.recyclerDialogBuyList.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
MyApp.getInstance().initialReportData();
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
|
||||
MyApp.getInstance().getPostInvoiceByGreenData().setOrderId(MyApp.getInstance().getReportFlowInfoData().getOrderId());
|
||||
|
||||
this.binding.layoutPaymentTransactions.setVisibility(View.VISIBLE);
|
||||
this.binding.layoutVerificationPickup.setVisibility(View.GONE);
|
||||
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
|
||||
|
||||
this.binding.buttonPaymentTransactions.setOnClickListener(view -> {
|
||||
this.binding.layoutPaymentTransactions.setVisibility(View.VISIBLE);
|
||||
this.binding.layoutVerificationPickup.setVisibility(View.GONE);
|
||||
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
this.binding.buttonVerificationPickup.setOnClickListener(view -> {
|
||||
this.binding.layoutPaymentTransactions.setVisibility(View.GONE);
|
||||
this.binding.layoutVerificationPickup.setVisibility(View.VISIBLE);
|
||||
this.binding.layoutMemberTransactions.setVisibility(View.GONE);
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
this.binding.buttonMemberTransactions.setOnClickListener(view -> {
|
||||
|
||||
});
|
||||
|
||||
this.binding.buttonNfcCard.setOnClickListener(v -> {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(1);
|
||||
@ -207,171 +169,121 @@ public class BuyDialog extends DialogAbstract {
|
||||
gotoDialog();
|
||||
});
|
||||
|
||||
this.binding.buttonAddProduct.setOnClickListener(v -> gotoAddProduct());
|
||||
// 來店禮入口:獨立判斷,由雲端 B014 FunctionSet.WelcomeGift 下發控制(DevSet.isWelcomeGift)。
|
||||
// 與取貨模組無關;關閉時隱藏「來店禮」按鈕,避免進入 flowType 7。
|
||||
// 防呆:本筆已套用過來店禮(freeGiftId 非空)即隱藏入口,禁止重複掃描覆蓋/疊加折扣。
|
||||
boolean welcomeGiftApplied =
|
||||
!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty();
|
||||
this.binding.buttonFreeGift.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().isWelcomeGift() && !welcomeGiftApplied)
|
||||
? View.VISIBLE : View.GONE);
|
||||
|
||||
// 通行碼入口(付款格內 buttonAccessCode,flowType 5):權威判斷,涵蓋購物車/單品所有模式。
|
||||
// 從屬取貨模組,PickupModule 或 PassCode 任一關閉即隱藏;購物車模式下方不會再覆寫此值。
|
||||
this.binding.buttonAccessCode.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().isPickupModule()
|
||||
&& MyApp.getInstance().getDevSet().isPassCode()) ? View.VISIBLE : View.GONE);
|
||||
|
||||
this.binding.buttonCancel.setOnClickListener(v -> dialogCancel());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
String status = "";
|
||||
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
|
||||
switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
status = "("+context.getString(R.string.discounted)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountAmount()+")";
|
||||
break;
|
||||
case 2:
|
||||
status = "("+context.getString(R.string.discounted1)+MyApp.getInstance().getReportFreeGiftCodeStructure().getDiscountPercentage()+")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
binding.textPrice.setText(getTotalPrice()+status);
|
||||
refreshAmountBar();
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
|
||||
binding.buttonAddProduct.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonAddProduct.setVisibility(View.GONE);
|
||||
}
|
||||
// 刷卡機(母 isDevNFCPay)三子項:各自 母 && 子 判斷,母關則三者全隱藏。
|
||||
// 信用卡=NfcCard(canCreditCard)、電子票證/卡片=NfcRecharge(canCardPay)、手機=NfcPhone(canMobilePay)
|
||||
binding.buttonNfcCard.setVisibility(MyApp.getInstance().getDevSet().canCreditCard() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonNfcRecharge.setVisibility(MyApp.getInstance().getDevSet().canCardPay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonNfcPhone.setVisibility(MyApp.getInstance().getDevSet().canMobilePay() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
|
||||
if (MyApp.getInstance().getDevSet().isDevNFCPay()) {
|
||||
// binding.layoutNfcPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// binding.layoutNfcPay.setVisibility(View.GONE);
|
||||
}
|
||||
// 掃碼(母 isDevScanPay)子項:玉山 / TapPay,母關則隱藏。
|
||||
binding.buttonEsunPay.setVisibility(MyApp.getInstance().getDevSet().canEsunPay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonTapPay.setVisibility(MyApp.getInstance().getDevSet().canTapPay() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevEsunPay()) {
|
||||
binding.buttonEsunPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonEsunPay.setVisibility(View.GONE);
|
||||
}
|
||||
// Line 官方支付:獨立大項,不從屬掃碼。
|
||||
binding.buttonLinePay.setVisibility(MyApp.getInstance().getDevSet().canLinePay() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevLinePay()) {
|
||||
binding.buttonLinePay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonLinePay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevCash()) {
|
||||
binding.buttonCash.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonCash.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevTapPay()) {
|
||||
binding.buttonTapPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonTapPay.setVisibility(View.GONE);
|
||||
}
|
||||
// 現金:獨立大項。
|
||||
binding.buttonCash.setVisibility(MyApp.getInstance().getDevSet().canCash() ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
BuyStructure buyData = MyApp.getInstance().getBuyList().get(0);
|
||||
|
||||
try {
|
||||
SlotField slotField = SlotField.getSlotField(buyData.getField());
|
||||
// 貨道層級是否支援 NFC(VMC/ELECTRIC 不同);三個刷卡機子項共用這個貨道旗標。
|
||||
boolean slotNfc = slotField.isDevNFCPay();
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevNFCPay()) {
|
||||
if (slotField.isDevNFCPay()) {
|
||||
getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
|
||||
// binding.layoutNfcPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// binding.layoutNfcPay.setVisibility(View.GONE);
|
||||
}
|
||||
// 刷卡機開啟:三子項各自 (母&&子) 且貨道支援 NFC 才顯示。
|
||||
binding.buttonNfcCard.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canCreditCard() && slotNfc) ? View.VISIBLE : View.GONE);
|
||||
binding.buttonNfcRecharge.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canCardPay() && slotNfc) ? View.VISIBLE : View.GONE);
|
||||
binding.buttonNfcPhone.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canMobilePay() && slotNfc) ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
getLogs().info("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB2");
|
||||
binding.buttonPaymentTransactions.setVisibility(View.GONE);
|
||||
// 刷卡機關閉:三子項全隱藏,並啟用通行碼備援入口(仍須從屬取貨模組且通行碼開啟,
|
||||
// 否則 NFC 不支援時會把已關閉的通行碼漏出來)。
|
||||
binding.buttonNfcCard.setVisibility(View.GONE);
|
||||
binding.buttonNfcPhone.setVisibility(View.GONE);
|
||||
binding.buttonNfcRecharge.setVisibility(View.GONE);
|
||||
binding.buttonAccessCode.setVisibility(View.VISIBLE);
|
||||
binding.buttonVerificationPickup.setVisibility(View.GONE);
|
||||
binding.layoutVerificationPickup.setVisibility(View.VISIBLE);
|
||||
// binding.layoutNfcPay.setVisibility(View.GONE);
|
||||
boolean passCodeOn = MyApp.getInstance().getDevSet().isPickupModule()
|
||||
&& MyApp.getInstance().getDevSet().isPassCode();
|
||||
binding.buttonAccessCode.setVisibility(passCodeOn ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevEsunPay()) {
|
||||
if (slotField.isDevEsunpay()) {
|
||||
binding.buttonEsunPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonEsunPay.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.buttonEsunPay.setVisibility(View.GONE);
|
||||
}
|
||||
// 掃碼子項:母(isDevScanPay)&&子 且貨道支援。
|
||||
binding.buttonEsunPay.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canEsunPay() && slotField.isDevEsunpay()) ? View.VISIBLE : View.GONE);
|
||||
binding.buttonTapPay.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canTapPay() && slotField.isDevTapPay()) ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevLinePay()) {
|
||||
if (slotField.isDevLinePay()) {
|
||||
binding.buttonLinePay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonLinePay.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.buttonLinePay.setVisibility(View.GONE);
|
||||
}
|
||||
// Line 官方支付:獨立大項,僅貨道支援與自身開關。
|
||||
binding.buttonLinePay.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canLinePay() && slotField.isDevLinePay()) ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevTapPay()) {
|
||||
if (slotField.isDevTapPay()) {
|
||||
binding.buttonTapPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonTapPay.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.buttonTapPay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevCash()) {
|
||||
if (slotField.isDevCash()) {
|
||||
binding.buttonCash.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonCash.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
binding.buttonCash.setVisibility(View.GONE);
|
||||
}
|
||||
// 現金:獨立大項。
|
||||
binding.buttonCash.setVisibility(
|
||||
(MyApp.getInstance().getDevSet().canCash() && slotField.isDevCash()) ? View.VISIBLE : View.GONE);
|
||||
|
||||
} catch (LogsEmptyException e) {
|
||||
if (MyApp.getInstance().getDevSet().isDevNFCPay()) {
|
||||
// binding.layoutNfcPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// binding.layoutNfcPay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevEsunPay()) {
|
||||
binding.buttonEsunPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonEsunPay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevLinePay()) {
|
||||
binding.buttonLinePay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonLinePay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevTapPay()) {
|
||||
binding.buttonTapPay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonTapPay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (MyApp.getInstance().getDevSet().isDevCash()) {
|
||||
binding.buttonCash.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.buttonCash.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 無貨道資訊:略過貨道層級限制,只依「母&&子」開關判斷。
|
||||
binding.buttonNfcCard.setVisibility(MyApp.getInstance().getDevSet().canCreditCard() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonNfcRecharge.setVisibility(MyApp.getInstance().getDevSet().canCardPay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonNfcPhone.setVisibility(MyApp.getInstance().getDevSet().canMobilePay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonEsunPay.setVisibility(MyApp.getInstance().getDevSet().canEsunPay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonTapPay.setVisibility(MyApp.getInstance().getDevSet().canTapPay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonLinePay.setVisibility(MyApp.getInstance().getDevSet().canLinePay() ? View.VISIBLE : View.GONE);
|
||||
binding.buttonCash.setVisibility(MyApp.getInstance().getDevSet().canCash() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
/** 商品原始總額(未套用來店禮折扣),不含免費商品。 */
|
||||
public int getOriginalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().isEmpty()) {
|
||||
return totalPrice;
|
||||
}
|
||||
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
|
||||
if (!buy.isFreeGift()) totalPrice += buy.getFullPrice();
|
||||
}
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
/** 商品總件數(不含免費商品),用於支付頁顯示「共 X 件」。 */
|
||||
public int getTotalCount() {
|
||||
int count = 0;
|
||||
for(BuyStructure buy: MyApp.getInstance().getBuyList()) {
|
||||
if (!buy.isFreeGift()) count += buy.getCount();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
if (MyApp.getInstance().getBuyList().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int totalPrice = getOriginalPrice();
|
||||
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
|
||||
switch (MyApp.getInstance().getReportFreeGiftCodeStructure().getMode()) {
|
||||
case 0:
|
||||
@ -384,55 +296,75 @@ public class BuyDialog extends DialogAbstract {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (totalPrice == 0) totalPrice = 1;
|
||||
if (totalPrice < 1) totalPrice = 1; // 折抵後至少 1 元(避免 0 元交易)
|
||||
return totalPrice;
|
||||
}
|
||||
|
||||
public void gotoAddProduct() {
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.ADD_PRODUCT.getOption(), "@請40秒內完成加購動作");
|
||||
closeDialogCountdown.shutdown();
|
||||
cancel();
|
||||
/**
|
||||
* 刷新支付頁上方「件數 / 應付金額」列。
|
||||
* 金額已含來店禮折抵;有套用來店禮時於後方標示實際折抵金額(折抵/打折統一以省下的錢顯示)。
|
||||
*/
|
||||
private void refreshAmountBar() {
|
||||
binding.textCount.setText(getContext().getString(R.string.pay_item_count, getTotalCount()));
|
||||
|
||||
int payable = getTotalPrice();
|
||||
String priceText = "NT$" + payable;
|
||||
android.text.SpannableStringBuilder spannable = new android.text.SpannableStringBuilder(priceText);
|
||||
spannable.setSpan(
|
||||
new android.text.style.ForegroundColorSpan(androidx.core.content.ContextCompat.getColor(getContext(), R.color.primary)),
|
||||
0,
|
||||
priceText.length(),
|
||||
android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
|
||||
if (!MyApp.getInstance().getReportFreeGiftCodeStructure().getFreeGiftId().isEmpty()) {
|
||||
int saved = getOriginalPrice() - payable;
|
||||
if (saved > 0) {
|
||||
String discountPart = " (" + getContext().getString(R.string.pay_discounted_format, saved) + ")";
|
||||
int start = spannable.length();
|
||||
spannable.append(discountPart);
|
||||
spannable.setSpan(
|
||||
new android.text.style.ForegroundColorSpan(androidx.core.content.ContextCompat.getColor(getContext(), R.color.success)),
|
||||
start,
|
||||
spannable.length(),
|
||||
android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
binding.textPrice.setText(spannable);
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
|
||||
closeDialogCountdown.shutdown();
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
if (buyListSize > 0) {
|
||||
recyclerDialogBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
if (buyListSize > 0) {
|
||||
recyclerDialogBuyListAdpter.notifyItemRangeRemoved(0, buyListSize);
|
||||
}
|
||||
getTools().dialogClose();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText(getContext().getString(R.string.cancel)+"("+countdown+")");//取消
|
||||
binding.buttonCancel.setText(getContext().getString(R.string.cancel) + "(" + countdown + "秒)");//取消
|
||||
}
|
||||
|
||||
public void gotoDialog() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
getLogs().info("aaaaaaaaaaaaaaaaaaaaaaaa"+flowType);
|
||||
getLogs().debug("buy flowType:" + flowType);
|
||||
// if (flowType == 5 || flowType == 7 || flowType == 30) {
|
||||
if (flowType == 5 || flowType == 7) {
|
||||
changeCheckout();
|
||||
} else {
|
||||
if (MyApp.getInstance().getDevSet().isShoppingCar()) {
|
||||
if (MyApp.getInstance().getDevSet().isInvoice()) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
changeDialog(InvoiceTypeDialog.class);
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
@ -442,12 +374,12 @@ public class BuyDialog extends DialogAbstract {
|
||||
try {
|
||||
SlotField slotField = SlotField.getSlotField(buyData.getField());
|
||||
if (slotField.isInvoice()) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
changeDialog(InvoiceTypeDialog.class);
|
||||
} else {
|
||||
changeCheckout();
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
changeDialog(InvoiceTypeDialog.class);
|
||||
}
|
||||
} else {
|
||||
changeCheckout();
|
||||
@ -459,6 +391,28 @@ public class BuyDialog extends DialogAbstract {
|
||||
public void changeCheckout() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
// 入口防呆:即使按鈕被繞過(殘留事件/外部觸發),進入支付 dialog 前再以同一份「母&&子」
|
||||
// 開關權威判斷一次;母開關或子項任一關閉即擋下,避免已停用的支付方式漏出。
|
||||
if (!isPaymentFlowAllowed(flowType)) {
|
||||
getLogs().info("⛔ 支付方式已停用,封堵入口 flowType=" + flowType);
|
||||
dialogCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// 進入付款:先清掉上一筆殘留的 MQTT 交易流水號,避免被本筆誤用(flow_id 撞單)。
|
||||
com.unibuy.smartdevice.structure.ReportFlowInfoStructure flowInfo =
|
||||
MyApp.getInstance().getReportFlowInfoData();
|
||||
flowInfo.setMqttFlowId("");
|
||||
// basic 購物模式(本專案 general/demo) 且為實際付款方式(1/2/3/4/9/30/70) 時:
|
||||
// 為本筆生成新流水號,並上報 pending(「進付款一律留底」)。pending → completed/failed 全程共用此 flow_id。
|
||||
// 通行碼(5)/來店禮(7) 非付款,不在此處理;employee_card(晟崴)/pickup_sheet(中國醫) 無付款不送,後台資料不受影響。
|
||||
boolean realPayment = (flowType == 1 || flowType == 2 || flowType == 3 || flowType == 4
|
||||
|| flowType == 9 || flowType == 30 || flowType == 70);
|
||||
if ("basic".equals(MyApp.getInstance().getShoppingMode()) && realPayment) {
|
||||
flowInfo.setMqttFlowId(com.unibuy.smartdevice.tools.FlowIdGenerator.next(MyApp.getInstance()));
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishPending();
|
||||
}
|
||||
|
||||
switch (flowType) {
|
||||
case 1:
|
||||
changeDialog(CheckoutNfcCardDialog.class);
|
||||
@ -479,7 +433,7 @@ public class BuyDialog extends DialogAbstract {
|
||||
changeDialog(CheckoutFreeGiftCodeDialog.class);
|
||||
break;
|
||||
case 9:
|
||||
changeDialog(CashPayDialog.class);
|
||||
changeDialog(cashDialogClass());
|
||||
break;
|
||||
case 30:
|
||||
changeDialog(CheckoutTapPayDialog.class);
|
||||
@ -490,6 +444,24 @@ public class BuyDialog extends DialogAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口防呆:依 flowType 對應的支付方式,判斷其「母&&子」開關是否允許。
|
||||
* flowType 5(通行碼)、7(來店禮)非支付方式,由取貨模組/來店禮各自把關,此處放行。
|
||||
*/
|
||||
private boolean isPaymentFlowAllowed(int flowType) {
|
||||
DevSetStructure d = MyApp.getInstance().getDevSet();
|
||||
switch (flowType) {
|
||||
case 1: return d.canCreditCard(); // 信用卡(刷卡機)
|
||||
case 2: return d.canCardPay(); // 電子票證/卡片支付(刷卡機)
|
||||
case 4: return d.canMobilePay(); // 手機支付(刷卡機)
|
||||
case 3: return d.canEsunPay(); // 玉山(掃碼)
|
||||
case 30: return d.canTapPay(); // TapPay(掃碼)
|
||||
case 70: return d.canLinePay(); // Line 官方支付(獨立)
|
||||
case 9: return d.canCash(); // 現金(獨立)
|
||||
default: return true; // 5/7 等非支付流程放行
|
||||
}
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
|
||||
@ -81,7 +81,6 @@ public class BuyPickupCodeDialog extends DialogAbstract {
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case SET_PRICE:
|
||||
binding.textPrice.setText(String.valueOf(getTotalPrice()));
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
break;
|
||||
case RESET_COUNTDOWM:
|
||||
@ -130,7 +129,6 @@ public class BuyPickupCodeDialog extends DialogAbstract {
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
binding.textPrice.setText(String.valueOf(getTotalPrice()));
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
File diff suppressed because it is too large
Load Diff
@ -46,6 +46,7 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
CHANGE_DISPATCH(6),
|
||||
CHANGE_BUY(7),
|
||||
SET_STATUS_TEXT(8),
|
||||
SET_SCAN_INPUT(9),
|
||||
;
|
||||
|
||||
private int option;
|
||||
@ -113,6 +114,11 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
case SET_STATUS_TEXT:
|
||||
setStatusText(message);
|
||||
break;
|
||||
case SET_SCAN_INPUT:
|
||||
// 掃碼即時回饋:把掃到的內容顯示在輸入框,讓使用者確認有掃到
|
||||
binding.editTextNumber.setText(message);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,6 +144,18 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
openBarcode = true;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
|
||||
setInputMode(true);
|
||||
|
||||
binding.btnModeKeypad.setOnClickListener(v -> {
|
||||
setInputMode(true);
|
||||
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.btnModeScan.setOnClickListener(v -> {
|
||||
setInputMode(false);
|
||||
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.buttonFinish.setOnClickListener(v -> changeOk());
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
@ -241,7 +259,8 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
|
||||
// 立即開始聽掃描器(原本延遲 5 秒,導致對話框剛彈出時掃碼被整個丟掉 → 時好時壞、無回饋)
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
|
||||
@ -265,19 +284,14 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
usbDev.read(buffer, 100);
|
||||
for (int i=0; i<7; i++) {
|
||||
for (int i=0; i<9; i++) { // 約 45 秒讀取窗口,覆蓋 40 秒對話框存活時間
|
||||
if (isRun() && openBarcode) {
|
||||
int size = usbDev.read(buffer, 5000);
|
||||
if (size > 5) {
|
||||
String barCodeString = new String(buffer, 0, size);
|
||||
getHandlerMain().start(getLogs().getClassName(), getContext().getString(R.string.receive_barcode)+":"+ Tools.randomReplace(barCodeString, 0.3)+","+getContext().getString(R.string.send_transaction));//收到條碼,送出交易
|
||||
|
||||
try {
|
||||
MyApp.getInstance().getAccessCodeStructure().setPassCode(barCodeString);
|
||||
httpAPI.checkAccessCode(MyApp.getInstance().getAccessCodeStructure());
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
String barCodeString = new String(buffer, 0, size).trim();
|
||||
// 掃碼即時回饋:先把掃到的內容顯示在輸入框,再送出驗證
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
|
||||
submitPassCode(barCodeString);
|
||||
|
||||
openBarcode = false;
|
||||
}
|
||||
@ -295,6 +309,14 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
private void setInputMode(boolean isKeyboard) {
|
||||
binding.btnModeKeypad.setSelected(isKeyboard);
|
||||
binding.btnModeScan.setSelected(!isKeyboard);
|
||||
|
||||
binding.linearNumpad.setVisibility(isKeyboard ? android.view.View.VISIBLE : android.view.View.GONE);
|
||||
binding.imageViewScanHint.setVisibility(isKeyboard ? android.view.View.GONE : android.view.View.VISIBLE);
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
|
||||
@ -307,26 +329,70 @@ public class CheckoutAccessCodeDialog extends DialogAbstract {
|
||||
public void changeDispatch() {
|
||||
if (isChangeDispatch) {
|
||||
closeDialogCountdown.shutdown();
|
||||
// 進入出貨前,務必關閉 FontendActivity 的「加購倒數」(AddProductOnCountdown)。
|
||||
// 否則該 40 秒倒數會在出貨中途到期 → 自動發 BUY_DIALOG_OPEN → 支付畫面疊在出貨畫面上跳出。
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(),
|
||||
FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
|
||||
isChangeDispatch = false;
|
||||
changeDialog(DispatchDialog.class);
|
||||
}
|
||||
}
|
||||
|
||||
private long lastChangeOkClickTime = 0;
|
||||
|
||||
public void changeOk() {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
// 長度檢查
|
||||
if (number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
|
||||
// 防連點:3 秒內重複點擊忽略(自 Luzui 移植)
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastChangeOkClickTime < 3000) {
|
||||
return;
|
||||
}
|
||||
MyApp.getInstance().getAccessCodeStructure().setPassCode(number);
|
||||
httpAPI.checkAccessCode(MyApp.getInstance().getAccessCodeStructure());
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
lastChangeOkClickTime = now;
|
||||
|
||||
String number = binding.editTextNumber.getText().toString().trim();
|
||||
// 長度檢查
|
||||
if (number.isEmpty() || number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "通行碼長度不能少於 8 碼");
|
||||
return;
|
||||
}
|
||||
submitPassCode(number);
|
||||
} catch (Exception e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通行碼驗證改走 StarCloudAPI B670(邏輯比照員工卡 B680)。
|
||||
* 成功後保存 code_id 供 MQTT finalize(payment_type=5 通行碼),再進入出貨。
|
||||
*/
|
||||
private void submitPassCode(String code) {
|
||||
// 防護:通行碼從屬「取貨模組」,任一關閉即不開放呼叫 B670(即使 UI 漏出入口也擋下)
|
||||
if (!(MyApp.getInstance().getDevSet().isPickupModule() && MyApp.getInstance().getDevSet().isPassCode())) {
|
||||
openBarcode = true;
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "通行碼功能未開放");
|
||||
return;
|
||||
}
|
||||
// 送出前顯示「驗證中…」,避免使用者以為沒反應
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
|
||||
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
|
||||
.verifyPassCode(code, new com.unibuy.smartdevice.external.StarCloudAPI.VerificationCallback() {
|
||||
@Override
|
||||
public void onSuccess(String codeId) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(5); // 通行碼 → payment_type 5
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setAccessCodeId(codeId);
|
||||
getHandlerMain().send(getClass().getSimpleName(), Option.CHANGE_DISPATCH.getOption(), "change dispatch");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String message) {
|
||||
openBarcode = true; // 允許重新掃碼/輸入
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void changeBuy() {
|
||||
closeDialogCountdown.shutdown();
|
||||
changeDialog(BuyDialog.class);
|
||||
|
||||
@ -100,25 +100,22 @@ public class CheckoutEsunPayDialog extends DialogAbstract {
|
||||
case TRANSACTION_SUCCESSFUL:
|
||||
if (isTransaction) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo);
|
||||
// 直接進出貨流程,由 DispatchDialog 以 MQTT Finalize 統一上報結案(含發票/扣庫存)。
|
||||
// payment_type=3(掃碼支付)已於 BuyDialog 設定。
|
||||
isTransaction = false;
|
||||
changeDispatch();
|
||||
}
|
||||
break;
|
||||
case TRANSACTION_FAILED:
|
||||
if (isTransaction) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 交易失敗不再經 B600;直接提示並返回購買頁,不進出貨。
|
||||
// [交易生命週期] 掃碼支付失敗:上報 failed(flag 開才送),救回原本默默消失的失敗單。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
|
||||
isTransaction = false;
|
||||
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
|
||||
changeBuy();
|
||||
}
|
||||
break;
|
||||
case CHANGE_DISPATCH:
|
||||
|
||||
@ -39,6 +39,7 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
CHANGE_DISPATCH(6),
|
||||
CHANGE_BUY(7),
|
||||
SET_STATUS_TEXT(8),
|
||||
SET_SCAN_INPUT(9),
|
||||
;
|
||||
|
||||
private int option;
|
||||
@ -106,6 +107,11 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
case SET_STATUS_TEXT:
|
||||
setStatusText(message);
|
||||
break;
|
||||
case SET_SCAN_INPUT:
|
||||
// 掃碼即時回饋:把掃到的內容顯示在輸入框,讓使用者確認有掃到
|
||||
binding.editTextNumber.setText(message);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,6 +137,18 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
openBarcode = true;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
|
||||
setInputMode(true);
|
||||
|
||||
binding.btnModeKeypad.setOnClickListener(v -> {
|
||||
setInputMode(true);
|
||||
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.btnModeScan.setOnClickListener(v -> {
|
||||
setInputMode(false);
|
||||
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.buttonFinish.setOnClickListener(v -> changeOk());
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
@ -232,7 +250,8 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
|
||||
// 立即開始聽掃描器(原本延遲 5 秒,導致對話框剛彈出時掃碼被整個丟掉 → 時好時壞、無回饋)
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
|
||||
@ -256,20 +275,14 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
usbDev.read(buffer, 100);
|
||||
for (int i=0; i<7; i++) {
|
||||
for (int i=0; i<9; i++) { // 約 45 秒讀取窗口,覆蓋 40 秒對話框存活時間
|
||||
if (isRun() && openBarcode) {
|
||||
int size = usbDev.read(buffer, 5000);
|
||||
if (size > 5) {
|
||||
String barCodeString = new String(buffer, 0, size);
|
||||
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+":"+ Tools.randomReplace(barCodeString, 0.3)+","+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
|
||||
|
||||
try {
|
||||
MyApp.getInstance().getFreeGiftStructure().setPassCode(barCodeString);
|
||||
httpAPI.freeGiftCode(MyApp.getInstance().getFreeGiftStructure());
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
|
||||
String barCodeString = new String(buffer, 0, size).trim();
|
||||
// 掃碼即時回饋:先把掃到的內容顯示在輸入框,再送出驗證
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
|
||||
submitWelcomeGift(barCodeString);
|
||||
openBarcode = false;
|
||||
}
|
||||
}
|
||||
@ -286,6 +299,14 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
private void setInputMode(boolean isKeyboard) {
|
||||
binding.btnModeKeypad.setSelected(isKeyboard);
|
||||
binding.btnModeScan.setSelected(!isKeyboard);
|
||||
|
||||
binding.linearNumpad.setVisibility(isKeyboard ? android.view.View.VISIBLE : android.view.View.GONE);
|
||||
binding.imageViewScanHint.setVisibility(isKeyboard ? android.view.View.GONE : android.view.View.VISIBLE);
|
||||
}
|
||||
|
||||
public int getTotalPrice() {
|
||||
int totalPrice = 0;
|
||||
if (MyApp.getInstance().getBuyList().size() == 0) return totalPrice;
|
||||
@ -304,15 +325,57 @@ public class CheckoutFreeGiftCodeDialog extends DialogAbstract {
|
||||
}
|
||||
|
||||
public void changeOk() {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
MyApp.getInstance().getFreeGiftStructure().setPassCode(number);
|
||||
httpAPI.freeGiftCode(MyApp.getInstance().getFreeGiftStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
String number = binding.editTextNumber.getText().toString().trim();
|
||||
if (number.isEmpty() || number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "來店禮代碼需 8 碼");
|
||||
return;
|
||||
}
|
||||
submitWelcomeGift(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* 來店禮驗證改走 StarCloudAPI B690(取代舊 HttpAPI.freeGiftCode)。
|
||||
* B690 只回折扣(amount/percentage),無「免費商品」型;成功即套折扣並返回 BuyDialog 結帳。
|
||||
*/
|
||||
private void submitWelcomeGift(String code) {
|
||||
// 防護:來店禮關閉即不開放呼叫 B690(即使 UI 漏出入口也擋下)
|
||||
if (!MyApp.getInstance().getDevSet().isWelcomeGift()) {
|
||||
openBarcode = true;
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "來店禮功能未開放");
|
||||
return;
|
||||
}
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
|
||||
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
|
||||
.verifyWelcomeGift(code, new com.unibuy.smartdevice.external.StarCloudAPI.WelcomeGiftCallback() {
|
||||
@Override
|
||||
public void onSuccess(String codeId, String discountType, int discountValue, String discountLabel, String name) {
|
||||
// 映射到既有折扣機制:mode 1=折抵金額、mode 2=折扣百分比(乘數)。
|
||||
MyApp.getInstance().getReportFreeGiftCodeStructure().setFreeGiftId(codeId);
|
||||
MyApp.getInstance().getReportFreeGiftCodeStructure().setPassCode(code);
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
|
||||
if ("amount".equals(discountType)) {
|
||||
MyApp.getInstance().getReportFreeGiftCodeStructure().setMode(1);
|
||||
MyApp.getInstance().getReportFreeGiftCodeStructure().setDiscountAmount(discountValue);
|
||||
} else if ("percentage".equals(discountType)) {
|
||||
MyApp.getInstance().getReportFreeGiftCodeStructure().setMode(2);
|
||||
// discountValue 為折扣趴數(八折=20)→ 換算成總價乘數 0.8
|
||||
MyApp.getInstance().getReportFreeGiftCodeStructure()
|
||||
.setDiscountPercentage((100 - discountValue) / 100f);
|
||||
} else {
|
||||
openBarcode = true;
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "不支援的折扣類型");
|
||||
return;
|
||||
}
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.CHANGE_BUY.getOption(), "change buy");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String message) {
|
||||
openBarcode = true; // 允許重新掃碼/輸入
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void changeBuy() {
|
||||
|
||||
@ -101,25 +101,22 @@ public class CheckoutLinePayDialog extends DialogAbstract {
|
||||
case TRANSACTION_SUCCESSFUL:
|
||||
if (isTransaction) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo);
|
||||
// 直接進出貨流程,由 DispatchDialog 以 MQTT Finalize 統一上報結案(含發票/扣庫存)。
|
||||
// payment_type=70(LINE Pay 官方)已於 BuyDialog 設定。
|
||||
isTransaction = false;
|
||||
changeDispatch();
|
||||
}
|
||||
break;
|
||||
case TRANSACTION_FAILED:
|
||||
if (isTransaction) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
// exception(context, e.getErrorCode(), e.getLocalizedMessage());
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 交易失敗不再經 B600;直接提示並返回購買頁,不進出貨。
|
||||
// [交易生命週期] LINE Pay 失敗:上報 failed(flag 開才送),救回原本默默消失的失敗單。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
|
||||
isTransaction = false;
|
||||
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
|
||||
changeBuy();
|
||||
}
|
||||
break;
|
||||
case CHANGE_DISPATCH:
|
||||
|
||||
@ -109,18 +109,21 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
|
||||
if (message.equals("0000")) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo);
|
||||
// 直接進出貨流程,由 DispatchDialog 以 MQTT Finalize 統一上報結案(含發票/扣庫存)。
|
||||
// payment_type=1(信用卡)已於 BuyDialog 設定。
|
||||
changeDispatch();
|
||||
} else {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
// [交易生命週期] 刷卡機回非 0000:上報 failed(帶代碼),救回原本默默消失的失敗單(flag 開才送)。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
|
||||
}
|
||||
} else {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
// [交易生命週期] 刷卡機回空電文:視為失敗,上報 failed(flag 開才送)。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
|
||||
}
|
||||
|
||||
// binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
|
||||
@ -108,18 +108,24 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
|
||||
if (message.equals("0000")) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// NfcPhone(手機支付):刷卡機 NCCC 電文無法區分手機錢包/實體卡,
|
||||
// 依使用者所選付款鈕歸類為 payment_type=10(手機支付)。
|
||||
// 路由鍵 flowType=4 僅用於 BuyDialog 開啟本 Dialog,這裡覆寫為實際上報值。
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(10);
|
||||
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo);
|
||||
// 直接進出貨流程,由 DispatchDialog 以 MQTT Finalize 統一上報結案(含發票/扣庫存)。
|
||||
changeDispatch();
|
||||
} else {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
// [交易生命週期] 刷卡機回非 0000:上報 failed(帶代碼),救回原本默默消失的失敗單(flag 開才送)。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
|
||||
}
|
||||
} else {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
// [交易生命週期] 刷卡機回空電文:視為失敗,上報 failed(flag 開才送)。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
|
||||
}
|
||||
|
||||
// binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
|
||||
@ -108,18 +108,21 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
|
||||
if (message.equals("0000")) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo);
|
||||
// 直接進出貨流程,由 DispatchDialog 以 MQTT Finalize 統一上報結案(含發票/扣庫存)。
|
||||
// payment_type=2(悠遊卡/一卡通)已於 BuyDialog 設定。
|
||||
changeDispatch();
|
||||
} else {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
// [交易生命週期] 刷卡機回非 0000:上報 failed(帶代碼),救回原本默默消失的失敗單(flag 開才送)。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
|
||||
}
|
||||
} else {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
// [交易生命週期] 刷卡機回空電文:視為失敗,上報 failed(flag 開才送)。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
|
||||
}
|
||||
|
||||
// binding.textStatus.setText(DevNFCPay.recordMessage(message));
|
||||
|
||||
@ -123,22 +123,21 @@ public class CheckoutTapPayDialog extends DialogAbstract {
|
||||
if (isTransaction) {
|
||||
isTransaction = false; // 🚨 馬上關閉,避免送出第二筆
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(1);
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 支付成功不再呼叫舊 B600(reportFlowInfo);
|
||||
// 直接進出貨流程,由 DispatchDialog 以 MQTT Finalize 統一上報結案(含發票/扣庫存)。
|
||||
// payment_type=30~34(TapPay 子項)已於本 Dialog 選擇時設定。
|
||||
changeDispatch();
|
||||
}
|
||||
break;
|
||||
case TRANSACTION_FAILED:
|
||||
if (isTransaction) {
|
||||
isTransaction = false; // 🚨 馬上關閉
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
|
||||
try {
|
||||
httpAPI.reportFlowInfo(MyApp.getInstance().getReportFlowInfoData());
|
||||
} catch (LogsSettingEmptyException | LogsParseException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
// [MQTT Finalize 重構] 交易失敗不再經 B600;直接提示並返回購買頁,不進出貨。
|
||||
// [交易生命週期] 掃碼支付失敗:上報 failed(flag 開才送),救回原本默默消失的失敗單。
|
||||
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
|
||||
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
|
||||
changeBuy();
|
||||
}
|
||||
break;
|
||||
case TRANSACTION_AGAIN:
|
||||
|
||||
@ -0,0 +1,220 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.blankj.utilcode.util.ToastUtils;
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.DialogProductDetailBinding;
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import com.unibuy.smartdevice.ui.SaleFlowHandler;
|
||||
import com.unibuy.smartdevice.ui.tools.ImageGlide;
|
||||
import com.unibuy.v2.UnibuyHelper;
|
||||
|
||||
/**
|
||||
* 商品詳情對話框。
|
||||
* 點選商品後一律先彈出此頁,依雲端 B014 DevSet.ShoppingCar 決定按鈕:
|
||||
* - 購物車開啟:顯示「數量調整 + 加入購物車 + 直接購買」。
|
||||
* - 購物車關閉(一般版):隱藏「數量調整 + 加入購物車」,只留「直接購買」(數量固定 1)。
|
||||
* 「直接購買」沿用既有單品直購流程(清空 buyList → BuyDialog 選付款)。
|
||||
*/
|
||||
public class DialogProductDetail extends DialogAbstract {
|
||||
|
||||
private DialogProductDetailBinding binding;
|
||||
private final SlotStructure slot;
|
||||
// 建構子傳入的原始 Context(真正的 FontendActivity)。
|
||||
// 不可用 onCreate 的 getContext(),那是 AlertDialog 包裝過的 ContextThemeWrapper,
|
||||
// instanceof FontendActivity 會是 false,導致購物車頁/結帳入口取不到 SaleFlowHandler。
|
||||
private final Context hostContext;
|
||||
private int quantity = 1;
|
||||
|
||||
public DialogProductDetail(Context context, HandlerMain srcHandlerMain, SlotStructure slot) {
|
||||
super(context, srcHandlerMain);
|
||||
this.hostContext = context;
|
||||
if (slot == null || slot.getProduct() == null) {
|
||||
this.slot = createMockSlot();
|
||||
} else {
|
||||
this.slot = slot;
|
||||
}
|
||||
}
|
||||
|
||||
private SlotStructure createMockSlot() {
|
||||
com.unibuy.smartdevice.structure.ProductStructure mockProduct = new com.unibuy.smartdevice.structure.ProductStructure(
|
||||
"mock_001",
|
||||
"",
|
||||
"模擬商品名稱 (Mock)",
|
||||
30,
|
||||
30,
|
||||
30,
|
||||
10,
|
||||
"",
|
||||
"MOCK_CODE",
|
||||
"Mock Product Name",
|
||||
"模擬の商品"
|
||||
);
|
||||
mockProduct.setSpecI18nJson("{\"zh_TW\":\"此為模擬商品描述,用於當系統尚未寫入真實商品資料時,可供開發者與測試人員模擬查看詳細頁面的版面配置與樣式呈現。\\n支援換行與多種排版。\",\"en\":\"This is a mock product description used for simulating layout and styling before real data is populated.\"}");
|
||||
return new SlotStructure(1, 99, mockProduct, 5);
|
||||
}
|
||||
|
||||
private boolean isMock() {
|
||||
return slot != null && slot.getProduct() != null && "mock_001".equals(slot.getProduct().getProductID());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getContext(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
// 本對話框不需處理回呼指令
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogProductDetailBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// 商品圖片
|
||||
try {
|
||||
new ImageGlide(context).fileload(slot.getProduct().getProductImg(), binding.imageProduct);
|
||||
} catch (LogsEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
|
||||
// 商品名稱(多語)、貨道、庫存
|
||||
binding.textProductName.setText(slot.getProduct().getLocalizedName(context));
|
||||
binding.slotNumber.setText("貨道" + slot.getSlot());
|
||||
binding.inventoryNumber.setText("庫存" + slot.getCount());
|
||||
|
||||
// 商品規格/描述(多語,純文字含換行;後台空白則隱藏整塊)。
|
||||
// setText 原樣呈現換行與空格/縮排;版面已設 gravity=start,故縮排不會被置中打亂。
|
||||
String spec = slot.getProduct().getLocalizedSpec(context);
|
||||
if (spec == null || spec.trim().isEmpty()) {
|
||||
binding.textProductDescription.setVisibility(android.view.View.GONE);
|
||||
} else {
|
||||
binding.textProductDescription.setText(spec);
|
||||
}
|
||||
|
||||
// 依購物車開關決定按鈕呈現
|
||||
boolean cartOn = MyApp.getInstance().getDevSet().isShoppingCar();
|
||||
if (!cartOn) {
|
||||
// 一般版:隱藏數量調整與加入購物車,只留直接購買,數量固定 1
|
||||
binding.layoutQuantityControl.setVisibility(android.view.View.GONE);
|
||||
binding.btnAddToCart.setVisibility(android.view.View.GONE);
|
||||
quantity = 1;
|
||||
}
|
||||
|
||||
refreshQuantity();
|
||||
|
||||
binding.ivCancel.setOnClickListener(v -> cancel());
|
||||
|
||||
binding.buttonMinus.setOnClickListener(v -> {
|
||||
if (quantity > 1) {
|
||||
quantity--;
|
||||
refreshQuantity();
|
||||
}
|
||||
});
|
||||
|
||||
binding.buttonPlus.setOnClickListener(v -> {
|
||||
// 上限:貨道庫存與購物車總量上限(8 件)
|
||||
if (quantity >= slot.getCount()) {
|
||||
ToastUtils.showShort(R.string.cart_out_of_stock);
|
||||
return;
|
||||
}
|
||||
if (quantity >= 8) {
|
||||
ToastUtils.showShort(R.string.cart_max_items);
|
||||
return;
|
||||
}
|
||||
quantity++;
|
||||
refreshQuantity();
|
||||
});
|
||||
|
||||
// 加入購物車:以選定數量加入購物車後停留商品列表(CartViewModel 內建庫存/8 件上限防呆)
|
||||
binding.btnAddToCart.setOnClickListener(v -> {
|
||||
if (isMock()) {
|
||||
ToastUtils.showShort("此為模擬商品,無法加入購物車");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
addSelectedQuantityToCart();
|
||||
ToastUtils.showShort(R.string.cart_added);
|
||||
cancel();
|
||||
});
|
||||
|
||||
// 直接購買:
|
||||
// - 購物車開啟:以選定數量加入購物車後,前往購物車列表頁(數量帶入,不直接結帳)。
|
||||
// - 購物車關閉(一般版):單品直購,清空 buyList 放入本商品後彈 BuyDialog 選付款。
|
||||
binding.btnBuyNow.setOnClickListener(v -> {
|
||||
if (isMock()) {
|
||||
ToastUtils.showShort("此為模擬商品,無法購買");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (cartOn) {
|
||||
addSelectedQuantityToCart();
|
||||
// 先切到購物車品項頁(於詳情頁視窗後方),再關閉詳情頁顯示出來,
|
||||
// 避免「關閉對話框」與「fragment 切換」時序互相干擾導致沒跳頁。
|
||||
openCartList();
|
||||
cancel();
|
||||
} else {
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
MyApp.getInstance().getBuyList().add(
|
||||
new BuyStructure(slot.getField(), slot.getSlot(), slot.getProduct(), quantity));
|
||||
cancel();
|
||||
requestBuy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 以詳情頁選定數量加入購物車(逐件累加,數量確實帶入;受庫存/8 件上限限制)。 */
|
||||
private void addSelectedQuantityToCart() {
|
||||
for (int i = 0; i < quantity; i++) {
|
||||
UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/** 前往購物車列表頁(沿用各 flavor 的購物車入口)。 */
|
||||
private void openCartList() {
|
||||
if (hostContext instanceof FontendActivity) {
|
||||
SaleFlowHandler handler = ((FontendActivity) hostContext).getSaleFlowHandler();
|
||||
if (handler != null) {
|
||||
handler.onShoppingCartRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshQuantity() {
|
||||
binding.textQuantity.setText(String.valueOf(quantity));
|
||||
int total = slot.getProduct().getRealPrice() * quantity;
|
||||
binding.textPrice.setText("$" + total);
|
||||
}
|
||||
|
||||
/** 沿用各 flavor 的結帳入口(basic→BuyDialog);非 FontendActivity 時直接開 BuyDialog。 */
|
||||
private void requestBuy() {
|
||||
if (hostContext instanceof FontendActivity) {
|
||||
SaleFlowHandler handler = ((FontendActivity) hostContext).getSaleFlowHandler();
|
||||
if (handler != null) {
|
||||
handler.onBuyRequested();
|
||||
return;
|
||||
}
|
||||
}
|
||||
new BuyDialog(hostContext, getSrcHandlerMain()).show();
|
||||
}
|
||||
}
|
||||
@ -253,14 +253,34 @@ public class DispatchDialog extends DialogAbstract {
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogDispatchBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// 進入出貨統一保險:關閉 FontendActivity 的「加購倒數」(AddProductOnCountdown)。
|
||||
// 任一支付方式只要走到出貨,就不該再被 40 秒加購倒數到期觸發 BUY_DIALOG_OPEN,
|
||||
// 避免支付畫面在出貨/取貨倒數中途疊出(通行碼、TapPay 等成功路徑原本未送 STOP_COUNTDOWN)。
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(),
|
||||
FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
|
||||
|
||||
isReportDispatch = true;
|
||||
tvCountdown = binding.tvCountdown;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
// 初始化 Finalize Builder(在支付完成、進入出貨流程時建立)
|
||||
finalizeBuilder = new TransactionFinalizeBuilder();
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
// 進入出貨流程當下 buyList 必為完整(下方即用它組 dispatchList),先擷取商品明細快照。
|
||||
// 之後結案 build() 會優先採用此快照,避免掃碼支付(玉山)等慢流程在結案前 buyList
|
||||
// 被取消/倒數路徑清空,導致 order.items 變空(無商品紀錄)但出貨其實成功。
|
||||
finalizeBuilder.captureItems();
|
||||
if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(42);
|
||||
}
|
||||
// 快照訂單層級欄位(flowType / 金額 / 找零 / 點數)。須在上方 isCmuh→42 覆寫「之後」、
|
||||
// 且趁全域 flowInfo 仍正確時擷取;結案 build() 與「通行碼(5)不上報」判斷都改用此快照,
|
||||
// 不再讀結案當下的全域即時值——避免出貨中途任何路徑(如殘留加購倒數彈出 BuyDialog →
|
||||
// initialReportData())把全域洗回 0,導致通行碼誤報、payment_type/total 落地成 0「未知」。
|
||||
finalizeBuilder.captureOrderInfo();
|
||||
|
||||
// [MQTT Finalize 重構] 電子發票改由「後台」開立:機台不再直連綠界,
|
||||
// 僅把發票輸入(載具/愛心碼/統編/金額)隨 finalize 的 invoice 物件上報(TransactionFinalizeBuilder 組裝)。
|
||||
// 後台收到 finalize 後建 pending 發票並派 Job 去綠界開立,避免機台網路不確定性與等待。
|
||||
|
||||
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
|
||||
|
||||
@ -311,7 +331,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
|
||||
getLogs().info("DispatchList size:" + this.dispatchList.size());
|
||||
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
|
||||
try {
|
||||
String patName = "未知患者";
|
||||
String sn = "";
|
||||
@ -443,6 +463,15 @@ public class DispatchDialog extends DialogAbstract {
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
// 安全網:出貨流程已開始(dispatchList 非空)但尚未結案(既非成功 finalize、也非錯誤 finalize)時,
|
||||
// 在關閉前補送一次 MQTT Finalize(部分出貨/未完成),避免走「倒數結束 / dispatchIndex 越界 / 中止」
|
||||
// 等非錯誤關閉路徑時整筆交易完全不上報。isFinalizing 擋住正常成功/錯誤路徑,不會重複上報。
|
||||
// 注意:須在下方 buyList.clear() 之前執行,因 finalizeBuilder.build() 會讀 buyList 組出商品明細。
|
||||
if (!isFinalizing && finalizeBuilder != null && dispatchList != null && !dispatchList.isEmpty()) {
|
||||
getLogs().info("dialogCancel:偵測到尚未結案的出貨流程,補送 MQTT Finalize(部分/未完成)");
|
||||
finalizeFailedDispatch("出貨流程關閉前補送結案");
|
||||
}
|
||||
|
||||
closeDialogCountdown.shutdown();
|
||||
int buyListSize = MyApp.getInstance().getBuyList().size();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
@ -485,7 +514,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
|
||||
if (isCheckSlotByVMC)
|
||||
return;
|
||||
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA006");
|
||||
getLogs().debug("dispatch flow 006");
|
||||
isCheckSlotByVMC = true; // 進來立即設定true,可以判斷掉重複出貨
|
||||
|
||||
boolean isCMD = false;
|
||||
@ -529,7 +558,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));// 請檢查商品有無取出再確認取貨
|
||||
}
|
||||
} else {
|
||||
boolean isCmuh = "Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project);
|
||||
boolean isCmuh = com.unibuy.smartdevice.AppEntry.isCmuh();
|
||||
int maxRetries = isCmuh ? 10 : 3;
|
||||
if (checkSlotRunCount >= maxRetries) {
|
||||
buttonControlCountdown.shutdown();
|
||||
@ -660,7 +689,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
getSrcHandlerMain().send(getClass().getSimpleName(), Option.NEXT_PROCESS_GOODS.getOption(),
|
||||
"next process goods");
|
||||
} else {
|
||||
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA002");
|
||||
getLogs().debug("dispatch flow 002");
|
||||
MyApp.getInstance().getDevXinYuanController()
|
||||
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L);
|
||||
}
|
||||
@ -673,7 +702,7 @@ public class DispatchDialog extends DialogAbstract {
|
||||
getCtx().getString(R.string.product_delivery_issues_contact_the_counter_window));
|
||||
}
|
||||
} else {
|
||||
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA003");
|
||||
getLogs().debug("dispatch flow 003");
|
||||
MyApp.getInstance().getDevXinYuanController()
|
||||
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L);
|
||||
MyApp.getInstance().getDevXinYuanController()
|
||||
@ -940,10 +969,13 @@ public class DispatchDialog extends DialogAbstract {
|
||||
isFinalizing = true;
|
||||
|
||||
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
// 整筆交易出貨成功結案:清空 v2 多品項購物車(單例),避免下一位客人看到上一筆殘留商品。
|
||||
// 僅在「成功結案」清,取消結帳不清(保留購物車供客人重試)。
|
||||
com.unibuy.v2.UnibuyHelper.INSTANCE.getCartViewModel().clearCart();
|
||||
if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
|
||||
insertDrgnoToMemo();
|
||||
}
|
||||
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
|
||||
publishFinalizeUnlessPassCode();
|
||||
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(),
|
||||
"update data");
|
||||
saveData();
|
||||
@ -1052,19 +1084,33 @@ public class DispatchDialog extends DialogAbstract {
|
||||
|
||||
addFailedDispenseRecordsFromCurrent();
|
||||
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project) && hasAnySuccessfulDispatch()) {
|
||||
if (com.unibuy.smartdevice.AppEntry.isCmuh() && hasAnySuccessfulDispatch()) {
|
||||
insertDrgnoToMemo();
|
||||
} else if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
} else if (com.unibuy.smartdevice.AppEntry.isCmuh()) {
|
||||
getLogs().info("CMUH 整單未出貨成功,不寫入 SN/DRGNO Memo,允許重新掃碼領藥");
|
||||
}
|
||||
|
||||
getLogs().info("出貨失敗,發送 MQTT Finalize 結案訊息: " + message);
|
||||
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
|
||||
publishFinalizeUnlessPassCode();
|
||||
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(),
|
||||
"update data");
|
||||
saveData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 發送 MQTT 交易結案;但 flowType 5(通行碼)為工程師/內部硬體測試,不上報交易
|
||||
* (B670 驗證已於後台記 log;避免污染銷售/營收,且不扣減雲端庫存)。
|
||||
*/
|
||||
private void publishFinalizeUnlessPassCode() {
|
||||
// 用 finalizeBuilder 在進入出貨當下擷取的快照判斷,而非結案當下的全域即時值;
|
||||
// 防止出貨中途全域 flowType 被洗回 0(如殘留加購倒數彈出 BuyDialog)而導致通行碼被誤報。
|
||||
if (finalizeBuilder != null && finalizeBuilder.getOrderFlowType() == 5) {
|
||||
getLogs().info("通行碼(payment_type=5) 為內部測試出貨,略過 MQTT 交易結案上報。");
|
||||
return;
|
||||
}
|
||||
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
|
||||
}
|
||||
|
||||
private void addFailedDispenseRecordsFromCurrent() {
|
||||
if (dispatchList == null || dispatchList.isEmpty()) {
|
||||
return;
|
||||
|
||||
@ -46,6 +46,7 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
CHANGE_DISPATCH(6),
|
||||
CHANGE_BUY(7),
|
||||
SET_STATUS_TEXT(8),
|
||||
SET_SCAN_INPUT(9),
|
||||
;
|
||||
|
||||
private int option;
|
||||
@ -151,6 +152,11 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
case SET_STATUS_TEXT:
|
||||
setStatusText(message);
|
||||
break;
|
||||
case SET_SCAN_INPUT:
|
||||
// 掃碼即時回饋:把掃到的內容顯示在輸入框,讓使用者確認有掃到
|
||||
binding.editTextNumber.setText(message);
|
||||
binding.editTextNumber.setSelection(binding.editTextNumber.getText().length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -174,6 +180,18 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
|
||||
MyApp.getInstance().initialReportData();
|
||||
|
||||
setInputMode(true);
|
||||
|
||||
binding.btnModeKeypad.setOnClickListener(v -> {
|
||||
setInputMode(true);
|
||||
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.btnModeScan.setOnClickListener(v -> {
|
||||
setInputMode(false);
|
||||
if (closeDialogCountdown != null) closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
isChangeDispatch = true;
|
||||
openBarcode = true;
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
@ -278,11 +296,23 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(5, TimeUnit.SECONDS);
|
||||
// 立即開始聽掃描器(原本延遲 5 秒,導致對話框剛彈出時掃碼被整個丟掉 → 時好時壞、無回饋)
|
||||
new ReadBarCodeOnScheduler(getHandlerMain()).start(0, TimeUnit.SECONDS);
|
||||
|
||||
// 進入頁面時立即檢查取貨口 (使用貨道1作為代表)
|
||||
// callback 為 null,表示檢查通過後不做額外動作,僅在檢查失敗時顯示警告
|
||||
checkSlotHaveProduct(1, 1, null);
|
||||
// pickup_sheet(原 Cmuh)流程不做取貨口檢查,維持原行為。
|
||||
if (!"pickup_sheet".equals(MyApp.getInstance().getShoppingMode())) {
|
||||
checkSlotHaveProduct(1, 1, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void setInputMode(boolean isKeyboard) {
|
||||
binding.btnModeKeypad.setSelected(isKeyboard);
|
||||
binding.btnModeScan.setSelected(!isKeyboard);
|
||||
|
||||
binding.linearNumpad.setVisibility(isKeyboard ? android.view.View.VISIBLE : android.view.View.GONE);
|
||||
binding.imageViewScanHint.setVisibility(isKeyboard ? android.view.View.GONE : android.view.View.VISIBLE);
|
||||
}
|
||||
|
||||
public class ReadBarCodeOnScheduler extends HandlerMainScheduler {
|
||||
@ -306,21 +336,18 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
usbDev.read(buffer, 100);
|
||||
for (int i=0; i<7; i++) {
|
||||
for (int i=0; i<9; i++) { // 約 45 秒讀取窗口,覆蓋 40 秒對話框存活時間
|
||||
if (isRun() && openBarcode) {
|
||||
int size = usbDev.read(buffer, 5000);
|
||||
if (size > 5) {
|
||||
String barCodeString = new String(buffer, 0, size);
|
||||
getHandlerMain().start(getLogs().getClassName(), getCtx().getString(R.string.receive_barcode)+":"+ Tools.randomReplace(barCodeString, 0.3)+","+getCtx().getString(R.string.send_transaction));//收到條碼,送出交易
|
||||
String barCodeString = new String(buffer, 0, size).trim();
|
||||
// 掃碼即時回饋:先把掃到的內容顯示在輸入框,再送出驗證
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_SCAN_INPUT.getOption(), barCodeString);
|
||||
|
||||
try {
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(barCodeString);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
if ("pickup_sheet".equals(MyApp.getInstance().getShoppingMode())) {
|
||||
submitViaMemberVerification(barCodeString);
|
||||
} else {
|
||||
submitPickupCode(barCodeString);
|
||||
}
|
||||
|
||||
openBarcode = false;
|
||||
@ -357,25 +384,94 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
}
|
||||
|
||||
public void changeOk() {
|
||||
String number = binding.editTextNumber.getText().toString().trim();
|
||||
// pickup_sheet(原 Cmuh):取貨碼走舊 memberVerification API,維持原行為。
|
||||
if ("pickup_sheet".equals(MyApp.getInstance().getShoppingMode())) {
|
||||
submitViaMemberVerification(number);
|
||||
return;
|
||||
}
|
||||
// 長度檢查
|
||||
if (number.isEmpty() || number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "取貨碼長度不能少於 8 碼");
|
||||
return;
|
||||
}
|
||||
submitPickupCode(number);
|
||||
}
|
||||
|
||||
/** pickup_sheet 模式:取貨碼/條碼走舊 {@code httpAPI.memberVerification}(沿用 Cmuh 行為)。 */
|
||||
private void submitViaMemberVerification(String code) {
|
||||
try {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
// 長度檢查
|
||||
if (number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
|
||||
return;
|
||||
}
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductId("0");
|
||||
MyApp.getInstance().getMemberVerificationStructure().setProductPrice(0);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(number);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setMemberBarCode(code);
|
||||
MyApp.getInstance().getMemberVerificationStructure().setVerificationType(2);
|
||||
httpAPI.memberVerification(MyApp.getInstance().getMemberVerificationStructure());
|
||||
} catch (LogsParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsSettingEmptyException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (LogsParseException | LogsSettingEmptyException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取貨碼驗證改走 StarCloudAPI B660(邏輯比照員工卡 B680)。
|
||||
* 成功回傳 code_id 與要出貨的 slot_no:據此建立 BuyList、保存 code_id 供 MQTT
|
||||
* finalize(payment_type=6 取貨碼),再進入取貨確認 → 出貨。
|
||||
*/
|
||||
private void submitPickupCode(String code) {
|
||||
// 防護:取貨碼從屬「取貨模組」,任一關閉即不開放呼叫 B660(即使 UI 漏出入口也擋下)
|
||||
if (!(MyApp.getInstance().getDevSet().isPickupModule() && MyApp.getInstance().getDevSet().isPickupCode())) {
|
||||
openBarcode = true;
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "取貨碼功能未開放");
|
||||
return;
|
||||
}
|
||||
// 送出前顯示「驗證中…」,避免使用者以為沒反應
|
||||
getHandlerMain().start(getClass().getSimpleName(), Option.SET_STATUS_TEXT.getOption(), "驗證中…");
|
||||
new com.unibuy.smartdevice.external.StarCloudAPI(getHandlerMain(), getHandlerMain())
|
||||
.verifyPickupCode(code, new com.unibuy.smartdevice.external.StarCloudAPI.PickupVerificationCallback() {
|
||||
@Override
|
||||
public void onSuccess(String codeId, int slotNo) {
|
||||
try {
|
||||
SlotStructure slotProduct = MyApp.getInstance().getSlotData(SlotField.VMC.getField(), slotNo);
|
||||
if (slotProduct == null) {
|
||||
setStatusText(getContext().getString(R.string.product_not_found));
|
||||
openBarcode = true;
|
||||
return;
|
||||
}
|
||||
if (slotProduct.isLock()) {
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@" + getContext().getString(R.string.product_sales_suspended));
|
||||
dialogCancel();
|
||||
return;
|
||||
}
|
||||
if (slotProduct.getCount() == 0) {
|
||||
getSrcHandlerMain().start("FontendPickupCodeDialog", "@" + getContext().getString(R.string.product_out_of_stock));
|
||||
dialogCancel();
|
||||
return;
|
||||
}
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
MyApp.getInstance().getBuyList().add(
|
||||
new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1));
|
||||
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(6); // 取貨碼 → payment_type 6
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(code);
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(code);
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(Tools.generateTimeBasedUUID());
|
||||
MyApp.getInstance().getReportFlowInfoData().setTotalPrice(0); // 取貨碼為已預付/補出貨,付款金額 0
|
||||
MyApp.getInstance().getReportAccessCodeStructure().setAccessCodeId(codeId);
|
||||
|
||||
changeBuy(); // 開啟取貨確認 BuyPickupCodeDialog → 出貨
|
||||
} catch (LogsEmptyException e) {
|
||||
setStatusText(getContext().getString(R.string.product_not_found));
|
||||
openBarcode = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String message) {
|
||||
openBarcode = true;
|
||||
setStatusText(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void changeBuy() {
|
||||
closeDialogCountdown.shutdown();
|
||||
BuyPickupCodeDialog buyDialog = new BuyPickupCodeDialog(getContext(), getSrcHandlerMain());
|
||||
@ -395,7 +491,7 @@ public class FontendPickupCodeDialog extends DialogAbstract {
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText(getContext().getString(R.string.return_to)+"("+countdown+")");//返回
|
||||
binding.buttonCancel.setText("取消 " + countdown + "秒");
|
||||
}
|
||||
|
||||
public void setStatusText(String message) {
|
||||
@ -179,7 +179,7 @@ public class InvoiceBarcodeDialog extends DialogAbstract {
|
||||
|
||||
binding.buttonCustomerIdentifier.setOnClickListener(v -> changeCustomerIdentifier());
|
||||
// binding.buttonDonation.setOnClickListener(v -> changeDonation());
|
||||
binding.buttonDonation.setOnClickListener(v -> showDonationDialog());
|
||||
binding.buttonDonation.setOnClickListener(v -> changeDialog(InvoiceDonationDialog.class));
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
binding.buttonInputVehicle.setOnClickListener(v -> showInputVehicleDialog());
|
||||
@ -295,116 +295,6 @@ public class InvoiceBarcodeDialog extends DialogAbstract {
|
||||
changeCheckout();
|
||||
}
|
||||
|
||||
private void showDonationDialog() {
|
||||
closeDialogCountdown.shutdown();
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getCtx());
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View dialogView = inflater.inflate(R.layout.dialog_donation, null);
|
||||
builder.setView(dialogView);
|
||||
|
||||
CheckBox checkboxDefault = dialogView.findViewById(R.id.checkboxDefault);
|
||||
Spinner spinnerCharity = dialogView.findViewById(R.id.spinnerCharity);
|
||||
TextView tvlovecode = dialogView.findViewById(R.id.tvlovecode);
|
||||
|
||||
// 1. 建立捐贈機構對應表
|
||||
// 假設這是你的捐贈機構清單
|
||||
List<String> organizationNames = new ArrayList<>(donationMap.keySet());
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>( getCtx(),
|
||||
R.layout.spinner_item, // ✅ 改成自訂 layout
|
||||
R.id.spinnerText,
|
||||
organizationNames);
|
||||
adapter.setDropDownViewResource(R.layout.spinner_item); // 下拉選單也套用
|
||||
spinnerCharity.setAdapter(adapter);
|
||||
|
||||
// 2. 預設勾選 checkbox,並禁用 spinner
|
||||
checkboxDefault.setChecked(true);
|
||||
spinnerCharity.setEnabled(false);
|
||||
|
||||
// 3. 隨機一組愛心碼,並立刻寫入
|
||||
Map.Entry<String, String> randomEntry = getRandomDonationEntry(donationMap);
|
||||
if (randomEntry != null) {
|
||||
MyApp.getInstance().getInvoiceData().setLoveCode(randomEntry.getValue());
|
||||
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("loveCode:" + MyApp.getInstance().getInvoiceData().getLoveCode());
|
||||
// Toast.makeText(getCtx(), "預設捐贈:" + randomEntry.getKey(), Toast.LENGTH_SHORT).show();
|
||||
tvlovecode.setText("捐贈機構:"+randomEntry.getKey());
|
||||
}
|
||||
|
||||
// 自訂 Title
|
||||
TextView title = new TextView(getCtx());
|
||||
title.setText("選擇捐贈方式");
|
||||
title.setTextSize(24); // 放大字體
|
||||
title.setPadding(40, 40, 40, 40);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
|
||||
donationDialog = new AlertDialog.Builder(getCtx())
|
||||
.setCustomTitle(title) // 使用自訂 Title
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("確定", (d, which) -> {
|
||||
if (!checkboxDefault.isChecked()) {
|
||||
String selectedName = (String) spinnerCharity.getSelectedItem();
|
||||
String loveCode = donationMap.get(selectedName);
|
||||
MyApp.getInstance().getInvoiceData().setLoveCode(loveCode);
|
||||
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("loveCode:" + MyApp.getInstance().getInvoiceData().getLoveCode());
|
||||
Toast.makeText(getCtx(), "選擇:" + selectedName, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
// 若是勾選預設,之前就寫入過,不需處理
|
||||
closeDialogCountdown.start(40); // 再次開始倒數
|
||||
changeCheckout();
|
||||
})
|
||||
.setNegativeButton("取消", (d, which) -> {
|
||||
// 使用者取消後,重新開始倒數 40 秒
|
||||
closeDialogCountdown.start(40);
|
||||
})
|
||||
.create();
|
||||
|
||||
donationDialog.show();
|
||||
|
||||
Button positive = donationDialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
Button negative = donationDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
|
||||
|
||||
if (positive != null) {
|
||||
positive.setTextSize(24);
|
||||
positive.setPadding(30, 20, 130, 20);
|
||||
}
|
||||
if (negative != null) {
|
||||
negative.setTextSize(24);
|
||||
negative.setPadding(30, 20, 30, 20);
|
||||
}
|
||||
|
||||
// 放大框架 (寬度 90%)
|
||||
Window window = donationDialog.getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
|
||||
params.copyFrom(window.getAttributes());
|
||||
params.width = (int) (getCtx().getResources().getDisplayMetrics().widthPixels * 0.9);
|
||||
params.height = (int) (getCtx().getResources().getDisplayMetrics().heightPixels * 0.6); // 高度 60%
|
||||
window.setAttributes(params);
|
||||
}
|
||||
|
||||
// 加入:20秒後自動關閉 Dialog
|
||||
Handler handler = new Handler();
|
||||
Runnable timeoutRunnable = () -> {
|
||||
if (donationDialog.isShowing()) {
|
||||
donationDialog.dismiss();
|
||||
Toast.makeText(getCtx(), "已逾時取消", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
handler.postDelayed(timeoutRunnable, 10000); // 20秒
|
||||
|
||||
// 4. Checkbox 切換時控制 Spinner
|
||||
checkboxDefault.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
spinnerCharity.setEnabled(!isChecked);
|
||||
handler.removeCallbacks(timeoutRunnable); // 取消 timeout
|
||||
});
|
||||
|
||||
spinnerCharity.setOnTouchListener((v, event) -> {
|
||||
handler.removeCallbacks(timeoutRunnable); // 取消 timeout
|
||||
return false;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void showInputVehicleDialog() {
|
||||
closeDialogCountdown.shutdown(); // 停止倒數,避免中途跳掉
|
||||
@ -489,7 +379,7 @@ public class InvoiceBarcodeDialog extends DialogAbstract {
|
||||
changeDialog(CheckoutNfcPhoneDialog.class);
|
||||
break;
|
||||
case 9:
|
||||
changeDialog(CashPayDialog.class);
|
||||
changeDialog(cashDialogClass());
|
||||
break;
|
||||
case 30:
|
||||
changeDialog(CheckoutTapPayDialog.class);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
@ -117,6 +118,7 @@ public class InvoiceCustomerIdentifierDialog extends DialogAbstract {
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
binding.buttonFinish.setOnClickListener(v -> changeOk());
|
||||
binding.buttonNextStep.setOnClickListener(v -> changeNextStep());
|
||||
binding.buttonDonation.setOnClickListener(v -> changeDonation());
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
@ -323,6 +325,25 @@ public class InvoiceCustomerIdentifierDialog extends DialogAbstract {
|
||||
cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 步驟1(輸入統編)→ 步驟2(輸入電話)。
|
||||
* 先驗證統編長度,通過才切換畫面:隱藏統編區與「下一步」,顯示電話區與「完成輸入」。
|
||||
* 原本此按鈕未綁 listener,導致統編流程卡死、統編永遠送不到後台。
|
||||
*/
|
||||
public void changeNextStep() {
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
if (number.length() < 8) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "統一編號長度不能少於 8 碼");
|
||||
return;
|
||||
}
|
||||
binding.linearStep1.setVisibility(View.GONE);
|
||||
binding.linearStep2.setVisibility(View.VISIBLE);
|
||||
binding.buttonNextStep.setVisibility(View.GONE);
|
||||
binding.buttonFinish.setVisibility(View.VISIBLE);
|
||||
binding.textInvoiceCarrier.setText("輸入電話號碼");
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
}
|
||||
|
||||
public void changeOk() {
|
||||
//----- 發票統編 ------
|
||||
String number = binding.editTextNumber.getText().toString();
|
||||
@ -384,7 +405,7 @@ public class InvoiceCustomerIdentifierDialog extends DialogAbstract {
|
||||
changeDialog(CheckoutNfcPhoneDialog.class);
|
||||
break;
|
||||
case 9:
|
||||
changeDialog(CashPayDialog.class);
|
||||
changeDialog(cashDialogClass());
|
||||
break;
|
||||
case 30:
|
||||
changeDialog(CheckoutTapPayDialog.class);
|
||||
|
||||
@ -0,0 +1,334 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.DialogDonationBinding;
|
||||
import com.unibuy.smartdevice.ui.recycler.RecyclerDonationListAdpter;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class InvoiceDonationDialog extends DialogAbstract {
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
INVOICE_SUCCESSFUL(4),
|
||||
INVOICE_FAILED(5),
|
||||
CHANGE_DISPATCH(6),
|
||||
CHANGE_BUY(7),
|
||||
CHANGE_CHECKOUT(8),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getCtx(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
if (MyApp.getInstance().getBuyList().size() <= 0) {
|
||||
dialogCancel();
|
||||
} else {
|
||||
Option option = optionMap.get(commandCode);
|
||||
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
case INVOICE_SUCCESSFUL:
|
||||
break;
|
||||
case INVOICE_FAILED:
|
||||
break;
|
||||
case CHANGE_DISPATCH:
|
||||
changeDispatch();
|
||||
break;
|
||||
case CHANGE_BUY:
|
||||
changeBuy();
|
||||
break;
|
||||
case CHANGE_CHECKOUT:
|
||||
changeCheckout();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogDonationBinding binding;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
private LinkedHashMap<String, String> donationMap;
|
||||
private List<String> organizationNames;
|
||||
private RecyclerDonationListAdpter donationAdapter;
|
||||
|
||||
public InvoiceDonationDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogDonationBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
donationMap = new LinkedHashMap<>();
|
||||
intiDonationMap();
|
||||
|
||||
binding.buttonFinish.setOnClickListener(v -> changeOk());
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
// 隱藏切換 Tab 按鈕(由 InvoiceTypeDialog 統一管理入口)
|
||||
binding.buttonCustomerIdentifier.setVisibility(View.GONE);
|
||||
binding.buttonDonation.setVisibility(View.GONE);
|
||||
binding.buttonInputVehicle.setVisibility(View.GONE);
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
|
||||
setupDonationSpinner();
|
||||
}
|
||||
|
||||
private void updateButtonState(com.google.android.material.button.MaterialButton selectedButton) {
|
||||
binding.buttonInputVehicle.setChecked(selectedButton.getId() == R.id.buttonInputVehicle);
|
||||
binding.buttonDonation.setChecked(selectedButton.getId() == R.id.buttonDonation);
|
||||
binding.buttonCustomerIdentifier.setChecked(selectedButton.getId() == R.id.buttonCustomerIdentifier);
|
||||
}
|
||||
|
||||
public void intiDonationMap(){
|
||||
donationMap.put("社團法人嘉義市腦性麻痺協會", "2855669");
|
||||
donationMap.put("社團法人桃園市唐氏症家長協會", "520321");
|
||||
donationMap.put("社團法人中華民國自閉症總會", "1577");
|
||||
donationMap.put("社團法人臺北市蒲公英聽語協會", "71521");
|
||||
donationMap.put("社團法人桃園市失智症關懷協會", "8185");
|
||||
donationMap.put("財團法人台灣痲瘋救濟基金會", "1954");
|
||||
donationMap.put("社團法人中華小腦萎縮症病友協會", "9972");
|
||||
donationMap.put("社團法人高雄市唐氏症歡喜協會", "7717299");
|
||||
donationMap.put("社團法人屏東縣腦性麻痺服務協會", "115");
|
||||
donationMap.put("社團法人台南市智障者福利家長協進會", "2070");
|
||||
donationMap.put("財團法人台灣肯納自閉症基金會", "1018");
|
||||
donationMap.put("社團法人高雄市腦性麻痺服務協會", "29636");
|
||||
donationMap.put("社團法人高雄市自閉症協進會", "8585885");
|
||||
donationMap.put("社團法人臺灣自閉兒家庭關懷協會", "338518");
|
||||
donationMap.put("社團法人臺東縣自閉症協進會", "3685");
|
||||
donationMap.put("社團法人中華民國外傷性腦損傷照顧者關懷協會", "5841");
|
||||
donationMap.put("財團法人天主教靈醫會私立聖嘉民啟智中心", "57880");
|
||||
donationMap.put("社團法人新竹市智障福利協進會", "6684");
|
||||
donationMap.put("社團法人屏東縣自閉症協進會", "7351024");
|
||||
donationMap.put("社團法人台北市自閉症家長協會", "772");
|
||||
donationMap.put("社團法人屏東縣啟智協進會", "159");
|
||||
donationMap.put("社團法人花蓮縣自閉症協會", "8588");
|
||||
donationMap.put("澎湖縣私立財團法人天主教澎湖教區附設惠民啟智中心", "9261790");
|
||||
donationMap.put("財團法人新竹市私立天主教仁愛啟智中心", "227");
|
||||
donationMap.put("社團法人中華民國唐氏症關愛者協會", "88321");
|
||||
donationMap.put("財團法人天主教會台中教區附設南投縣私立復活啟智中心", "7717");
|
||||
donationMap.put("財團法人喜憨兒社會福利基金會", "88432");
|
||||
donationMap.put("社團法人花蓮縣智障福利協進會", "7756");
|
||||
donationMap.put("社團法人雲林縣啟智協會", "1311");
|
||||
donationMap.put("社團法人新竹縣智障福利協進會", "3170");
|
||||
donationMap.put("社團法人台南市腦性麻痺之友協會", "815");
|
||||
donationMap.put("社團法人台南市癲癇之友協會", "2350253");
|
||||
donationMap.put("新北市自閉症服務協進會", "7688");
|
||||
donationMap.put("社團法人台中市?智協進會", "450");
|
||||
donationMap.put("社團法人台中市自閉症教育協進會", "1127");
|
||||
donationMap.put("財團法人中華民國自閉症基金會", "402");
|
||||
donationMap.put("社團法人台灣流浪貓狗關懷協會", "9958");
|
||||
donationMap.put("社團法人台灣愛貓協會", "921314");
|
||||
donationMap.put("社團法人台灣幸褔狗流浪中途協會", "856203");
|
||||
donationMap.put("社團法人新北市流浪貓狗再生保護協會", "8866");
|
||||
donationMap.put("社團法人台灣流浪動物希望協會", "119");
|
||||
donationMap.put("社團法人高雄市幸福狗尾巴友善動物協會", "6848");
|
||||
donationMap.put("社團法人臺南市貓狗流浪終點協會", "5866");
|
||||
donationMap.put("社團法人台灣黑糖流浪動物救援協會", "958");
|
||||
donationMap.put("社團法人台灣動物輔助活動及治療協會", "916");
|
||||
donationMap.put("社團法人台灣友善動物協會", "9305");
|
||||
donationMap.put("社團法人中華民國反棄養寵物協會", "11289");
|
||||
donationMap.put("社團法人台灣流浪貓關懷協會", "71706");
|
||||
donationMap.put("社團法人台灣浪愛不流浪關懷動物協會", "9458");
|
||||
donationMap.put("台灣米樂流浪動物中途協會", "9925");
|
||||
donationMap.put("社團法人中華民國寵物友善協會", "52982");
|
||||
donationMap.put("社團法人台灣愛兔協會", "5822");
|
||||
donationMap.put("社團法人台灣拾貓生命教育創新協會", "880222");
|
||||
donationMap.put("社團法人台灣防止虐待動物協會", "1772");
|
||||
donationMap.put("社團法人好好善待動物協會", "6666666");
|
||||
donationMap.put("社團法人臺北市毛小孩幸福聯盟協會", "99891");
|
||||
donationMap.put("社團法人台南市流浪動物愛護協會", "17966");
|
||||
donationMap.put("中華民國流浪動物花園協會", "528");
|
||||
donationMap.put("台灣流浪兔保護協會", "17922");
|
||||
donationMap.put("社團法人台北市愛兔協會", "5222");
|
||||
donationMap.put("社團法人臺灣照顧生命協會", "589");
|
||||
donationMap.put("新竹市保護動物協會", "860713");
|
||||
donationMap.put("社團法人中華民國動物福利環保協進會", "529529");
|
||||
donationMap.put("財團法人中華民國兒童癌症基金會", "88888");
|
||||
donationMap.put("中華民國癌病腫瘤患者扶助協會", "6088");
|
||||
donationMap.put("財團法人臺灣癌症基金會", "1799");
|
||||
}
|
||||
|
||||
private void setupDonationSpinner() {
|
||||
organizationNames = new ArrayList<>(donationMap.keySet());
|
||||
|
||||
donationAdapter = new RecyclerDonationListAdpter(organizationNames, (name, position) -> {
|
||||
closeDialogCountdown.setCountdown(40);
|
||||
});
|
||||
|
||||
binding.recyclerViewCharity.setAdapter(donationAdapter);
|
||||
|
||||
// 隨機預選一個捐贈機構
|
||||
int randomIndex = new Random().nextInt(organizationNames.size());
|
||||
donationAdapter.setSelectedPosition(randomIndex);
|
||||
binding.recyclerViewCharity.scrollToPosition(randomIndex);
|
||||
}
|
||||
|
||||
public void changeInvoiceBarcode() {
|
||||
closeDialogCountdown.shutdown();
|
||||
new InvoiceBarcodeDialog(getCtx(), getSrcHandlerMain()).show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void changeCustomerIdentifier() {
|
||||
closeDialogCountdown.shutdown();
|
||||
new InvoiceCustomerIdentifierDialog(getCtx(), getSrcHandlerMain()).show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void changeDispatch() {
|
||||
closeDialogCountdown.shutdown();
|
||||
DispatchDialog dispatchDialog = new DispatchDialog(getContext(), getSrcHandlerMain());
|
||||
dispatchDialog.show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void changeOk() {
|
||||
int selectedPosition = donationAdapter.getSelectedPosition();
|
||||
if (selectedPosition < 0) {
|
||||
Toast.makeText(getCtx(), "請選擇捐贈機構", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
String selectedName = organizationNames.get(selectedPosition);
|
||||
String loveCode = donationMap.get(selectedName);
|
||||
MyApp.getInstance().getInvoiceData().setLoveCode(loveCode);
|
||||
MyApp.getInstance().getReportFlowInfoData().setInvoiceInfo("loveCode:" + loveCode);
|
||||
Toast.makeText(getCtx(), "選擇:" + selectedName, Toast.LENGTH_SHORT).show();
|
||||
changeCheckout();
|
||||
}
|
||||
|
||||
public void changeBuy() {
|
||||
closeDialogCountdown.shutdown();
|
||||
new InvoiceTypeDialog(getContext(), getSrcHandlerMain()).show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void changeCheckout() {
|
||||
closeDialogCountdown.shutdown();
|
||||
int flowType = MyApp.getInstance().getReportFlowInfoData().getFlowType();
|
||||
switch (flowType) {
|
||||
case 1:
|
||||
changeDialog(CheckoutNfcCardDialog.class);
|
||||
break;
|
||||
case 2:
|
||||
changeDialog(CheckoutNfcRechargeDialog.class);
|
||||
break;
|
||||
case 3:
|
||||
changeDialog(CheckoutEsunPayDialog.class);
|
||||
break;
|
||||
case 4:
|
||||
changeDialog(CheckoutNfcPhoneDialog.class);
|
||||
break;
|
||||
case 9:
|
||||
changeDialog(CashPayDialog.class);
|
||||
break;
|
||||
case 30:
|
||||
changeDialog(CheckoutTapPayDialog.class);
|
||||
break;
|
||||
case 70:
|
||||
changeDialog(CheckoutLinePayDialog.class);
|
||||
break;
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
closeDialogCountdown.shutdown();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText("返回發票選擇(" + countdown + ")");
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
handlerMain.start(getClass().getSimpleName(), CashPayDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), BuyDialog.Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.databinding.DialogInvoiceTypeBinding;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 選擇發票開立方式介面
|
||||
* 提供三個選項:輸入載具 / 捐贈發票 / 公司統編
|
||||
* 位於 BuyDialog(選擇支付方式)與各發票設定頁面之間
|
||||
*/
|
||||
public class InvoiceTypeDialog extends DialogAbstract {
|
||||
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getCtx(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
if (MyApp.getInstance().getBuyList().size() <= 0) {
|
||||
dialogCancel();
|
||||
} else {
|
||||
Option option = optionMap.get(commandCode);
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonCancelText(Integer.valueOf(message));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogInvoiceTypeBinding binding;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
|
||||
public InvoiceTypeDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogInvoiceTypeBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
android.view.Window window = getWindow();
|
||||
if (window != null) {
|
||||
android.view.WindowManager.LayoutParams layoutParams = window.getAttributes();
|
||||
layoutParams.gravity = android.view.Gravity.CENTER;
|
||||
window.setAttributes(layoutParams);
|
||||
}
|
||||
|
||||
// 選擇:輸入載具(手機條碼)
|
||||
binding.cardVehicle.setOnClickListener(v -> {
|
||||
closeDialogCountdown.shutdown();
|
||||
changeDialog(InvoiceBarcodeDialog.class);
|
||||
});
|
||||
|
||||
// 選擇:捐贈發票
|
||||
binding.cardDonation.setOnClickListener(v -> {
|
||||
closeDialogCountdown.shutdown();
|
||||
changeDialog(InvoiceDonationDialog.class);
|
||||
});
|
||||
|
||||
// 選擇:公司統編
|
||||
binding.cardCustomerIdentifier.setOnClickListener(v -> {
|
||||
closeDialogCountdown.shutdown();
|
||||
changeDialog(InvoiceCustomerIdentifierDialog.class);
|
||||
});
|
||||
|
||||
// 返回支付方式
|
||||
binding.buttonCancel.setOnClickListener(v -> changeBuy());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 BuyDialog(選擇支付方式)
|
||||
*/
|
||||
public void changeBuy() {
|
||||
closeDialogCountdown.shutdown();
|
||||
new BuyDialog(getContext(), getSrcHandlerMain()).show();
|
||||
cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 倒數逾時時清空購物車並關閉
|
||||
*/
|
||||
public void cancelOnThreadCountdown() {
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
closeDialogCountdown.shutdown();
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.buttonCancel.setText("返回" + countdown + "秒");
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(getSrcCountdown()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupSuccessBinding;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PickupSuccessDialog extends DialogAbstract {
|
||||
public enum Option {
|
||||
TOAST(0),
|
||||
COUNTDOWN_CANCEL(1),
|
||||
SET_COUNTDOWN_TEXT(2),
|
||||
RESET_COUNTDOWN(3),
|
||||
;
|
||||
|
||||
private int option;
|
||||
|
||||
public int getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
Option(int option) {
|
||||
this.option = option;
|
||||
}
|
||||
}
|
||||
|
||||
public PickupSuccessDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
getLogs().info("PickupSuccessDialog:" + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
public static final Map<Integer, Option> optionMap = new HashMap<>();
|
||||
static {
|
||||
for (Option option : Option.values()) {
|
||||
optionMap.put(option.getOption(), option);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return this.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getContext(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
Option option = optionMap.get(commandCode);
|
||||
if (option != null) {
|
||||
switch (option) {
|
||||
case TOAST:
|
||||
super.execute(context, commandCode, message);
|
||||
break;
|
||||
case COUNTDOWN_CANCEL:
|
||||
cancelOnThreadCountdown();
|
||||
break;
|
||||
case SET_COUNTDOWN_TEXT:
|
||||
setButtonConfirmText(Integer.valueOf(message));
|
||||
break;
|
||||
case RESET_COUNTDOWN:
|
||||
closeDialogCountdown.setCountdown(15);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogPickupSuccessBinding binding;
|
||||
private CloseDialogOnThreadHandler closeDialogCountdown;
|
||||
private String laneNumber = "99"; // 預設貨道號碼
|
||||
|
||||
public void setLaneNumber(String laneNumber) {
|
||||
this.laneNumber = laneNumber;
|
||||
if (binding != null) {
|
||||
binding.tvLaneNumber.setText(laneNumber);
|
||||
binding.tvSubtitle.setText("請至" + laneNumber + "號櫃門拿取您的商品");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogPickupSuccessBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// 初始化貨道數字與副標文字
|
||||
setLaneNumber(laneNumber);
|
||||
|
||||
binding.btnConfirm.setOnClickListener(v -> dialogCancel());
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(15);
|
||||
}
|
||||
|
||||
public void dialogCancel() {
|
||||
if (closeDialogCountdown != null) {
|
||||
closeDialogCountdown.shutdown();
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void cancelOnThreadCountdown() {
|
||||
getTools().dialogClose();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonConfirmText(long countdown) {
|
||||
binding.btnConfirm.setText("確認已取貨(" + countdown + "秒)");
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
super(handlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return getSrcHandlerMain();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void close(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.COUNTDOWN_CANCEL.getOption(), "close dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(long countdown, HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.databinding.DialogSystemInspectionBinding;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
|
||||
public class SystemInspectionDialog extends DialogAbstract {
|
||||
|
||||
public SystemInspectionDialog(Context context, HandlerMain srcHandlerMain) {
|
||||
super(context, srcHandlerMain);
|
||||
getLogs().info("SystemInspectionDialog:" + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return this.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends DialogAbstract> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getContext(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
super.execute(context, commandCode, message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private DialogSystemInspectionBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Context context) {
|
||||
binding = DialogSystemInspectionBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
}
|
||||
}
|
||||
@ -76,20 +76,8 @@ public class RecyclerDialogBuyListAdpter extends RecyclerView.Adapter<RecyclerDi
|
||||
holder.binding.textPosition.setText(String.valueOf(position));
|
||||
String productID = buyProduct.getProduct().getProductID();
|
||||
if (!productID.isEmpty() && !productID.equals("?")) {
|
||||
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
|
||||
//多國語的判斷
|
||||
// String currentLang = LanguageHelper.getSavedLanguage(context);
|
||||
// switch (currentLang) {
|
||||
// case "tw":
|
||||
// holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());
|
||||
// break;
|
||||
// case "en":
|
||||
// holder.binding.textProductName.setText(buyProduct.getProduct().getProductNameEn());
|
||||
// break;
|
||||
// case "ja":
|
||||
// holder.binding.textProductName.setText(buyProduct.getProduct().getProductNameJp());
|
||||
// break;
|
||||
// }
|
||||
// 多國語的判斷(統一由 ProductStructure 處理,英/日未翻譯自動 fallback 中文)
|
||||
holder.binding.textProductName.setText(buyProduct.getProduct().getLocalizedName(context));
|
||||
} else {
|
||||
holder.binding.textProductName.setText(context.getString(R.string.click_select_product));//點我選擇商品
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
package com.unibuy.smartdevice.ui.recycler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.unibuy.smartdevice.R;
|
||||
@ -83,29 +84,31 @@ public class RecyclerDialogDispatchListAdpter extends RecyclerView.Adapter<Recyc
|
||||
|
||||
int price = machinePrice > 0? machinePrice: sellingPrice;
|
||||
|
||||
holder.binding.textCount.setText(String.valueOf(count));
|
||||
holder.binding.textQuantity.setText(String.valueOf(quantity));
|
||||
holder.binding.textCount.setText(String.format("已出貨數量(%d / %d)", count, quantity));
|
||||
|
||||
logs.info("isShipped:" + dispatchProduct.isShipped());
|
||||
|
||||
if (dispatchProduct.getCount() == dispatchProduct.getQuantity()) {
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipped));//已出貨
|
||||
holder.binding.textDispatchStatus.setTextColor(Color.RED);
|
||||
holder.binding.textDispatchStatus.setTextColor(ContextCompat.getColor(context, R.color.onSuccess));
|
||||
holder.binding.textDispatchStatus.setBackground(createTagBackground(R.color.success));
|
||||
} else if (dispatchProduct.isShipped()) {
|
||||
try {
|
||||
if (SlotField.getSlotField(dispatchProduct.getField()).isCanOneShipped()) {
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"("+(count+1)+")");//出貨中
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"("+(count+1)+"件)");//出貨中
|
||||
} else {
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"(All)");//出貨中
|
||||
}
|
||||
} catch (LogsEmptyException e) {
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"("+(count+1)+")");//出貨中
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.shipping)+"("+(count+1)+"件)");//出貨中
|
||||
}
|
||||
|
||||
holder.binding.textDispatchStatus.setTextColor(Color.RED);
|
||||
holder.binding.textDispatchStatus.setTextColor(ContextCompat.getColor(context, R.color.onPrimary));
|
||||
holder.binding.textDispatchStatus.setBackground(createTagBackground(R.color.primary));
|
||||
} else {
|
||||
holder.binding.textDispatchStatus.setText(context.getString(R.string.waiting));//等待中
|
||||
holder.binding.textDispatchStatus.setTextColor(Color.BLACK);
|
||||
holder.binding.textDispatchStatus.setTextColor(ContextCompat.getColor(context, R.color.black));
|
||||
holder.binding.textDispatchStatus.setBackground(createTagBackground(R.color.gray));
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +124,14 @@ public class RecyclerDialogDispatchListAdpter extends RecyclerView.Adapter<Recyc
|
||||
delProductData(position);
|
||||
}
|
||||
}
|
||||
/** 建立圓角標籤背景 */
|
||||
private GradientDrawable createTagBackground(int colorResId) {
|
||||
GradientDrawable drawable = new GradientDrawable();
|
||||
drawable.setColor(ContextCompat.getColor(context, colorResId));
|
||||
drawable.setCornerRadius(context.getResources().getDisplayMetrics().density * 20);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return dispatchList.size();
|
||||
|
||||
@ -84,7 +84,7 @@ public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<
|
||||
int price = machinePrice > 0? machinePrice: sellingPrice;
|
||||
|
||||
holder.binding.textPrice.setText("$"+price);
|
||||
holder.binding.textCount.setText("x"+String.valueOf(count));
|
||||
holder.binding.textCount.setText("出貨數量:" + count + "件");
|
||||
|
||||
logs.info("field:" + buyProduct.getField());
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package com.unibuy.smartdevice.ui.recycler;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.unibuy.smartdevice.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RecyclerDonationListAdpter extends RecyclerView.Adapter<RecyclerDonationListAdpter.ViewHolder> {
|
||||
|
||||
private List<String> organizationNames;
|
||||
private int selectedPosition = -1;
|
||||
private OnItemClickListener listener;
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(String name, int position);
|
||||
}
|
||||
|
||||
public RecyclerDonationListAdpter(List<String> organizationNames, OnItemClickListener listener) {
|
||||
this.organizationNames = organizationNames;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.spinner_item, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
String name = organizationNames.get(position);
|
||||
holder.textView.setText(name);
|
||||
|
||||
// 高亮選中項
|
||||
if (position == selectedPosition) {
|
||||
holder.textView.setBackgroundResource(R.drawable.bg_selected_item);
|
||||
holder.textView.setTextColor(holder.itemView.getContext().getResources().getColor(R.color.white));
|
||||
} else {
|
||||
holder.textView.setBackgroundResource(android.R.color.transparent);
|
||||
holder.textView.setTextColor(holder.itemView.getContext().getResources().getColor(R.color.black));
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
int previousPosition = selectedPosition;
|
||||
selectedPosition = position;
|
||||
notifyItemChanged(previousPosition);
|
||||
notifyItemChanged(selectedPosition);
|
||||
if (listener != null) {
|
||||
listener.onItemClick(name, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return organizationNames.size();
|
||||
}
|
||||
|
||||
public void setSelectedPosition(int position) {
|
||||
this.selectedPosition = position;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getSelectedPosition() {
|
||||
return selectedPosition;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView textView;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.spinnerText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,7 +56,7 @@ public class RecyclerProductListAdpter extends RecyclerView.Adapter<RecyclerProd
|
||||
}
|
||||
|
||||
holder.binding.textProductId.setText(product.getProductID());
|
||||
holder.binding.textProductName.setText(product.getProductName());
|
||||
holder.binding.textProductName.setText(product.getLocalizedName(context));
|
||||
holder.binding.textPrice.setText(String.valueOf(product.getMachinePrice() > 0? product.getMachinePrice(): product.getSellingPrice()));
|
||||
}
|
||||
|
||||
|
||||
@ -68,8 +68,8 @@ public class RecyclerProductSearchAdpter extends RecyclerView.Adapter<RecyclerPr
|
||||
}
|
||||
|
||||
holder.binding.textProductId.setText(product.getProductID());
|
||||
holder.binding.textProductName.setText(product.getProductName());
|
||||
holder.binding.buttonSlotProductSelect.setOnClickListener(new View.OnClickListener() {
|
||||
holder.binding.textProductName.setText(product.getLocalizedName(context));
|
||||
holder.binding.cardProduct.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Log.i("select product", "點選商品:"+product.getProductName());
|
||||
|
||||
@ -58,9 +58,9 @@ public class RecyclerSearchTextAdpter extends RecyclerView.Adapter<RecyclerSearc
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String nowText = activityProductSearchBinding.searchProductList.getQuery().toString();
|
||||
Log.i("SearchTextOnClick", nowText + oneText);
|
||||
activityProductSearchBinding.searchProductList.setQuery(nowText+oneText, false);
|
||||
Log.i("SearchTextOnClick", "Selected full name: " + oneText);
|
||||
activityProductSearchBinding.searchProductList.setQuery(oneText, false);
|
||||
activityProductSearchBinding.searchProductList.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@ -13,9 +12,130 @@ public class LanguageHelper {
|
||||
private static final String PREFS_NAME = "app_prefs";
|
||||
private static final String KEY_LANGUAGE = "language";
|
||||
|
||||
// App 介面支援的語系(顯示順序)。對應 res/values-* 資料夾:
|
||||
// zh-TW=values(預設,繁中)、zh-CN=values-zh-rCN、en、ja、ko、vi、th、id(=values-in)。
|
||||
public static final String[] SUPPORTED_LANGUAGES = {
|
||||
"zh-TW", "zh-CN", "en", "ja", "ko", "vi", "th", "id", "fr", "de", "es"
|
||||
};
|
||||
|
||||
// 各語系以「該語言自己的文字」顯示的名稱(給語言選單用)
|
||||
public static String getDisplayName(String code) {
|
||||
switch (canonicalize(code)) {
|
||||
case "zh-TW": return "中文(繁體)";
|
||||
case "zh-CN": return "中文(简体)";
|
||||
case "en": return "English";
|
||||
case "ja": return "日本語";
|
||||
case "ko": return "한국어";
|
||||
case "vi": return "Tiếng Việt";
|
||||
case "th": return "ภาษาไทย";
|
||||
case "id": return "Bahasa Indonesia";
|
||||
case "fr": return "Français";
|
||||
case "de": return "Deutsch";
|
||||
case "es": return "Español";
|
||||
default: return code;
|
||||
}
|
||||
}
|
||||
|
||||
// 將各種歷史/縮寫代碼正規化成標準 UI 語系標籤(含 region)。
|
||||
// 舊資料相容:tw→zh-TW、jp→ja、單獨 zh→zh-TW(過去預設為繁中)。
|
||||
public static String canonicalize(String code) {
|
||||
if (code == null || code.trim().isEmpty()) {
|
||||
return fromLocale(Locale.getDefault());
|
||||
}
|
||||
String c = code.trim().toLowerCase(Locale.ROOT).replace('_', '-');
|
||||
switch (c) {
|
||||
case "zh-tw":
|
||||
case "zh-hant":
|
||||
case "zh-hant-tw":
|
||||
case "tw":
|
||||
case "zh": // 過去單獨 zh 一律視為繁中
|
||||
return "zh-TW";
|
||||
case "zh-cn":
|
||||
case "zh-hans":
|
||||
case "zh-hans-cn":
|
||||
case "cn":
|
||||
return "zh-CN";
|
||||
case "ja":
|
||||
case "jp":
|
||||
return "ja";
|
||||
case "ko":
|
||||
case "kr":
|
||||
return "ko";
|
||||
case "vi":
|
||||
case "vn":
|
||||
return "vi";
|
||||
case "th":
|
||||
return "th";
|
||||
case "id":
|
||||
case "in": // Android/Java 對印尼文的舊代碼
|
||||
return "id";
|
||||
case "fr":
|
||||
return "fr";
|
||||
case "de":
|
||||
return "de";
|
||||
case "es":
|
||||
return "es";
|
||||
default:
|
||||
if (c.startsWith("en")) return "en";
|
||||
if (c.startsWith("zh")) return "zh-TW";
|
||||
return "zh-TW";
|
||||
}
|
||||
}
|
||||
|
||||
private static String fromLocale(Locale locale) {
|
||||
String lang = locale.getLanguage();
|
||||
if ("zh".equals(lang)) {
|
||||
String region = locale.getCountry();
|
||||
return ("CN".equalsIgnoreCase(region) || "SG".equalsIgnoreCase(region)) ? "zh-CN" : "zh-TW";
|
||||
}
|
||||
// in = 印尼文舊代碼,統一回 id
|
||||
if ("in".equals(lang)) return "id";
|
||||
return canonicalize(lang);
|
||||
}
|
||||
|
||||
// 由 UI 語系標籤建立對應的 Locale(處理 region 與印尼文資源代碼)
|
||||
private static Locale buildLocale(String canonical) {
|
||||
switch (canonical) {
|
||||
case "zh-TW": return new Locale("zh", "TW");
|
||||
case "zh-CN": return new Locale("zh", "CN");
|
||||
case "id": return new Locale("id"); // 系統會對應到 res/values-in
|
||||
default: return new Locale(canonical);
|
||||
}
|
||||
}
|
||||
|
||||
// App 標準語系標籤(zh-TW) → 後台 locale 代碼(zh_TW)。後台一律用底線格式。
|
||||
public static String toBackendLocale(String code) {
|
||||
return canonicalize(code).replace('-', '_');
|
||||
}
|
||||
|
||||
// 語言選單的語系來源:優先用後台 B014 LangSet 下發的「機台啟用語系」,
|
||||
// 尚未取得(離線/未綁定)時退回全部 SUPPORTED_LANGUAGES。回傳標準標籤。
|
||||
public static String[] getMachineLanguages() {
|
||||
java.util.List<String> machine = com.unibuy.smartdevice.MyApp.getInstance().getMachineLanguages();
|
||||
if (machine != null && !machine.isEmpty()) {
|
||||
return machine.toArray(new String[0]);
|
||||
}
|
||||
return SUPPORTED_LANGUAGES;
|
||||
}
|
||||
|
||||
// 供商品名稱等資料層使用:後台目前只有 中/英/日 三語,
|
||||
// 其餘語言(簡中/韓/越/泰/印尼)一律 fallback 回中文主值。
|
||||
public static String toProductLang(String code) {
|
||||
String c = canonicalize(code);
|
||||
if (c.startsWith("en")) return "en";
|
||||
if (c.equals("ja")) return "ja";
|
||||
return "zh";
|
||||
}
|
||||
|
||||
// 取得目前已儲存、且正規化為商品資料用的語言代碼(zh/en/ja)
|
||||
public static String normalizedLang(Context context) {
|
||||
return toProductLang(getSavedLanguage(context));
|
||||
}
|
||||
|
||||
// 切換語言
|
||||
public static void changeLanguage(Context context, String languageCode) {
|
||||
Locale locale = new Locale(languageCode);
|
||||
String canonical = canonicalize(languageCode);
|
||||
Locale locale = buildLocale(canonical);
|
||||
Locale.setDefault(locale);
|
||||
|
||||
Resources resources = context.getResources();
|
||||
@ -29,8 +149,8 @@ public class LanguageHelper {
|
||||
|
||||
resources.updateConfiguration(config, resources.getDisplayMetrics());
|
||||
|
||||
// 保存語言設定
|
||||
saveLanguagePreference(context, languageCode);
|
||||
// 保存語言設定(一律存正規化後的標準標籤)
|
||||
saveLanguagePreference(context, canonical);
|
||||
}
|
||||
|
||||
// 保存語言到 SharedPreferences
|
||||
@ -39,10 +159,11 @@ public class LanguageHelper {
|
||||
prefs.edit().putString(KEY_LANGUAGE, languageCode).apply();
|
||||
}
|
||||
|
||||
// 讀取語言設定
|
||||
// 讀取語言設定(回傳正規化後的標準標籤)
|
||||
public static String getSavedLanguage(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
return prefs.getString(KEY_LANGUAGE, Locale.getDefault().getLanguage());
|
||||
String saved = prefs.getString(KEY_LANGUAGE, null);
|
||||
return canonicalize(saved);
|
||||
}
|
||||
|
||||
// 初始化語言(在 App 啟動時呼叫)
|
||||
|
||||
35
app/src/main/java/com/unibuy/v2/UnibuyHelper.kt
Normal file
35
app/src/main/java/com/unibuy/v2/UnibuyHelper.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package com.unibuy.v2
|
||||
|
||||
import com.blankj.utilcode.util.ActivityUtils
|
||||
import com.blankj.utilcode.util.AppUtils
|
||||
import com.unibuy.smartdevice.ui.FontendActivity
|
||||
import com.unibuy.v2.vm.CartViewModel
|
||||
|
||||
/**
|
||||
* v2 購物車的橋接 helper(自 Luzui 移植並裁剪)。
|
||||
*
|
||||
* 與 Luzui 差異:
|
||||
* - getCartViewModel() 改為自持 singleton,不綁 FontendActivity 的 ViewModelProvider,
|
||||
* 避免修改 main 共用的 FontendActivity(其無 cartViewModel 欄位)。
|
||||
* - 移除 dismissAllAlertDialogs()/getSPStrNotNull():前者依賴 DialogAbstract.dialogInstances
|
||||
* (TaiwanStar 為 private static 無法存取),後者購物車未使用。
|
||||
*/
|
||||
object UnibuyHelper {
|
||||
|
||||
private val cartViewModel: CartViewModel = CartViewModel()
|
||||
|
||||
fun getCartViewModel(): CartViewModel = cartViewModel
|
||||
|
||||
fun getFontendActivity(): FontendActivity {
|
||||
if (ActivityUtils.getTopActivity() is FontendActivity) {
|
||||
return ActivityUtils.getTopActivity() as FontendActivity
|
||||
} else if (ActivityUtils.isActivityExists(AppUtils.getAppPackageName(), FontendActivity::class.java.simpleName)) {
|
||||
ActivityUtils.getActivityList().forEach { activity ->
|
||||
if (activity is FontendActivity) {
|
||||
return activity
|
||||
}
|
||||
}
|
||||
}
|
||||
return FontendActivity()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package com.unibuy.v2.adapter
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.chad.library.adapter.base.BaseQuickAdapter
|
||||
import com.chad.library.adapter.base.viewholder.BaseViewHolder
|
||||
import com.unibuy.smartdevice.R
|
||||
import com.unibuy.smartdevice.exception.LogsEmptyException
|
||||
import com.unibuy.smartdevice.structure.SlotStructure
|
||||
import com.unibuy.smartdevice.ui.tools.ImageGlide
|
||||
import com.unibuy.v2.UnibuyHelper
|
||||
|
||||
/**
|
||||
* 購物車品項清單 Adapter(自 Luzui v2 移植)。
|
||||
* 依賴 BaseRecyclerViewAdapterHelper 的 BaseQuickAdapter。
|
||||
*
|
||||
* Disabled 按鈕規則:
|
||||
* - buttonMinus:當 cartSelectedCount == 1 時 disabled(最低 1 件,不允許繼續減少)
|
||||
* - buttonPlus :當 cartSelectedCount >= slot.count(庫存上限)或全車總件數 >= 8 時 disabled
|
||||
* Disabled 外觀:背景換 bg_quantity_button_disabled、圖示染灰(#ADADAD)、clickable = false。
|
||||
*/
|
||||
class CartOperationAdapter(layoutResId: Int, data: MutableList<SlotStructure>) :
|
||||
BaseQuickAdapter<SlotStructure, BaseViewHolder>(layoutResId, data) {
|
||||
|
||||
init {
|
||||
addChildClickViewIds(R.id.buttonMinus, R.id.buttonPlus, R.id.buttonWaste, R.id.layout_product, R.id.image_product)
|
||||
}
|
||||
|
||||
override fun convert(holder: BaseViewHolder, item: SlotStructure) {
|
||||
val result = UnibuyHelper.getCartViewModel().createResult(item)
|
||||
val ctx = holder.itemView.context
|
||||
|
||||
// 多國語商品名稱(統一由 ProductStructure 處理:i18n map → zh_TW → 舊 _en/_jp → 中文主值)
|
||||
val name = item.product.getLocalizedName(ctx)
|
||||
|
||||
holder.setText(R.id.textProductName, name)
|
||||
.setText(R.id.totalPriceTextView, "NT$ ${result.currentItemPrice}")
|
||||
.setText(R.id.textStock, String.format("庫存%02d件", item.count))
|
||||
.setText(R.id.textQuantity, "${result.currentItemCount}")
|
||||
.setText(R.id.textPrice, "NT$ ${item.product.realPrice}")
|
||||
|
||||
// ── 商品圖片 ────────────────────────────────────────────────────
|
||||
val imageProductPicture = holder.getView<ImageView>(R.id.image_product)
|
||||
try {
|
||||
ImageGlide(imageProductPicture.context).fileload(item.product.productImg, imageProductPicture)
|
||||
} catch (e: LogsEmptyException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
// ── 按鈕 Disabled 狀態判斷 ──────────────────────────────────────
|
||||
val buttonMinus = holder.getView<ImageView>(R.id.buttonMinus)
|
||||
val buttonPlus = holder.getView<ImageView>(R.id.buttonPlus)
|
||||
|
||||
val isAtMinCount = item.cartSelectedCount <= 1
|
||||
val isAtStockLimit = item.cartSelectedCount >= item.count
|
||||
val isCartFull = UnibuyHelper.getCartViewModel().hasMinimumCartItems()
|
||||
val isPlusDisabled = isAtStockLimit || isCartFull
|
||||
|
||||
// ── 減少按鈕 ────────────────────────────────────────────────────
|
||||
applyButtonState(buttonMinus, disabled = isAtMinCount, ctx)
|
||||
|
||||
// ── 增加按鈕 ────────────────────────────────────────────────────
|
||||
applyButtonState(buttonPlus, disabled = isPlusDisabled, ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* 套用啟用 / 停用外觀到 ImageView 按鈕。
|
||||
*
|
||||
* @param button 目標按鈕
|
||||
* @param disabled true = 停用(light_gray 背景 + 灰色圖示 + 不可點擊)
|
||||
* @param ctx Context(取色用)
|
||||
*/
|
||||
private fun applyButtonState(button: ImageView, disabled: Boolean, ctx: android.content.Context) {
|
||||
if (disabled) {
|
||||
button.setBackgroundResource(R.drawable.bg_quantity_button_disabled)
|
||||
// setBackgroundResource 會重置 stateListAnimator,需立刻清除以防止陰影跳動
|
||||
button.stateListAnimator = null
|
||||
button.setColorFilter(
|
||||
ContextCompat.getColor(ctx, R.color.gray),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
button.isClickable = false
|
||||
} else {
|
||||
button.setBackgroundResource(R.drawable.bg_quantity_outline_button)
|
||||
// setBackgroundResource 會重置 stateListAnimator,需立刻清除以防止陰影跳動
|
||||
button.stateListAnimator = null
|
||||
button.clearColorFilter()
|
||||
button.isClickable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package com.unibuy.v2.bean
|
||||
|
||||
// 購物車操作結果資料類
|
||||
data class CartOperationResult(
|
||||
val totalPrice: Int, // 購物車總價
|
||||
val totalCount: Int, // 購物車總數量
|
||||
val currentItemCount: Int, // 當前商品數量
|
||||
val currentItemPrice: Int // 當前商品價格
|
||||
)
|
||||
254
app/src/main/java/com/unibuy/v2/ui/ShoppingCartFragment.java
Normal file
254
app/src/main/java/com/unibuy/v2/ui/ShoppingCartFragment.java
Normal file
@ -0,0 +1,254 @@
|
||||
package com.unibuy.v2.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.blankj.utilcode.util.ClickUtils;
|
||||
import com.blankj.utilcode.util.ToastUtils;
|
||||
import com.chad.library.adapter.base.BaseQuickAdapter;
|
||||
import com.unibuy.smartdevice.FragmentAbstract;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.FragmentShoppingCartBinding;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import com.unibuy.v2.UnibuyHelper;
|
||||
import com.unibuy.v2.adapter.CartOperationAdapter;
|
||||
import com.unibuy.v2.bean.CartOperationResult;
|
||||
import com.unibuy.v2.utils.SafeCountdownUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* General flavor 多品項購物車 Fragment(自 Luzui v2 移植並適配)。
|
||||
*
|
||||
* 與 Luzui 差異:
|
||||
* - 移除 HttpAPI / DialogXManager / FlowerFragment / 格子機相關邏輯。
|
||||
* - 「加購」回 VMC 商品列表;「結帳」改為 convertToBuyStructure() + SaleFlowHandler.onBuyRequested()
|
||||
* 交棒給 TaiwanStar 既有結帳鏈(BuyDialog → 付款 → DispatchDialog → MQTT 結案)。
|
||||
* - 移除 MyApp.setShoppingCartFragmentInstance(TaiwanStar 無此 API)。
|
||||
*/
|
||||
public class ShoppingCartFragment extends FragmentAbstract {
|
||||
private FragmentShoppingCartBinding binding;
|
||||
private CartOperationAdapter cartAdapter;
|
||||
private CartOperationResult allCartItems;
|
||||
private SafeCountdownUtil countdownUtil;
|
||||
|
||||
private static final int DEBOUNCE_DURATION = 500;
|
||||
private static final int COUNTDOWN_DURATION = 180;
|
||||
|
||||
/** 回傳當前倒數秒數(未初始化回 -1)。 */
|
||||
public int getCurrentCountdownSeconds() {
|
||||
if (countdownUtil != null) {
|
||||
return countdownUtil.getCurrentSeconds();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String setName() {
|
||||
return getString(R.string.shopping_cart_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> setCls() {
|
||||
return getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMain setHandlerMain() {
|
||||
return new ToastHandlerMain(getCtx(), getLogs()) {
|
||||
@Override
|
||||
protected void execute(Context context, int commandCode, String message) {
|
||||
// 購物車本身不需處理額外指令
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public ShoppingCartFragment() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public ShoppingCartFragment(HandlerMain srcHandlerMain) {
|
||||
super(srcHandlerMain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
binding = FragmentShoppingCartBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setupUI();
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
setupCartRecyclerView();
|
||||
setupClickListeners();
|
||||
refreshTitleView();
|
||||
initCountdownUtil();
|
||||
}
|
||||
|
||||
private void initCountdownUtil() {
|
||||
if (countdownUtil == null) {
|
||||
countdownUtil = new SafeCountdownUtil(getViewLifecycleOwner().getLifecycle());
|
||||
countdownUtil.setCountdownListener(new SafeCountdownUtil.CountdownListener() {
|
||||
@Override
|
||||
public void onTick(int seconds) {
|
||||
updateCountdownText(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
// isAdded() 在 onSaveInstanceState 後仍為 true,無法擋住背景 commit;
|
||||
// 改以 lifecycle 是否達 STARTED 為準(最終 commit 防呆在 FontendActivity)。
|
||||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)
|
||||
&& getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> clearCartAndBack());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCountdownText(int seconds) {
|
||||
if (!isAdded() || getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (binding != null && binding.cancelText != null) {
|
||||
binding.cancelText.setText(getString(R.string.cart_cancel_button_format, seconds));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCartItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
|
||||
try {
|
||||
SlotStructure slot = (SlotStructure) adapter.getData().get(position);
|
||||
int viewId = view.getId();
|
||||
if (viewId == R.id.buttonMinus) {
|
||||
UnibuyHelper.INSTANCE.getCartViewModel().removeFromCart(slot);
|
||||
adapter.notifyItemChanged(position);
|
||||
countdownReset(); // 使用者調整數量=有互動,續命 180 秒倒數
|
||||
} else if (viewId == R.id.buttonPlus) {
|
||||
UnibuyHelper.INSTANCE.getCartViewModel().addToCart(slot);
|
||||
adapter.notifyItemChanged(position);
|
||||
countdownReset(); // 使用者調整數量=有互動,續命 180 秒倒數
|
||||
} else if (viewId == R.id.buttonWaste) {
|
||||
UnibuyHelper.INSTANCE.getCartViewModel().removeItemFromCart(slot);
|
||||
adapter.remove(position);
|
||||
// 購物車清空後返回 VMC 商品列表
|
||||
if (UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot().isEmpty()) {
|
||||
backToVmc();
|
||||
} else {
|
||||
countdownReset(); // 還有商品,視為有互動,續命 180 秒倒數
|
||||
}
|
||||
}
|
||||
// layout_product / image_product 的商品詳情 popup 屬 Luzui DialogXManager,General 不採用
|
||||
refreshTitleView();
|
||||
} catch (Exception e) {
|
||||
ToastUtils.showLong(getString(R.string.data_error));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupClickListeners() {
|
||||
// 加購商品:回到 VMC 商品列表繼續挑選
|
||||
ClickUtils.applySingleDebouncing(binding.addMoreItemsButton, DEBOUNCE_DURATION, v -> backToVmc());
|
||||
|
||||
// 前往結帳:寫入 buyList 後交棒給既有結帳鏈
|
||||
ClickUtils.applySingleDebouncing(binding.confirmButton, DEBOUNCE_DURATION, v -> {
|
||||
if (UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot().isEmpty()) {
|
||||
ToastUtils.showLong(getString(R.string.cart_cannot_decrease));
|
||||
return;
|
||||
}
|
||||
UnibuyHelper.INSTANCE.getCartViewModel().convertToBuyStructure();
|
||||
FontendActivity activity = getFontendActivity();
|
||||
if (activity != null && activity.getSaleFlowHandler() != null) {
|
||||
activity.getSaleFlowHandler().onBuyRequested();
|
||||
}
|
||||
});
|
||||
|
||||
// 取消:清空購物車並返回 VMC
|
||||
ClickUtils.applySingleDebouncing(binding.cancelButton, DEBOUNCE_DURATION, v -> clearCartAndBack());
|
||||
}
|
||||
|
||||
private void setupCartRecyclerView() {
|
||||
List<SlotStructure> cartItems = UnibuyHelper.INSTANCE.getCartViewModel().getCartItemsSnapshot();
|
||||
cartAdapter = new CartOperationAdapter(R.layout.recycler_cart_operation, cartItems);
|
||||
cartAdapter.setOnItemChildClickListener(this::handleCartItemClick);
|
||||
binding.shoppingCartRecyclerView.setAdapter(cartAdapter);
|
||||
}
|
||||
|
||||
private void refreshTitleView() {
|
||||
allCartItems = UnibuyHelper.INSTANCE.getCartViewModel().getAllCartItems();
|
||||
binding.totalItemsTextView.setText(getString(R.string.total_items_format, allCartItems.getTotalCount()));
|
||||
binding.totalAmountTextView.setText(getString(R.string.total_amount_format, allCartItems.getTotalPrice()));
|
||||
|
||||
if (UnibuyHelper.INSTANCE.getCartViewModel().hasMinimumCartItems()) {
|
||||
binding.maxItemsWarningTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.maxItemsWarningTextView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空購物車並切回 VMC 商品列表。 */
|
||||
private void clearCartAndBack() {
|
||||
UnibuyHelper.INSTANCE.getCartViewModel().clearCart();
|
||||
backToVmc();
|
||||
}
|
||||
|
||||
private void backToVmc() {
|
||||
FontendActivity activity = getFontendActivity();
|
||||
if (activity != null) {
|
||||
activity.backToVmc();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private FontendActivity getFontendActivity() {
|
||||
if (getActivity() instanceof FontendActivity) {
|
||||
return (FontendActivity) getActivity();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startOnSwitchFragment() {
|
||||
setupUI();
|
||||
countdownUtil.startOrReset(COUNTDOWN_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopOnSwitchFragment() {
|
||||
if (countdownUtil != null) {
|
||||
countdownUtil.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void countdownReset() {
|
||||
countdownUtil.startOrReset(COUNTDOWN_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
76
app/src/main/java/com/unibuy/v2/utils/SafeCountdownUtil.kt
Normal file
76
app/src/main/java/com/unibuy/v2/utils/SafeCountdownUtil.kt
Normal file
@ -0,0 +1,76 @@
|
||||
package com.unibuy.v2.utils
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
/**
|
||||
* 與 Fragment 生命週期綁定的安全倒數計時器(自 Luzui v2 移植)。
|
||||
*/
|
||||
class SafeCountdownUtil(private val lifecycle: Lifecycle) : DefaultLifecycleObserver {
|
||||
private var timer: Timer? = null
|
||||
private var currentTime = 0
|
||||
private var listener: CountdownListener? = null
|
||||
private var lastCallTime: Long = 0
|
||||
|
||||
fun getCurrentSeconds(): Int {
|
||||
return currentTime
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
interface CountdownListener {
|
||||
fun onTick(seconds: Int)
|
||||
fun onFinish()
|
||||
}
|
||||
|
||||
fun setCountdownListener(listener: CountdownListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
// 安全啟動/重置方法(帶防抖)
|
||||
fun startOrReset(seconds: Int) {
|
||||
synchronized(this) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastCallTime < 200) {
|
||||
return
|
||||
}
|
||||
lastCallTime = now
|
||||
|
||||
timer?.cancel()
|
||||
currentTime = seconds
|
||||
listener?.onTick(currentTime)
|
||||
|
||||
timer = Timer().apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
if (currentTime > 0) {
|
||||
currentTime--
|
||||
listener?.onTick(currentTime)
|
||||
} else {
|
||||
stop()
|
||||
listener?.onFinish()
|
||||
}
|
||||
}
|
||||
}, 1000, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
synchronized(this) {
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
stop()
|
||||
lifecycle.removeObserver(this)
|
||||
listener = null
|
||||
}
|
||||
}
|
||||
148
app/src/main/java/com/unibuy/v2/vm/CartViewModel.kt
Normal file
148
app/src/main/java/com/unibuy/v2/vm/CartViewModel.kt
Normal file
@ -0,0 +1,148 @@
|
||||
package com.unibuy.v2.vm
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.unibuy.smartdevice.MyApp
|
||||
import com.unibuy.smartdevice.R
|
||||
import com.unibuy.smartdevice.structure.BuyStructure
|
||||
import com.unibuy.smartdevice.structure.SlotStructure
|
||||
import com.unibuy.v2.UnibuyHelper
|
||||
import com.unibuy.v2.bean.CartOperationResult
|
||||
|
||||
/**
|
||||
* 多品項購物車 ViewModel(自 Luzui v2 移植)。
|
||||
*
|
||||
* 與 Luzui 版差異:
|
||||
* - 移除 updateLocalGoods():Luzui 用 SlotField.FLOWER + SlotsDao 在出貨後扣本地庫存,
|
||||
* TaiwanStar 為 VMC 貨道機、扣庫存由既有 DispatchDialog / MQTT 結案負責,購物車不重複處理。
|
||||
* - 寫死中文改用 R.string.cart_max_items。
|
||||
*/
|
||||
class CartViewModel : ViewModel() {
|
||||
private val _cartItems = MutableLiveData<List<SlotStructure>>(emptyList())
|
||||
val cartItems: LiveData<List<SlotStructure>> = _cartItems
|
||||
val freeGoods: MutableList<BuyStructure> = mutableListOf()
|
||||
|
||||
/** 把購物車內容寫入 MyApp.buyList,交棒給既有結帳鏈(BuyDialog → 付款 → DispatchDialog → MQTT)。 */
|
||||
fun convertToBuyStructure() {
|
||||
MyApp.getInstance().apply {
|
||||
buyList.clear()
|
||||
// 1. 免費商品(來店禮;General 不做時恆空)
|
||||
buyList += freeGoods
|
||||
// 2. 購物車商品
|
||||
buyList += UnibuyHelper.getCartViewModel()
|
||||
.getCartItemsSnapshot()
|
||||
.map { cartItem ->
|
||||
BuyStructure(cartItem.field, cartItem.slot, cartItem.product, cartItem.cartSelectedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 購物車最多 8 件 */
|
||||
fun hasMinimumCartItems(): Boolean {
|
||||
return getAllCartItems().totalCount >= 8
|
||||
}
|
||||
|
||||
fun getCartItemsSnapshot(): List<SlotStructure> = _cartItems.value ?: emptyList()
|
||||
|
||||
// 添加商品到購物車(以 field + slot + productID 唯一識別)
|
||||
fun addToCart(slot: SlotStructure): CartOperationResult {
|
||||
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
|
||||
val item = currentItems.find {
|
||||
it.field == slot.field &&
|
||||
it.slot == slot.slot &&
|
||||
it.product?.productID == slot.product?.productID
|
||||
}
|
||||
|
||||
return if (item == null) {
|
||||
if (hasMinimumCartItems()) {
|
||||
ToastUtils.showLong(R.string.cart_max_items)
|
||||
return createResult(null)
|
||||
}
|
||||
slot.cartSelectedCount = 1
|
||||
val newList = currentItems.toMutableList().apply { add(slot) }
|
||||
_cartItems.value = newList
|
||||
createResult(slot)
|
||||
} else if (item.cartSelectedCount >= item.count) {
|
||||
ToastUtils.showLong(R.string.cart_out_of_stock)
|
||||
createResult(item)
|
||||
} else {
|
||||
if (UnibuyHelper.getCartViewModel().hasMinimumCartItems()) {
|
||||
ToastUtils.showLong(R.string.cart_max_items)
|
||||
createResult(item)
|
||||
} else {
|
||||
item.cartSelectedCount++
|
||||
_cartItems.value = currentItems
|
||||
createResult(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 從購物車減少一件
|
||||
fun removeFromCart(slot: SlotStructure): CartOperationResult {
|
||||
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
|
||||
val item = currentItems.find {
|
||||
it.field == slot.field &&
|
||||
it.slot == slot.slot &&
|
||||
it.product?.productID == slot.product?.productID
|
||||
} ?: return CartOperationResult(0, 0, 0, 0)
|
||||
|
||||
return if (item.cartSelectedCount > 1) {
|
||||
item.cartSelectedCount--
|
||||
_cartItems.value = currentItems
|
||||
createResult(item)
|
||||
} else {
|
||||
ToastUtils.showLong(R.string.cart_cannot_decrease)
|
||||
createResult(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空購物車
|
||||
fun clearCart(): CartOperationResult {
|
||||
val result = createResult(null)
|
||||
_cartItems.value = emptyList()
|
||||
return result.copy(
|
||||
totalPrice = 0,
|
||||
totalCount = 0,
|
||||
currentItemCount = 0,
|
||||
currentItemPrice = 0
|
||||
)
|
||||
}
|
||||
|
||||
fun createResult(item: SlotStructure?): CartOperationResult {
|
||||
val currentCount = item?.cartSelectedCount ?: 0
|
||||
val currentPrice = (item?.product?.realPrice ?: 0) * currentCount
|
||||
val currentItems = _cartItems.value ?: emptyList()
|
||||
|
||||
return CartOperationResult(
|
||||
totalPrice = currentItems.sumOf { it.cartSelectedCount * (it.product?.realPrice ?: 0) },
|
||||
totalCount = currentItems.sumOf { it.cartSelectedCount },
|
||||
currentItemCount = currentCount,
|
||||
currentItemPrice = currentPrice
|
||||
)
|
||||
}
|
||||
|
||||
// 移除整筆商品
|
||||
fun removeItemFromCart(slot: SlotStructure): CartOperationResult {
|
||||
val currentItems = _cartItems.value?.toMutableList() ?: mutableListOf()
|
||||
val productIdToRemove = slot.product?.productID
|
||||
if (productIdToRemove != null) {
|
||||
currentItems.removeAll { item ->
|
||||
item.product?.productID == productIdToRemove
|
||||
}
|
||||
}
|
||||
_cartItems.value = currentItems
|
||||
return getAllCartItems()
|
||||
}
|
||||
|
||||
fun getAllCartItems(): CartOperationResult {
|
||||
val currentItems = _cartItems.value ?: emptyList()
|
||||
return CartOperationResult(
|
||||
totalPrice = currentItems.sumOf { it.cartSelectedCount * (it.product?.realPrice ?: 0) },
|
||||
totalCount = currentItems.sumOf { it.cartSelectedCount },
|
||||
currentItemCount = 0,
|
||||
currentItemPrice = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/color/color_input_mode_bg.xml
Normal file
5
app/src/main/res/color/color_input_mode_bg.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/primary" />
|
||||
<item android:color="@android:color/transparent" />
|
||||
</selector>
|
||||
5
app/src/main/res/color/color_input_mode_text.xml
Normal file
5
app/src/main/res/color/color_input_mode_text.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/white" />
|
||||
<item android:color="@color/black" />
|
||||
</selector>
|
||||
5
app/src/main/res/color/switch_thumb_selector.xml
Normal file
5
app/src/main/res/color/switch_thumb_selector.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="@color/primary" />
|
||||
<item android:color="@color/light_gray" />
|
||||
</selector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user