diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 9a735be..7bc0d1d 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -841,6 +841,8 @@ class SalesController extends Controller $machine = Machine::findOrFail($validated['machine_id']); $quantity = (int) ($validated['quantity'] ?? 1); $expiresAt = $request->expires_days ? now()->addDays((int) $request->expires_days) : null; + // 勾「只能使用一次」→ 使用上限 1;未勾 → null(無限次,維持原行為)。 + $usageLimit = $request->boolean('single_use') ? 1 : null; // 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。 if ($quantity > 1) { @@ -859,6 +861,8 @@ class SalesController extends Controller 'batch_no' => $batchNo, 'expires_at' => $expiresAt, 'status' => 'active', + 'usage_limit' => $usageLimit, + 'usage_count' => 0, 'created_by' => Auth::id(), 'created_at' => $nowTs, 'updated_at' => $nowTs, @@ -902,6 +906,8 @@ class SalesController extends Controller 'code' => $request->custom_code ?: PassCode::generateUniqueCode($validated['machine_id']), 'expires_at' => $expiresAt, 'status' => 'active', + 'usage_limit' => $usageLimit, + 'usage_count' => 0, 'company_id' => $machine->company_id, 'created_by' => Auth::id(), ]); diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 57d3167..731e68e 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -852,6 +852,17 @@ class MachineController extends Controller 'raw_data' => ['machine_sn' => $machine->serial_no, 'code' => $passCode->code] ]); + // 單次/限次通行碼:驗證成功即計為使用一次;達上限就標記 used, + // 下次驗證會被上方 where('status','active') 過濾擋掉,達成「只能使用一次」。 + // usage_limit 為 null(未勾單次)時不計數,維持原本無限次行為。 + if (!is_null($passCode->usage_limit)) { + $newCount = $passCode->usage_count + 1; + $passCode->update([ + 'usage_count' => $newCount, + 'status' => $newCount >= $passCode->usage_limit ? 'used' : $passCode->status, + ]); + } + return response()->json([ 'success' => true, diff --git a/app/Models/Transaction/Order.php b/app/Models/Transaction/Order.php index 3a235e7..29e5c06 100644 --- a/app/Models/Transaction/Order.php +++ b/app/Models/Transaction/Order.php @@ -60,6 +60,7 @@ class Order extends Model 'discount_amount', 'pay_amount', 'change_amount', + 'cash_detail', 'points_used', 'original_amount', 'payment_status', @@ -82,6 +83,7 @@ class Order extends Model 'discount_amount' => 'decimal:2', 'pay_amount' => 'decimal:2', 'change_amount' => 'decimal:2', + 'cash_detail' => 'array', 'original_amount' => 'decimal:2', 'payment_at' => 'datetime', 'machine_time' => 'datetime', @@ -90,6 +92,42 @@ class Order extends Model 'delivery_status' => 'integer', ]; + /** + * 現金收款摘要字串:「收:{總收金額}(只列有收到的面額明細) 找:{找零}」。 + * 例:收:120(佰:1、10:2) 找:30。無 cash_detail(非現金或舊資料)或全為 0 時回 null。 + * 總收金額由各面額張數加總得出,與括號內明細一致。 + */ + public function getCashReceivedSummaryAttribute(): ?string + { + $cd = $this->cash_detail; + if (empty($cd)) { + return null; + } + // 面額顯示標籤 + 幣值(依大到小) + $denoms = [ + 'b1000' => ['label' => '仟', 'unit' => 1000], + 'b500' => ['label' => '伍佰', 'unit' => 500], + 'b100' => ['label' => '佰', 'unit' => 100], + 'c50' => ['label' => '50', 'unit' => 50], + 'c10' => ['label' => '10', 'unit' => 10], + 'c5' => ['label' => '5', 'unit' => 5], + 'c1' => ['label' => '1', 'unit' => 1], + ]; + $received = 0; + $parts = []; + foreach ($denoms as $key => $d) { + $n = (int) ($cd[$key] ?? 0); + if ($n > 0) { + $parts[] = $d['label'] . ':' . $n; // 只列有收到的幣別 + $received += $n * $d['unit']; + } + } + if (empty($parts)) { + return null; + } + return '收:' . $received . '(' . implode('、', $parts) . ') 找:' . number_format($this->change_amount, 0); + } + /** * 取得所有支付類型的對照標籤 */ diff --git a/app/Models/Transaction/OrderItem.php b/app/Models/Transaction/OrderItem.php index 6f12d30..9780a90 100644 --- a/app/Models/Transaction/OrderItem.php +++ b/app/Models/Transaction/OrderItem.php @@ -14,6 +14,7 @@ class OrderItem extends Model 'order_id', 'product_id', 'product_name', + 'slot_no', 'barcode', 'price', 'quantity', diff --git a/app/Models/Transaction/PassCode.php b/app/Models/Transaction/PassCode.php index 8803eae..b9b21fc 100644 --- a/app/Models/Transaction/PassCode.php +++ b/app/Models/Transaction/PassCode.php @@ -21,6 +21,8 @@ class PassCode extends Model 'batch_no', 'expires_at', 'status', + 'usage_limit', // 可用次數上限;null=無限次。勾「只能使用一次」時=1 + 'usage_count', // 已使用次數 'created_by', ]; @@ -77,6 +79,11 @@ class PassCode extends Model return false; } + // 有設使用次數上限(如「只能使用一次」usage_limit=1)且已用完 → 失效。null=無限次。 + if (!is_null($this->usage_limit) && $this->usage_count >= $this->usage_limit) { + return false; + } + return true; } diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index d3aa656..f066534 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -71,6 +71,8 @@ class TransactionService 'discount_amount' => $data['discount_amount'] ?? 0, 'pay_amount' => $data['pay_amount'], 'change_amount' => $data['change_amount'] ?? 0, + // 現金面額明細(僅現金 payment_type=9 會帶;其餘為 null) + 'cash_detail' => $data['cash_detail'] ?? null, 'points_used' => $data['points_used'] ?? 0, 'original_amount' => $data['original_amount'] ?? $data['total_amount'], 'payment_type' => (int) ($data['payment_type'] ?? 0), @@ -124,6 +126,8 @@ class TransactionService 'original_amount' => $data['original_amount'] ?? $order->original_amount, 'discount_amount' => $data['discount_amount'] ?? $order->discount_amount, 'change_amount' => $data['change_amount'] ?? $order->change_amount, + // 現金面額明細:completed finalize 才帶(pending 階段無投幣結果) + 'cash_detail' => $data['cash_detail'] ?? $order->cash_detail, 'points_used' => $data['points_used'] ?? $order->points_used, 'invoice_info' => $data['invoice_info'] ?? $order->invoice_info, 'delivery_status' => isset($data['delivery_status']) @@ -158,6 +162,8 @@ class TransactionService $order->items()->create([ 'product_id' => $item['product_id'], 'product_name' => $productName, + // 消費者選擇的貨道/格子號(App items[].slot_no 帶入);不管出貨成功或失敗都記錄。 + 'slot_no' => $item['slot_no'] ?? null, 'barcode' => $item['barcode'] ?? null, 'price' => $item['price'], 'quantity' => $item['quantity'], @@ -460,6 +466,7 @@ class TransactionService } // 3. Record Dispense Results (B602) - Optional/Multiple + $dispensedSlots = []; // 收集實際出貨貨道/櫃號,供取貨碼核銷日誌記錄「真正取貨的貨道」 if (!empty($data['dispense'])) { $dispenseList = isset($data['dispense'][0]) ? $data['dispense'] : [$data['dispense']]; foreach ($dispenseList as $dispenseItem) { @@ -472,6 +479,10 @@ class TransactionService $dispenseItem['member_barcode'] = $order->member_barcode; } + if (!empty($dispenseItem['slot_no'])) { + $dispensedSlots[] = (string) $dispenseItem['slot_no']; + } + $this->recordDispense($dispenseItem); } } @@ -523,7 +534,11 @@ class TransactionService 'action' => $isUsedUp ? 'used' : 'consume', 'remark' => "MQTT 交易完成自動核銷 (" . ($newCount) . "/" . $pickupCode->usage_limit . "),訂單: #{$order->id}", 'raw_data' => [ - 'slot' => $pickupCode->slot_no, + // 取貨碼綁「商品」時 pickupCode->slot_no 為 null; + // 改記「實際出貨的貨道/櫃號」(dispense),取不到才退回取貨碼綁定貨道。 + 'slot' => !empty($dispensedSlots) + ? implode(', ', array_unique($dispensedSlots)) + : $pickupCode->slot_no, 'code' => $pickupCode->code, 'count' => $newCount, 'limit' => $pickupCode->usage_limit @@ -533,6 +548,8 @@ class TransactionService break; case 5: // 通行碼 (Pass Code) + // 注意:通行碼「用掉一次」的計數改在 B670 驗證(verifyPassCode)當下處理, + // 因為 App 對通行碼會略過交易結案上報,此 case 5 實務上不會被觸發。 // 建立通行碼核銷日誌 PassCodeLog::create([ 'company_id' => $order->company_id, diff --git a/database/migrations/2026_07_01_150000_add_slot_no_to_order_items_table.php b/database/migrations/2026_07_01_150000_add_slot_no_to_order_items_table.php new file mode 100644 index 0000000..a72289b --- /dev/null +++ b/database/migrations/2026_07_01_150000_add_slot_no_to_order_items_table.php @@ -0,0 +1,24 @@ +string('slot_no', 20)->nullable()->after('product_name'); + }); + } + + public function down(): void + { + Schema::table('order_items', function (Blueprint $table) { + $table->dropColumn('slot_no'); + }); + } +}; diff --git a/database/migrations/2026_07_01_160000_add_usage_limit_to_pass_codes_table.php b/database/migrations/2026_07_01_160000_add_usage_limit_to_pass_codes_table.php new file mode 100644 index 0000000..9496ff3 --- /dev/null +++ b/database/migrations/2026_07_01_160000_add_usage_limit_to_pass_codes_table.php @@ -0,0 +1,24 @@ +unsignedInteger('usage_limit')->nullable()->after('status'); + $table->unsignedInteger('usage_count')->default(0)->after('usage_limit'); + }); + } + + public function down(): void + { + Schema::table('pass_codes', function (Blueprint $table) { + $table->dropColumn(['usage_limit', 'usage_count']); + }); + } +}; diff --git a/database/migrations/2026_07_03_120000_add_cash_detail_to_orders_table.php b/database/migrations/2026_07_03_120000_add_cash_detail_to_orders_table.php new file mode 100644 index 0000000..2ab4645 --- /dev/null +++ b/database/migrations/2026_07_03_120000_add_cash_detail_to_orders_table.php @@ -0,0 +1,35 @@ +json('cash_detail')->nullable() + ->comment('現金收款面額明細 {b1000,b500,b100,c50,c10,c5,c1}') + ->after('change_amount'); + } + }); + } + + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + if (Schema::hasColumn('orders', 'cash_detail')) { + $table->dropColumn('cash_detail'); + } + }); + } +}; diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 54f76e3..df7ed1f 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1,4 +1,5 @@ { + "Single use only": "只能使用一次", "Search Product...": "搜尋商品...", "Generate Quantity": "產生數量", "Batch mode: codes are auto-generated and the custom code is ignored.": "批次模式:系統自動產生隨機碼,將忽略自訂碼。", @@ -1806,7 +1807,7 @@ "Remote Change": "遠端找零", "Remote Checkout": "遠端結帳", "Remote Command Center": "遠端指令中心", - "Remote Dispense": "遠端出貨", + "Remote Dispense": "遠端開櫃", "Remote Lock": "遠端鎖定", "Remote Management": "遠端管理", "Remote Permissions": "遠端管理權限", diff --git a/resources/views/admin/sales/partials/order-detail-panel.blade.php b/resources/views/admin/sales/partials/order-detail-panel.blade.php index d93a9ca..02472c2 100644 --- a/resources/views/admin/sales/partials/order-detail-panel.blade.php +++ b/resources/views/admin/sales/partials/order-detail-panel.blade.php @@ -233,6 +233,10 @@
{{ $item->product_name }}