Compare commits

...

95 Commits

Author SHA1 Message Date
TaiwanStar Developer
4afc3a59d4 refactor(ui): 優化支付選項對話框之版面配置與應付金額顯示樣式 2026-06-16 17:25:12 +08:00
sky121113
309518f3ca [FEAT] 選擇支付方式頁顯示件數與應付金額,並防止來店禮重複掃描
1. dialog_buy.xml:將原本隱藏的金額列改為可見,新增「共 X 件」件數顯示與「應付金額」標題,金額靠右呈現。
2. BuyDialog.java:新增 refreshAmountBar() 統一刷新件數與應付金額,onCreate 與 SET_PRICE 事件皆改走此方法。
3. BuyDialog.java:抽出 getOriginalPrice()(未折扣原價)與 getTotalCount()(總件數,排除免費商品),getTotalPrice() 改用 getOriginalPrice() 並沿用折抵後至少 1 元保護。
4. BuyDialog.java:來店禮折抵改以「實際省下的金額」顯示(NT$XXX (已折抵 NT$YY)),取代原本 mode 2 顯示折扣乘數 0.8 的難懂呈現。
5. BuyDialog.java:防呆——本筆已套用來店禮(freeGiftId 非空)即隱藏來店禮入口按鈕,禁止重複掃描覆蓋/疊加折扣。
6. strings.xml:新增 pay_item_count、pay_payable_amount、pay_discounted_format 字串。
7. app/build.gradle:verPatch 1 → 2。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:01:45 +08:00
sky121113
c986b69e16 [FIX] 修復出貨中途加購倒數疊畫面、結案欄位被洗成 0、對話框誤判待機,並修打包腳本自殺
1. 結案上報穩定化:TransactionFinalizeBuilder 新增 captureOrderInfo() 快照(flowType/總金額/找零/點數),DispatchDialog 進入出貨時(isCmuh→42 覆寫後)擷取;build() 與「通行碼(5)不上報」判斷改用快照(effectiveXxx/getOrderFlowType),避免出貨中途全域 flowInfo 被洗回 0 導致 payment_type/total 落地成 0「未知」或通行碼被誤報。
2. 加購倒數疊畫面:DispatchDialog.onCreate 與 CheckoutAccessCodeDialog.changeDispatch 進入出貨前統一補送 STOP_COUNTDOWN,關閉 FontendActivity 的 40 秒加購倒數,避免出貨/取貨倒數中途到期觸發 BUY_DIALOG_OPEN、支付畫面疊在出貨畫面上跳出(通行碼、TapPay 等成功路徑原本未送)。
3. 移除加購功能殘留:BuyDialog 刪除 buttonAddProduct 監聽與 gotoAddProduct(),dialog_buy.xml 移除加購按鈕。
4. 待機誤判修復:FontendActivity.onUserInteraction 一併 resetIdleTimer(對話框觸控不會觸發 dispatchTouchEvent),idleRunnable 以全域最後互動時間二次確認,剩餘時間不足才真正踢回待機,避免操作付款/詳情等對話框時被誤判閒置。
5. 打包腳本修復:compiler-apk.md 的 daemon-kill 段把 pgrep/pkill 比對字串拆開拼接並排除自身 PID,修正背景任務情境下腳本命令列含完整 GradleDaemon class 字串導致 pgrep 比對到自己、pkill 自殺、編譯 exit 144 未啟動的問題。
6. 升版號:build.gradle verMinor=2、verPatch=1(S9XYU=21_02_1_R/210201)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:32:13 +08:00
sky121113
9a4f68455d [FIX] 修復發票統編「下一步」失效與待機死結,並調整銷售平台資訊欄
1. 修復發票統編對話框「下一步」按鈕未綁定點擊事件,導致統編→電話兩步驟流程卡死、統編永遠送不到後台;新增 changeNextStep() 驗證統編長度並切換至電話輸入步驟。
2. 修復購物車殘留造成「永遠忙碌→永不待機→永不清車」死結:忙碌判定移除「購物車非空」條件,進行中狀態改由對話框開啟(isShowDialog)與感應支付(isNfcPaymentActive)涵蓋。
3. 銷售平台移除上、下方灰色資訊欄(linearLayout5、bottom_bar),改於 applySaleOverlayVisibility 一律隱藏。
4. 機台資訊列新增時間顯示(置於網路前),並以獨立的 ClockTickScheduler 每秒刷新,與 10 秒上報重活迴圈分離,避免將上報/重啟判斷拉到每秒。
5. 版號遞增 verPatch 97→98。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:26:07 +08:00
TaiwanStar Developer
dd87f1ca6e Merge remote-tracking branch 'origin/feat/general-cart-and-payment' into ui-design 2026-06-16 13:52:36 +08:00
TaiwanStar Developer
eae0342ab8 refactor(ui): 優化商品詳情對話框與列表樣式,並抽離共用文字尺寸配置 2026-06-16 13:51:57 +08:00
sky121113
c7bc554eca [FIX] 修復掃碼支付(玉山)結案前購物車被清空導致「無商品紀錄」
1. 根因:order.items 在結案 build() 當下才即時讀 live buyList,掃碼支付(玉山)流程較慢(掃碼+玉山API約20s),結案前 buyList 會被取消/倒數路徑清空,造成 order.items 變空陣列(無商品紀錄),但出貨其實成功(dispense 有商品)。
2. TransactionFinalizeBuilder 新增 itemsSnapshot 商品明細快照欄位與 captureItems() 方法;buyList 為空時不覆寫既有快照。
3. 抽出共用 buildItemRecords() helper,captureItems() 與 build() fallback 共用。
4. build()/buildOrderPayload() 改為優先採用快照,無快照時才 fallback 讀 live buyList。
5. DispatchDialog onCreate 建立 finalizeBuilder 後立即 captureItems(),此時 buyList 必完整。
6. 升版 verPatch 95 → 96 (S9XYU versionName 21_01_96_R / versionCode 210196)。

正常情況快照==live,逐字等價;僅在 buyList 被清空的 bug 情境修正。純 App 端,不影響後台與線上 main。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:13:46 +08:00
TaiwanStar Developer
647e72aa9b refactor(ui): 優化支付選項對話框樣式與倒數計時文字 2026-06-16 11:30:51 +08:00
sky121113
89c7d04f7a [FIX] 修復遠端更新設定誤刪廣告,並完善機台資訊列顯示
1. [FIX] persistCurrentSettings() 原以 deleteAll() 清空整張 SettingsDao,
   連坐刪除廣告(VideoEoxPlayer)等持久資料;改為僅 deleteWhere 逐筆刪除
   接下來要重寫的 4 個設定 key(Machine/Esunpay/GreenInvoice/Greenpaysetting)。
   修正「遠端更新設定→重啟 App 後廣告消失,須再更新廣告才出現」問題。
2. [FEAT] 機台資訊列左側改顯示「序號 + 版本號(Ver versionName)」。
3. [FEAT] 機台資訊列網路訊號改為動態:依 WiFi RSSI / 行動網路 dBm 切換訊號格數
   icon 與文字(無網路/WiFi/行動/有線),並於 SET_NETWORK 與輪詢時即時更新。
4. [REFACTOR] 進站後門觸控由舊 textNetwork 遷移至機台資訊列的「序號+版本號」元素
   (舊系統列已隱藏不再接收觸控)。
5. [STYLE] activity_fontend 機台資訊名稱樣式調整(灰色/14sp/透明度 0.7)。
6. 版號 verPatch 93 → 94。
2026-06-16 11:09:45 +08:00
TaiwanStar Developer
f700881788 Merge remote-tracking branch 'origin/feat/general-cart-and-payment' into ui-design 2026-06-16 09:53:26 +08:00
TaiwanStar Developer
7ec2d3916a feat(ui): 實作首頁機台資訊列,包含網路、溫度及多國語言切換功能 2026-06-16 09:51:47 +08:00
sky121113
6caaf0890e [FEAT] 交易生命週期上報(pending/failed)與發布 v93
1. app/build.gradle:verPatch 91 → 93(高於機台已 OTA 的 v92,確保修正上得了機台)。
2. structure/ReportFlowInfoStructure.java:新增 mqttFlowId 欄位,作為一次交易的 MQTT 流水號,pending → completed/failed 全程共用,與取貨碼/通行碼用的 reportFlowId 分離。
3. structure/mqtt/TransactionFinalizePayload.java:OrderPayload 新增 status 欄位(pending/completed/failed/abandoned),未帶時後台預設 completed,確保線上 main 機台行為不變。
4. tools/TransactionFinalizeBuilder.java:改用 mqttFlowId(無則生成並回存)、新增可變 status/payment_status 與失敗代碼;新增 publishPending()/publishFailed() 靜態上報;以 shopping_mode == "basic" 判斷是否上報(晟崴 employee_card/中國醫 pickup_sheet 無實際付款不送);pending/failed 不開立發票。
5. ui/dialog/BuyDialog.java:changeCheckout 進入付款時為實際付款方式(1/2/3/4/9/30/70)生成新 mqttFlowId 並上報 pending(進付款一律留底),先清除上一筆避免 flow_id 撞單。
6. ui/dialog/CheckoutNfcCardDialog、CheckoutNfcRechargeDialog、CheckoutNfcPhoneDialog:刷卡機回非 0000 或空電文的失敗分支補上 publishFailed(代碼),救回原本默默消失的失敗單。
7. ui/dialog/CheckoutEsunPayDialog、CheckoutTapPayDialog、CheckoutLinePayDialog:掃碼/官方支付 TRANSACTION_FAILED 分支補上 publishFailed。

