feat(sales): 批次產碼數量上限依角色分級(客戶端最多20、管理員最多1000)
- 平台管理員(isSystemAdmin):數量上限 1000,快捷 10/50/100/1000 - 客戶端(租戶):數量上限 20,快捷 10/20,欄位輸入超過自動夾回 20 - 後端 store 以角色動態 max 驗證,防止前端被繞過送出大量產碼 - 取貨碼/通行碼兩表單同步;前端 max 與快捷選項由後端依角色下發 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9dbde50f56
commit
179348ad66
@ -374,6 +374,11 @@ class SalesController extends Controller
|
|||||||
'tab' => $tab,
|
'tab' => $tab,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 批次產生數量上限/快捷選項依角色分級:平台管理員 1000(10/50/100/1000)、客戶端 20(10/20)
|
||||||
|
$isSystemAdmin = Auth::user()->isSystemAdmin();
|
||||||
|
$data['maxBatchQuantity'] = $isSystemAdmin ? 1000 : 20;
|
||||||
|
$data['batchQuickOptions'] = $isSystemAdmin ? [10, 50, 100, 1000] : [10, 20];
|
||||||
|
|
||||||
// 1. 取貨碼列表 (list)
|
// 1. 取貨碼列表 (list)
|
||||||
if (!$isAjax || $tab === 'list') {
|
if (!$isAjax || $tab === 'list') {
|
||||||
// 僅顯示目前帳號可存取機台的取貨碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
|
// 僅顯示目前帳號可存取機台的取貨碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
|
||||||
@ -475,6 +480,10 @@ class SalesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function storePickupCode(Request $request)
|
public function storePickupCode(Request $request)
|
||||||
{
|
{
|
||||||
|
// 批次數量上限依角色分級:平台管理員最多 1000 筆;客戶端(租戶)最多 20 筆。
|
||||||
|
// 後端強制把關,避免前端被繞過送出大量產碼請求。
|
||||||
|
$maxQuantity = Auth::user()->isSystemAdmin() ? 1000 : 20;
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'machine_id' => 'required|exists:machines,id',
|
'machine_id' => 'required|exists:machines,id',
|
||||||
// 取貨碼改綁「商品」:新流程傳 product_id;舊的綁「貨道」流程仍可傳 slot_no(向後相容)。
|
// 取貨碼改綁「商品」:新流程傳 product_id;舊的綁「貨道」流程仍可傳 slot_no(向後相容)。
|
||||||
@ -484,7 +493,7 @@ class SalesController extends Controller
|
|||||||
'expires_hours' => 'nullable|integer|min:1|max:720',
|
'expires_hours' => 'nullable|integer|min:1|max:720',
|
||||||
'expires_at' => 'nullable|date|after:now',
|
'expires_at' => 'nullable|date|after:now',
|
||||||
'custom_code' => 'nullable|string|min:4|max:12',
|
'custom_code' => 'nullable|string|min:4|max:12',
|
||||||
'quantity' => 'nullable|integer|min:1|max:1000',
|
'quantity' => 'nullable|integer|min:1|max:' . $maxQuantity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$machine = Machine::findOrFail($validated['machine_id']);
|
$machine = Machine::findOrFail($validated['machine_id']);
|
||||||
@ -704,6 +713,11 @@ class SalesController extends Controller
|
|||||||
'tab' => $tab,
|
'tab' => $tab,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 批次產生數量上限/快捷選項依角色分級:平台管理員 1000(10/50/100/1000)、客戶端 20(10/20)
|
||||||
|
$isSystemAdmin = Auth::user()->isSystemAdmin();
|
||||||
|
$data['maxBatchQuantity'] = $isSystemAdmin ? 1000 : 20;
|
||||||
|
$data['batchQuickOptions'] = $isSystemAdmin ? [10, 50, 100, 1000] : [10, 20];
|
||||||
|
|
||||||
// 1. 通行碼列表 (list)
|
// 1. 通行碼列表 (list)
|
||||||
if (!$isAjax || $tab === 'list') {
|
if (!$isAjax || $tab === 'list') {
|
||||||
// 僅顯示目前帳號可存取機台的通行碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
|
// 僅顯示目前帳號可存取機台的通行碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
|
||||||
@ -813,12 +827,15 @@ class SalesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function storePassCode(Request $request)
|
public function storePassCode(Request $request)
|
||||||
{
|
{
|
||||||
|
// 批次數量上限依角色分級:平台管理員最多 1000 筆;客戶端(租戶)最多 20 筆(後端強制把關)。
|
||||||
|
$maxQuantity = Auth::user()->isSystemAdmin() ? 1000 : 20;
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'machine_id' => 'required|exists:machines,id',
|
'machine_id' => 'required|exists:machines,id',
|
||||||
'name' => 'required|string|max:50',
|
'name' => 'required|string|max:50',
|
||||||
'expires_days' => 'nullable|integer|min:0',
|
'expires_days' => 'nullable|integer|min:0',
|
||||||
'custom_code' => 'nullable|string|min:4|max:12',
|
'custom_code' => 'nullable|string|min:4|max:12',
|
||||||
'quantity' => 'nullable|integer|min:1|max:1000',
|
'quantity' => 'nullable|integer|min:1|max:' . $maxQuantity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$machine = Machine::findOrFail($validated['machine_id']);
|
$machine = Machine::findOrFail($validated['machine_id']);
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
expiresDays: 7,
|
expiresDays: 7,
|
||||||
customCode: '',
|
customCode: '',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
maxBatchQty: {{ (int) ($maxBatchQuantity ?? 20) }},
|
||||||
|
batchQuickOptions: @js($batchQuickOptions ?? [10, 20]),
|
||||||
status: '{{ request('status', '') }}',
|
status: '{{ request('status', '') }}',
|
||||||
showQrModal: false,
|
showQrModal: false,
|
||||||
activeQrCode: '',
|
activeQrCode: '',
|
||||||
@ -309,7 +311,7 @@
|
|||||||
{{-- Quantity (Batch Generate) --}}
|
{{-- Quantity (Batch Generate) --}}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
||||||
{{ __('Generate Quantity') }} (1-1000)
|
{{ __('Generate Quantity') }} (1-{{ $maxBatchQuantity ?? 20 }})
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex items-center h-12 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden flex-1 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
|
<div class="flex items-center h-12 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden flex-1 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
|
||||||
@ -318,16 +320,17 @@
|
|||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||||
<input type="number" name="quantity" x-model="quantity" min="1" max="1000"
|
<input type="number" name="quantity" x-model="quantity" min="1" :max="maxBatchQty"
|
||||||
|
@input="if (parseInt(quantity) > maxBatchQty) quantity = maxBatchQty"
|
||||||
class="flex-1 min-w-0 bg-transparent border-none text-center text-lg font-black text-slate-800 dark:text-white focus:ring-0 p-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
class="flex-1 min-w-0 bg-transparent border-none text-center text-lg font-black text-slate-800 dark:text-white focus:ring-0 p-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||||
<button type="button" @click="quantity = Math.min(1000, parseInt(quantity || 1) + 1)"
|
<button type="button" @click="quantity = Math.min(maxBatchQty, parseInt(quantity || 1) + 1)"
|
||||||
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<template x-for="val in [10, 50, 100, 1000]">
|
<template x-for="val in batchQuickOptions" :key="val">
|
||||||
<button type="button" @click="quantity = val"
|
<button type="button" @click="quantity = val"
|
||||||
class="px-2 h-10 rounded-xl border border-slate-200 dark:border-slate-700 flex items-center justify-center text-[11px] font-black transition-all shrink-0"
|
class="px-2 h-10 rounded-xl border border-slate-200 dark:border-slate-700 flex items-center justify-center text-[11px] font-black transition-all shrink-0"
|
||||||
:class="quantity == val ? 'bg-cyan-500 text-white border-cyan-500 shadow-lg shadow-cyan-500/20' : 'bg-white dark:bg-slate-800 text-slate-500 hover:border-cyan-500/50'">
|
:class="quantity == val ? 'bg-cyan-500 text-white border-cyan-500 shadow-lg shadow-cyan-500/20' : 'bg-white dark:bg-slate-800 text-slate-500 hover:border-cyan-500/50'">
|
||||||
|
|||||||
@ -295,10 +295,10 @@ transition-all duration-300",
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Quantity (Batch Generate) — 獨立整列,避免與使用次數上限擠在同一列重疊 --}}
|
{{-- Quantity (Batch Generate) — 獨立整列,避免與使用次數上限擠在同一列重疊。上限/快捷選項依角色分級 --}}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||||
__('Generate Quantity') }} (1-1000)</label>
|
__('Generate Quantity') }} (1-{{ $maxBatchQuantity ?? 20 }})</label>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="flex items-center h-12 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden flex-1 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
|
<div class="flex items-center h-12 rounded-2xl border border-slate-200/50 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/50 overflow-hidden flex-1 group focus-within:ring-2 focus-within:ring-cyan-500/20 transition-all">
|
||||||
<button type="button" @click="quantity = Math.max(1, parseInt(quantity || 1) - 1)"
|
<button type="button" @click="quantity = Math.max(1, parseInt(quantity || 1) - 1)"
|
||||||
@ -306,16 +306,17 @@ transition-all duration-300",
|
|||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 12H4"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||||
<input type="number" name="quantity" x-model="quantity" min="1" max="1000"
|
<input type="number" name="quantity" x-model="quantity" min="1" :max="maxBatchQty"
|
||||||
|
@input="if (parseInt(quantity) > maxBatchQty) quantity = maxBatchQty"
|
||||||
class="flex-1 min-w-0 bg-transparent border-none text-center text-lg font-black text-slate-800 dark:text-white focus:ring-0 p-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
class="flex-1 min-w-0 bg-transparent border-none text-center text-lg font-black text-slate-800 dark:text-white focus:ring-0 p-0 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
|
||||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||||
<button type="button" @click="quantity = Math.min(1000, parseInt(quantity || 1) + 1)"
|
<button type="button" @click="quantity = Math.min(maxBatchQty, parseInt(quantity || 1) + 1)"
|
||||||
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
class="shrink-0 w-12 h-full flex items-center justify-center text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 active:scale-90 transition-all">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 5v14M5 12h14"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<template x-for="val in [10, 50, 100, 1000]">
|
<template x-for="val in batchQuickOptions" :key="val">
|
||||||
<button type="button" @click="quantity = val"
|
<button type="button" @click="quantity = val"
|
||||||
class="px-2 h-10 rounded-xl border border-slate-200 dark:border-slate-700 flex items-center justify-center text-[11px] font-black transition-all shrink-0"
|
class="px-2 h-10 rounded-xl border border-slate-200 dark:border-slate-700 flex items-center justify-center text-[11px] font-black transition-all shrink-0"
|
||||||
:class="quantity == val ? 'bg-cyan-500 text-white border-cyan-500 shadow-lg shadow-cyan-500/20' : 'bg-white dark:bg-slate-800 text-slate-500 hover:border-cyan-500/50'">
|
:class="quantity == val ? 'bg-cyan-500 text-white border-cyan-500 shadow-lg shadow-cyan-500/20' : 'bg-white dark:bg-slate-800 text-slate-500 hover:border-cyan-500/50'">
|
||||||
@ -541,6 +542,8 @@ transition-all duration-300",
|
|||||||
customExpiry: '',
|
customExpiry: '',
|
||||||
usageLimit: 1,
|
usageLimit: 1,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
maxBatchQty: {{ (int) ($maxBatchQuantity ?? 20) }},
|
||||||
|
batchQuickOptions: @js($batchQuickOptions ?? [10, 20]),
|
||||||
showQrModal: false,
|
showQrModal: false,
|
||||||
activeQrCode: '',
|
activeQrCode: '',
|
||||||
activeTicketUrl: '',
|
activeTicketUrl: '',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user