diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 934d398..01f8f24 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -376,7 +376,7 @@ class SalesController extends Controller // 1. 取貨碼列表 (list) if (!$isAjax || $tab === 'list') { // 僅顯示目前帳號可存取機台的取貨碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope) - $query = PickupCode::with(['machine.slots.product', 'creator', 'order']) + $query = PickupCode::with(['machine.slots.product', 'product', 'creator', 'order']) ->whereHas('machine') ->latest(); @@ -476,23 +476,37 @@ class SalesController extends Controller { $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', - 'slot_no' => 'required|string', + // 取貨碼改綁「商品」:新流程傳 product_id;舊的綁「貨道」流程仍可傳 slot_no(向後相容)。 + 'product_id' => 'required_without:slot_no|nullable|exists:products,id', + 'slot_no' => 'required_without:product_id|nullable|string', 'usage_limit' => 'nullable|integer|min:1|max:20', - 'expires_hours' => 'nullable|integer|min:1|max:720', + 'expires_hours' => 'nullable|integer|min:1|max:720', 'expires_at' => 'nullable|date|after:now', 'custom_code' => 'nullable|string|min:4|max:12', ]); $machine = Machine::findOrFail($validated['machine_id']); - + + // 綁商品時驗證該商品屬於此機台所屬公司(不要求機台當下有貨道;機台未上架仍可產碼, + // 由前端提示、刷碼時若無貨道再由 B660 擋下)。 + if (!empty($validated['product_id'])) { + $belongsToCompany = \App\Models\Product\Product::where('id', $validated['product_id']) + ->where('company_id', $machine->company_id) + ->exists(); + if (!$belongsToCompany) { + return back()->withErrors(['product_id' => __('The selected product does not belong to this machine\'s company.')]); + } + } + // 處理過期時間:優先使用直接傳入的日期,否則使用時數計算 - $expiresAt = $request->filled('expires_at') - ? Carbon::parse($validated['expires_at']) + $expiresAt = $request->filled('expires_at') + ? Carbon::parse($validated['expires_at']) : now()->addHours((int) ($request->expires_hours ?? 24)); $pickupCode = PickupCode::create([ 'machine_id' => $validated['machine_id'], - 'slot_no' => $validated['slot_no'], + 'product_id' => $validated['product_id'] ?? null, + 'slot_no' => $validated['slot_no'] ?? null, 'code' => $request->custom_code ?? PickupCode::generateUniqueCode($validated['machine_id']), 'usage_limit' => $validated['usage_limit'] ?? 1, 'usage_count' => 0, @@ -515,6 +529,30 @@ class SalesController extends Controller return back()->with('success', __('Pickup code generated: :code', ['code' => $pickupCode->code])); } + /** + * 產碼用 AJAX:回傳機台所屬「公司」的商品清單,以及機台目前已上架的 product_id。 + * 商品下拉以公司商品為準(非僅機台貨道);前端用 machine_product_ids 標示「此機台未上架」提示。 + */ + public function pickupCompanyProducts(Machine $machine) + { + $products = \App\Models\Product\Product::where('company_id', $machine->company_id) + ->select('id', 'name') + ->orderBy('name') + ->get(); + + $machineProductIds = $machine->slots() + ->whereNotNull('product_id') + ->pluck('product_id') + ->unique() + ->values(); + + return response()->json([ + 'success' => true, + 'products' => $products, + 'machine_product_ids' => $machineProductIds, + ]); + } + /** * 更新取貨碼 (僅限修改時間) */ diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index e636c34..57d3167 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -701,9 +701,10 @@ class MachineController extends Controller // 一般取貨碼的「核銷」仍由 MQTT finalizeTransaction 流程處理(行為不變)。 $data = [ - 'slot_no' => $pickupCode->slot_no, // 保留:既有單貨道機台讀此欄(領藥單為 null) + 'slot_no' => $pickupCode->slot_no, // 保留:既有綁貨道碼讀此欄(綁商品碼/領藥單為 null) + 'product_id' => $pickupCode->product_id, // 綁商品碼:App 據此自挑可出貨道(綁貨道碼為 null) 'pickup_code_id' => $pickupCode->id, - 'code_id' => $pickupCode->id, // 統一回傳 code_id + 'code_id' => $pickupCode->id, // 統一回傳 code_id 'status' => $pickupCode->status, ]; @@ -780,6 +781,27 @@ class MachineController extends Controller $data['status'] = $pickupCode->status; } + // === 綁商品取貨碼(非領藥單)擋關:機台須仍有此商品貨道,否則 App 必然挑不到貨道 === + // 即時庫存/鎖定/效期由 App 刷碼當下自行判斷;此處只擋「機台根本沒上這商品」的明確錯誤。 + if (!$isPharmacy && $pickupCode->product_id) { + $hasProduct = $machine->slots()->where('product_id', $pickupCode->product_id)->exists(); + if (!$hasProduct) { + PickupCodeLog::create([ + 'company_id' => $machine->company_id, + 'machine_id' => $machine->id, + 'pickup_code_id' => $pickupCode->id, + 'action' => 'verify_failed', + 'remark' => 'log.pickup.no_slot_for_product', + 'raw_data' => ['code' => $pickupCode->code, 'product_id' => $pickupCode->product_id], + ]); + return response()->json([ + 'success' => false, + 'code' => 409, + 'message' => '此機台目前無此商品貨道,無法取貨', + ], 409); + } + } + return response()->json([ 'success' => true, 'code' => 200, diff --git a/app/Models/Transaction/PickupCode.php b/app/Models/Transaction/PickupCode.php index a5e52f4..1e84a75 100644 --- a/app/Models/Transaction/PickupCode.php +++ b/app/Models/Transaction/PickupCode.php @@ -15,6 +15,7 @@ class PickupCode extends Model protected $fillable = [ 'company_id', 'machine_id', + 'product_id', 'slot_no', 'code', 'slug', @@ -58,6 +59,14 @@ class PickupCode extends Model return $this->belongsTo(Machine::class); } + /** + * 關聯商品(取貨碼改綁商品時使用) + */ + public function product(): BelongsTo + { + return $this->belongsTo(\App\Models\Product\Product::class); + } + /** * 關聯建立者 */ diff --git a/database/migrations/2026_06_26_120000_add_product_id_to_pickup_codes_table.php b/database/migrations/2026_06_26_120000_add_product_id_to_pickup_codes_table.php new file mode 100644 index 0000000..744a0e7 --- /dev/null +++ b/database/migrations/2026_06_26_120000_add_product_id_to_pickup_codes_table.php @@ -0,0 +1,28 @@ +unsignedBigInteger('product_id')->nullable()->after('machine_id') + ->comment('綁定商品(取貨碼改綁商品時使用;貨道由 App 刷碼當下挑)'); + $table->index('product_id'); + }); + } + + public function down(): void + { + Schema::table('pickup_codes', function (Blueprint $table) { + $table->dropIndex(['product_id']); + $table->dropColumn('product_id'); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 0bc6287..079f8ba 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1422,6 +1422,7 @@ "Not Used": "Not Used", "Not Used Description": "Not Used Description", "Not a co-branded card": "Not a co-branded card", + "Not stocked on this machine": "Not stocked on this machine", "Note": "Note", "Note (optional)": "Note (optional)", "Notes": "Notes", @@ -1649,6 +1650,7 @@ "Please select a machine to view and manage its advertisements.": "Please select a machine to view and manage its advertisements.", "Please select a machine to view metrics": "Please select a machine to view metrics", "Please select a material": "Please select a material", + "Please select a product": "Please select a product", "Please select a slot": "Please select a slot", "Please select an APK file to upload": "Please select an APK file to upload", "Please select at least one medicine with quantity.": "Please select at least one medicine with quantity.", @@ -2216,6 +2218,7 @@ "The image is too large. Please upload an image smaller than 1MB.": "The image is too large. Please upload an image smaller than 1MB.", "The image is too large. Please upload an image smaller than 5MB.": "The image is too large. Please upload an image smaller than 5MB.", "The machine will be notified to fetch the latest system settings. It may take a moment to apply.": "The machine will be notified to fetch the latest system settings. It may take a moment to apply.", + "The selected product does not belong to this machine's company.": "The selected product does not belong to this machine's company.", "This Month": "This Month", "This Week": "This Week", "This account does not belong to this company.": "This account does not belong to this company.", @@ -2226,6 +2229,7 @@ "This machine has e-invoice disabled; cannot issue": "This machine has e-invoice disabled; cannot issue", "This machine has no ECPay invoice settings": "This machine has no ECPay invoice settings", "This pharmacy pickup order can no longer be cancelled.": "This pharmacy pickup order can no longer be cancelled.", + "This product is not stocked on the selected machine; the code may not be redeemable until restocked.": "This product is not stocked on the selected machine; the code may not be redeemable until restocked.", "This role belongs to another company and cannot be assigned.": "This role belongs to another company and cannot be assigned.", "This slot has a pending command. Please wait.": "This slot has a pending command. Please wait.", "This slot has a pending update. Please wait for the previous command to complete.": "This slot has a pending update. Please wait for the previous command to complete.", diff --git a/lang/ja.json b/lang/ja.json index c6f3835..6fc3c9c 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1422,6 +1422,7 @@ "Not Used": "使用しない", "Not Used Description": "サードパーティ決済を使用しない", "Not a co-branded card": "提携カードではありません", + "Not stocked on this machine": "この機器に未陳列", "Note": "備考", "Note (optional)": "備考 (任意)", "Notes": "備考", @@ -1649,6 +1650,7 @@ "Please select a machine to view and manage its advertisements.": "機器を選択して、その広告を表示および管理してください。", "Please select a machine to view metrics": "指標を表示する機器を選択してください", "Please select a material": "素材を選択してください", + "Please select a product": "商品を選択してください", "Please select a slot": "スロットを選択してください", "Please select an APK file to upload": "Please select an APK file to upload", "Please select at least one medicine with quantity.": "数量を入力した薬品を少なくとも1つ選択してください。", @@ -2216,6 +2218,7 @@ "The image is too large. Please upload an image smaller than 1MB.": "画像サイズが大きすぎます。1MB未満の画像をアップロードしてください。", "The image is too large. Please upload an image smaller than 5MB.": "画像サイズが大きすぎます。5MB未満の画像をアップロードしてください。", "The machine will be notified to fetch the latest system settings. It may take a moment to apply.": "最新のシステム設定を取得するよう機台に通知しました。反映まで少々時間がかかる場合があります。", + "The selected product does not belong to this machine's company.": "選択した商品はこの機器の所属会社に属していません。", "This Month": "今月", "This Week": "今週", "This account does not belong to this company.": "このアカウントは該当企業に所属していません。", @@ -2226,6 +2229,7 @@ "This machine has e-invoice disabled; cannot issue": "この機台は電子インボイスが無効のため発行できません", "This machine has no ECPay invoice settings": "This machine has no ECPay invoice settings", "This pharmacy pickup order can no longer be cancelled.": "この薬品受取注文はこれ以上取り消せません。", + "This product is not stocked on the selected machine; the code may not be redeemable until restocked.": "この商品は選択した機器に陳列されていません。補充されるまでコードを引き換えできない場合があります。", "This role belongs to another company and cannot be assigned.": "このロールは別の会社に属しているため、割り当てることができません。", "This slot has a pending command. Please wait.": "このスロットには実行中のコマンドがあります。しばらくお待ちください。", "This slot has a pending update. Please wait for the previous command to complete.": "このスロットには保留中の更新があります。前のコマンドが完了するまでお待ちください。", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index f898cd0..93d16c8 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1422,6 +1422,7 @@ "Not Used": "不使用", "Not Used Description": "不使用第三方支付介接", "Not a co-branded card": "非聯名卡", + "Not stocked on this machine": "未在此機台上架", "Note": "備註", "Note (optional)": "備註 (選填)", "Notes": "備註", @@ -1649,6 +1650,7 @@ "Please select a machine to view and manage its advertisements.": "請選擇一個機台以查看並管理其廣告。", "Please select a machine to view metrics": "請選擇機台以查看數據", "Please select a material": "請選擇素材", + "Please select a product": "請選擇商品", "Please select a slot": "請選擇貨道", "Please select an APK file to upload": "請選擇要上傳的 APK 檔案", "Please select at least one medicine with quantity.": "請至少勾選一項藥品並填寫數量。", @@ -2216,6 +2218,7 @@ "The image is too large. Please upload an image smaller than 1MB.": "圖片檔案太大,請上傳小於 1MB 的圖片。", "The image is too large. Please upload an image smaller than 5MB.": "圖片檔案太大,請上傳小於 5MB 的圖片。", "The machine will be notified to fetch the latest system settings. It may take a moment to apply.": "已通知機台回抓最新系統設定,套用可能需要一點時間。", + "The selected product does not belong to this machine's company.": "所選商品不屬於此機台所屬的公司。", "This Month": "本月", "This Week": "本週", "This account does not belong to this company.": "此帳號不屬於該公司。", @@ -2226,6 +2229,7 @@ "This machine has e-invoice disabled; cannot issue": "此機台未啟用電子發票,無法開立", "This machine has no ECPay invoice settings": "此機台未設定綠界電子發票", "This pharmacy pickup order can no longer be cancelled.": "此領藥單已無法作廢。", + "This product is not stocked on the selected machine; the code may not be redeemable until restocked.": "此商品未在所選機台上架,補貨前取貨碼可能無法兌換。", "This role belongs to another company and cannot be assigned.": "此角色屬於其他公司,無法指派。", "This slot has a pending command. Please wait.": "此貨道已有指令正在執行,請稍後。", "This slot has a pending update. Please wait for the previous command to complete.": "此貨道已有更新指令在執行中,請等候前一個指令完成。", diff --git a/resources/views/admin/sales/pickup-codes/index.blade.php b/resources/views/admin/sales/pickup-codes/index.blade.php index 2ddd259..2328787 100644 --- a/resources/views/admin/sales/pickup-codes/index.blade.php +++ b/resources/views/admin/sales/pickup-codes/index.blade.php @@ -234,7 +234,7 @@ transition-all duration-300", __('Target Machine') }} * + @change="selectedMachine = $event.target.value; fetchCompanyProducts()"> @foreach($machines as $machine)