過渡期保護:晟崴/中國醫 跑 main 分支、無實際付款,且後台四鐵律(無 status 預設 completed、終態冪等、pending 才轉移、abandoned 只掃 pending)保證其後台資料逐字不變。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:54:42 +08:00
sky121113
9bfb429392 [FIX] 修復刷卡機交易成功卻不繼續出貨流程
1. 修正 DevNFCPayController.execute():刷卡機(NFC/悠遊卡/手機/信用卡)回傳的 404 byte 電文本身已含 responseCode(成功為 0000),直接以 devNFCPay.read() 解析該段並送出 RESPONSE。
2. 移除先前誤加的「再以 readListen 等待第二段電文」邏輯:刷卡機僅送一段,等不到第二段導致倒數逾時、RESPONSE 從未送達對話框、changeDispatch() 無法觸發,造成「刷卡機顯示成功卻不出貨」。
3. 保留 isShowDialog / isShuttingDown 生命週期防護;此 controller 為所有感應付款共用,一次修復全部感應類付款。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:54:19 +08:00
TaiwanStar Developer
317dec1084 feat(ui): 優化取貨碼相關對話框與列表之 UI 排版 2026-06-15 16:43:58 +08:00
TaiwanStar Developer
be3c25bbf5 Merge remote-tracking branch 'origin/feat/general-cart-and-payment' into ui-design 2026-06-15 16:24:58 +08:00
sky121113
ff65559b4c [FEAT] 電子發票改由後台開立:機台只隨 finalize 上報發票輸入
1. TransactionFinalizePayload 新增 InvoicePayload 物件(status/amount/carrier_id/business_tax_id/love_code 等),讓 finalize 一併帶上發票開立輸入。
2. TransactionFinalizeBuilder:有發票需求(invoiceInfo 非空)時,從 InvoiceData 組發票輸入並固定 status=pending 帶進 finalize;機台不再判定開立結果。
3. DispatchDialog:移除機台直連綠界開票,改為進出貨流程僅上報;發票由後台收到 finalize 後建 pending 並去綠界開立(避免機台網路不確定性與等待)。
4. ReportInvoiceInfoStructure:保留 relateNumber 欄位(冪等鍵相關)。
5. 開票邏輯不放在即將淘汰的 HttpAPI(先前暫放 StarCloudAPI 的機台開票方法亦已移除)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:18:11 +08:00
TaiwanStar Developer
092c0e9a16 feat(ui): 調整系統設定頁面排版與玉山支付模組設定
- 將「購物方式」資訊移至 Header 區域並移除身份說明文字

- 將機台貨道顯示方式改為 TableLayout 並設為唯讀狀態,隱藏儲存按鈕

- 為大卡片容器增加 app:cardUseCompatPadding=true 並取消父層剪裁,修復卡片陰影遭裁切問題

- 修改玉山支付模組,將台灣Pay子標籤改為「玉山支付模組」,並徹底移除街口支付、LinePay、全支付、PiPay 等子項目及其連動邏輯

- 統一設定項目文字顏色,並新增相關支付圖示資源
2026-06-15 16:07:05 +08:00
sky121113
7875db4c7d [FIX] 升版 v91(OTA 版號遞增,內容同 v90 支付 MQTT finalize 修正)
1. build.gradle verPatch 90 → 91,供實體機台 OTA 升級。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:03:17 +08:00
TaiwanStar Developer
b32f76c753 style: 調整結帳對話框交易金額冒號與按鈕顏色
- 將Line Pay、NFC卡片、NFC手機、NFC加值對話框中的交易金額冒號改為全形

- 調整NFC卡片對話框取消按鈕文字色為onWarning,修改支付方式背景色為gray
2026-06-15 13:57:41 +08:00
sky121113
3fd34becb6 [FIX] 支付成功改走 MQTT finalize、修正卡在倒數不出貨;升版 v90
真因:各支付 Dialog 成功後呼叫已停用的 B600(reportFlowInfo),再靠 B600 的 HTTP 回應回呼 REPORT_FLOW 才進 DispatchDialog;B600 已失敗 → 永遠不出貨,卡到倒數 COUNTDOWN_CANCEL(症狀:刷卡機顯示成功但流程不繼續)。

1. CheckoutNfcCardDialog / CheckoutNfcRechargeDialog:支付成功(0000)不再呼叫 B600,直接 changeDispatch() 進 DispatchDialog,由 MQTT publishTransactionFinalize 統一上報(payload 已含 invoice_info,B600/B650 皆不需要)。
2. CheckoutNfcPhoneDialog:同上,且進 dispatch 前覆寫 payment_type=10(手機支付);刷卡機 NCCC 電文無法區分手機錢包/實體卡,依使用者所選按鈕分類。
3. CheckoutEsunPayDialog / CheckoutLinePayDialog / CheckoutTapPayDialog:交易成功直接 changeDispatch;交易失敗改為提示「交易失敗」並返回購買頁(changeBuy),不再卡住或誤出貨。
4. LinePay 維持 payment_type=70(官方直連),後台另補定義;TapPay 子項 30~34 沿用。
5. 舊 CheckoutCashDialog/CashPayDialog 為 legacy,basic shopping_mode 不使用(現金走 CashPayDialogGeneral),本次未動。
6. build.gradle 版號 verPatch 89 → 90。

注意:後台 star-cloud 需同步新增 payment_type 10(手機支付)/70(LINE Pay 官方) 與翻譯(另一 repo,另行提交)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 13:56:56 +08:00
sky121113
a99f7b0572 [FEAT] 雲端日誌分級(LogLevel)與除錯雜訊清理
1. MyApp 新增 logLevel 欄位(預設 INFO),由雲端 B014 LogLevel 下發並回送,現場除錯硬體可設 DEBUG。
2. Logs.debug()/ignore() 改為僅在 LogLevel=DEBUG 時寫入 debug.txt;logcat 一律照印不受影響。
3. Logs 新增單行寫檔上限 MAX_LINE(1000),超長 JSON/payload dump 截斷並保留原始長度,避免淹沒 debug.txt。
4. 將 HttpAPI/HttpTapPay/DevXinYuan/DevXinYuanController/DevNFCPayController/BuyDialog/DispatchDialog 大量 "AAAA…/bbbb…" 開發雜訊 logs.info 改為 logs.debug 並補上具體訊息。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 13:56:32 +08:00
sky121113
7cb9039a50 [FEAT] 商品詳情頁接 B012 t060v03_i18n 規格多語(純文字含換行原樣呈現)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:50:00 +08:00
sky121113
d7bef7b827 [FIX] 修正直接購買(購物車版)未跳購物車品項頁的真因;升版 v89
1. 真因:DialogProductDetail 取 SaleFlowHandler 時用 onCreate 的 getContext(),那是 AlertDialog 包裝過的 ContextThemeWrapper,instanceof FontendActivity 為 false,導致 openCartList 靜默 return(一般版因 requestBuy 有 new BuyDialog 後路才照常開支付頁,故僅購物車版受影響)。
2. 修法:建構子保存原始傳入的 Context(真正的 FontendActivity)為 hostContext,openCartList/requestBuy 改用 hostContext 做 instanceof 與取 SaleFlowHandler。
3. 升版號 verPatch 88 → 89。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:24:28 +08:00
sky121113
1c9022cc5e [FIX] 修正商品格線右緣觸控失效與直接購買未跳購物車頁;升版 v88
1. 修正 fragment_vmc.xml 商品格線 recyclerBuy 在 ConstraintLayout 缺水平約束(原 match_parent 僅綁上下)導致右側佈局/觸控異常:改為 0dp + start/end 綁 parent 確保全寬與右緣可點。
2. RecyclerVmcBuyListAdpter 讓整張商品卡片皆可點開詳情頁(原僅圖片可點),避免點到名稱/價格區無反應。
3. 商品詳情頁「直接購買」(購物車版)改為先切換至購物車品項頁、再關閉詳情頁,避免關閉對話框與 fragment 切換時序互相干擾導致未跳頁。
4. 升版號 verPatch 87 → 88。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 11:13:36 +08:00
sky121113
df96f33d5f [FEAT] 商品詳情頁直接購買分流與數量帶入;升版 v87
1. 詳情頁「直接購買」依購物車開關分流:購物車開啟時以詳情頁選定數量加入購物車後前往購物車列表頁(不直接結帳);購物車關閉(一般版)維持單品直購彈 BuyDialog 選付款。
2. 修正數量未帶入問題:加入購物車與直接購買統一走 addSelectedQuantityToCart(),依詳情頁數量逐件加入,點 N 件購物車即為 N 件。
3. 升版號 verPatch 86 → 87。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:44:19 +08:00
sky121113
bd23c31ba1 [FEAT] 商品詳情頁接線:點商品開詳情、依購物車開關呈現按鈕
1. 新增 DialogProductDetail:把原本只有佈局的商品詳情頁接活,顯示商品圖、多語名稱、貨道、庫存、數量與金額小計。
2. 依雲端 DevSet.ShoppingCar 呈現按鈕:購物車開啟顯示「數量調整+加入購物車+直接購買」;購物車關閉(一般版)隱藏「數量調整+加入購物車」,只留「直接購買」且數量固定 1。
3. 加入購物車以選定數量逐件加入(沿用 CartViewModel 庫存/8 件上限防呆);直接購買清空 buyList 放入本商品後走既有 BuyDialog 選付款。
4. BasicSaleFlow.handleProductSelected 改為點商品一律先彈出詳情頁。
5. RecyclerVmcBuyListAdpter 商品卡右下角「+」(buttonQuickAdd) 接線:僅購物車開啟時顯示並快速加購一件,關閉時隱藏。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:16:29 +08:00
sky121113
0900dc20f9 Merge branch 'feat/general-cart-and-payment' into ui-design 2026-06-15 09:47:41 +08:00
sky121113
35af9fec8f [FEAT] 升版 v85 並打包 S9XYU(多語系 11 語+後台 API 對接+ui-design 合併)
1. verPatch 84 → 85,對應自 v84 以來累積的多語系擴充(法德西)、後台 B014 LangSet/B012 i18n 對接、ui-design UI 改版合併與 10 語系字串對齊等變更,供 OTA 版號對齊。
2. 產出 S9XYU Debug APK:versionName 21_01_85_R、versionCode 210185。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:36:57 +08:00
TaiwanStar Developer
eb0b9c8271 feat(ui): 遷移商品詳情對話框佈局並修復編譯資源錯誤
- 加入 dialog_product_detail.xml

