diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 56bcf45..fd5b87a 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -4,6 +4,11 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; +use App\Models\Transaction\PickupCode; +use App\Models\Transaction\PassCode; +use App\Models\Machine\Machine; +use Carbon\Carbon; +use Illuminate\Support\Facades\Auth; class SalesController extends Controller { @@ -23,14 +28,87 @@ class SalesController extends Controller } // 取貨碼設定 - public function pickupCodes() + public function pickupCodes(Request $request) { - return view('admin.placeholder', [ + $query = PickupCode::with(['machine', 'creator'])->latest(); + + if ($request->search) { + $query->where(function($q) use ($request) { + $q->where('code', 'like', "%{$request->search}%") + ->orWhereHas('machine', function($mq) use ($request) { + $mq->where('name', 'like', "%{$request->search}%") + ->orWhere('serial_no', 'like', "%{$request->search}%"); + }); + }); + } + + if ($request->status) { + $query->where('status', $request->status); + } + + $pickupCodes = $query->paginate(15)->withQueryString(); + + // 供新增彈窗使用的機台清單 + $machines = Machine::all(); + + return view('admin.sales.pickup-codes.index', [ 'title' => '取貨碼設定', - 'description' => '取貨驗證碼管理', + 'description' => '產生與管理商品取貨驗證碼', + 'pickupCodes' => $pickupCodes, + 'machines' => $machines, ]); } + /** + * 產生取貨碼 + */ + public function storePickupCode(Request $request) + { + $validated = $request->validate([ + 'machine_id' => 'required|exists:machines,id', + 'slot_no' => 'required|string', + 'expires_hours' => 'nullable|integer|min:1|max:720', // 最長一個月 + ]); + + $expiresAt = now()->addHours($request->expires_hours ?? 24); + + $pickupCode = PickupCode::create([ + 'machine_id' => $validated['machine_id'], + 'slot_no' => $validated['slot_no'], + 'code' => PickupCode::generateUniqueCode($validated['machine_id']), + 'expires_at' => $expiresAt, + 'status' => 'active', + 'created_by' => Auth::id(), + ]); + + return back()->with('success', __('Pickup code generated: :code', ['code' => $pickupCode->code])); + } + + /** + * 更新取貨碼 (僅限修改時間) + */ + public function updatePickupCode(Request $request, PickupCode $pickupCode) + { + $validated = $request->validate([ + 'expires_at' => 'required|date|after:now', + ]); + + $pickupCode->update([ + 'expires_at' => $validated['expires_at'], + ]); + + return back()->with('success', __('Pickup code updated.')); + } + + /** + * 刪除/取消取貨碼 + */ + public function destroyPickupCode(PickupCode $pickupCode) + { + $pickupCode->update(['status' => 'cancelled']); + return back()->with('success', __('Pickup code cancelled.')); + } + // 購買單 public function orders() { @@ -50,14 +128,83 @@ class SalesController extends Controller } // 通行碼設定 - public function passCodes() + public function passCodes(Request $request) { - return view('admin.placeholder', [ + $query = PassCode::with(['machine', 'creator'])->latest(); + + if ($request->search) { + $query->where(function($q) use ($request) { + $q->where('code', 'like', "%{$request->search}%") + ->orWhere('name', 'like', "%{$request->search}%") + ->orWhereHas('machine', function($mq) use ($request) { + $mq->where('name', 'like', "%{$request->search}%") + ->orWhere('serial_no', 'like', "%{$request->search}%"); + }); + }); + } + + $passCodes = $query->paginate(15)->withQueryString(); + $machines = Machine::all(); + + return view('admin.sales.pass-codes.index', [ 'title' => '通行碼設定', - 'description' => '特殊通行碼權限管理', + 'description' => '特殊通行權限碼管理 (測試/補貨用)', + 'passCodes' => $passCodes, + 'machines' => $machines, ]); } + /** + * 產生通行碼 + */ + public function storePassCode(Request $request) + { + $validated = $request->validate([ + 'machine_id' => 'required|exists:machines,id', + 'name' => 'nullable|string|max:50', + 'expires_days' => 'nullable|integer|min:1', + 'custom_code' => 'nullable|string|min:4|max:12', + ]); + + $expiresAt = $request->expires_days ? now()->addDays($request->expires_days) : null; + + $passCode = PassCode::create([ + 'machine_id' => $validated['machine_id'], + 'name' => $validated['name'] ?? 'Manual Generate', + 'code' => $validated['custom_code'] ?? PassCode::generateUniqueCode($validated['machine_id']), + 'expires_at' => $expiresAt, + 'status' => 'active', + 'created_by' => Auth::id(), + ]); + + return back()->with('success', __('Pass code created: :code', ['code' => $passCode->code])); + } + + /** + * 更新通行碼 + */ + public function updatePassCode(Request $request, PassCode $passCode) + { + $validated = $request->validate([ + 'name' => 'nullable|string|max:50', + 'expires_at' => 'nullable|date', + 'status' => 'required|in:active,disabled', + ]); + + $passCode->update($validated); + + return back()->with('success', __('Pass code updated.')); + } + + /** + * 刪除通行碼 + */ + public function destroyPassCode(PassCode $passCode) + { + $passCode->delete(); + return back()->with('success', __('Pass code deleted.')); + } + // 來店禮設定 public function storeGifts() { diff --git a/app/Models/Transaction/PassCode.php b/app/Models/Transaction/PassCode.php new file mode 100644 index 0000000..142f3e4 --- /dev/null +++ b/app/Models/Transaction/PassCode.php @@ -0,0 +1,80 @@ + 'datetime', + ]; + + /** + * 關聯機台 + */ + public function machine(): BelongsTo + { + return $this->belongsTo(Machine::class); + } + + /** + * 關聯建立者 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * 判斷是否可用 + */ + public function isValid(): bool + { + if ($this->status !== 'active') { + return false; + } + + if ($this->expires_at && $this->expires_at->isPast()) { + return false; + } + + return true; + } + + /** + * 產生唯一的通行碼 (8位) + */ + public static function generateUniqueCode(int $machineId): string + { + do { + $code = str_pad(rand(0, 99999999), 8, '0', STR_PAD_LEFT); + $exists = self::where('machine_id', $machineId) + ->where('code', $code) + ->where('status', 'active') + ->where(function($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->exists(); + } while ($exists); + + return $code; + } +} diff --git a/app/Models/Transaction/PickupCode.php b/app/Models/Transaction/PickupCode.php new file mode 100644 index 0000000..960174f --- /dev/null +++ b/app/Models/Transaction/PickupCode.php @@ -0,0 +1,71 @@ + 'datetime', + 'used_at' => 'datetime', + ]; + + /** + * 關聯機台 + */ + public function machine(): BelongsTo + { + return $this->belongsTo(Machine::class); + } + + /** + * 關聯建立者 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * 判斷是否可用 + */ + public function isValid(): bool + { + return $this->status === 'active' && $this->expires_at->isFuture(); + } + + /** + * 產生唯一的取貨碼 (4位) + */ + public static function generateUniqueCode(int $machineId): string + { + do { + $code = str_pad(rand(0, 9999), 4, '0', STR_PAD_LEFT); + $exists = self::where('machine_id', $machineId) + ->where('code', $code) + ->where('status', 'active') + ->where('expires_at', '>', now()) + ->exists(); + } while ($exists); + + return $code; + } +} diff --git a/database/migrations/2026_04_27_143339_create_pickup_codes_table.php b/database/migrations/2026_04_27_143339_create_pickup_codes_table.php new file mode 100644 index 0000000..4797390 --- /dev/null +++ b/database/migrations/2026_04_27_143339_create_pickup_codes_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('company_id')->constrained('companies')->onDelete('cascade'); + $table->foreignId('machine_id')->constrained('machines')->onDelete('cascade'); + $table->string('slot_no')->comment('貨道編號'); + $table->string('code', 10)->comment('取貨碼 (通常為 4 位)'); + $table->timestamp('expires_at')->comment('到期時間'); + $table->timestamp('used_at')->nullable()->comment('使用時間'); + $table->string('status')->default('active')->comment('狀態: active, used, expired, cancelled'); + $table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamps(); + + $table->index(['company_id', 'machine_id', 'code', 'status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pickup_codes'); + } +}; diff --git a/database/migrations/2026_04_27_143345_create_pass_codes_table.php b/database/migrations/2026_04_27_143345_create_pass_codes_table.php new file mode 100644 index 0000000..ef82331 --- /dev/null +++ b/database/migrations/2026_04_27_143345_create_pass_codes_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('company_id')->constrained('companies')->onDelete('cascade'); + $table->foreignId('machine_id')->constrained('machines')->onDelete('cascade'); + $table->string('name')->nullable()->comment('通行碼名稱/用途'); + $table->string('code', 20)->comment('通行碼 (預設 8 位)'); + $table->timestamp('expires_at')->nullable()->comment('到期時間 (null 為永久有效)'); + $table->string('status')->default('active')->comment('狀態: active, expired, disabled'); + $table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamps(); + + $table->index(['company_id', 'machine_id', 'code', 'status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pass_codes'); + } +}; diff --git a/lang/en.json b/lang/en.json index adcb258..25dccde 100644 --- a/lang/en.json +++ b/lang/en.json @@ -341,6 +341,7 @@ "Describe the repair or maintenance status...": "Describe the repair or maintenance status...", "Deselect All": "Deselect All", "Detail": "Detail", + "Details": "Details", "Device Information": "Device Information", "Device Status Logs": "Device Status Logs", "Devices": "Devices", diff --git a/lang/ja.json b/lang/ja.json index 2074430..88e1665 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -227,6 +227,7 @@ "Update Settings": "設定を更新", "Video": "動画", "View Details": "詳細を表示", + "Details": "詳細", "View Inventory": "在庫を表示", "View Slots": "貨道を表示", "Visual overview of all machine locations": "全機器位置のビジュアル概要", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 5464362..d17d09c 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -363,6 +363,7 @@ "Describe the repair or maintenance status...": "請描述維修或保養狀況...", "Deselect All": "取消全選", "Detail": "詳細", + "Details": "明細", "Device Information": "設備資訊", "Device Status Logs": "設備狀態紀錄", "Devices": "台設備", diff --git a/resources/views/admin/sales/pass-codes/index.blade.php b/resources/views/admin/sales/pass-codes/index.blade.php new file mode 100644 index 0000000..8111ad7 --- /dev/null +++ b/resources/views/admin/sales/pass-codes/index.blade.php @@ -0,0 +1,193 @@ +@extends('layouts.admin') + +@section('content') +
{{ __('Manage multi-use authorization codes for testing and maintenance') }}
+| {{ __('Pass Code') }} | +{{ __('Name / Machine') }} | +{{ __('Expires At') }} | +{{ __('Status') }} | +{{ __('Actions') }} | +
|---|---|---|---|---|
| + + {{ $code->code }} + + | +
+ {{ $code->name }}
+ {{ $code->machine->name }}
+ |
+
+ @if($code->expires_at)
+ {{ $code->expires_at->format('Y-m-d H:i') }}
+
+ {{ $code->expires_at->isPast() ? __('Expired') : $code->expires_at->diffForHumans() }}
+
+ @else
+ {{ __('Permanent') }}
+ @endif
+ |
+ + @php + $isValid = $code->isValid(); + @endphp + + {{ $isValid ? __('Active') : __('Inactive') }} + + | +
+
+
+
+ |
+
|
+ {{ __('No pass codes found') }} + |
+ ||||
{{ __('Generate and manage one-time pickup codes for customers') }}
+| {{ __('Code') }} | +{{ __('Machine / Slot') }} | +{{ __('Expires At') }} | +{{ __('Status') }} | +{{ __('Actions') }} | +
|---|---|---|---|---|
| + + {{ $code->code }} + + | +
+ {{ $code->machine->name }}
+ {{ __('Slot') }}: {{ $code->slot_no }}
+ |
+
+ {{ $code->expires_at->format('Y-m-d H:i') }}
+ {{ $code->expires_at->diffForHumans() }}
+ |
+ + @php + $statusClasses = [ + 'active' => 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-100 dark:border-emerald-500/20', + 'used' => 'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400 border-blue-100 dark:border-blue-500/20', + 'expired' => 'bg-rose-50 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400 border-rose-100 dark:border-rose-500/20', + 'cancelled' => 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-600', + ]; + $displayStatus = $code->status === 'active' && $code->expires_at->isPast() ? 'expired' : $code->status; + @endphp + + {{ __($displayStatus) }} + + | +
+
+ @if($code->status === 'active' && $code->expires_at->isFuture())
+
+ @endif
+
+ |
+
|
+
+
+
+
+
+ {{ __('No pickup codes found') }} + |
+ ||||
{{ __('Track stock-in orders and movement history') }}
diff --git a/resources/views/admin/warehouses/machine-inventory.blade.php b/resources/views/admin/warehouses/machine-inventory.blade.php index 6290c44..fe3dc9f 100644 --- a/resources/views/admin/warehouses/machine-inventory.blade.php +++ b/resources/views/admin/warehouses/machine-inventory.blade.php @@ -84,18 +84,20 @@{{ __('Real-time slot-level inventory across all machines') }}
+{{ + __('Real-time slot-level inventory across all machines') }}