[RELEASE] demo -> main: 取貨碼/通行碼/銷售紀錄後台 + 現金支付面額明細(收各面額張數+找零)
This commit is contained in:
commit
c2d289c92a
@ -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(),
|
||||
]);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得所有支付類型的對照標籤
|
||||
*/
|
||||
|
||||
@ -14,6 +14,7 @@ class OrderItem extends Model
|
||||
'order_id',
|
||||
'product_id',
|
||||
'product_name',
|
||||
'slot_no',
|
||||
'barcode',
|
||||
'price',
|
||||
'quantity',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?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('order_items', function (Blueprint $table) {
|
||||
// 消費者選擇的貨道/格子號(格子櫃=櫃號如 501,VMC=貨道號)。
|
||||
// 由 App transaction finalize 的 items[].slot_no 帶入;不管出貨成功或失敗都記錄。
|
||||
$table->string('slot_no', 20)->nullable()->after('product_name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_items', function (Blueprint $table) {
|
||||
$table->dropColumn('slot_no');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
<?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('pass_codes', function (Blueprint $table) {
|
||||
// 可用次數上限;null=無限次(維持原行為)。勾「只能使用一次」時=1。
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 現金支付面額明細:orders 增加 cash_detail JSON 欄位。
|
||||
* 記錄本筆現金交易「收」進的各面額張數(仟/伍佰/佰/50/10/5/1),
|
||||
* 供銷售紀錄在支付金額下方顯示「收:仟:x、伍佰:x… 找:x」。
|
||||
* 找零金額沿用既有 change_amount,不另存。
|
||||
* 僅現金(payment_type=9)交易會帶此欄,其餘交易為 null,不影響既有行為。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('orders', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('orders', 'cash_detail')) {
|
||||
$table->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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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": "遠端管理權限",
|
||||
|
||||
@ -233,6 +233,10 @@
|
||||
<p class="text-base font-extrabold text-slate-800 dark:text-white truncate tracking-tight">{{ $item->product_name }}</p>
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">${{ number_format($item->price, 0) }} × {{ $item->quantity }}</span>
|
||||
@if(!empty($item->slot_no))
|
||||
{{-- 消費者選擇的貨道/格子(不管出貨成功失敗都會記錄) --}}
|
||||
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{ __('Slot') }}: {{ $item->slot_no }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
|
||||
@ -285,6 +285,18 @@
|
||||
{{ __('Discounted') }} ${{ number_format($order->discount_amount, 0) }}
|
||||
</span>
|
||||
@endif
|
||||
@if($order->cash_received_summary)
|
||||
<span class="text-[10px] text-emerald-600 dark:text-emerald-400 font-bold mt-1 tracking-tight leading-relaxed">
|
||||
{{ $order->cash_received_summary }}
|
||||
</span>
|
||||
@endif
|
||||
@if((int) $order->payment_type === 6 && !empty($order->member_barcode))
|
||||
{{-- 取貨碼交易:於支付金額欄換行加註取貨碼序號 --}}
|
||||
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 mt-1.5 flex items-center gap-1">
|
||||
{{ __('Pickup Code') }}:
|
||||
<span class="font-mono tracking-tighter normal-case text-slate-700 dark:text-slate-200">{{ $order->member_barcode }}</span>
|
||||
</span>
|
||||
@endif
|
||||
@if($order->payment_type == 41 && $order->staffCardLog && $order->staffCardLog->staffCard)
|
||||
<span class="text-[10px] text-cyan-600 dark:text-cyan-400 font-extrabold mt-1.5 flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
@ -461,6 +473,11 @@
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 dark:bg-slate-800/50 text-slate-500">{{
|
||||
$paymentTypes[$order->payment_type] ?? '??' }}</span>
|
||||
</p>
|
||||
@if($order->cash_received_summary)
|
||||
<div class="text-[10px] text-emerald-600 dark:text-emerald-400 font-bold mt-1 leading-relaxed">
|
||||
{{ $order->cash_received_summary }}
|
||||
</div>
|
||||
@endif
|
||||
@if($order->payment_type == 41 && $order->staffCardLog && $order->staffCardLog->staffCard)
|
||||
<div class="text-[10px] text-cyan-600 dark:text-cyan-400 font-extrabold mt-1 flex items-center gap-1">
|
||||
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
|
||||
@ -421,6 +421,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 只能使用一次:勾選 → usage_limit=1;不勾 → 無限次(維持原行為) --}}
|
||||
<label class="flex items-center gap-3 px-1 cursor-pointer select-none">
|
||||
<input type="checkbox" name="single_use" value="1"
|
||||
class="w-5 h-5 rounded border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500 cursor-pointer">
|
||||
<span class="text-xs font-black text-slate-500 uppercase tracking-widest">{{ __('Single use only') }}</span>
|
||||
</label>
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-8">
|
||||
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost px-8">
|
||||
{{ __('Cancel') }}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user