- 移除本專案未支援的 RoundLinearLayout 與 RoundTextView 等自訂元件與屬性

- 替換缺漏的圖示、文字與顏色資源以維持正常編譯
2026-06-15 09:35:53 +08:00
sky121113
8e02e7adeb [FEAT] 補齊 10 語系合併後缺漏字串,全語系對齊主檔
1. ui-design 合併後主檔 values/strings.xml 新增的 15 個顧客可見字串(員工卡支付、下載/上傳同步狀態、購物車數量上限、商品出貨成功、櫃門取貨、確認取貨倒數、機器巡檢等),補譯進全部語系。
2. 英/簡中/韓/越/泰/印尼/法/德/西 各補 15 條;日文補 12 條(原已有 back/syncing/sync_completed)。
3. 補齊後 10 個語系皆與主檔 0 缺漏,XML 全部 well-formed,這批新 UI 字串不再 fallback 中文。

註:機器翻譯品質,customer-facing 重點詞(員工卡、載具/統編等)建議母語者校對。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:30:48 +08:00
sky121113
a80bb4dd85 Merge remote-tracking branch 'origin/ui-design' into feat/general-cart-and-payment
# Conflicts:
#	app/src/main/java/com/unibuy/v2/adapter/CartOperationAdapter.kt
2026-06-15 09:21:49 +08:00
sky121113
92553d96d2 [FEAT] 多語系擴充法德西並對接後台 B014/B012 多語 API
1. 新增法文(values-fr)、德文(values-de)、西班牙文(values-es)三語系資源,各 286 條以 en 為範本,App 支援語系達 11 種;LanguageHelper 的 SUPPORTED_LANGUAGES、getDisplayName、canonicalize 一併補上 fr/de/es。
2. 語言選單改由後台決定:StarCloudAPI.saveSettings 解析 B014 新增的 LangSet(Languages 清單 + Default),FontendActivity 選單改讀 LanguageHelper.getMachineLanguages(),只顯示該機台後台啟用的語系;未取得 B014 時退回全部支援語系。首次(使用者未手動選過)自動套用後台預設語系。
3. 商品名稱改讀 B012 新增的 t060v01_i18n 多語 map:StarCloudAPI.saveProducts 解析並帶入每個商品;ProductStructure.getLocalizedName fallback 鏈改為 i18n map 對應語系 → map 的 zh_TW → 舊 _en/_jp → 中文主值,韓/泰/越等語商品名可正確顯示。
4. 語系代碼雙向轉換:後台底線格式 zh_TW 與 App 標準 zh-TW 互轉(LanguageHelper.toBackendLocale 與 canonicalize)。
5. 零 DB 風險持久化:機台語系(machineLanguages/defaultLanguage)與商品多語 blob 均存於既有 MemoDao(比照 shopping_mode),不動 products 表 schema、不升 DB 版本,避免破壞性 onUpgrade 清除現場機台綁定資料。

註:商品規格 spec 後台已送 t060v03_i18n 但 App 尚無顯示 UI 故未接;舊 HttpAPI 解析路徑未加 i18n,會 graceful fallback 至 _en/_jp。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:15:53 +08:00
TaiwanStar Developer
c7134fe712 feat(ui): 更新發票選擇對話框 UI 佈局為垂直排列
- 將發票選擇對話框 (InvoiceTypeDialog) 根佈局改為 ConstraintLayout,並加入半透明深色背景。
- 將發票選項(手機條碼、捐贈發票、公司統編)改為垂直排列,並移除副標題文字。
- 新增 invoice_button_height 定義發票選項卡片的高度。
- 調整返回按鈕位置,並優化倒數計時文字顯示邏輯。
2026-06-15 09:03:37 +08:00
TaiwanStar Developer
a10a51ec10 UI: 優化商品卡片結構與樣式
- 新增快速新增按鈕
- 卡片版面區分為上下區塊,統一由最外層控制邊距
- 調整商品圖片為 1:1,並帶有 padding 白框效果
- 貨道與庫存資訊移至左上角垂直排列,分別套用圓角背景色
- 修正金額為 0 時不顯示的問題
- 售罄遮罩覆蓋全卡片,文字改為白色「售完」
2026-06-12 17:27:12 +08:00
TaiwanStar Developer
0a19e965a8 feat(cart): 優化購物車品項數量調整邏輯與介面顯示
- 新增單一品項與總數量的調整限制邏輯,達上限時按鈕切換為 disabled 狀態

- 移除數量調整按鈕的陰影跳動與裁切問題

- 新增增加、減少、刪除按鈕的水波紋點擊反饋

- 在品項金額後方新增「庫存xx件」資訊,並統一樣式

- 購物車達數量上限時,顯示「已達數量上限」警告文字
2026-06-12 13:35:39 +08:00
TaiwanStar Developer
e331e399bc feat(ui): 統一來店禮與通行碼的對話框樣式與輸入模式切換
- 參照取貨碼對話框 (dialog_pickup_code_fontend) 重新設計 dialog_free_gift_code.xml 與 dialog_access_code.xml
- 於 CheckoutFreeGiftCodeDialog 與 CheckoutAccessCodeDialog 加入鍵盤輸入與掃描條碼之模式切換邏輯 (btnModeKeypad / btnModeScan)
2026-06-12 12:16:08 +08:00
sky121113
5b8a0d7cf6 [FEAT] 新增遠端 update_settings MQTT 指令並收斂系統設定為唯讀;升版 v84
1. MqttService 新增 update_settings 指令分支:收到後重抓系統設定(B014),
   不串接商品/廣告同步,僅更新設定本身。
2. 忙碌防呆:交易/購物車/對話框進行中(getBusyReason 非空)時拒收並回 failed
   ACK,後台 RemoteCommand 約 1 分鐘 timeout 後可重發。
3. 重連時序修正:先發成功 ACK,再延遲 1.5 秒重連 MQTT,避免重連把送指令的
   連線斷掉導致 ACK 發不出去(涵蓋 token 輪換情境)。
4. StarCloudAPI.downloadSetting 新增可選 reconnectMqtt 參數(saveSettings 同步),
   綁定 UI 流程維持 true、update_settings 路徑傳 false 由呼叫端控制重連時機;
   無參數多載維持原行為,向下相容。
5. SystemSetActivity 收斂:機台端系統設定全面唯讀,唯一可編輯項為硬體貨道
   (彈簧/履帶,僅系統方);移除現金總開關連動,回寫雲端只送 is_spring_slot_*。
