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') +
+ {{-- Page Header --}} +
+
+

{{ __('Pass Codes') }}

+

{{ __('Manage multi-use authorization codes for testing and maintenance') }}

+
+ +
+ + {{-- Search & Filters --}} +
+
+
+ + + + + + +
+ +
+
+ + {{-- Table --}} +
+
+ + + + + + + + + + + + @forelse ($passCodes as $code) + + + + + + + + @empty + + + + @endforelse + +
{{ __('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') }} + + +
+
+ @csrf + @method('DELETE') + +
+
+
+

{{ __('No pass codes found') }}

+
+
+ @if ($passCodes->hasPages()) +
+ {{ $passCodes->links() }} +
+ @endif +
+ + {{-- Create Modal --}} +
+
+
+ +
+
+ @csrf +
+
+

{{ __('Create Pass Code') }}

+ +
+ +
+ {{-- Machine Selection --}} +
+ + +
+ + {{-- Name --}} +
+ + +
+ + {{-- Code --}} +
+
+ + +
+ +
+ + {{-- Validity --}} +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/sales/pickup-codes/index.blade.php b/resources/views/admin/sales/pickup-codes/index.blade.php new file mode 100644 index 0000000..d2b26cd --- /dev/null +++ b/resources/views/admin/sales/pickup-codes/index.blade.php @@ -0,0 +1,226 @@ +@extends('layouts.admin') + +@section('content') +
+ {{-- Page Header --}} +
+
+

{{ __('Pickup Codes') }}

+

{{ __('Generate and manage one-time pickup codes for customers') }}

