[PROMOTE] dev -> demo: 取貨碼/通行碼批次新增(≤1000)+CSV下載+角色數量分級+cache表migration
This commit is contained in:
commit
d90621f8c7
@ -19,6 +19,7 @@ use App\Models\Machine\Machine;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Models\System\SystemOperationLog;
|
||||
|
||||
@ -373,6 +374,11 @@ class SalesController extends Controller
|
||||
'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)
|
||||
if (!$isAjax || $tab === 'list') {
|
||||
// 僅顯示目前帳號可存取機台的取貨碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
|
||||
@ -474,6 +480,10 @@ class SalesController extends Controller
|
||||
*/
|
||||
public function storePickupCode(Request $request)
|
||||
{
|
||||
// 批次數量上限依角色分級:平台管理員最多 1000 筆;客戶端(租戶)最多 20 筆。
|
||||
// 後端強制把關,避免前端被繞過送出大量產碼請求。
|
||||
$maxQuantity = Auth::user()->isSystemAdmin() ? 1000 : 20;
|
||||
|
||||
$validated = $request->validate([
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
// 取貨碼改綁「商品」:新流程傳 product_id;舊的綁「貨道」流程仍可傳 slot_no(向後相容)。
|
||||
@ -483,9 +493,11 @@ class SalesController extends Controller
|
||||
'expires_hours' => 'nullable|integer|min:1|max:720',
|
||||
'expires_at' => 'nullable|date|after:now',
|
||||
'custom_code' => 'nullable|string|min:4|max:12',
|
||||
'quantity' => 'nullable|integer|min:1|max:' . $maxQuantity,
|
||||
]);
|
||||
|
||||
$machine = Machine::findOrFail($validated['machine_id']);
|
||||
$quantity = (int) ($validated['quantity'] ?? 1);
|
||||
|
||||
// 綁商品時驗證該商品屬於此機台所屬公司(不要求機台當下有貨道;機台未上架仍可產碼,
|
||||
// 由前端提示、刷碼時若無貨道再由 B660 擋下)。
|
||||
@ -502,6 +514,66 @@ class SalesController extends Controller
|
||||
$expiresAt = $request->filled('expires_at')
|
||||
? Carbon::parse($validated['expires_at'])
|
||||
: now()->addHours((int) ($request->expires_hours ?? 24));
|
||||
$usageLimit = $validated['usage_limit'] ?? 1;
|
||||
|
||||
// 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。
|
||||
// 沿用單筆的綁定方式(優先 product_id,相容舊的 slot_no)。
|
||||
if ($quantity > 1) {
|
||||
$batchNo = 'PUB' . now()->format('YmdHis') . strtoupper(Str::random(4));
|
||||
$codes = $this->generateUniqueCodeBatch(PickupCode::class, $machine->id, $quantity);
|
||||
$nowTs = now();
|
||||
|
||||
$rows = [];
|
||||
foreach ($codes as $code) {
|
||||
$rows[] = [
|
||||
'company_id' => $machine->company_id,
|
||||
'machine_id' => $machine->id,
|
||||
'product_id' => $validated['product_id'] ?? null,
|
||||
'slot_no' => $validated['slot_no'] ?? null,
|
||||
'code' => $code,
|
||||
'slug' => Str::random(16),
|
||||
'batch_no' => $batchNo,
|
||||
'status' => 'active',
|
||||
'usage_limit' => $usageLimit,
|
||||
'usage_count' => 0,
|
||||
'expires_at' => $expiresAt,
|
||||
'created_by' => Auth::id(),
|
||||
'created_at' => $nowTs,
|
||||
'updated_at' => $nowTs,
|
||||
];
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($rows) {
|
||||
foreach (array_chunk($rows, 500) as $chunk) {
|
||||
PickupCode::insert($chunk);
|
||||
}
|
||||
});
|
||||
|
||||
SystemOperationLog::create([
|
||||
'company_id' => $machine->company_id,
|
||||
'user_id' => Auth::id(),
|
||||
'module' => 'pickup_code',
|
||||
'action' => 'batch_create',
|
||||
'target_id' => null,
|
||||
'target_type' => PickupCode::class,
|
||||
'new_values' => [
|
||||
'batch_no' => $batchNo,
|
||||
'count' => count($rows),
|
||||
'machine_id' => $machine->id,
|
||||
'product_id' => $validated['product_id'] ?? null,
|
||||
'slot_no' => $validated['slot_no'] ?? null,
|
||||
],
|
||||
]);
|
||||
|
||||
return back()
|
||||
->with('success', __(':count pickup codes generated', ['count' => count($rows)]))
|
||||
->with('code_batch', [
|
||||
'type' => 'pickup',
|
||||
'batch_no' => $batchNo,
|
||||
'count' => count($rows),
|
||||
'download_url' => route('admin.sales.pickup-codes.batch-download', $batchNo),
|
||||
]);
|
||||
}
|
||||
|
||||
$pickupCode = PickupCode::create([
|
||||
'machine_id' => $validated['machine_id'],
|
||||
@ -641,6 +713,11 @@ class SalesController extends Controller
|
||||
'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)
|
||||
if (!$isAjax || $tab === 'list') {
|
||||
// 僅顯示目前帳號可存取機台的通行碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope)
|
||||
@ -750,20 +827,79 @@ class SalesController extends Controller
|
||||
*/
|
||||
public function storePassCode(Request $request)
|
||||
{
|
||||
// 批次數量上限依角色分級:平台管理員最多 1000 筆;客戶端(租戶)最多 20 筆(後端強制把關)。
|
||||
$maxQuantity = Auth::user()->isSystemAdmin() ? 1000 : 20;
|
||||
|
||||
$validated = $request->validate([
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
'name' => 'required|string|max:50',
|
||||
'expires_days' => 'nullable|integer|min:0',
|
||||
'custom_code' => 'required|string|min:4|max:12',
|
||||
'custom_code' => 'nullable|string|min:4|max:12',
|
||||
'quantity' => 'nullable|integer|min:1|max:' . $maxQuantity,
|
||||
]);
|
||||
|
||||
$machine = Machine::findOrFail($validated['machine_id']);
|
||||
$quantity = (int) ($validated['quantity'] ?? 1);
|
||||
$expiresAt = $request->expires_days ? now()->addDays((int) $request->expires_days) : null;
|
||||
|
||||
// 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。
|
||||
if ($quantity > 1) {
|
||||
$batchNo = 'PSB' . now()->format('YmdHis') . strtoupper(Str::random(4));
|
||||
$codes = $this->generateUniqueCodeBatch(PassCode::class, $machine->id, $quantity);
|
||||
$nowTs = now();
|
||||
|
||||
$rows = [];
|
||||
foreach ($codes as $i => $code) {
|
||||
$rows[] = [
|
||||
'company_id' => $machine->company_id,
|
||||
'machine_id' => $machine->id,
|
||||
'name' => $validated['name'] . ' #' . ($i + 1),
|
||||
'code' => $code,
|
||||
'slug' => Str::random(16),
|
||||
'batch_no' => $batchNo,
|
||||
'expires_at' => $expiresAt,
|
||||
'status' => 'active',
|
||||
'created_by' => Auth::id(),
|
||||
'created_at' => $nowTs,
|
||||
'updated_at' => $nowTs,
|
||||
];
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($rows) {
|
||||
foreach (array_chunk($rows, 500) as $chunk) {
|
||||
PassCode::insert($chunk);
|
||||
}
|
||||
});
|
||||
|
||||
SystemOperationLog::create([
|
||||
'company_id' => $machine->company_id,
|
||||
'user_id' => Auth::id(),
|
||||
'module' => 'pass_code',
|
||||
'action' => 'batch_create',
|
||||
'target_id' => null,
|
||||
'target_type' => PassCode::class,
|
||||
'new_values' => [
|
||||
'batch_no' => $batchNo,
|
||||
'count' => count($rows),
|
||||
'machine_id' => $machine->id,
|
||||
'name' => $validated['name'],
|
||||
],
|
||||
]);
|
||||
|
||||
return back()
|
||||
->with('success', __(':count pass codes created', ['count' => count($rows)]))
|
||||
->with('code_batch', [
|
||||
'type' => 'pass',
|
||||
'batch_no' => $batchNo,
|
||||
'count' => count($rows),
|
||||
'download_url' => route('admin.sales.pass-codes.batch-download', $batchNo),
|
||||
]);
|
||||
}
|
||||
|
||||
$passCode = PassCode::create([
|
||||
'machine_id' => $validated['machine_id'],
|
||||
'name' => $validated['name'] ?? 'Manual Generate',
|
||||
'code' => $validated['custom_code'] ?? PassCode::generateUniqueCode($validated['machine_id']),
|
||||
'code' => $request->custom_code ?: PassCode::generateUniqueCode($validated['machine_id']),
|
||||
'expires_at' => $expiresAt,
|
||||
'status' => 'active',
|
||||
'company_id' => $machine->company_id,
|
||||
@ -783,6 +919,125 @@ class SalesController extends Controller
|
||||
return back()->with('success', __('Pass code created: :code', ['code' => $passCode->code]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批次產生指定數量的唯一 8 位數字碼(避開該機台目前有效的碼,並去除同批重複)
|
||||
*
|
||||
* @param class-string<\Illuminate\Database\Eloquent\Model> $modelClass PickupCode::class 或 PassCode::class
|
||||
* @return string[]
|
||||
*/
|
||||
private function generateUniqueCodeBatch(string $modelClass, int $machineId, int $quantity): array
|
||||
{
|
||||
// 載入該機台目前仍有效的碼,避免與既有碼衝突
|
||||
$existing = array_flip(
|
||||
$modelClass::where('machine_id', $machineId)
|
||||
->where('status', 'active')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->pluck('code')
|
||||
->all()
|
||||
);
|
||||
|
||||
$codes = [];
|
||||
$seen = [];
|
||||
while (count($codes) < $quantity) {
|
||||
$code = str_pad((string) random_int(0, 99999999), 8, '0', STR_PAD_LEFT);
|
||||
if (isset($existing[$code]) || isset($seen[$code])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$code] = true;
|
||||
$codes[] = $code;
|
||||
}
|
||||
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下載某一批取貨碼清單 (CSV)
|
||||
*/
|
||||
public function downloadPickupCodeBatch(string $batchNo)
|
||||
{
|
||||
$codes = PickupCode::with(['machine:id,name,serial_no', 'product:id,name'])
|
||||
->where('batch_no', $batchNo)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
abort_if($codes->isEmpty(), 404);
|
||||
|
||||
$header = [
|
||||
__('Pickup Code'),
|
||||
__('Target Machine'),
|
||||
__('Select Product'),
|
||||
__('Select Slot'),
|
||||
__('Usage Limit'),
|
||||
__('Expires At'),
|
||||
__('Link'),
|
||||
__('Created At'),
|
||||
];
|
||||
|
||||
return response()->streamDownload(function () use ($codes, $header) {
|
||||
$out = fopen('php://output', 'w');
|
||||
fwrite($out, "\xEF\xBB\xBF"); // UTF-8 BOM,Excel 開啟才不會亂碼
|
||||
fputcsv($out, $header);
|
||||
foreach ($codes as $c) {
|
||||
fputcsv($out, [
|
||||
$c->code,
|
||||
optional($c->machine)->name . ' (' . optional($c->machine)->serial_no . ')',
|
||||
optional($c->product)->name ?? '',
|
||||
$c->slot_no ?? '',
|
||||
$c->usage_limit,
|
||||
optional($c->expires_at)->format('Y-m-d H:i') ?? __('Permanent'),
|
||||
$c->ticket_url,
|
||||
optional($c->created_at)->format('Y-m-d H:i'),
|
||||
]);
|
||||
}
|
||||
fclose($out);
|
||||
}, 'pickup-codes-' . $batchNo . '.csv', [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下載某一批通行碼清單 (CSV)
|
||||
*/
|
||||
public function downloadPassCodeBatch(string $batchNo)
|
||||
{
|
||||
$codes = PassCode::with('machine:id,name,serial_no')
|
||||
->where('batch_no', $batchNo)
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
abort_if($codes->isEmpty(), 404);
|
||||
|
||||
$header = [
|
||||
__('Pass Code'),
|
||||
__('Description / Name'),
|
||||
__('Target Machine'),
|
||||
__('Expires At'),
|
||||
__('Link'),
|
||||
__('Created At'),
|
||||
];
|
||||
|
||||
return response()->streamDownload(function () use ($codes, $header) {
|
||||
$out = fopen('php://output', 'w');
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
fputcsv($out, $header);
|
||||
foreach ($codes as $c) {
|
||||
fputcsv($out, [
|
||||
$c->code,
|
||||
$c->name,
|
||||
optional($c->machine)->name . ' (' . optional($c->machine)->serial_no . ')',
|
||||
optional($c->expires_at)->format('Y-m-d H:i') ?? __('Permanent'),
|
||||
$c->ticket_url,
|
||||
optional($c->created_at)->format('Y-m-d H:i'),
|
||||
]);
|
||||
}
|
||||
fclose($out);
|
||||
}, 'pass-codes-' . $batchNo . '.csv', [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通行碼
|
||||
*/
|
||||
|
||||
@ -18,6 +18,7 @@ class PassCode extends Model
|
||||
'name',
|
||||
'code',
|
||||
'slug',
|
||||
'batch_no',
|
||||
'expires_at',
|
||||
'status',
|
||||
'created_by',
|
||||
|
||||
@ -19,6 +19,7 @@ class PickupCode extends Model
|
||||
'slot_no',
|
||||
'code',
|
||||
'slug',
|
||||
'batch_no',
|
||||
'status',
|
||||
'expires_at',
|
||||
'used_at',
|
||||
|
||||
38
database/migrations/2026_06_26_130000_create_cache_table.php
Normal file
38
database/migrations/2026_06_26_130000_create_cache_table.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 建立 Laravel 標準 cache / cache_locks 表。
|
||||
* 對應 dev 的修正 55cf6b8:CACHE_STORE=database 須有這兩張表,
|
||||
* 否則 ProcessHeartbeat 等的 Cache::put 會因缺表而失敗。
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('cache')) {
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('cache_locks')) {
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 取貨碼 / 通行碼 批次新增:以 batch_no 標記同一批產生的碼,供後台批次下載清單使用。
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pickup_codes', function (Blueprint $table) {
|
||||
$table->string('batch_no', 32)->nullable()->after('slug')->index();
|
||||
});
|
||||
|
||||
Schema::table('pass_codes', function (Blueprint $table) {
|
||||
$table->string('batch_no', 32)->nullable()->after('slug')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pickup_codes', function (Blueprint $table) {
|
||||
$table->dropIndex(['batch_no']);
|
||||
$table->dropColumn('batch_no');
|
||||
});
|
||||
|
||||
Schema::table('pass_codes', function (Blueprint $table) {
|
||||
$table->dropIndex(['batch_no']);
|
||||
$table->dropColumn('batch_no');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,11 @@
|
||||
{
|
||||
"Generate Quantity": "Generate Quantity",
|
||||
"Batch mode: codes are auto-generated and the custom code is ignored.": "Batch mode: codes are auto-generated and the custom code is ignored.",
|
||||
"Batch generated successfully": "Batch generated successfully",
|
||||
"codes": "codes",
|
||||
"Download List (CSV)": "Download List (CSV)",
|
||||
":count pickup codes generated": ":count pickup codes generated",
|
||||
":count pass codes created": ":count pass codes created",
|
||||
"(deducted by issued pickup orders)": "(deducted by issued pickup orders)",
|
||||
"-- Select Machine --": "-- Select Machine --",
|
||||
"10s": "10s",
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
{
|
||||
"Generate Quantity": "生成数量",
|
||||
"Batch mode: codes are auto-generated and the custom code is ignored.": "バッチモード:コードは自動生成され、カスタムコードは無視されます。",
|
||||
"Batch generated successfully": "バッチ生成が完了しました",
|
||||
"codes": "件",
|
||||
"Download List (CSV)": "リストをダウンロード (CSV)",
|
||||
":count pickup codes generated": ":count 件の受取コードを生成しました",
|
||||
":count pass codes created": ":count 件の通行コードを作成しました",
|
||||
"(deducted by issued pickup orders)": "(発行済み受取注文の予約分を差し引き済み)",
|
||||
"-- Select Machine --": "-- 機器を選択 --",
|
||||
"10s": "10秒",
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
{
|
||||
"Generate Quantity": "產生數量",
|
||||
"Batch mode: codes are auto-generated and the custom code is ignored.": "批次模式:系統自動產生隨機碼,將忽略自訂碼。",
|
||||
"Batch generated successfully": "批次產生成功",
|
||||
"codes": "筆",
|
||||
"Download List (CSV)": "下載清單 (CSV)",
|
||||
":count pickup codes generated": "已產生 :count 筆取貨碼",
|
||||
":count pass codes created": "已建立 :count 筆通行碼",
|
||||
"(deducted by issued pickup orders)": "(已扣除已生成領藥單的預留量)",
|
||||
"-- Select Machine --": "-- 選擇機台 --",
|
||||
"10s": "10秒",
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
@if(session('code_batch'))
|
||||
@php($batch = session('code_batch'))
|
||||
<div class="luxury-card rounded-3xl p-6 border border-emerald-500/30 bg-emerald-500/5 flex flex-col sm:flex-row sm:items-center gap-4 animate-luxury-in">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<div class="w-11 h-11 rounded-2xl bg-emerald-500/15 text-emerald-500 flex items-center justify-center shrink-0">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-black text-slate-800 dark:text-white">{{ __('Batch generated successfully') }}</p>
|
||||
<p class="text-xs font-bold text-slate-500">
|
||||
{{ __('Batch No') }}: <span class="font-mono text-cyan-600 dark:text-cyan-400">{{ $batch['batch_no'] }}</span>
|
||||
· {{ $batch['count'] }} {{ __('codes') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ $batch['download_url'] }}"
|
||||
class="btn-luxury-primary whitespace-nowrap flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M7.5 12l4.5 4.5m0 0l4.5-4.5M12 3v13.5" />
|
||||
</svg>
|
||||
{{ __('Download List (CSV)') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@ -12,6 +12,9 @@
|
||||
passCodeName: '',
|
||||
expiresDays: 7,
|
||||
customCode: '',
|
||||
quantity: 1,
|
||||
maxBatchQty: {{ (int) ($maxBatchQuantity ?? 20) }},
|
||||
batchQuickOptions: @js($batchQuickOptions ?? [10, 20]),
|
||||
status: '{{ request('status', '') }}',
|
||||
showQrModal: false,
|
||||
activeQrCode: '',
|
||||
@ -145,7 +148,7 @@
|
||||
window.showToast('{{ __('Please enter a description') }}', 'error');
|
||||
return;
|
||||
}
|
||||
if (!this.customCode.trim()) {
|
||||
if (parseInt(this.quantity) <= 1 && !this.customCode.trim()) {
|
||||
window.showToast('{{ __('Please enter or generate a pass code') }}', 'error');
|
||||
return;
|
||||
}
|
||||
@ -194,12 +197,15 @@
|
||||
this.selectedMachine = '';
|
||||
this.passCodeName = '';
|
||||
this.expiresDays = 7;
|
||||
this.quantity = 1;
|
||||
this.generateRandomCode();
|
||||
this.syncSelect('modal-pass-machine', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
} " @ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)">
|
||||
@include('admin.sales.partials.batch-result-banner')
|
||||
|
||||
{{-- Page Header --}}
|
||||
<x-page-header :title="__('Pass Codes')"
|
||||
:subtitle="__('Manage multi-use authorization codes for testing and maintenance')">
|
||||
@ -302,6 +308,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Quantity (Batch Generate) --}}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
||||
{{ __('Generate Quantity') }} (1-{{ $maxBatchQuantity ?? 20 }})
|
||||
</label>
|
||||
<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">
|
||||
<button type="button" @click="quantity = Math.max(1, 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">
|
||||
<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>
|
||||
<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="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">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<template x-for="val in batchQuickOptions" :key="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="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'">
|
||||
<span x-text="val"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[10px] font-bold text-amber-500 pl-1" x-show="parseInt(quantity) > 1" x-cloak>
|
||||
{{ __('Batch mode: codes are auto-generated and the custom code is ignored.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
|
||||
@ -317,8 +359,10 @@
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" name="custom_code" x-model="customCode"
|
||||
:disabled="parseInt(quantity) > 1" :required="parseInt(quantity) <= 1"
|
||||
:class="parseInt(quantity) > 1 ? 'opacity-40 cursor-not-allowed' : ''"
|
||||
class="luxury-input w-full py-4 font-mono text-2xl tracking-[0.3em] text-center text-cyan-600 dark:text-cyan-400 bg-cyan-50/30 dark:bg-cyan-500/5"
|
||||
maxlength="12" required>
|
||||
maxlength="12">
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
@ -37,7 +37,7 @@ transition-all duration-300",
|
||||
<div class="space-y-2 pb-20" x-data="pickupCodeManager(@js($slotSelectConfig))"
|
||||
@ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)">
|
||||
|
||||
|
||||
@include('admin.sales.partials.batch-result-banner')
|
||||
|
||||
{{-- Page Header --}}
|
||||
<x-page-header :title="__('Pickup Codes')"
|
||||
@ -295,6 +295,41 @@ transition-all duration-300",
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Quantity (Batch Generate) — 獨立整列,避免與使用次數上限擠在同一列重疊。上限/快捷選項依角色分級 --}}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||
__('Generate Quantity') }} (1-{{ $maxBatchQuantity ?? 20 }})</label>
|
||||
<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">
|
||||
<button type="button" @click="quantity = Math.max(1, 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">
|
||||
<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>
|
||||
<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="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">
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700/50"></div>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<template x-for="val in batchQuickOptions" :key="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="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'">
|
||||
<span x-text="val"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[10px] font-bold text-amber-500 pl-1" x-show="parseInt(quantity) > 1" x-cloak>
|
||||
{{ __('Batch mode: codes are auto-generated and the custom code is ignored.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Pickup Code Preview Section --}}
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
@ -311,8 +346,10 @@ transition-all duration-300",
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" name="custom_code" x-model="customCode"
|
||||
:disabled="parseInt(quantity) > 1" :required="parseInt(quantity) <= 1"
|
||||
:class="parseInt(quantity) > 1 ? 'opacity-40 cursor-not-allowed' : ''"
|
||||
class="luxury-input w-full py-4 font-mono text-2xl tracking-[0.3em] text-center text-cyan-600 dark:text-cyan-400 bg-cyan-50/30 dark:bg-cyan-500/5"
|
||||
maxlength="12" required>
|
||||
maxlength="12">
|
||||
</div>
|
||||
|
||||
{{-- Validity Period Section (Flexible) --}}
|
||||
@ -504,6 +541,9 @@ transition-all duration-300",
|
||||
expiryMode: 'hours',
|
||||
customExpiry: '',
|
||||
usageLimit: 1,
|
||||
quantity: 1,
|
||||
maxBatchQty: {{ (int) ($maxBatchQuantity ?? 20) }},
|
||||
batchQuickOptions: @js($batchQuickOptions ?? [10, 20]),
|
||||
showQrModal: false,
|
||||
activeQrCode: '',
|
||||
activeTicketUrl: '',
|
||||
@ -801,6 +841,7 @@ transition-all duration-300",
|
||||
this.selectedProduct = '';
|
||||
this.productNotOnMachine = false;
|
||||
this.usageLimit = 1;
|
||||
this.quantity = 1;
|
||||
this.expiryMode = 'hours';
|
||||
this.expiresHours = 24;
|
||||
const tomorrow = new Date();
|
||||
|
||||
@ -167,12 +167,14 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
||||
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');
|
||||
Route::get('/pickup-codes/batch/{batchNo}/download', [App\Http\Controllers\Admin\SalesController::class, 'downloadPickupCodeBatch'])->name('pickup-codes.batch-download');
|
||||
|
||||
// 通行碼
|
||||
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes');
|
||||
Route::post('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePassCode'])->name('pass-codes.store');
|
||||
Route::patch('/pass-codes/{passCode}', [App\Http\Controllers\Admin\SalesController::class, 'updatePassCode'])->name('pass-codes.update');
|
||||
Route::delete('/pass-codes/{passCode}', [App\Http\Controllers\Admin\SalesController::class, 'destroyPassCode'])->name('pass-codes.destroy');
|
||||
Route::get('/pass-codes/batch/{batchNo}/download', [App\Http\Controllers\Admin\SalesController::class, 'downloadPassCodeBatch'])->name('pass-codes.batch-download');
|
||||
|
||||
Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders');
|
||||
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user