6. app/build.gradle verPatch 升至 84。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:01:50 +08:00
TaiwanStar Developer
2e70c16aa3 UI: 更新取貨碼視窗樣式與佈局
- 將「請輸入取貨碼」標題與副標調整為單一標題
- 調整視窗上方 padding 與標題間距
- 實作「鍵盤輸入」與「掃描條碼」的分段控制按鈕,包含背景、文字顏色變化與陰影效果
- 按鈕群組移除外框並增加陰影
- 返回按鈕置中並修改文字為「取消xx秒」
- 「輸入完成」按鈕修改為「確認」
- 掃描條碼圖片等比例填滿容器
2026-06-12 11:49:14 +08:00
sky121113
318bfde1fb [FEAT] 多語系擴充至 8 語並改用語言選單;升版 v83
1. 新增 5 個語系資源:簡體中文(values-zh-rCN)、韓文(values-ko)、越南文(values-vi)、泰文(values-th)、印尼文(values-in,採 Android 舊代碼 in),各 285 條以 en 已翻譯集合為範本,缺漏字串自動 fallback 回預設繁中。
2. LanguageHelper 重寫支援 8 語完整 locale:SUPPORTED_LANGUAGES 清單、canonicalize() 相容舊碼(tw/jp)並帶 region、buildLocale() 處理 zh-TW/zh-CN 與印尼資源代碼、getDisplayName() 以各語自身文字顯示。
3. 商品名稱多語以 toProductLang() 收斂:僅 en/ja 取對應翻譯,其餘語言(簡中/韓/越/泰/印尼)一律 fallback 中文主值(後台目前僅中/英/日)。
4. FontendActivity 語言入口由 3 顆內聯鈕改為單一 🌐 鈕開啟 AlertDialog 單選選單(showLanguagePicker),隱藏 En/Ja 舊鈕;切換後重綁定入口。
5. 新增字串 select_language 並補齊全 8 語系翻譯。
6. 升版號 verPatch 82 → 83。

註:泰文需於真機驗證系統字型支援;5 種新翻譯為機器品質,customer-facing 字串建議母語者校對。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:30:22 +08:00
sky121113
fdf02189fb [FEAT] 解除 App 多語系隱藏並收斂商品名稱多語邏輯;升版 v82
1. FontendActivity 重新顯示中/En/日語言切換鈕(移除 visibility=gone),點擊監聽改傳統一代碼 zh/en/ja。
2. LanguageHelper 新增 normalizeCode()/normalizedLang(),將歷史 tw、Locale zh、日文 jp 全部收斂為 zh/en/ja 一套,changeLanguage 一律儲存正規化代碼。
3. ProductStructure 新增 getLocalizedName(),依目前語言取商品名稱,英/日未翻譯時自動 fallback 回中文主值,作為商品名稱顯示的單一來源。
4. 各 adapter 改用 getLocalizedName():RecyclerVmcBuyListAdpter、CartOperationAdapter、RecyclerDialogBuyListAdpter(原被註解)、RecyclerProductListAdpter 與 RecyclerProductSearchAdpter(原永遠顯示中文),刪除各自重複的語言 switch。
5. applySavedLanguage() 集中至 base class AppCompatActivityAbstract.onCreate(),所有 Activity 於 inflate 前統一套用語言設定。
6. 升版號 verPatch 81 → 82。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 09:59:47 +08:00
sky121113
73dee29485 [FIX] 修復購物車倒數返回 VMC 時的 onSaveInstanceState 崩潰
1. FontendActivity.switchFragment:commit 前檢查 Activity 是否已達 STARTED,非前景則跳過並記 log;並將 .commit() 改為 .commitAllowingStateLoss(),作為背景切換的最終防線(切換僅為 Fragment hide/show,資料在 ViewModel,state loss 無實害)。
2. FontendActivity.openFragment:同步加上 lifecycle 防呆,避免背景狀態下 commitNow() 丟出 IllegalStateException。
3. ShoppingCartFragment.onFinish:原以 isAdded() 判斷無法擋住 onSaveInstanceState 後的 commit,改以 lifecycle 是否達 STARTED 為準,避免 180 秒倒數於背景觸發 backToVmc 導致崩潰。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 09:35:20 +08:00
sky121113
a33c0c97bd Merge remote-tracking branch 'origin/ui-design' into feat/general-cart-and-payment 2026-06-12 08:47:50 +08:00
sky121113
39245a0608 [FIX] TapPay 圖示檔名改小寫修正 aapt2 命名違規;升版 v81
- ui-design 合併帶進的 TapPay30~34.png 用大寫檔名,違反 Android
  resource「只能 a-z/0-9/底線」規則,導致 mergeResources 失敗。
  改名為 tappay30~34.png(git mv 保留歷史)。圖示目前未被引用,
  改名不影響功能,保留供日後接線。
- verPatch 80→81

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 08:47:43 +08:00
TaiwanStar Developer
f6028ffb80 UI: 優化購物車頁面排版與樣式
1. 商品列表滿版顯示,品項數量移至最上方並縮小字級。
2. 小記與總額樣式調整:小記字級縮小且改為黑色,總額改為主題色,並對齊底部。
3. 底部操作區重新排版:左右比例改為 4:6,對齊底部水平線。
4. 按鈕樣式更新:
   - 「繼續選購」:改為 outline button。
   - 「清空購物車」:改為 warning 顏色的 outline button,移除計時括號並縮小字級。
   - 「前往結帳」:寬度填滿容器的按鈕。
5. 背景與陰影調整:底部操作區增加陰影並移除外框;商品區背景色調整。
6. 購物車單品操作按鈕(增減、刪除)增加 elevation 陰影效果。
2026-06-11 17:20:30 +08:00
sky121113
79e1f69325 Merge remote-tracking branch 'origin/ui-design' into feat/general-cart-and-payment 2026-06-11 16:58:21 +08:00
sky121113
8d2adee5ab [FEAT] 支付方式「母&&子」集中判斷 + 入口防呆;對話框期間隱藏銷售平台浮層
- DevSetStructure 新增 canCreditCard/canCardPay/canMobilePay/canEsunPay/
  canTapPay/canLinePay/canCash,集中「母開關 && 子項」真相
- BuyDialog 購物車/單品/例外三分支改用上述便利方法,並在 changeCheckout
  入口以 isPaymentFlowAllowed 再次防呆,擋下被繞過的已停用支付
- 浮層連動:HIDDEN/SHOW_MULTILANGUAGE_BUTTON 改為 HIDE/SHOW_SALE_OVERLAY,
  對話框與購物車 Fragment 期間統一隱藏取貨碼/購物車浮鈕與上下系統列
- 無操作逾時 3 分鐘調整為 5 分鐘;verPatch 79→80

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:58:14 +08:00
TaiwanStar Developer
b0f7381832 新增 TapPay 相關圖示資源 2026-06-11 16:21:49 +08:00
TaiwanStar Developer
5af9a94a0c 優化購物車品項排版與按鈕樣式
1. 新增按鈕背景與 Icon 向量資源
- 新增 bg_quantity_outline_button.xml:為加減按鈕設計的背景,底色為頁面背景,帶有 1.5dp 黑色圓角外框。
- 新增 bg_waste_outline_button.xml:為移除按鈕設計的背景,底色為頁面背景,帶有 1.5dp 警告色圓角外框。
- 新增 add_24.xml、remove_24.xml、close_24.xml:將原本填充式圖示改寫為純線段,設定黑色線條且加粗 (strokeWidth="3")。
- 新增 close_warning_24.xml:移除按鈕專用的叉叉圖示,線條同樣加粗,顏色改為警告色。

2. 優化購物車清單排版 (recycler_cart_operation.xml)
- 圖片縮小:將商品圖片寬高從 120dp 縮減為 80dp。
- 文字調整:將商品名稱字級放大至 24sp,並將字重調整為 normal。
- 將單一品項金額設為隱藏,並將小計金額從最右側移至商品名稱下方。
- 加減按鈕:尺寸調整為 44dp 並加上 10dp padding,套用新的黑色外框背景與加粗 Icon。
- 移除按鈕:尺寸同樣調整為 44dp,改用 Warning 樣式外框與 Icon,並加大左側邊距 (marginStart=32dp) 以拉開間隔。

3. 其他連帶變更
- 包含 fragment_shopping_cart.xml 與多語系 strings.xml 的相關微小變動。
2026-06-11 16:21:49 +08:00
TaiwanStar Developer
ee732e03a3 UI: 更新購物車與取貨碼懸浮按鈕樣式及動態角標
- 將購物車與取貨碼改為統一的帶圓角 CardView 容器設計,使用 surface 背景與黑色圖示文字。
- 修正陰影裁切問題,移除外層容器的 clipChildren 限制。
- 購物車新增右上角紅色 Badge 顯示當前選取的商品總數,並與 CartViewModel 動態綁定。
- 調整懸浮按鈕位置,使其與底部機台資訊列保持 24dp 的合適間距。
- 新增 Google Material Icons: package_24 與 shopping_cart_24。
- 同步更新相關的 Agent 發版規則文件,移除廢棄的多維度 flavor 定義。
2026-06-11 15:18:38 +08:00
sky121113
fc21186a53 [FEAT] 來店禮串接 B690 + 掃碼即時回饋與空窗修正、通行碼購物車模式封堵
1. 來店禮改串新端點 B690(取代舊 HttpAPI.freeGiftCode):
   - StarCloudAPI 新增 verifyWelcomeGift(),POST machine/welcome-gift/verify/B690(Bearer 機台 token,body {code})。
   - CheckoutFreeGiftCodeDialog 手動輸入與掃碼皆改走 B690;折扣映射 amount→mode1(折抵金額)、percentage→mode2(折扣趴數換算總價乘數,八折20→0.8)。
   - 加 isWelcomeGift API 防護;非 amount/percentage 型別擋下提示。
   - 注意:B690 無舊「免費商品」型,純折扣。
