[RELEASE] demo -> main: 取貨碼改綁商品
1. [FEAT] 取貨碼改綁「商品」而非「貨道」:發碼時指定機台+商品,刷碼當下由 App 依即時庫存挑可出貨道。
2. 新增 pickup_codes.product_id 欄位 migration(nullable + index,向後相容綁貨道碼)。
3. 產碼端點驗證商品屬機台所屬公司,新增公司商品清單 AJAX 與「機台未上架」前端提示。
4. B660 刷碼新增綁商品擋關(無貨道回 409 並寫 log),回傳補 product_id 供 App 自挑貨道。
5. 取貨碼列表改以商品為主顯示:綁商品碼直接顯示商品名、不再顯示空貨道;綁貨道碼(舊)維持反查。
6. 多語系三語系同步新增詞彙。
⚠️ 部署後須於正式站執行 migrate 建立 product_id 欄位。
This commit is contained in:
commit
65f8ce2d37
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新取貨碼 (僅限修改時間)
|
||||
*/
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯建立者
|
||||
*/
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pickup_codes', function (Blueprint $table) {
|
||||
// 取貨碼改綁「商品」而非「貨道」:發碼時只指定機台+商品,
|
||||
// 刷碼當下由 App 依本機即時庫存/鎖定/效期挑可出貨道(slot_no 對綁商品碼為 null)。
|
||||
// 既有綁貨道碼(slot_no 有值、product_id 為 null)行為不變,向後相容。
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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.",
|
||||
|
||||
@ -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.": "このスロットには保留中の更新があります。前のコマンドが完了するまでお待ちください。",
|
||||
|
||||
@ -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.": "此貨道已有更新指令在執行中,請等候前一個指令完成。",
|
||||
|
||||
@ -234,7 +234,7 @@ transition-all duration-300",
|
||||
__('Target Machine') }} <span class="text-rose-500">*</span></label>
|
||||
<x-searchable-select name="machine_id" id="modal-pickup-machine"
|
||||
placeholder="{{ __('Please select a machine') }}" x-model="selectedMachine"
|
||||
@change="selectedMachine = $event.target.value; fetchSlots()">
|
||||
@change="selectedMachine = $event.target.value; fetchCompanyProducts()">
|
||||
@foreach($machines as $machine)
|
||||
<option value="{{ $machine->id }}"
|
||||
data-title="{{ $machine->name }} ({{ $machine->serial_no }})">{{ $machine->name }}
|
||||
@ -244,7 +244,7 @@ transition-all duration-300",
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Select Slot') }} <span class="text-rose-500">*</span></label>
|
||||
__('Select Product') }} <span class="text-rose-500">*</span></label>
|
||||
<div class="relative">
|
||||
<div id="slot-select-wrapper" class="relative">
|
||||
{{-- Rebuilt by Alpine --}}
|
||||
@ -253,6 +253,10 @@ transition-all duration-300",
|
||||
<x-luxury-spinner show="loadingSlots" />
|
||||
</div>
|
||||
</div>
|
||||
<p x-show="productNotOnMachine" x-cloak class="text-[11px] font-bold text-amber-500 pl-1 flex items-center gap-1">
|
||||
<svg class="size-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M5.07 19h13.86c1.54 0 2.5-1.67 1.73-3L13.73 4a2 2 0 0 0-3.46 0L3.34 16c-.77 1.33.19 3 1.73 3z"/></svg>
|
||||
{{ __('This product is not stocked on the selected machine; the code may not be redeemable until restocked.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@ -494,7 +498,7 @@ transition-all duration-300",
|
||||
tabLoading: null,
|
||||
showCreateModal: false,
|
||||
selectedMachine: '',
|
||||
selectedSlot: '',
|
||||
selectedProduct: '',
|
||||
expiresHours: 24,
|
||||
maxHours: 168,
|
||||
expiryMode: 'hours',
|
||||
@ -503,7 +507,9 @@ transition-all duration-300",
|
||||
showQrModal: false,
|
||||
activeQrCode: '',
|
||||
activeTicketUrl: '',
|
||||
slots: [],
|
||||
companyProducts: [],
|
||||
machineProductIds: [],
|
||||
productNotOnMachine: false,
|
||||
tabLoading: null,
|
||||
loadingSlots: false,
|
||||
showCancelModal: false,
|
||||
@ -599,18 +605,22 @@ transition-all duration-300",
|
||||
return this.calculateExpiry();
|
||||
},
|
||||
|
||||
async fetchSlots() {
|
||||
// 取貨碼綁商品:下拉以「機台所屬公司的商品」為準(非僅機台貨道);
|
||||
// machineProductIds 用來標示/提示「此機台目前未上架」的商品。
|
||||
async fetchCompanyProducts() {
|
||||
if (!this.selectedMachine) {
|
||||
this.slots = [];
|
||||
this.companyProducts = [];
|
||||
this.machineProductIds = [];
|
||||
return;
|
||||
}
|
||||
this.loadingSlots = true;
|
||||
try {
|
||||
const response = await fetch(`/admin/machines/${this.selectedMachine}/slots-ajax`);
|
||||
const response = await fetch(`/admin/sales/pickup-codes/company-products/${this.selectedMachine}`);
|
||||
const data = await response.json();
|
||||
this.slots = data.slots || [];
|
||||
this.companyProducts = data.products || [];
|
||||
this.machineProductIds = (data.machine_product_ids || []).map(id => String(id));
|
||||
} catch (error) {
|
||||
console.error('Error fetching slots:', error);
|
||||
console.error('Error fetching company products:', error);
|
||||
} finally {
|
||||
this.loadingSlots = false;
|
||||
}
|
||||
@ -621,8 +631,8 @@ transition-all duration-300",
|
||||
window.showToast('{{ __("Please select a machine") }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (!this.selectedSlot) {
|
||||
window.showToast('{{ __("Please select a slot") }}', 'error');
|
||||
if (!this.selectedProduct) {
|
||||
window.showToast('{{ __("Please select a product") }}', 'error');
|
||||
return;
|
||||
}
|
||||
this.$refs.createForm.submit();
|
||||
@ -674,6 +684,8 @@ transition-all duration-300",
|
||||
this.showCancelModal = false;
|
||||
},
|
||||
|
||||
// 取貨碼改綁商品:下拉改列「此機台有上架的商品」(由貨道清單去重),
|
||||
// 提交 product_id;貨道改由 App 刷碼當下依即時庫存挑選。
|
||||
updateSlotSelect() {
|
||||
this.$nextTick(() => {
|
||||
const wrapper = document.getElementById('slot-select-wrapper');
|
||||
@ -687,15 +699,15 @@ transition-all duration-300",
|
||||
if (!this.selectedMachine) {
|
||||
const dummy = document.createElement('div');
|
||||
dummy.className = 'luxury-input opacity-50 cursor-not-allowed flex items-center justify-between';
|
||||
dummy.innerHTML = `<span>{{ __("Select Slot") }}</span><svg class="size-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/></svg>`;
|
||||
dummy.innerHTML = `<span>{{ __("Select Product") }}</span><svg class="size-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/></svg>`;
|
||||
wrapper.appendChild(dummy);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectEl = document.createElement('select');
|
||||
selectEl.name = 'slot_no';
|
||||
selectEl.name = 'product_id';
|
||||
// Remove required to allow our custom validation toast to trigger
|
||||
selectEl.id = 'modal-pickup-slot-' + Date.now();
|
||||
selectEl.id = 'modal-pickup-product-' + Date.now();
|
||||
selectEl.className = 'hidden';
|
||||
|
||||
// 清理配置字串中的換行符
|
||||
@ -706,22 +718,32 @@ transition-all duration-300",
|
||||
|
||||
const placeholderOpt = document.createElement('option');
|
||||
placeholderOpt.value = "";
|
||||
placeholderOpt.textContent = "{{ __('Select Slot') }}";
|
||||
placeholderOpt.setAttribute('data-title', "{{ __('Select Slot') }}");
|
||||
placeholderOpt.textContent = "{{ __('Select Product') }}";
|
||||
placeholderOpt.setAttribute('data-title', "{{ __('Select Product') }}");
|
||||
selectEl.appendChild(placeholderOpt);
|
||||
|
||||
this.slots.forEach(slot => {
|
||||
// 列出機台所屬公司的商品;機台目前未上架者於名稱後標示提示
|
||||
this.companyProducts.forEach(product => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = slot.slot_no;
|
||||
const text = `${slot.slot_no} - ${slot.product ? slot.product.name : 'Empty'}`;
|
||||
opt.value = product.id;
|
||||
const onMachine = this.machineProductIds.includes(String(product.id));
|
||||
const text = onMachine ? product.name : `${product.name}({{ __('Not stocked on this machine') }})`;
|
||||
opt.textContent = text;
|
||||
opt.setAttribute('data-title', text);
|
||||
if (this.selectedSlot == slot.slot_no) opt.selected = true;
|
||||
if (this.selectedProduct == product.id) opt.selected = true;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
|
||||
wrapper.appendChild(selectEl);
|
||||
selectEl.addEventListener('change', e => { this.selectedSlot = e.target.value; });
|
||||
selectEl.addEventListener('change', e => {
|
||||
this.selectedProduct = e.target.value;
|
||||
// 選到機台未上架的商品 → 提示,但仍允許產碼
|
||||
this.productNotOnMachine = !!this.selectedProduct
|
||||
&& !this.machineProductIds.includes(String(this.selectedProduct));
|
||||
if (this.productNotOnMachine) {
|
||||
window.showToast('{{ __("This product is not stocked on the selected machine; the code may not be redeemable until restocked.") }}', 'warning');
|
||||
}
|
||||
});
|
||||
|
||||
if (window.HSStaticMethods?.autoInit) {
|
||||
window.HSStaticMethods.autoInit(['select']);
|
||||
@ -757,13 +779,14 @@ transition-all duration-300",
|
||||
});
|
||||
});
|
||||
|
||||
this.$watch('slots', () => this.updateSlotSelect());
|
||||
this.$watch('companyProducts', () => this.updateSlotSelect());
|
||||
this.$watch('selectedMachine', (val) => {
|
||||
this.syncSelect('modal-pickup-machine', val);
|
||||
this.selectedSlot = ''; // Reset slot when machine changes
|
||||
this.fetchSlots();
|
||||
this.selectedProduct = ''; // Reset product when machine changes
|
||||
this.productNotOnMachine = false;
|
||||
this.fetchCompanyProducts();
|
||||
});
|
||||
this.$watch('selectedSlot', (val) => {
|
||||
this.$watch('selectedProduct', (val) => {
|
||||
// Selection toast removed per user request
|
||||
});
|
||||
this.$watch('expiryMode', (mode) => {
|
||||
@ -775,7 +798,8 @@ transition-all duration-300",
|
||||
this.$watch('showCreateModal', (val) => {
|
||||
if (val) {
|
||||
this.selectedMachine = '';
|
||||
this.selectedSlot = '';
|
||||
this.selectedProduct = '';
|
||||
this.productNotOnMachine = false;
|
||||
this.usageLimit = 1;
|
||||
this.expiryMode = 'hours';
|
||||
this.expiresHours = 24;
|
||||
@ -783,7 +807,8 @@ transition-all duration-300",
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setMinutes(0);
|
||||
this.customExpiry = this.formatDate(tomorrow);
|
||||
this.slots = [];
|
||||
this.companyProducts = [];
|
||||
this.machineProductIds = [];
|
||||
this.generateRandomCode();
|
||||
this.updateSlotSelect();
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
{{ __('Product / Slot') }}
|
||||
{{ __('Product') }}
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
|
||||
@ -125,15 +125,25 @@
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
@php
|
||||
$slot = $code->machine?->slots->where('slot_no', $code->slot_no)->first();
|
||||
$productName = $slot ? ($slot->product?->name ?? __('Empty')) : __('Empty');
|
||||
// 綁商品碼(新):直接取 product 名稱,不顯示貨道。
|
||||
// 綁貨道碼(舊,向後相容):由貨道反查商品,並保留貨道資訊。
|
||||
if ($code->product_id) {
|
||||
$productName = $code->product?->name ?? __('Empty');
|
||||
$slotLabel = null;
|
||||
} else {
|
||||
$slot = $code->machine?->slots->where('slot_no', $code->slot_no)->first();
|
||||
$productName = $slot ? ($slot->product?->name ?? __('Empty')) : __('Empty');
|
||||
$slotLabel = $code->slot_no;
|
||||
}
|
||||
@endphp
|
||||
<div class="text-sm font-black text-cyan-600 dark:text-cyan-400 mb-0.5">{{
|
||||
$productName
|
||||
}}</div>
|
||||
@if($slotLabel)
|
||||
<div class="text-xs font-bold text-slate-400 uppercase tracking-widest">{{
|
||||
__('Slot')
|
||||
}}: {{ $code->slot_no }}</div>
|
||||
}}: {{ $slotLabel }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6 whitespace-nowrap">
|
||||
<div class="text-sm font-black text-slate-600 dark:text-slate-300">
|
||||
@ -281,19 +291,24 @@
|
||||
<div
|
||||
class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
|
||||
<div>
|
||||
@php
|
||||
if ($code->product_id) {
|
||||
$prodMobile = $code->product?->name ?? __('Empty');
|
||||
$slotMobileLabel = null;
|
||||
} else {
|
||||
$slotMobile = $code->machine?->slots->where('slot_no', $code->slot_no)->first();
|
||||
$prodMobile = $slotMobile ? ($slotMobile->product?->name ?? __('Empty')) : __('Empty');
|
||||
$slotMobileLabel = $code->slot_no;
|
||||
}
|
||||
@endphp
|
||||
<p
|
||||
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
|
||||
{{ __('Slot') }}</p>
|
||||
{{ __('Product') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
|
||||
{{ $code->slot_no }}
|
||||
@php
|
||||
$slotMobile = $code->machine?->slots->where('slot_no',
|
||||
$code->slot_no)->first();
|
||||
$prodMobile = $slotMobile ? ($slotMobile->product?->name ?? __('Empty')) :
|
||||
__('Empty');
|
||||
@endphp
|
||||
<span class="ml-1 text-cyan-600 dark:text-cyan-400">({{ $prodMobile
|
||||
}})</span>
|
||||
<span class="text-cyan-600 dark:text-cyan-400">{{ $prodMobile }}</span>
|
||||
@if($slotMobileLabel)
|
||||
<span class="ml-1 text-[10px] text-slate-400">{{ __('Slot') }}: {{ $slotMobileLabel }}</span>
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -162,6 +162,8 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
||||
|
||||
// 取貨碼
|
||||
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'pickupCodes'])->name('pickup-codes');
|
||||
// 產碼用:回傳機台所屬公司的商品清單 + 機台目前已上架的 product_id(供前端標示「機台未上架」提示)
|
||||
Route::get('/pickup-codes/company-products/{machine}', [App\Http\Controllers\Admin\SalesController::class, 'pickupCompanyProducts'])->name('pickup-codes.company-products');
|
||||
Route::post('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePickupCode'])->name('pickup-codes.store');
|
||||
Route::patch('/pickup-codes/{pickupCode}', [App\Http\Controllers\Admin\SalesController::class, 'updatePickupCode'])->name('pickup-codes.update');
|
||||
Route::delete('/pickup-codes/{pickupCode}', [App\Http\Controllers\Admin\SalesController::class, 'destroyPickupCode'])->name('pickup-codes.destroy');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user