+
+ +
+ + {{-- Search & Filters --}} +
+
+
+ + + + + + +
+
+ +
+ +
+
+ + {{-- Table --}} +
+
+ + + + + + + + + + + + @forelse ($pickupCodes as $code) + + + + + + + + @empty + + + + @endforelse + +
{{ __('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()) +
+ @csrf + @method('DELETE') + +
+ @endif +
+
+
+
+ + + +
+

{{ __('No pickup codes found') }}

+
+
+
+ @if ($pickupCodes->hasPages()) +
+ {{ $pickupCodes->links() }} +
+ @endif +
+ + {{-- Create Modal --}} +
+
+
+ +
+
+ @csrf +
+
+

{{ __('Generate Pickup Code') }}

+ +
+ +
+ {{-- Machine Selection --}} +
+ + +
+ + {{-- Slot Selection --}} +
+ +
+ +
+ +
+
+
+ + {{-- Validity --}} +
+ + +

{{ __('Default is 24 hours. Max is 720 hours (30 days).') }}

+
+
+
+ +
+ + +
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/warehouses/inventory.blade.php b/resources/views/admin/warehouses/inventory.blade.php index 3199342..2ace2dc 100644 --- a/resources/views/admin/warehouses/inventory.blade.php +++ b/resources/views/admin/warehouses/inventory.blade.php @@ -270,9 +270,9 @@ -
+
-

{{ __('Inventory Management') }}

+

{{ __('Inventory Management') }}

{{ __('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 @@
-

{{ __('Machine Inventory Overview') }}

-

{{ __('Real-time slot-level inventory across all machines') }}

+

{{ + __('Machine Inventory Overview') }}

+

{{ + __('Real-time slot-level inventory across all machines') }}

{{-- List Mode --}}
-
-
+
+
-
+ class="flex flex-wrap items-center gap-4 flex-1"> +
@@ -104,30 +106,34 @@
- +
+ - + +
@@ -184,7 +190,8 @@
- {{ + {{ $machine->serial_no }} @@ -326,7 +333,8 @@
-
+
@@ -343,7 +351,8 @@
-
+
@@ -360,7 +369,8 @@
-
+
-
+
@@ -465,7 +476,8 @@
-
+
diff --git a/resources/views/admin/warehouses/partials/tab-movements.blade.php b/resources/views/admin/warehouses/partials/tab-movements.blade.php index 74d685a..1eddf0f 100644 --- a/resources/views/admin/warehouses/partials/tab-movements.blade.php +++ b/resources/views/admin/warehouses/partials/tab-movements.blade.php @@ -1,10 +1,10 @@
-
-
-
+
+
-
+
{{-- Date Range --}} -
-
-
-
+ {{-- Table Mode (Desktop) --}} +
@@ -162,6 +164,60 @@
+ + {{-- Card Mode (Mobile & Tablet) --}} +
+ @forelse($movements as $mv) +
+
+
+
+ + + +
+
+

{{ $mv->product?->localized_name }}

+

ID: {{ $mv->product_id }}

+
+
+
+ @php $typeColor = in_array($mv->type, ['in', 'transfer_in']) ? 'cyan' : (in_array($mv->type, ['out', 'transfer_out']) ? 'rose' : 'amber'); @endphp + + {{ __(\App\Models\Warehouse\StockMovement::TYPE_LABELS[$mv->type] ?? $mv->type) }} + +
+
+ +
+
+

{{ __('Warehouse') }}

+

{{ $mv->warehouse?->name }}

+
+
+

{{ __('Qty Change') }}

+

+ {{ in_array($mv->type, ['in', 'transfer_in']) ? '+' : '-' }}{{ $mv->quantity }} + ({{ $mv->before_qty }}→{{ $mv->after_qty }}) +

+
+
+

{{ __('Operator') }}

+

{{ $mv->creator?->name ?? '-' }}

+
+
+

{{ __('Time') }}

+

{{ $mv->created_at?->format('Y-m-d H:i:s') }}

+
+
+ +
+ @empty +
+

{{ __('No movement records found') }}

+
+ @endforelse +
{{ $movements->links('vendor.pagination.luxury') }}
diff --git a/resources/views/admin/warehouses/partials/tab-stock-in.blade.php b/resources/views/admin/warehouses/partials/tab-stock-in.blade.php index 81a76d3..9740a2a 100644 --- a/resources/views/admin/warehouses/partials/tab-stock-in.blade.php +++ b/resources/views/admin/warehouses/partials/tab-stock-in.blade.php @@ -1,11 +1,11 @@
{{-- Filters --}} -
-
-
+
+
@@ -15,11 +15,11 @@ + class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
{{-- Date Range --}} -
- +
+ - + +
@if(isset($stock_in_status_options)) -
+
- {{-- Table --}} -
+ {{-- Table Mode (Desktop) --}} + + {{-- Card Mode (Mobile & Tablet) --}} +
+ @forelse($orders as $order) +
+
+
+
+ + + +
+
+

{{ $order->order_no }}

+

{{ $order->created_at->format('Y-m-d H:i:s') }}

+
+
+
+ @if($order->status === 'draft') + + {{ __('Draft') }} + + @elseif($order->status === 'cancelled') + + {{ __('Cancelled') }} + + @else + + {{ __('Completed') }} + + @endif +
+
+ +
+
+

{{ __('Warehouse') }}

+

{{ $order->warehouse?->name }}

+
+
+

{{ __('Items') }}

+

{{ $order->items_count }}

+
+ @if($order->note) +
+

{{ __('Note') }}

+

"{{ $order->note }}"

+
+ @endif +
+ +
+ + @if($order->status === 'draft') + + @csrf @method('PATCH') + + + @endif +
+
+ @empty +
+

{{ __('No stock-in orders found') }}

+
+ @endforelse +
+
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:stock-in']) }}
diff --git a/resources/views/admin/warehouses/partials/tab-stock.blade.php b/resources/views/admin/warehouses/partials/tab-stock.blade.php index 2aa7601..6e24e00 100644 --- a/resources/views/admin/warehouses/partials/tab-stock.blade.php +++ b/resources/views/admin/warehouses/partials/tab-stock.blade.php @@ -1,59 +1,75 @@
-
+ -
- - + +
+ + + + +
{{-- 多選倉庫篩選器 --}} -
- +
+
-
- -
-
+ -
{{ $products->links('vendor.pagination.luxury') }}
-
+ + {{-- Mobile Card View --}} +
+ @forelse($products as $product) + @php + $viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray(); + $totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity'); + @endphp +
+ + {{-- Card Header --}} +
+
+
+ @if($product->image_url) + + @else + + + + @endif +
+
+

+ {{ $product->localized_name }} +

+ @if($product->barcode) +

+ {{ $product->barcode }} +

+ @endif +
+
+
+ + {{ $totalQuantity }} + + + + +
+
+ + {{-- Collapsible Content --}} +
+ @foreach($inventory_warehouses->sortBy('name', SORT_NATURAL) as $w) + @php + $stock = $product->stocks->firstWhere('warehouse_id', $w->id); + $isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock; + @endphp +
+
+ {{ $w->name }} + + {{ $w->type === 'main' ? __('Main') : __('Branch') }} + +
+
+ @if($stock && $stock->quantity > 0) + + {{ $stock->quantity }} + + @if($isLow) +
+ {{ + __('Low') }} +
+ @endif + @else + - + @endif +
+
+ @endforeach +
+
+ @empty +
+
+
+ + + +
+

{{ __('No products found') }}

+
+
+ @endforelse +
+ +
{{ + $products->links('vendor.pagination.luxury') }}
+
\ No newline at end of file diff --git a/resources/views/admin/warehouses/replenishments.blade.php b/resources/views/admin/warehouses/replenishments.blade.php index a2fd693..c8f29ed 100644 --- a/resources/views/admin/warehouses/replenishments.blade.php +++ b/resources/views/admin/warehouses/replenishments.blade.php @@ -57,7 +57,7 @@ this.autoPreviewLoaded = false; this.autoSlots = []; this.autoNote = ''; - + // 同步 HSSelect UI this.$nextTick(() => { const select = document.querySelector('[name="auto_machine_id"]'); @@ -113,9 +113,9 @@ try { const res = await fetch('{{ route("admin.warehouses.ajax.stock") }}?warehouse_id=' + this.fromId); const json = await res.json(); - if (json.success) { - this.availableProducts = json.data; - this.updateProductDisplay(); + if (json.success) { + this.availableProducts = json.data; + this.updateProductDisplay(); } } catch (e) { console.error(e); } finally { this.isLoadingProducts = false; } @@ -123,9 +123,9 @@ async fetchMachineSlots() { this.machineSlots = []; - if (!this.targetMachineId) { + if (!this.targetMachineId) { this.items.forEach((_, idx) => this.updateSlotSelects(idx)); - return; + return; } this.isLoadingSlots = true; try { @@ -133,7 +133,7 @@ headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }); const json = await res.json(); - if (json.success) { + if (json.success) { this.machineSlots = json.slots; this.items.forEach((_, idx) => this.updateSlotSelects(idx)); } @@ -143,7 +143,7 @@ updateSlotSelects(index = null) { const indices = index !== null ? [index] : this.items.map((_, i) => i); - + indices.forEach(idx => { const item = this.items[idx]; const wrapper = document.getElementById(`slot-select-wrapper-${idx}`); @@ -151,7 +151,7 @@ const oldSelect = wrapper.querySelector('select'); if (oldSelect && window.HSSelect?.getInstance(oldSelect)) { - try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) {} + try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) { } } wrapper.innerHTML = ''; @@ -171,12 +171,12 @@ opt.value = s.slot_no; const stockInfo = ` [${s.stock || 0}/${s.max_stock || 0}]`; opt.textContent = s.slot_no + (s.product ? ` (${s.product.name})` : '') + stockInfo; - + // Enhanced style for HSSelect opt.setAttribute('data-title', s.slot_no + (s.product ? ` (${s.product.name})` : '')); const stockHtml = `
${s.stock || 0}/${s.max_stock || 0}
`; opt.setAttribute('data-description', stockHtml); - + if (item.slot_no == s.slot_no) opt.selected = true; selectEl.appendChild(opt); }); @@ -193,7 +193,7 @@ })); wrapper.appendChild(selectEl); - selectEl.addEventListener('change', (e) => { + selectEl.addEventListener('change', (e) => { item.slot_no = e.target.value; // Auto-select product based on slot const slotData = this.machineSlots.find(s => s.slot_no == item.slot_no); @@ -211,14 +211,14 @@ updateProductDisplay(index = null) { const indices = index !== null ? [index] : this.items.map((_, i) => i); - + indices.forEach(idx => { const item = this.items[idx]; const wrapper = document.getElementById(`product-select-wrapper-${idx}`); if (!wrapper) return; - + wrapper.innerHTML = ''; - + if (!item.product_id) { wrapper.innerHTML = `
@@ -231,7 +231,7 @@ const productInWarehouse = this.availableProducts.find(p => p.id == item.product_id); const productInfo = this.allProducts.find(p => p.id == item.product_id); const productName = productInfo ? productInfo.name : (productInWarehouse ? productInWarehouse.name : 'Unknown'); - + // Priority: stock from selected warehouse, otherwise N/A or 0 const stock = productInWarehouse ? productInWarehouse.quantity : 0; const stockText = this.fromId ? `{{ __('Stock') }}: ${stock}` : '{{ __('Select Warehouse') }}'; @@ -266,23 +266,23 @@ const form = document.getElementById('replenishmentForm'); const warehouseId = this.fromId; const machineId = form.querySelector('[name="machine_id"]')?.value; - - if (!warehouseId || warehouseId === ' ') { - window.showToast?.('{{ __("Please select source warehouse") }}', 'error'); return; + + if (!warehouseId || warehouseId === ' ') { + window.showToast?.('{{ __("Please select source warehouse") }}', 'error'); return; } - if (!machineId || machineId === ' ') { - window.showToast?.('{{ __("Please select target machine") }}', 'error'); return; + if (!machineId || machineId === ' ') { + window.showToast?.('{{ __("Please select target machine") }}', 'error'); return; } - if (this.items.length === 0) { - window.showToast?.('{{ __("Please add at least one item") }}', 'error'); return; + if (this.items.length === 0) { + window.showToast?.('{{ __("Please add at least one item") }}', 'error'); return; } - + for (let i = 0; i < this.items.length; i++) { - if (!this.items[i].product_id) { - window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i+1), 'error'); return; + if (!this.items[i].product_id) { + window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i + 1), 'error'); return; } - if (!this.items[i].quantity || this.items[i].quantity < 1) { - window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i+1), 'error'); return; + if (!this.items[i].quantity || this.items[i].quantity < 1) { + window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i + 1), 'error'); return; } } @@ -309,8 +309,8 @@ this.autoInsufficient = data.insufficient || {}; this.autoWarehouseStocks = data.warehouse_stocks || {}; this.autoPreviewLoaded = true; - } else { - window.showToast?.(data.message || '{{ __("Failed to load preview") }}', 'error'); + } else { + window.showToast?.(data.message || '{{ __("Failed to load preview") }}', 'error'); } } catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); } finally { this.autoLoading = false; } @@ -319,7 +319,7 @@ getActualFillable(index) { const slot = this.autoSlots[index]; if (!slot) return 0; - + let available = this.autoWarehouseStocks[slot.product_id] || 0; for (let i = 0; i < index; i++) { if (this.autoSlots[i].product_id === slot.product_id) { @@ -337,33 +337,33 @@ if (!this.autoSlots || this.autoSlots.length === 0) { window.showToast?.('{{ __("No items to replenish") }}', 'error'); return; } - + this.autoLoading = true; const items = this.autoSlots.map(s => ({ - slot_no: s.slot_no, - product_id: s.product_id, + slot_no: s.slot_no, + product_id: s.product_id, quantity: s.quantity, - current_stock: s.current_stock, + current_stock: s.current_stock, max_stock: s.max_stock, })); - + try { const res = await fetch('{{ route("admin.warehouses.replenishments.auto") }}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, - body: JSON.stringify({ - warehouse_id: this.autoWarehouseId, - machine_id: this.autoMachineId, - note: this.autoNote, - items + body: JSON.stringify({ + warehouse_id: this.autoWarehouseId, + machine_id: this.autoMachineId, + note: this.autoNote, + items }) }); const data = await res.json(); if (data.success) { - this.showAutoModal = false; + this.showAutoModal = false; window.location.reload(); - } else { - window.showToast?.(data.message || '{{ __("Creation failed") }}', 'error'); + } else { + window.showToast?.(data.message || '{{ __("Creation failed") }}', 'error'); } } catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); } finally { this.autoLoading = false; } @@ -373,11 +373,11 @@ async advanceStatus(orderId, newStatus) { this.statusTargetId = orderId; this.statusTargetValue = newStatus; - - const labels = { - prepared: '{{ __("Confirm Prepare") }}', - delivering: '{{ __("Start Delivery") }}', - completed: '{{ __("Confirm Complete") }}' + + const labels = { + prepared: '{{ __("Confirm Prepare") }}', + delivering: '{{ __("Start Delivery") }}', + completed: '{{ __("Confirm Complete") }}' }; const messages = { prepared: '{{ __("Are you sure you want to confirm preparation? This will deduct stock from the warehouse.") }}', @@ -396,11 +396,11 @@ try { const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${this.statusTargetId}/status`, { method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - 'Accept': 'application/json', - 'X-CSRF-TOKEN': '{{ csrf_token() }}' + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, body: JSON.stringify({ status: this.statusTargetValue }) }); @@ -411,11 +411,11 @@ const data = await res.json(); window.showToast?.(data.message || '{{ __("Operation failed") }}', 'error'); } - } catch (e) { - console.error(e); - window.showToast?.('{{ __("System Error") }}', 'error'); - } finally { - this.loading = false; + } catch (e) { + console.error(e); + window.showToast?.('{{ __("System Error") }}', 'error'); + } finally { + this.loading = false; } }, @@ -483,15 +483,16 @@ } -
{{-- Header --}}
-

{{ __('Machine Replenishment') }}

-

{{ __('Create and manage replenishment orders from warehouse to machines') }}

+

{{ + __('Machine Replenishment') }}

+

{{ __('Create + and manage replenishment orders from warehouse to machines') }}

@@ -522,29 +528,52 @@ {{-- Filters --}}
-
-
+ +
- + + + + - +
- - - - - + + + + +
- -
@@ -558,4 +587,4 @@ @include('admin.warehouses.partials.modal-replenishment-assign') @include('admin.warehouses.partials.modal-replenishment-status')
-@endsection +@endsection \ No newline at end of file diff --git a/resources/views/admin/warehouses/transfers.blade.php b/resources/views/admin/warehouses/transfers.blade.php index ca4cb7a..c4c5b40 100644 --- a/resources/views/admin/warehouses/transfers.blade.php +++ b/resources/views/admin/warehouses/transfers.blade.php @@ -318,9 +318,9 @@ @ajax:navigate:transfers.window.prevent="fetchTabData($event.detail.url)"> {{-- Header --}} -
+
-

{{ __('Transfer Orders') }}

+

{{ __('Transfer Orders') }}

{{ __('Manage stock transfers between warehouses and machine returns') }}

-
- + -
+
@@ -363,27 +362,29 @@ + class="py-2.5 pl-12 pr-6 block w-full luxury-input text-sm font-bold" placeholder="{{ __('Search order number...') }}">
-
+
-
+
@@ -393,7 +394,7 @@
{{-- Date Range --}} -
+
+ placeholder="{{ __('Select Date Range') }}" class="luxury-input luxury-input-sm py-2.5 pl-12 pr-6 block w-full cursor-pointer font-bold">
- +
+ -
003 8.003 0 01-15.357-2m15.357 2H15" /> diff --git a/routes/web.php b/routes/web.php index 2fc21b9..4bcd1a2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -116,10 +116,21 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name( // 6. 銷售管理 Route::prefix('sales')->name('sales.')->group(function () { Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index'); + + // 取貨碼 Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'pickupCodes'])->name('pickup-codes'); + 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('/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('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders'); Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions'); - Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes'); Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class, 'storeGifts'])->name('store-gifts'); });