2. 掃碼 UX 空窗修正(取貨碼/通行碼/來店禮三個對話框):
   - 掃描器啟動延遲 5 秒→0,對話框一彈出即監聽,解決「剛彈出馬上掃會整個丟失、時好時壞、無回饋」。
   - 讀取窗口 7→9 次(約 45 秒),覆蓋 40 秒對話框存活時間。
   - 新增 SET_SCAN_INPUT:掃到的碼即時顯示在輸入框;送出前顯示「驗證中…」。
3. 通行碼購物車模式封堵:
   - BuyDialog 購物車模式的付款清單中,通行碼(buttonAccessCode)從未 gating;改在 onCreate 加權威判斷,需 isPickupModule && isPassCode 才顯示,涵蓋購物車/單品所有模式。
4. 版號 verPatch 76→79。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 15:09:42 +08:00
sky121113
2683b850a2 Merge remote-tracking branch 'origin/ui-design' into ui-design 2026-06-11 14:12:22 +08:00
sky121113
e8b3185b8a [FIX] 取貨碼/通行碼:封堵付款備援漏出、API 防護、掃碼即時回饋
通行碼漏洞封堵
- BuyDialog 單品模式 NFC 不支援時會強制顯示通行碼(buttonAccessCode)當備援,
  未判斷 isPassCode;改為從屬取貨模組(isPickupModule && isPassCode)才顯示

API 防護(defense-in-depth,UI 漏出入口也擋下)
- submitPassCode/submitPickupCode 開頭加守門:功能關閉即不呼叫 B670/B660,狀態列提示「功能未開放」

掃碼 UX 回饋
- 新增 SET_SCAN_INPUT 事件:掃碼後即時把掃到的碼顯示在輸入框
- 送出驗證前顯示「驗證中…」狀態,取代原本易忽略的遮罩 Toast,避免使用者誤以為沒掃到
- 版號 verPatch 75→76

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:11:06 +08:00
sky121113
018da036b1 [FEAT] B014 系統設定全面接入:取貨模組/來店禮 gating、購物車單品直購、購物方式顯示與設定權限分流
雲端 B014 設定解析與持久化(StarCloudAPI / MyApp / DevSetStructure)
- FunctionSet 改接 PickupModule/WelcomeGift/MemberSystem/AmbientTemp(原僅 PickupCode/PassCode)
- DevSet 補接刷卡機/掃碼支付細項;新增 HardwareSet 貨道型式(彈簧/履帶)解析
- B000 登入解析 identity/loginToken,供系統設定唯讀身分判斷與 B016 回寫
- 新增 bootstrap 預設(devset_defaults)、initialSystemSet/saveSystemSet DB 持久化

前台流程 gating
- 取貨碼、通行碼從屬「取貨模組」(PickupModule):模組關閉時連動隱藏(畫面與流程都不出現)
- 來店禮獨立判斷(WelcomeGift):原按鈕恆亮,改為依雲端開關顯示
- BasicSaleFlow:購物車(ShoppingCar)關閉時點商品改單品直購,清空後僅放本品並直接開 BuyDialog

系統設定頁(SystemSetActivity / activity_system_set / styles)
- 新增「購物方式」唯讀顯示(基礎版/員工卡/取貨單);僅 basic 展開其下可設定項
- 權限分流:商業政策項(購物車/發票/各支付方式/取貨模組/來店禮/會員)恆唯讀,
  機台實體項(刷卡機/掃碼/現金總開關、面額、環溫、硬體貨道)系統方可改並寫回雲端

其他
- DispatchDialog 成功結案清空 v2 購物車;ShoppingCartFragment 調整數量續命倒數
- 版號 verPatch 72→75

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 12:33:57 +08:00
TaiwanStar Developer
3b1764b75c feat: 新增商品出貨成功與機器巡檢中 UI 彈跳視窗 2026-06-11 10:24:04 +08:00
sky121113
b56c539549 [CHORE] 新增 .editorconfig 防止 XML 檔頭 UTF-8 BOM(aapt2 打包防呆)
供 UIUX 設計師直接在本 repo 改 UI 時,編輯器不再寫入 BOM 導致 aapt2 失敗。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:09:23 +08:00
sky121113
42947dce0c [FIX] DispatchDialog 關閉未結案補送 MQTT Finalize,修正多品項部分出貨不上報
1. 問題:出貨流程若走 dialogCancel() 收場(倒數結束 / dispatchIndex 越界 /
   中途中止),整筆交易完全不上報。批次 MQTT Finalize 僅在「成功完成」與
   「ERROR_PROCESS_GOODS 失敗」兩條路送出,dialogCancel 這條共通關閉路徑漏掉。
   此缺口於 main 既存,因通用版原無多品項出貨、Cmuh 失敗都走 ERROR_PROCESS_GOODS
   而未被觸發;多品項 basic 出貨為首次暴露。

2. 修正:在 dialogCancel() 開頭、buyList.clear() 之前加安全網——
   若出貨已開始(dispatchList 非空)且尚未結案(!isFinalizing),
   補呼叫既有 finalizeFailedDispatch() 送出一筆含全部明細的部分/未完成結案。

3. 守門:以 isFinalizing 擋住正常成功(:946)/錯誤(:1062)路徑,零重複上報;
   通行碼 flowType=5 仍由 publishFinalizeUnlessPassCode 自動略過,
   對晟崴/中國醫現行行為零影響。

雙硬體編譯綠。實機驗證待硬體(多品項第2件失敗→後台收到一筆部分出貨)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:47:10 +08:00
sky121113
1d4c4ab071 [FEAT] Phase3 購物車:移植 ShoppingCart 商品列外觀(表面移植,沿用現有 v2 購物車)
- recycler_cart_operation 改用 ShoppingCart 設計(120dp 圖、QuickCart 字樣、primary 色、56dp 加減鈕)
- 保留 buttonWaste 刪除鈕與 totalPriceTextView 小計(現有 CartOperationAdapter/ShoppingCartFragment 沿用)
- 新增 TextAppearance.App.QuickCart.Medium / .Small 樣式
- 不動 FontendActivity 架構與抽屜(DialogXManager/FlowerFragment 不在表面移植範圍)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:36:00 +08:00
sky121113
998b3ae89f [FEAT] Phase2 支付:dialog_buy 支付選單改用 Travis 版(沿用現有 BuyDialog 邏輯)
- dialog_buy 換成 Travis 付款選擇版面(gridPaymentIcons + ConstraintLayout Flow)
- binding 契約相符(layoutNfcPay 在現有 BuyDialog 已停用、逐顆控制),無需改 Java

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:19:49 +08:00
sky121113
33d79076a6 [FEAT] Phase2 發票:完整移植 Travis 發票設定新流程
- 新增 InvoiceTypeDialog(統一類型入口:載具/捐贈/統編)
- 新增 InvoiceDonationDialog + RecyclerDonationListAdpter(捐贈機構 RecyclerView)
- 新增 dialog_invoice_type / dialog_donation 版面 + bg_selected_item
- BuyDialog 發票入口改走 InvoiceTypeDialog(取代直接進 InvoiceBarcodeDialog)
- InvoiceBarcodeDialog:捐贈鈕改導向新 InvoiceDonationDialog,移除舊 inline
  showDonationDialog(其引用的舊 dialog_donation id 已不存在)
- 沿用現有 main 發票資料流(getInvoiceData/setLoveCode、ReportFlowInfoData)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:18:21 +08:00
sky121113
73e2b55c53 [FEAT] Phase2 發票:移植 Travis 統編/載具對話框外觀(沿用現有 Java)
- dialog_customer_identifier / dialog_invoice_barcode 換 Travis 版面
- 新增 bg_keypad_button / bg_step_active / bg_step_inactive 與 color/outlineVariant
- binding 契約相符,無需改 Java

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:07:39 +08:00
sky121113
696e6cb952 [FEAT] Phase2 通行碼/取貨碼:移植 Travis 對話框外觀(沿用現有 B660/B670 邏輯)
- dialog_access_code / dialog_pickup_code_fontend 換 Travis 版面
- binding 契約相符,無需改 Java(保留現有通行碼/取貨碼邏輯)
- 新增 color/transparent

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:05:21 +08:00
sky121113
d4fe3c6cf4 [FEAT] Phase2 支付流程:移植 Travis 現金/玉山結帳對話框外觀
分區授權:checkout 對話框採 Travis 設計,邏輯沿用現有 main。
- dialog_checkout_cash_pay / dialog_checkout_esun_pay 換 Travis 版面
- 補回 buttonRetryEscrow(CashPayDialogGeneral 退鈔重試邏輯沿用),以新 Material 樣式呈現
- 新增 pay_* 付款圖檔、icon_network_poor/temperature、img_reload
- 新增 colors infoContainer / surface

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:03:43 +08:00
sky121113
c24f409674 [FIX] 移除 origin/ui 帶入的 UTF-8 BOM(aapt2 無法解析導致打包失敗)
設計師端編輯器在 3 個 XML 開頭寫入 UTF-8 BOM,aapt2 報
「Cannot read field elmName because root is null」。移除 BOM 後 S12XYU/S9XYU
Debug 皆可正常打包。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:40:36 +08:00
sky121113
3d798eb94d [FEAT] 整合 UIUX origin/ui 改版(後台/貨道/商品卡/員工卡/出貨/StarCloud 圖示)
合併 origin/ui 設計師改版:前台商品卡、後台首頁/主控、貨道卡片式 Grid(停售遮罩、
一鍵補貨配色)、商品搜尋、員工卡取貨、出貨清單與進度對話框、各 checkout 對話框版面、
StarCloud App 圖示與名稱、付款管道/接受付款圖檔、switch 色票 selector。

衝突處理:
- 3 個 Cmuh res/layout 覆寫檔(activity_fontend、dialog_pickup_code_buy、
  dialog_pickup_code_fontend)已於 ab3b53d 收斂進 main,保留刪除,由 main 對應佈局承載。
- origin/ui 移除了 colors.xml 的 @color/red,但重構後移入 main 的 fragment_shopping_cart
  仍沿用,補回 red 定義避免 aapt 連結失敗。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:37:14 +08:00
sky121113
ab3b53da99 [REFACTOR] 收斂 project 維度為執行期 shopping_mode + B014 雲端設定唯讀同步
1. B014 雲端設定下發 → App 解析寫入 DevSet 並持久化:
   - StarCloudAPI.saveSettings() 解析 DevSet(14 開關)/CashSet(7 面額)/
     FunctionSet(取貨碼 PickupCode、通行碼 PassCode)/ShoppingMode;
     缺漏粒度區分(整個物件缺→不動本地防誤關;物件內 key 缺→視為 false)。
   - MyApp 修 TapPay30~34 持久化 bug(saveSystemSet 補序列化、initialSystemSet
     改 optBoolean 防舊紀錄 crash),並新增 PickupCode/PassCode/ShoppingMode 持久化。
   - DevSetStructure 新增 isPickupCode/isPassCode 欄位。
   - SystemSetActivity 全部 21 個 checkbox 鎖唯讀、返回鈕只 finish() 不回寫。
   - 取貨碼浮鈕(FontendActivity)改吃 DevSet.isPickupCode()、通行碼入口(BuyDialog)
     改吃 DevSet.isPassCode(),取代舊 flavor 資源/寫死。

2. 收掉 project flavor 維度,三種購買流程改由雲端 shopping_mode 執行期切換:
   - SaleFlowHandler 改為 main 抽象基底 + create() factory,依 shopping_mode 回傳
     BasicSaleFlow(basic/原 General)、EmployeeCardSaleFlow(employee_card/原 Chengwai)、
     PickupSheetSaleFlow(pickup_sheet/原 Cmuh) 三策略。
   - 三 flavor 的 dialog/adapter/Kotlin 購物車/Cmuh 醫令類別全併入 main;重複類別去重。
   - FontendPickupCodeDialog 內部依 shopping_mode 分支(basic/employee 走 B660、
     pickup_sheet 走舊 memberVerification 並跳過取貨口檢查)。
   - fragment_vmc 合併掃描單據按鈕,VmcFragment 依 shopping_mode toggle 可見性。
   - General strings(zh/en/ja)、drawable、購物車 layout 併入 main;
     devset_defaults 併為 basic-mode bootstrap 預設。

3. 取代 BuildConfig.FLAVOR_project 編譯期分支:
   - AppEntry.isCmuh() 改判 shopping_mode == pickup_sheet。
   - DispatchDialog 6 處 Cmuh 分支改用 AppEntry.isCmuh()。
   - ComPort.getFilepath() 改認硬體維度 BuildConfig.FLAVOR(S12XYU→ttyS7/S6、
     S9XYU→ttyS1/S2),與購買流程正交;對現役機台行為不變。

4. build.gradle 移除 project 維度,flavorDimensions 只留 hardware;輸出檔名去掉
   project 段(app-S_12_XY_U-<ver>-debug.apk)。刪除 app/src/{General,Chengwai,Cmuh}/。

5. compiler-apk / release-apk(workflow + .claude/commands)改以硬體規格 S9/S12 為
   打包對象,OTA keyin 改依硬體分版並提示 shopping_mode 部署鐵律。

6. verPatch 69→70(供 OTA 遠端更新)。

雙硬體 clean 編譯回歸 S12XYU/S9XYU 全綠。實機三流程驗證待硬體。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:28:59 +08:00
sky121113
2c3255cb64 [RELEASE] 升級修補版號 verPatch 68→69(供 General 下位機修正 OTA 更新)
ComPort 修正後需遞增版號讓現場機台可觸發 OTA 覆蓋更新。
General S9XYU → versionCode 210169 / versionName 21_01_69_R。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:14:48 +08:00
sky121113
275d6d1fa9 [FIX] 通用版下位機 COM port 改為與中國醫相同
General 的下位機硬體與中國醫(Cmuh)相同(晟崴為最新硬體),故 COM port
應接 ttyS1/ttyS2 而非原本的 ttyS7/ttyS6。

ComPort.getFilepath() 改為:只有晟崴(Chengwai) 用最新的 ttyS7/ttyS6,
其餘(General + Cmuh)用 ttyS1/ttyS2。晟崴/中國醫接線不變,僅 General 調整。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:58:23 +08:00
sky121113
588da4a2eb [CHORE] APK workflow 強殺防呆 + 新增 Claude Code slash command
- .agents/workflows/{compiler,release}-apk.md:Gradle Daemon 終止改防呆——
  先優雅 ./gradlew --stop,僅在仍殘留時才升級終止(先 TERM 再 KILL),
  避免無差別 pkill -9 撞鎖導致 clean 編譯隨機失敗。
- .claude/commands/{compiler,release}-apk.md:新增對應 slash command(薄包裝,
  指向 .agents/workflows 單一真相來源,處理參數注入與 Java 17 環境)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:25:48 +08:00
sky121113
53ea3dafa9 [FEAT] 取貨碼金額歸零 + 通行碼不上報交易
- 取貨碼(FontendPickupCodeDialog):setTotalPrice(0),因屬已預付來取貨/
  付款未出貨補出貨,本機交易付款金額為 0。
- 通行碼(payment_type=5):工程師/內部硬體測試用,DispatchDialog 照常出貨
  測試硬體,但略過 MQTT 交易結案上報(避免污染銷售/營收、不扣雲端庫存;
  B670 驗證已於後台記 log)。集中於 publishFinalizeUnlessPassCode(),
  成功/失敗兩路徑共用。

三 flavor 編譯/打包通過。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:01:51 +08:00
sky121113
1720d2007e [FEAT] 取貨碼/通行碼改走 StarCloudAPI B660/B670(比照員工卡 B680)
舊的 httpAPI(memberVerification / checkAccessCode B024)改為新 StarCloudAPI:
- StarCloudAPI 新增 verifyPickupCode(B660,回 code_id+slot_no)、
  verifyPassCode(B670,回 code_id),及 PickupVerificationCallback。
- 通行碼 CheckoutAccessCodeDialog:手動+掃碼兩入口改走 verifyPassCode,
  成功設 flowType=5、保存 accessCodeId 供 MQTT finalize(payment_type=5)。
- 取貨碼 FontendPickupCodeDialog:兩入口改走 verifyPickupCode,依回傳 slot_no
  建立 BuyList、設 flowType=6、保存 accessCodeId(payment_type=6),
  進入 BuyPickupCodeDialog 確認 → DispatchDialog;文案修正為「取貨碼」。

finalize 的 code_id 取自 ReportAccessCodeStructure.accessCodeId、payment_type
取自 flowType,App 端 TransactionFinalizeBuilder 既有支援。三 flavor 編譯/打包通過。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:52:20 +08:00
sky121113
f10f6f5879 [FEAT] General 接通取貨碼入口(供實機測試)
- General SaleFlowHandler.onPickupRequested() 開啟 FontendPickupCodeDialog
  (原為空,導致取貨碼點不到)
- 前台取貨碼浮鈕改由 flavor 資源 show_pickup_code_button 控制顯示:
  General=true 顯示,main/晟崴/中國醫=false 維持原本隱藏行為

至此 General 取貨碼與通行碼皆可實機測試。三 flavor 編譯/打包通過。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:42:30 +08:00
sky121113
e8856d0a8b [FIX] 通行碼 Dialog 防呆強化(Part 2-3)
CheckoutAccessCodeDialog.changeOk()(自 Luzui 移植防呆):
- 加 3 秒防連點,避免重複送出驗證
- 輸入加 .trim() 與 isEmpty 過濾
- 修正誤植文案「統一編號長度不能少於 8 碼」→「通行碼長度不能少於 8 碼」

三 flavor 完整 APK 打包成功(General/Chengwai/Cmuh)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:07:56 +08:00
sky121113
2315a32f56 [FEAT] General 現金強化版合併(Part 2-2)
移植 Luzui 現金 Dialog 的硬體容錯邏輯(卡鈔偵測+退鈔重試、零錢不足拒收+
退款保障、暫存區兩階段入帳、找零失敗自癒、百元鈔庫存輪詢、3分鐘閒置逾時),
上報層改接 TaiwanStar 新 MQTT(移除 B600/B650,改 DispatchDialog 統一 Finalize)。

採「獨立類別 + flavor 路由」避免 duplicate class、不污染已上線 flavor:
- 新增 src/General CashPayDialogGeneral(Luzui 強化版改名,只 General 編譯)
  + General layout override dialog_checkout_cash_pay.xml 補 buttonRetryEscrow
  - showDispatchDialog 改 MQTT 路徑、移除 callB600AndProceed/httpAPI
  - onCreate(Context) 移除 throws(對齊 TaiwanStar DialogAbstract 簽名)
- DialogAbstract.cashDialogClass() 經 SaleFlowHandler 解析各 flavor 現金 Dialog
- 三個 SaleFlowHandler 加 cashDialogClass():General→CashPayDialogGeneral、
  晟崴/中國醫→標準 CashPayDialog
- 3 個呼叫點(BuyDialog/InvoiceBarcodeDialog/InvoiceCustomerIdentifierDialog)
  case 9 改 changeDialog(cashDialogClass())

三 flavor 編譯通過、General APK 打包成功。現金硬體容錯需實機驗證(階段 7)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 09:05:59 +08:00
sky121113
402490ee58 [FEAT] General 付款選單啟用 + BuyDialog NFC 顯示控制(Part 2-1)
- DevSet 初始預設值改由 res/values/devset_defaults.xml 提供,各 flavor 可覆寫;
  main 維持原值(Chengwai/Cmuh 行為不變),General override 為購物車 + 現金/
  刷卡/玉山/LINE Pay/發票全開,裝上即可用。
- BuyDialog 購物車分支:NFC 卡/儲值/手機改逐顆依 isDevNFCPay 控制顯示
  (TaiwanStar 無 layoutNfcPay 容器,修正原本指向不存在 view 而被註解的問題)。

三 flavor 編譯通過、General APK 打包成功。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:48:07 +08:00
sky121113
cfc802dbb5 [FEAT] General flavor 多品項購物車(Part 1)
移植 Luzui v2 Kotlin MVVM 購物車到 General flavor,接 TaiwanStar 既有
結帳鏈(BuyDialog → 付款 → DispatchDialog → MQTT 結案)、沿用 VMC 出貨。

- build:啟用 Kotlin/lifecycle/BaseRecyclerViewAdapterHelper(全域 classpath,
  cart .kt 僅在 src/General 編譯,不影響 Chengwai/Cmuh 行為)
- 移植至 src/General:CartViewModel/CartOperationAdapter/CartOperationResult/
  SafeCountdownUtil/UnibuyHelper/ShoppingCartFragment + layout/drawable/三語 strings
  - 移除 Luzui 的 updateLocalGoods(FLOWER/DAO)、DialogXManager、HttpAPI、格子機依賴
  - 結帳改 convertToBuyStructure() + SaleFlowHandler.onBuyRequested()
- 共用檔最小改動(對 Chengwai/Cmuh 零行為影響):
  - SlotStructure 加 cartSelectedCount
  - FontendActivity 加 openFragment()/backToVmc()、購物車浮鈕改走 onShoppingCartRequested()
  - RecyclerVmcBuyListAdpter 點商品改走 flavor hook handleProductSelected()
  - 三個 SaleFlowHandler 加 onShoppingCartRequested()/handleProductSelected()
    (General:加入購物車;Chengwai/Cmuh:維持原行為)

三 flavor 編譯通過、General APK 打包成功。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:45:11 +08:00
TaiwanStar Developer
404ba69fba chore: 修改 APP 圖示與顯示名稱為 StarCloud 2026-06-09 16:58:11 +08:00
TaiwanStar Developer
8e4a0993c7 style: 優化首頁購物按鈕樣式與文字,並新增 logo 資源
- 優化首頁 (activity_home.xml) 的購物按鈕 (startButton) 樣式:
  1. 寬度由 0dp 改為固定寬度 400dp。
  2. 按鈕文字由「購物去」調整為「開始購物」。
  3. 背景與文字顏色改為套用主題色系 (@color/primary 與 @color/onPrimary)。
- 新增星雲 LOGO 圓形圖示資源 (starcloud_logo_circle.png)。
2026-06-09 16:46:11 +08:00
TaiwanStar Developer
4d7717edc3 style: 優化後台貨道清單介面佈局與狀態顯示
- 狀態辨識度提升:
  1. 貨道編號新增「貨道」前綴字樣。
  2. 販售與停售狀態加入顏色識別,文字與 Switch 顏色會根據狀態動態切換(成功色/警告色)。
- 佈局與操作體驗優化:
  1. 重新排列操作區塊,將「清空貨道」按鈕移至卡片最下方。
  2. 放大庫存加減按鈕的觸控範圍(高度調整為 60dp)與字體大小,提升機台觸控體驗。
  3. 「清空貨道」按鈕改用 warning 警示色系。
- 視覺微調:移除卡片外框線,使列表視覺更為乾淨。
2026-06-09 16:14:36 +08:00
sky121113
5f480d6cb6 [FIX] 修正非中國醫專案的物料代碼一致性驗證邏輯並升級修補版號
1. 於 VmcSlotListActivity 限制 validateMaterialCodeConsistency 僅在中國醫 (CMUH) 專案下執行,其他專案直接跳過。
2. 調整 app/build.gradle 的修補版號 (verPatch) 變數值,由 67 遞增至 68。
2026-06-09 16:00:58 +08:00
TaiwanStar Developer
b1732987ce feat: 優化商品搜尋介面並修復後台貨道編輯 Bug
- 商品搜尋介面大改版:
  1. 搜尋列表從單欄改為 3 欄網格排列,並導入 MaterialCardView 重新設計商品卡片。
  2. 搜尋框體驗優化:隱藏原生圖示,新增清空按鈕;支援點擊列表或候選字時自動收起鍵盤。
  3. 點擊候選字邏輯修改為直接套用完整字詞。
  4. 擴大點擊範圍,改為點擊整張商品卡片即可選取。
- 貨道編輯清單修復:
  1. 將 textCount 隱藏狀態改為 INVISIBLE 解決 GONE 導致輸入框寬度壓縮為 0 的問題。
  2. 輸入數量時改用 numberPassword 並移除密碼遮罩,以喚起乾淨的純數字鍵盤。
- 資源與樣式:
  1. 新增 button_outline_black.xml 黑框按鈕樣式。
  2. 修正圖片檔名大小寫 (eTicket -> eticket)。
2026-06-09 15:59:36 +08:00
TaiwanStar Developer
d917f6445c style(ui): 將出貨頁面標題改為灰色背景與黑色文字 2026-06-08 15:33:34 +08:00
TaiwanStar Developer
d1bf7d18ce style: 調整出貨清單 UI 的文字顏色與出貨中狀態格式
- 將商品名稱、順序(槽位號)與已出貨數量資訊的文字顏色改為黑色(@color/black)

- 將出貨中狀態的格式「出貨中(1)」改為「出貨中(1件)」
2026-06-08 14:34:26 +08:00
TaiwanStar Developer
f2ed393686 style(ui): 優化出貨進度對話框結構、版面自適應與配色調整
- 頁面標題改為「出貨進度」,背景調整為主色 (primary)
- 新增並更新多國語言字串 (processing, dispatch_progress)
- 重構底部的當前出貨進度區塊結構,移除冗餘嵌套,將區塊背景設為 cardSurface
- 取消按鈕樣式改為 warning/onWarning 配色,扁平化結構並去除冗餘 FrameLayout
- 清單區域高度改為 weight=1 自適應剩餘空間,移除 Space 元件,背景改為 background 色
2026-06-08 13:59:32 +08:00
TaiwanStar Developer
907d5095d5 style: 重構員工卡支付對話框佈局並更新身分證/員工卡相關圖檔資源 2026-06-08 11:54:15 +08:00
TaiwanStar Developer
28a3d623f0 feat: 新增支付管道與接受付款類型相關圖片資源 2026-06-08 09:53:40 +08:00
TaiwanStar Developer
2e71cc29b8 feat: 修改 VMC 貨道清單之未配置商品圖示、卡片背景色與一鍵補貨按鈕配色 2026-06-05 17:24:37 +08:00
TaiwanStar Developer
8f6b5217d6 feat(ui): 貨道停售中狀態時在商品圖片上方顯示透明黑色遮罩 2026-06-05 16:19:51 +08:00
TaiwanStar Developer
5d9c5ba5cf style(ui): 調整貨道管理儲存與一鍵補貨按鈕配色,並將 Switch 選中狀態設為 primary 色 2026-06-05 16:18:26 +08:00
TaiwanStar Developer
a933bb22f6 feat(ui): 改造貨道管理列表為卡片式 Grid 佈局與 Adapter 邏輯對接 2026-06-05 16:16:27 +08:00
TaiwanStar Developer
aed8d9bc1f style(ui): 依據參考介面調整貨道管理版面
調整項目:
1. 頂部區域:保留「貨道管理」標題,省略機櫃選擇與命名的部分。
2. 列表區域:將 recyclerSlotList 改為 Grid 網格排版,設定 spanCount 為 3。
3. 底部區域:省略批量匯出、批量匯入按鈕。將「返回」與「清空」按鈕設為隱藏以維持程式碼 ViewBinding 相容性。
4. 儲存按鈕:原「上傳後台並返回」按鈕文字修改為「儲存」,並維持原有點擊與同步邏輯。
2026-06-05 15:40:36 +08:00
TaiwanStar Developer
54e0c9ffa5 style(ui): 調整廣告區域容器比例至 21:9 與等比例縮放
1. 將廣告區域容器比例從 16:9 調整為 21:9

2. 設定影片播放器屬性為 app:resize_mode=zoom

3. 設定廣告圖片屬性為 android:scaleType=centerCrop
2026-06-04 16:28:34 +08:00
TaiwanStar Developer
02ae6623d9 style(ui): 調整前端首頁資訊排版與背景色
1. 將版本號與 TC/VC 移至最底部釘選

2. 頂部資訊順序改為「時間 | 溫度 | 網路」,文字改為 @color/gray

3. 上下區塊背景改為 @color/light_gray,文字改為 @color/gray

4. 頁面背景及商品卡片區域背景改為 @color/background
2026-06-04 15:41:56 +08:00
TaiwanStar Developer
12e61d2a10 feat(ui): 替換 VMC 購買清單為新版佈局並調適 Adapter
1. 替換 recycler_vmc_buy_list.xml 為新版 CardView 結構,微調定位使 textPrice 相容舊版價格邏輯
2. 將貨道 textSlot 移至左上角,大小設為 18sp,顏色改用 primary/onPrimary 語意色
3. 將庫存 textStock 獨立定位於右下角,大小設為 18sp,顏色改用 gray/black 資源
4. 修改 RecyclerVmcBuyListAdpter 綁定庫存數量,並將貨道文字調整為「貨道value」無補零格式
2026-06-04 15:24:22 +08:00
TaiwanStar Developer
c7d75a8522 fix(layout): 移除佈局 XML 檔案的 UTF-8 BOM 檔頭
修復 DataBinding 元件在 Gradle 建置過程中因 XML 檔案包含 BOM (Byte Order Mark) 檔頭而解析失敗的問題。

原因:XML 檔頭的前導字元 EF BB BF 導致 LayoutFileParser 在解析 root 節點時發生 NullPointerException。

調整項目:- 批次移除 layout 目錄下所有 XML 佈局檔案的 UTF-8 BOM 檔頭。
2026-06-04 14:22:32 +08:00
TaiwanStar Developer
c7b8372dfc style(ui): 簡化色彩系統設定與對齊佈局配置 2026-06-04 13:57:06 +08:00
237 changed files with 17105 additions and 6378 deletions

View File

@ -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` 自動連動生成,**嚴禁手動修改輸出檔名**。
---

View File

@ -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可在無串口設備的手機上正常運作不崩潰。

View File

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

View File

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

View File

@ -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 發版追溯標記指南"

View 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 檔名)。

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

View File

@ -24,7 +24,8 @@
- 禁止直接手改成品 `versionCode`/`versionName` 值。
- 版號管理透過 `verMajor`、`verMinor`、`verPatch` 變數。
- OTA 發版時,`versionCode` 必須嚴格遞增。
- 中國醫現場版 (`Cmuh`) 修正預設以 `:app:assembleCmuhS9XYUDebug` 驗證。
- 21.5 吋大螢幕現場版(如中國醫)修正預設以 `:app:assembleS9XYUDebug` 驗證。
- 10 吋一般現場版(如晟崴、通用)修正預設以 `:app:assembleS12XYUDebug` 驗證。
## When Rules Conflict

View File

@ -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 {
}
// =====================================================================
// Flavorproject× hardware
// Flavorhardware / 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 / MVVMbasic
implementation libs.kotlin.stdlib
implementation libs.lifecycle.viewmodel.ktx
implementation libs.lifecycle.livedata.ktx
implementation libs.lifecycle.runtime.ktx
implementation libs.brvah
}

View File

@ -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() {
// 晟崴版不處理取貨碼
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {
// 通用版不處理取貨碼
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}

View File

@ -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 的寫檔門檻等級預設 INFOdebug.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 = "";
// 商品名稱多語 mapB012 t060v01_i18nkey=商品IDvalue=底線代碼名稱 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 回傳 identitysystem=系統方可編輯系統設定並回寫雲端/ 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);
}
}
// 商品名稱多語 blobB012 t060v01_i18n離線重啟時供 fallback
if (memoDao.existOnlyOne("ProductNameI18n")) {
MyApp.getInstance().setProductNameI18nBlob(memoDao.getOnlyOne("ProductNameI18n").getData());
}
// 商品規格多語 blobB012 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;
}

View File

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

View File

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

View File

@ -46,17 +46,16 @@ public enum ComPort {
}
public String getFilepath() {
// 下位機接線由硬體 flavorIPC 世代 / 安卓版本決定與購買流程 shopping_mode 正交
// S12XYU新板原晟崴 10")→ ttyS7/ttyS6
// S9XYU Android 9 原中國醫/通用 21.5")→ ttyS1/ttyS2。
// 單一 flavor 維度hardwareAGP 僅產生 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;
}

View File

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

View File

@ -18,6 +18,10 @@ import java.util.Date;
import java.util.Locale;
public class Logs {
// 單筆寫檔上限超過即截斷避免整包 JSON/payload dump debug.txt 撐爆淹沒有用事件
// 只影響寫入 debug.txt那份logcatLog.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) {

View File

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

View File

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

View File

@ -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));
// 身分與登入 Tokenidentity 決定系統設定是否可編輯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成功回傳折扣資訊
* discountTypeamount折抵金額/ 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 登入核發的使用者 Tokenauth: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", ""));
// 新增商品名稱多語 mapB012 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) {}
}
}
// 新增商品規格多語 mapB012 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());

View File

@ -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. 延後重連 MQTTtoken 可能已輪換留時間讓 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");

View File

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

View File

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

View File

@ -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) 商品多語 mapzh_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();

View File

@ -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{" +

View File

@ -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{" +

View File

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

View File

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

View File

@ -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 重繪完成後才彈出鍵盤
// 若直接呼叫 showSoftInputWindow flag 變更會清除 focus導致鍵盤無法彈出
editText.post(() -> {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
});
}
public void setImmHideByFocusChange(EditText editText) {

View File

@ -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 進入出貨 CMUH42
* 覆寫之後之後 {@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);

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

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

View File

@ -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
* 點選取貨按鈕開啟輸入取貨碼的 Dialogpickup_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;

View File

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

View File

@ -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() {
// 預設無動作
}
}

View File

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

View File

@ -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, () -> {

View File

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

View File

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

View File

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

View File

@ -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);
// 通行碼入口付款格內 buttonAccessCodeflowType 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());
// 貨道層級是否支援 NFCVMC/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);

View File

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

View File

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

View File

@ -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直接提示並返回購買頁不進出貨
// [交易生命週期] 掃碼支付失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case CHANGE_DISPATCH:

View File

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

View File

@ -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=70LINE 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 失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case CHANGE_DISPATCH:

View File

@ -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));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -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));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -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));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -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~34TapPay 子項已於本 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直接提示並返回購買頁不進出貨
// [交易生命週期] 掃碼支付失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}
break;
case TRANSACTION_AGAIN:

View File

@ -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();
}
}

View File

@ -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 / 金額 / 找零 / 點數須在上方 isCmuh42 覆寫之後
// 且趁全域 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;

View File

@ -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
* finalizepayment_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) {

View File

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

View File

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

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}

View File

@ -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));//點我選擇商品
}

View File

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

View File

@ -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());
}

View File

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

View File

@ -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()));
}

View File

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

View File

@ -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();
}
}

View File

@ -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-rCNenjakovithid(=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
// 舊資料相容twzh-TWjpja單獨 zhzh-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 啟動時呼叫

View 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()
}
}

View File

@ -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圖示染灰#ADADADclickable = 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
}
}
}

View File

@ -0,0 +1,9 @@
package com.unibuy.v2.bean
// 購物車操作結果資料類
data class CartOperationResult(
val totalPrice: Int, // 購物車總價
val totalCount: Int, // 購物車總數量
val currentItemCount: Int, // 當前商品數量
val currentItemPrice: Int // 當前商品價格
)

View 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.setShoppingCartFragmentInstanceTaiwanStar 無此 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 DialogXManagerGeneral 不採用
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;
}
}

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

View 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
)
}
}

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

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

View 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