[STYLE] 庫存管理響應式 UI 優化與卡片顯示格式修正

1. 優化「目前庫存」分頁:修復平板模式下卡片展開導致的高度拉伸問題,並確保 Alpine.js 變數 (localCardOpen) 作用域隔離。
2. 優化「異動紀錄」分頁:將時間戳記從獨立行移入 2x2 內容網格,補上「時間」標題並修正字體顏色。
3. 優化「進貨單管理」分頁:卡片日期顯示格式修正為完整時分秒 (Y-m-d H:i:s)。
4. 統一搜尋佈局:將庫存管理各分頁與機台補貨單的「搜尋」與「重置」按鈕群組化,確保在窄螢幕換行時能保持連動。
5. 修正進貨單詳情:優化側邊欄 (Slide-over) 的資料載入邏輯與多語系顯示。
This commit is contained in:
sky121113 2026-04-27 15:40:57 +08:00
parent 9d7fdc9c7f
commit 772c7a62e8
18 changed files with 1377 additions and 249 deletions

View File

@ -4,6 +4,11 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; 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 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' => '取貨碼設定', '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() 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' => '通行碼設定', '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() public function storeGifts()
{ {

View File

@ -0,0 +1,80 @@
<?php
namespace App\Models\Transaction;
use App\Models\Machine\Machine;
use App\Models\User;
use App\Traits\TenantScoped;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PassCode extends Model
{
use TenantScoped;
protected $fillable = [
'company_id',
'machine_id',
'name',
'code',
'expires_at',
'status',
'created_by',
];
protected $casts = [
'expires_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
{
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;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Models\Transaction;
use App\Models\Machine\Machine;
use App\Models\User;
use App\Traits\TenantScoped;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PickupCode extends Model
{
use TenantScoped;
protected $fillable = [
'company_id',
'machine_id',
'slot_no',
'code',
'expires_at',
'used_at',
'status',
'created_by',
];
protected $casts = [
'expires_at' => '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;
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('pickup_codes', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('pass_codes', function (Blueprint $table) {
$table->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');
}
};

View File

@ -341,6 +341,7 @@
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...", "Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
"Deselect All": "Deselect All", "Deselect All": "Deselect All",
"Detail": "Detail", "Detail": "Detail",
"Details": "Details",
"Device Information": "Device Information", "Device Information": "Device Information",
"Device Status Logs": "Device Status Logs", "Device Status Logs": "Device Status Logs",
"Devices": "Devices", "Devices": "Devices",

View File

@ -227,6 +227,7 @@
"Update Settings": "設定を更新", "Update Settings": "設定を更新",
"Video": "動画", "Video": "動画",
"View Details": "詳細を表示", "View Details": "詳細を表示",
"Details": "詳細",
"View Inventory": "在庫を表示", "View Inventory": "在庫を表示",
"View Slots": "貨道を表示", "View Slots": "貨道を表示",
"Visual overview of all machine locations": "全機器位置のビジュアル概要", "Visual overview of all machine locations": "全機器位置のビジュアル概要",

View File

@ -363,6 +363,7 @@
"Describe the repair or maintenance status...": "請描述維修或保養狀況...", "Describe the repair or maintenance status...": "請描述維修或保養狀況...",
"Deselect All": "取消全選", "Deselect All": "取消全選",
"Detail": "詳細", "Detail": "詳細",
"Details": "明細",
"Device Information": "設備資訊", "Device Information": "設備資訊",
"Device Status Logs": "設備狀態紀錄", "Device Status Logs": "設備狀態紀錄",
"Devices": "台設備", "Devices": "台設備",

View File

@ -0,0 +1,193 @@
@extends('layouts.admin')
@section('content')
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="{
showCreateModal: false,
selectedMachine: '',
name: '',
expiresDays: 7,
customCode: '',
useCustomCode: false,
generateRandomCode() {
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
}
}">
{{-- Page Header --}}
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<div>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Pass Codes') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage multi-use authorization codes for testing and maintenance') }}</p>
</div>
<button @click="showCreateModal = true; generateRandomCode()" class="luxury-btn-primary group">
<svg class="w-5 h-5 mr-2 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
</svg>
{{ __('Create Pass Code') }}
</button>
</div>
{{-- Search & Filters --}}
<div class="luxury-card rounded-3xl p-6 mb-8">
<form action="{{ route('admin.sales.pass-codes') }}" method="GET" class="flex flex-col md:flex-row gap-4">
<div class="relative flex-1 group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search by code, name or machine...') }}">
</div>
<button type="submit" class="luxury-btn-secondary px-8">
{{ __('Search') }}
</button>
</form>
</div>
{{-- Table --}}
<div class="luxury-card rounded-3xl overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-800/50">
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Pass Code') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Name / Machine') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Expires At') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Status') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
@forelse ($passCodes as $code)
<tr class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-lg font-black font-mono text-slate-700 dark:text-slate-200 tracking-widest bg-white dark:bg-slate-900 px-4 py-1.5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm group-hover:border-cyan-500/50 transition-colors">
{{ $code->code }}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm font-black text-slate-800 dark:text-white">{{ $code->name }}</div>
<div class="text-xs font-bold text-slate-400 uppercase tracking-tighter">{{ $code->machine->name }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if($code->expires_at)
<div class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $code->expires_at->format('Y-m-d H:i') }}</div>
<div class="text-xs font-medium {{ $code->expires_at->isPast() ? 'text-rose-500' : 'text-slate-400' }}">
{{ $code->expires_at->isPast() ? __('Expired') : $code->expires_at->diffForHumans() }}
</div>
@else
<span class="text-xs font-black uppercase tracking-widest text-emerald-500">{{ __('Permanent') }}</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$isValid = $code->isValid();
@endphp
<span class="px-3 py-1 text-xs font-black uppercase tracking-widest rounded-full border {{ $isValid ? 'bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20' : 'bg-slate-100 text-slate-500 border-slate-200 dark:bg-slate-700 dark:text-slate-400 dark:border-slate-600' }}">
{{ $isValid ? __('Active') : __('Inactive') }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-2">
<form action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST" onsubmit="return confirm('{{ __('Are you sure you want to delete this pass code?') }}')">
@csrf
@method('DELETE')
<button type="submit" class="p-2 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-colors">
<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" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v2m3 4h.01" />
</svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-12 text-center">
<p class="text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No pass codes found') }}</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($passCodes->hasPages())
<div class="px-6 py-4 border-t border-slate-100 dark:border-slate-700">
{{ $passCodes->links() }}
</div>
@endif
</div>
{{-- Create Modal --}}
<div x-show="showCreateModal"
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showCreateModal = false"></div>
<div class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-200/50 dark:border-slate-700/50 animate-luxury-in">
<form action="{{ route('admin.sales.pass-codes.store') }}" method="POST">
@csrf
<div class="p-8">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Create Pass Code') }}</h3>
<button type="button" @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<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="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-6">
{{-- Machine Selection --}}
<div>
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Target Machine') }}</label>
<select name="machine_id" x-model="selectedMachine" class="luxury-input w-full" required>
<option value="">{{ __('Please select a machine') }}</option>
@foreach($machines as $machine)
<option value="{{ $machine->id }}">{{ $machine->name }} ({{ $machine->serial_no }})</option>
@endforeach
</select>
</div>
{{-- Name --}}
<div>
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Description / Name') }}</label>
<input type="text" name="name" x-model="name" class="luxury-input w-full" placeholder="{{ __('e.g. Test Code for Maintenance') }}" required>
</div>
{{-- Code --}}
<div>
<div class="flex items-center justify-between mb-2">
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Pass Code (8 Digits)') }}</label>
<button type="button" @click="generateRandomCode()" class="text-xs font-bold text-cyan-500 hover:text-cyan-600 uppercase tracking-widest">{{ __('Regenerate') }}</button>
</div>
<input type="text" name="custom_code" x-model="customCode" class="luxury-input w-full font-mono text-lg tracking-widest" maxlength="12" required>
</div>
{{-- Validity --}}
<div>
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Validity Period (Days)') }}</label>
<input type="number" name="expires_days" x-model="expiresDays" class="luxury-input w-full" min="1" placeholder="{{ __('Leave empty for permanent') }}">
</div>
</div>
</div>
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-800/50 flex justify-end gap-4 border-t border-slate-100 dark:border-slate-700">
<button type="button" @click="showCreateModal = false" class="luxury-btn-secondary">
{{ __('Cancel') }}
</button>
<button type="submit" class="luxury-btn-primary px-8">
{{ __('Create') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,226 @@
@extends('layouts.admin')
@section('content')
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="{
showCreateModal: false,
selectedMachine: '',
selectedSlot: '',
expiresHours: 24,
slots: [],
loadingSlots: false,
async fetchSlots() {
if (!this.selectedMachine) {
this.slots = [];
return;
}
this.loadingSlots = true;
try {
const response = await fetch(`/admin/machines/${this.selectedMachine}/slots-ajax`);
const data = await response.json();
this.slots = data.slots || [];
} catch (error) {
console.error('Error fetching slots:', error);
} finally {
this.loadingSlots = false;
}
}
}">
{{-- Page Header --}}
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<div>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Pickup Codes') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Generate and manage one-time pickup codes for customers') }}</p>
</div>
<button @click="showCreateModal = true" class="luxury-btn-primary group">
<svg class="w-5 h-5 mr-2 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
</svg>
{{ __('Generate New Code') }}
</button>
</div>
{{-- Search & Filters --}}
<div class="luxury-card rounded-3xl p-6 mb-8">
<form action="{{ route('admin.sales.pickup-codes') }}" method="GET" class="flex flex-col md:flex-row gap-4">
<div class="relative flex-1 group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search by code, machine name or serial...') }}">
</div>
<div class="w-full md:w-48">
<select name="status" class="luxury-input w-full" onchange="this.form.submit()">
<option value="">{{ __('All Status') }}</option>
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>{{ __('Active') }}</option>
<option value="used" {{ request('status') === 'used' ? 'selected' : '' }}>{{ __('Used') }}</option>
<option value="expired" {{ request('status') === 'expired' ? 'selected' : '' }}>{{ __('Expired') }}</option>
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }}>{{ __('Cancelled') }}</option>
</select>
</div>
<button type="submit" class="luxury-btn-secondary px-8">
{{ __('Filter') }}
</button>
</form>
</div>
{{-- Table --}}
<div class="luxury-card rounded-3xl overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-800/50">
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Code') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Machine / Slot') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Expires At') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Status') }}</th>
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
@forelse ($pickupCodes as $code)
<tr class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-lg font-black font-mono text-cyan-600 dark:text-cyan-400 tracking-tighter bg-cyan-50 dark:bg-cyan-500/10 px-3 py-1 rounded-lg border border-cyan-100 dark:border-cyan-500/20 shadow-sm">
{{ $code->code }}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm font-black text-slate-800 dark:text-white">{{ $code->machine->name }}</div>
<div class="text-xs font-bold text-slate-400 uppercase tracking-tighter">{{ __('Slot') }}: {{ $code->slot_no }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $code->expires_at->format('Y-m-d H:i') }}</div>
<div class="text-xs font-medium text-slate-400">{{ $code->expires_at->diffForHumans() }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@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
<span class="px-3 py-1 text-xs font-black uppercase tracking-widest rounded-full border {{ $statusClasses[$displayStatus] ?? $statusClasses['cancelled'] }}">
{{ __($displayStatus) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-2">
@if($code->status === 'active' && $code->expires_at->isFuture())
<form action="{{ route('admin.sales.pickup-codes.destroy', $code) }}" method="POST" onsubmit="return confirm('{{ __('Are you sure you want to cancel this code?') }}')">
@csrf
@method('DELETE')
<button type="submit" class="p-2 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-colors" title="{{ __('Cancel Code') }}">
<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" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<div class="w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
</div>
<p class="text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No pickup codes found') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($pickupCodes->hasPages())
<div class="px-6 py-4 border-t border-slate-100 dark:border-slate-700">
{{ $pickupCodes->links() }}
</div>
@endif
</div>
{{-- Create Modal --}}
<div x-show="showCreateModal"
class="fixed inset-0 z-50 overflow-y-auto"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showCreateModal = false"></div>
<div class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-200/50 dark:border-slate-700/50 animate-luxury-in">
<form action="{{ route('admin.sales.pickup-codes.store') }}" method="POST">
@csrf
<div class="p-8">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Generate Pickup Code') }}</h3>
<button type="button" @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<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="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="space-y-6">
{{-- Machine Selection --}}
<div>
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Select Machine') }}</label>
<select name="machine_id" x-model="selectedMachine" @change="fetchSlots()" class="luxury-input w-full" required>
<option value="">{{ __('Please select a machine') }}</option>
@foreach($machines as $machine)
<option value="{{ $machine->id }}">{{ $machine->name }} ({{ $machine->serial_no }})</option>
@endforeach
</select>
</div>
{{-- Slot Selection --}}
<div>
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Select Slot') }}</label>
<div class="relative">
<select name="slot_no" x-model="selectedSlot" class="luxury-input w-full" :disabled="!selectedMachine || loadingSlots" required>
<option value="">{{ __('Select Slot') }}</option>
<template x-for="slot in slots" :key="slot.slot_no">
<option :value="slot.slot_no" x-text="`${slot.slot_no} - ${slot.product_name || 'Empty'}`"></option>
</template>
</select>
<div x-show="loadingSlots" class="absolute right-10 top-2.5">
<svg class="animate-spin h-5 w-5 text-cyan-500" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
</div>
</div>
</div>
{{-- Validity --}}
<div>
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Validity Period (Hours)') }}</label>
<input type="number" name="expires_hours" x-model="expiresHours" class="luxury-input w-full" min="1" max="720" required>
<p class="mt-1 text-xs text-slate-400 font-medium">{{ __('Default is 24 hours. Max is 720 hours (30 days).') }}</p>
</div>
</div>
</div>
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-800/50 flex justify-end gap-4 border-t border-slate-100 dark:border-slate-700">
<button type="button" @click="showCreateModal = false" class="luxury-btn-secondary">
{{ __('Cancel') }}
</button>
<button type="submit" class="luxury-btn-primary px-8" :disabled="!selectedSlot">
{{ __('Generate') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@ -270,9 +270,9 @@
<!-- Header --> <!-- Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Inventory Management') }}</h1> <h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Inventory Management') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest"> <p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
{{ __('Track stock-in orders and movement history') }} {{ __('Track stock-in orders and movement history') }}
</p> </p>

View File

@ -84,18 +84,20 @@
</button> </button>
</template> </template>
<div> <div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Inventory Overview') }}</h1> <h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p> __('Machine Inventory Overview') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{
__('Real-time slot-level inventory across all machines') }}</p>
</div> </div>
</div> </div>
{{-- List Mode --}} {{-- List Mode --}}
<div x-show="viewMode === 'list'" class="space-y-6 animate-luxury-in" x-cloak> <div x-show="viewMode === 'list'" class="space-y-6 animate-luxury-in" x-cloak>
<div class="luxury-card rounded-3xl p-8 hover:translate-y-0 transition-transform duration-300"> <div class="luxury-card rounded-3xl p-8 hover:translate-y-0 transition-transform duration-300">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8"> <div class="flex flex-wrap items-center justify-between gap-4 mb-8">
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET" <form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET"
class="flex items-center gap-4"> class="flex flex-wrap items-center gap-4 flex-1">
<div class="relative group"> <div class="relative group w-full sm:w-72">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"> <span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" <svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
@ -104,30 +106,34 @@
</svg> </svg>
</span> </span>
<input type="text" name="search" value="{{ request('search') }}" <input type="text" name="search" value="{{ request('search') }}"
class="py-2.5 pl-12 pr-6 block w-72 luxury-input" class="py-2.5 pl-12 pr-6 block w-full luxury-input"
placeholder="{{ __('Search machines...') }}"> placeholder="{{ __('Search machines...') }}">
</div> </div>
<button type="submit" <div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" <button type="submit"
title="{{ __('Search') }}"> class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95"
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> title="{{ __('Search') }}">
<path stroke-linecap="round" stroke-linejoin="round" <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> stroke-width="2.5">
</svg> <path stroke-linecap="round" stroke-linejoin="round"
</button> d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button" @click=" <button type="button" @click="
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
$el.closest('form').submit(); $el.closest('form').submit();
" "
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95"
title="{{ __('Reset') }}"> title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round"
</svg> d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</button> </svg>
</button>
</div>
</form> </form>
</div> </div>
@ -184,7 +190,8 @@
</div> </div>
</td> </td>
<td class="px-6 py-5"> <td class="px-6 py-5">
<span class="font-mono font-bold text-slate-500 uppercase tracking-widest text-sm">{{ <span
class="font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest text-sm group-hover:text-cyan-500/70 transition-colors">{{
$machine->serial_no }}</span> $machine->serial_no }}</span>
</td> </td>
<td class="px-6 py-5 text-center"> <td class="px-6 py-5 text-center">
@ -326,7 +333,8 @@
</div> </div>
<!-- Active Slots --> <!-- Active Slots -->
<div class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02] hover:translate-y-0 transition-transform duration-300"> <div
class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02] hover:translate-y-0 transition-transform duration-300">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div <div
class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500"> class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
@ -343,7 +351,8 @@
</div> </div>
<!-- Empty Slots --> <!-- Empty Slots -->
<div class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02] hover:translate-y-0 transition-transform duration-300"> <div
class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02] hover:translate-y-0 transition-transform duration-300">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div <div
class="w-10 h-10 rounded-xl bg-slate-500/10 flex items-center justify-center text-slate-500"> class="w-10 h-10 rounded-xl bg-slate-500/10 flex items-center justify-center text-slate-500">
@ -360,7 +369,8 @@
</div> </div>
<!-- High Stock --> <!-- High Stock -->
<div class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2 hover:translate-y-0 transition-transform duration-300"> <div
class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2 hover:translate-y-0 transition-transform duration-300">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div <div
@ -418,7 +428,8 @@
<!-- Grid View --> <!-- Grid View -->
<div x-show="displayMode === 'grid'" class="space-y-6"> <div x-show="displayMode === 'grid'" class="space-y-6">
<div class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px] hover:translate-y-0 transition-transform duration-300"> <div
class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px] hover:translate-y-0 transition-transform duration-300">
<div x-show="loading" <div x-show="loading"
class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500"> class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500">
<div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"> <div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
@ -465,7 +476,8 @@
<!-- Table View --> <!-- Table View -->
<div x-show="displayMode === 'table'" class="animate-luxury-in" x-cloak> <div x-show="displayMode === 'table'" class="animate-luxury-in" x-cloak>
<div class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl hover:translate-y-0 transition-transform duration-300"> <div
class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl hover:translate-y-0 transition-transform duration-300">
<table class="w-full text-left"> <table class="w-full text-left">
<thead> <thead>
<tr class="bg-slate-50 dark:bg-slate-900/50"> <tr class="bg-slate-50 dark:bg-slate-900/50">

View File

@ -1,10 +1,10 @@
<div class="luxury-card rounded-3xl p-8 animate-luxury-in"> <div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4" <form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center justify-between mb-8 gap-4"
@submit.prevent="handleFilterSubmit('movements')"> @submit.prevent="handleFilterSubmit('movements')">
<input type="hidden" name="tab" value="movements"> <input type="hidden" name="tab" value="movements">
<div class="flex flex-wrap gap-4 items-center"> <div class="flex flex-wrap gap-4 items-center flex-1">
<div class="w-64"> <div class="w-full sm:w-64">
<x-searchable-select <x-searchable-select
name="warehouse_id" name="warehouse_id"
:options="$warehouses" :options="$warehouses"
@ -13,7 +13,7 @@
/> />
</div> </div>
<div class="w-64"> <div class="w-full sm:w-64">
<x-searchable-select <x-searchable-select
name="type" name="type"
:selected="request('type')" :selected="request('type')"
@ -28,7 +28,7 @@
</div> </div>
{{-- Date Range --}} {{-- Date Range --}}
<div class="relative group w-72" <div class="relative group w-full sm:w-72"
x-data="{ fp: null }" x-data="{ fp: null }"
x-init="fp = flatpickr($refs.dateRange, { x-init="fp = flatpickr($refs.dateRange, {
mode: 'range', mode: 'range',
@ -59,8 +59,8 @@
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer"> placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}"> <button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -74,9 +74,10 @@
const instance = window.HSSelect.getInstance(s); const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' '); if (instance) instance.setValue(' ');
}); });
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
handleFilterSubmit('movements'); handleFilterSubmit('movements');
" "
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}"> class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
@ -85,7 +86,8 @@
</div> </div>
</form> </form>
<div class="overflow-x-auto"> {{-- Table Mode (Desktop) --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0"> <table class="w-full text-left border-separate border-spacing-y-0">
<thead> <thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10"> <tr class="bg-slate-50/50 dark:bg-slate-900/10">
@ -162,6 +164,60 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{{-- Card Mode (Mobile & Tablet) --}}
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8">
@forelse($movements as $mv)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $mv->product?->localized_name }}</h3>
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">ID: {{ $mv->product_id }}</p>
</div>
</div>
<div class="shrink-0">
@php $typeColor = in_array($mv->type, ['in', 'transfer_in']) ? 'cyan' : (in_array($mv->type, ['out', 'transfer_out']) ? 'rose' : 'amber'); @endphp
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-{{ $typeColor }}-500/10 text-{{ $typeColor }}-500 border border-{{ $typeColor }}-500/20 uppercase tracking-widest shadow-sm">
{{ __(\App\Models\Warehouse\StockMovement::TYPE_LABELS[$mv->type] ?? $mv->type) }}
</span>
</div>
</div>
<div class="grid grid-cols-2 gap-y-4 mb-4 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Warehouse') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{ $mv->warehouse?->name }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Qty Change') }}</p>
<p class="text-sm font-extrabold {{ in_array($mv->type, ['in', 'transfer_in']) ? 'text-cyan-500' : 'text-rose-500' }}">
{{ in_array($mv->type, ['in', 'transfer_in']) ? '+' : '-' }}{{ $mv->quantity }}
<span class="text-[10px] text-slate-400 font-bold ml-1">({{ $mv->before_qty }}{{ $mv->after_qty }})</span>
</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Operator') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $mv->creator?->name ?? '-' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Time') }}</p>
<p class="text-sm font-mono font-bold text-slate-700 dark:text-slate-300">{{ $mv->created_at?->format('Y-m-d H:i:s') }}</p>
</div>
</div>
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No movement records found') }}</p>
</div>
@endforelse
</div>
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50"> <div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
{{ $movements->links('vendor.pagination.luxury') }} {{ $movements->links('vendor.pagination.luxury') }}
</div> </div>

View File

@ -1,11 +1,11 @@
<div class="luxury-card rounded-3xl p-8 animate-luxury-in"> <div class="luxury-card rounded-3xl p-8 animate-luxury-in">
{{-- Filters --}} {{-- Filters --}}
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4" <form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center justify-between mb-8 gap-4"
@submit.prevent="handleFilterSubmit('stock-in')"> @submit.prevent="handleFilterSubmit('stock-in')">
<input type="hidden" name="tab" value="stock-in"> <input type="hidden" name="tab" value="stock-in">
<div class="flex flex-col md:flex-row gap-4 items-center"> <div class="flex flex-wrap gap-4 items-center flex-1">
<div class="relative group"> <div class="relative group w-full sm:w-72">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"> <span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" <svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
@ -15,11 +15,11 @@
</span> </span>
<input type="text" name="search_order" value="{{ request('search_order') }}" <input type="text" name="search_order" value="{{ request('search_order') }}"
@keydown.enter.prevent="handleFilterSubmit('stock-in')" @keydown.enter.prevent="handleFilterSubmit('stock-in')"
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search order number...') }}"> class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
</div> </div>
{{-- Date Range --}} {{-- Date Range --}}
<div class="relative group w-72" <div class="relative group w-full sm:w-72"
x-data="{ fp: null }" x-data="{ fp: null }"
x-init="fp = flatpickr($refs.dateRange, { x-init="fp = flatpickr($refs.dateRange, {
mode: 'range', mode: 'range',
@ -50,31 +50,33 @@
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer"> placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
</div> </div>
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}"> <div class="flex items-center gap-2 ml-auto sm:ml-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</button> </svg>
</button>
<button type="button" <button type="button"
@click=" @click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => { $el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' '; s.value = ' ';
const instance = window.HSSelect.getInstance(s); const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' '); if (instance) instance.setValue(' ');
}); });
handleFilterSubmit('stock-in'); handleFilterSubmit('stock-in');
" "
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}"> class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
</button> </button>
</div>
</div> </div>
@if(isset($stock_in_status_options)) @if(isset($stock_in_status_options))
<div class="w-48"> <div class="w-full sm:w-48">
<x-searchable-select <x-searchable-select
name="status" name="status"
:selected="request('status')" :selected="request('status')"
@ -89,8 +91,8 @@
@endif @endif
</form> </form>
{{-- Table --}} {{-- Table Mode (Desktop) --}}
<div class="overflow-x-auto"> <div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0"> <table class="w-full text-left border-separate border-spacing-y-0">
<thead> <thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10"> <tr class="bg-slate-50/50 dark:bg-slate-900/10">
@ -220,6 +222,82 @@
</table> </table>
</div> </div>
{{-- Card Mode (Mobile & Tablet) --}}
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8">
@forelse($orders as $order)
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
@click="openOrderDetails('{{ $order->id }}')">
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $order->order_no }}</h3>
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">{{ $order->created_at->format('Y-m-d H:i:s') }}</p>
</div>
</div>
<div class="shrink-0">
@if($order->status === 'draft')
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest shadow-sm">
{{ __('Draft') }}
</span>
@elseif($order->status === 'cancelled')
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-slate-500/10 text-slate-500 border border-slate-500/20 uppercase tracking-widest shadow-sm">
{{ __('Cancelled') }}
</span>
@else
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 uppercase tracking-widest shadow-sm">
{{ __('Completed') }}
</span>
@endif
</div>
</div>
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Warehouse') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{ $order->warehouse?->name }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Items') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $order->items_count }}</p>
</div>
@if($order->note)
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Note') }}</p>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 italic line-clamp-1">"{{ $order->note }}"</p>
</div>
@endif
</div>
<div class="flex items-center gap-3">
<button type="button" @click="openOrderDetails('{{ $order->id }}')"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-[10px] sm:text-xs tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-300 border border-slate-200 dark:border-slate-700 shadow-sm">
<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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
{{ __('Details') }}
</button>
@if($order->status === 'draft')
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order) }}" method="POST" class="flex-1" @submit.prevent="confirmStockIn($el)">
@csrf @method('PATCH')
<button type="submit"
class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-emerald-500/10 text-emerald-500 font-black text-xs uppercase tracking-widest hover:bg-emerald-500 hover:text-white transition-all duration-300 border border-emerald-500/20 shadow-sm">
<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="M5 13l4 4L19 7" /></svg>
{{ __('Confirm') }}
</button>
</form>
@endif
</div>
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No stock-in orders found') }}</p>
</div>
@endforelse
</div>
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50"> <div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:stock-in']) }} {{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:stock-in']) }}
</div> </div>

View File

@ -1,59 +1,75 @@
<div class="luxury-card rounded-3xl p-8 animate-luxury-in"> <div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8" <form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8"
@submit.prevent="handleFilterSubmit('stock')"> @submit.prevent="handleFilterSubmit('stock')">
<input type="hidden" name="tab" value="stock"> <input type="hidden" name="tab" value="stock">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span> <div class="relative group w-full sm:w-72">
<input type="text" name="search" value="{{ request('search') }}" <span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
placeholder="{{ __('Search products...') }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
@keydown.enter.prevent="handleFilterSubmit('stock')"> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg></span>
<input type="text" name="search" value="{{ request('search') }}"
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search products...') }}"
@keydown.enter.prevent="handleFilterSubmit('stock')">
</div> </div>
{{-- 多選倉庫篩選器 --}} {{-- 多選倉庫篩選器 --}}
<div class="min-w-[260px]"> <div class="w-full sm:w-72">
<x-multi-select <x-multi-select name="warehouse_ids" :options="$warehouses" :selected="request('warehouse_ids', [])"
name="warehouse_ids" :placeholder="__('Filter by Warehouse Presence')" />
:options="$warehouses"
:selected="request('warehouse_ids', [])"
:placeholder="__('Filter by Warehouse Presence')"
/>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-2 ml-auto sm:ml-0">
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}"> <button type="submit"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path stroke-linecap="round" stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
</button> </button>
<button type="button" <button type="button" @click="
@click=" $el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' ';
const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' ');
});
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true })); $el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
handleFilterSubmit('stock'); $nextTick(() => handleFilterSubmit('stock'));
" "
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}"> class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
</button> </button>
</div> </div>
</form> </form>
<div class="overflow-x-auto -mx-8 px-8 custom-scrollbar"> <div class="hidden xl:block overflow-x-auto -mx-8 px-8 custom-scrollbar">
<table class="w-full text-left border-separate border-spacing-y-0 min-w-max"> <table class="w-full text-left border-separate border-spacing-y-0 min-w-max">
<thead> <thead>
<tr class="bg-slate-50/50 dark:bg-white/[0.02]"> <tr class="bg-slate-50/50 dark:bg-white/[0.02]">
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 sticky left-0 bg-white dark:bg-[#1e293b] z-20 w-[180px] md:w-[260px] min-w-[180px] md:min-w-[260px]">{{ __('Product') }}</th> <th
<th class="px-6 py-5 text-sm font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.1em] border-b border-slate-100 dark:border-slate-800 text-center bg-white dark:bg-[#1e293b] sticky md:left-[260px] z-20 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[1px_0_0_0_rgba(0,0,0,0.05)] dark:shadow-[1px_0_0_0_rgba(255,255,255,0.05)]"> class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 sticky left-0 bg-white dark:bg-[#1e293b] z-20 w-[180px] xl:w-[260px] min-w-[180px] xl:min-w-[260px]">
{{ __('Product') }}</th>
<th
class="px-6 py-5 text-sm font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.1em] border-b border-slate-100 dark:border-slate-800 text-center bg-white dark:bg-[#1e293b] sticky xl:left-[260px] z-20 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[1px_0_0_0_rgba(0,0,0,0.05)] dark:shadow-[1px_0_0_0_rgba(255,255,255,0.05)]">
{{ __('Total') }} {{ __('Total') }}
</th> </th>
@foreach($inventory_warehouses as $w) @foreach($inventory_warehouses as $w)
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center"> <th
class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<span class="text-base font-black text-slate-800 dark:text-slate-200 tracking-tight">{{ $w->name }}</span> <span class="text-base font-black text-slate-800 dark:text-slate-200 tracking-tight">{{
<span class="text-[10px] font-bold opacity-60 tracking-widest mt-0.5 uppercase">{{ $w->type === 'main' ? __('Main') : __('Branch') }}</span> $w->name }}</span>
<span class="text-[10px] font-bold opacity-60 tracking-widest mt-0.5 uppercase">{{ $w->type
=== 'main' ? __('Main') : __('Branch') }}</span>
</div> </div>
</th> </th>
@endforeach @endforeach
@ -61,62 +77,83 @@
</thead> </thead>
<tbody class="divide-y divide-slate-50 dark:divide-white/[0.05]"> <tbody class="divide-y divide-slate-50 dark:divide-white/[0.05]">
@forelse($products as $product) @forelse($products as $product)
@php @php
$viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray(); $viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray();
$totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity'); $totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity');
@endphp @endphp
<tr class="group hover:bg-slate-50 dark:hover:bg-white/[0.03] transition-all duration-300"> <tr class="group hover:bg-slate-50 dark:hover:bg-white/[0.03] transition-all duration-300">
<td class="px-6 py-4 sticky left-0 bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] z-10 transition-colors w-[180px] md:w-[260px] min-w-[180px] md:min-w-[260px]"> <td
<div class="flex items-center gap-3 md:gap-4"> class="px-6 py-4 sticky left-0 bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] z-10 transition-colors w-[180px] xl:w-[260px] min-w-[180px] xl:min-w-[260px]">
<div class="w-8 h-8 md:w-11 md:h-11 rounded-xl md:rounded-2xl bg-slate-100 dark:bg-slate-800/50 flex items-center justify-center overflow-hidden border border-slate-200/50 dark:border-white/5 group-hover:border-cyan-500/30 transition-colors shrink-0"> <div class="flex items-center gap-3 lg:gap-4">
<div
class="w-8 h-8 lg:w-11 lg:h-11 rounded-xl lg:rounded-2xl bg-slate-100 dark:bg-slate-800/50 flex items-center justify-center overflow-hidden border border-slate-200/50 dark:border-white/5 group-hover:border-cyan-500/30 transition-colors shrink-0">
@if($product->image_url) @if($product->image_url)
<img src="{{ $product->image_url }}" class="w-full h-full object-cover"> <img src="{{ $product->image_url }}" class="w-full h-full object-cover">
@else @else
<svg class="w-4 h-4 md:w-5 md:h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" /></svg> <svg class="w-4 h-4 lg:w-5 lg:h-5 text-slate-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
@endif @endif
</div> </div>
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<span class="text-xs md:text-sm font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors truncate">{{ $product->localized_name }}</span> <span
class="text-xs lg:text-sm font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors truncate">{{
$product->localized_name }}</span>
@if($product->barcode) @if($product->barcode)
<span class="text-[9px] md:text-[10px] font-bold text-slate-400 mt-0.5 tracking-tight truncate">{{ $product->barcode }}</span> <span
class="text-[9px] lg:text-[10px] font-bold text-slate-400 mt-0.5 tracking-tight truncate">{{
$product->barcode }}</span>
@endif @endif
</div> </div>
</div> </div>
</td> </td>
<td class="px-4 md:px-6 py-4 text-center bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] transition-colors sticky md:left-[260px] z-10 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[4px_0_12px_-4px_rgba(0,0,0,0.1)] dark:shadow-[4px_0_12px_-4px_rgba(0,0,0,0.3)]"> <td
<span class="text-lg md:text-xl font-mono font-black text-cyan-600 dark:text-cyan-400 drop-shadow-[0_0_10px_rgba(6,182,212,0.1)]"> class="px-4 lg:px-6 py-4 text-center bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] transition-colors sticky xl:left-[260px] z-10 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[4px_0_12px_-4px_rgba(0,0,0,0.1)] dark:shadow-[4px_0_12px_-4px_rgba(0,0,0,0.3)]">
<span
class="text-lg lg:text-xl font-mono font-black text-cyan-600 dark:text-cyan-400 drop-shadow-[0_0_10px_rgba(6,182,212,0.1)]">
{{ $totalQuantity }} {{ $totalQuantity }}
</span> </span>
</td> </td>
@foreach($inventory_warehouses as $w) @foreach($inventory_warehouses as $w)
@php @php
$stock = $product->stocks->firstWhere('warehouse_id', $w->id); $stock = $product->stocks->firstWhere('warehouse_id', $w->id);
$isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock; $isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock;
@endphp @endphp
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">
@if($stock && $stock->quantity > 0) @if($stock && $stock->quantity > 0)
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<span class="text-base font-black {{ $isLow ? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.2)]' : 'text-slate-700 dark:text-slate-200' }}"> <span
{{ $stock->quantity }} class="text-base font-black {{ $isLow ? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.2)]' : 'text-slate-700 dark:text-slate-200' }}">
</span> {{ $stock->quantity }}
@if($isLow) </span>
<span class="text-[9px] font-black text-rose-500 uppercase tracking-tighter opacity-80">{{ __('Low') }}</span> @if($isLow)
<span
class="text-[9px] font-black text-rose-500 uppercase tracking-tighter opacity-80">{{
__('Low') }}</span>
@endif
</div>
@else
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
@endif @endif
</div> </td>
@else @endforeach
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
@endif
</td>
@endforeach
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="{{ count($inventory_warehouses) + 2 }}" class="px-6 py-20 text-center"> <td colspan="{{ count($inventory_warehouses) + 2 }}" class="px-6 py-20 text-center">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4"> <div
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div> </div>
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}</p> <p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}
</p>
</div> </div>
</td> </td>
</tr> </tr>
@ -124,5 +161,112 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $products->links('vendor.pagination.luxury') }}</div>
</div> {{-- Mobile Card View --}}
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8 items-start">
@forelse($products as $product)
@php
$viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray();
$totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity');
@endphp
<div x-data="{ localCardOpen: false }"
class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
{{-- Card Header --}}
<div class="flex items-center justify-between gap-4 cursor-pointer" @click.stop="localCardOpen = !localCardOpen">
<div class="flex items-center gap-4 min-w-0">
<div
class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 transition-all duration-300 shadow-sm shrink-0">
@if($product->image_url)
<img src="{{ $product->image_url }}"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
@else
<svg class="w-7 h-7 text-slate-400 group-hover:text-white transition-colors" fill="none"
stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
@endif
</div>
<div class="min-w-0">
<h3
class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $product->localized_name }}
</h3>
@if($product->barcode)
<p
class="text-[10px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate mt-0.5">
{{ $product->barcode }}
</p>
@endif
</div>
</div>
<div class="flex flex-col items-end gap-1 shrink-0">
<span class="text-xl font-mono font-black text-cyan-600 dark:text-cyan-400">
{{ $totalQuantity }}
</span>
<svg class="w-4 h-4 text-slate-400 transition-transform duration-300"
:class="{ 'rotate-180': localCardOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</div>
</div>
{{-- Collapsible Content --}}
<div x-show="localCardOpen" x-collapse x-cloak
class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800/50 space-y-2">
@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
<div
class="flex items-center justify-between p-4 rounded-2xl bg-slate-50/50 dark:bg-white/[0.02] border border-transparent hover:border-slate-200 dark:hover:border-slate-700 transition-all">
<div class="flex items-center gap-3">
<span class="text-base font-black text-slate-800 dark:text-slate-100">{{ $w->name }}</span>
<span
class="text-[10px] font-bold text-slate-400 bg-slate-100 dark:bg-slate-800/50 px-2 py-0.5 rounded-lg uppercase tracking-tight">
{{ $w->type === 'main' ? __('Main') : __('Branch') }}
</span>
</div>
<div class="flex items-center gap-3">
@if($stock && $stock->quantity > 0)
<span
class="text-base font-mono font-black {{ $isLow ? 'text-rose-500' : 'text-slate-800 dark:text-slate-100' }}">
{{ $stock->quantity }}
</span>
@if($isLow)
<div class="px-2 py-0.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<span
class="text-[10px] font-black text-rose-600 dark:text-rose-400 uppercase tracking-tighter">{{
__('Low') }}</span>
</div>
@endif
@else
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
@endif
</div>
</div>
@endforeach
</div>
</div>
@empty
<div class="px-6 py-20 text-center luxury-card rounded-[2rem]">
<div class="flex flex-col items-center">
<div
class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}</p>
</div>
</div>
@endforelse
</div>
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{
$products->links('vendor.pagination.luxury') }}</div>
</div>

View File

@ -57,7 +57,7 @@
this.autoPreviewLoaded = false; this.autoPreviewLoaded = false;
this.autoSlots = []; this.autoSlots = [];
this.autoNote = ''; this.autoNote = '';
// 同步 HSSelect UI // 同步 HSSelect UI
this.$nextTick(() => { this.$nextTick(() => {
const select = document.querySelector('[name="auto_machine_id"]'); const select = document.querySelector('[name="auto_machine_id"]');
@ -113,9 +113,9 @@
try { try {
const res = await fetch('{{ route("admin.warehouses.ajax.stock") }}?warehouse_id=' + this.fromId); const res = await fetch('{{ route("admin.warehouses.ajax.stock") }}?warehouse_id=' + this.fromId);
const json = await res.json(); const json = await res.json();
if (json.success) { if (json.success) {
this.availableProducts = json.data; this.availableProducts = json.data;
this.updateProductDisplay(); this.updateProductDisplay();
} }
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
finally { this.isLoadingProducts = false; } finally { this.isLoadingProducts = false; }
@ -123,9 +123,9 @@
async fetchMachineSlots() { async fetchMachineSlots() {
this.machineSlots = []; this.machineSlots = [];
if (!this.targetMachineId) { if (!this.targetMachineId) {
this.items.forEach((_, idx) => this.updateSlotSelects(idx)); this.items.forEach((_, idx) => this.updateSlotSelects(idx));
return; return;
} }
this.isLoadingSlots = true; this.isLoadingSlots = true;
try { try {
@ -133,7 +133,7 @@
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
}); });
const json = await res.json(); const json = await res.json();
if (json.success) { if (json.success) {
this.machineSlots = json.slots; this.machineSlots = json.slots;
this.items.forEach((_, idx) => this.updateSlotSelects(idx)); this.items.forEach((_, idx) => this.updateSlotSelects(idx));
} }
@ -143,7 +143,7 @@
updateSlotSelects(index = null) { updateSlotSelects(index = null) {
const indices = index !== null ? [index] : this.items.map((_, i) => i); const indices = index !== null ? [index] : this.items.map((_, i) => i);
indices.forEach(idx => { indices.forEach(idx => {
const item = this.items[idx]; const item = this.items[idx];
const wrapper = document.getElementById(`slot-select-wrapper-${idx}`); const wrapper = document.getElementById(`slot-select-wrapper-${idx}`);
@ -151,7 +151,7 @@
const oldSelect = wrapper.querySelector('select'); const oldSelect = wrapper.querySelector('select');
if (oldSelect && window.HSSelect?.getInstance(oldSelect)) { if (oldSelect && window.HSSelect?.getInstance(oldSelect)) {
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) {} try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) { }
} }
wrapper.innerHTML = ''; wrapper.innerHTML = '';
@ -171,12 +171,12 @@
opt.value = s.slot_no; opt.value = s.slot_no;
const stockInfo = ` [${s.stock || 0}/${s.max_stock || 0}]`; const stockInfo = ` [${s.stock || 0}/${s.max_stock || 0}]`;
opt.textContent = s.slot_no + (s.product ? ` (${s.product.name})` : '') + stockInfo; opt.textContent = s.slot_no + (s.product ? ` (${s.product.name})` : '') + stockInfo;
// Enhanced style for HSSelect // Enhanced style for HSSelect
opt.setAttribute('data-title', s.slot_no + (s.product ? ` (${s.product.name})` : '')); opt.setAttribute('data-title', s.slot_no + (s.product ? ` (${s.product.name})` : ''));
const stockHtml = `<div class="flex items-baseline gap-0.5"><span class="text-xs font-black text-slate-800 dark:text-slate-100 tracking-tighter">${s.stock || 0}</span><span class="text-[9px] font-bold text-slate-400 opacity-50">/</span><span class="text-[10px] font-bold text-slate-500 opacity-80">${s.max_stock || 0}</span></div>`; const stockHtml = `<div class="flex items-baseline gap-0.5"><span class="text-xs font-black text-slate-800 dark:text-slate-100 tracking-tighter">${s.stock || 0}</span><span class="text-[9px] font-bold text-slate-400 opacity-50">/</span><span class="text-[10px] font-bold text-slate-500 opacity-80">${s.max_stock || 0}</span></div>`;
opt.setAttribute('data-description', stockHtml); opt.setAttribute('data-description', stockHtml);
if (item.slot_no == s.slot_no) opt.selected = true; if (item.slot_no == s.slot_no) opt.selected = true;
selectEl.appendChild(opt); selectEl.appendChild(opt);
}); });
@ -193,7 +193,7 @@
})); }));
wrapper.appendChild(selectEl); wrapper.appendChild(selectEl);
selectEl.addEventListener('change', (e) => { selectEl.addEventListener('change', (e) => {
item.slot_no = e.target.value; item.slot_no = e.target.value;
// Auto-select product based on slot // Auto-select product based on slot
const slotData = this.machineSlots.find(s => s.slot_no == item.slot_no); const slotData = this.machineSlots.find(s => s.slot_no == item.slot_no);
@ -211,14 +211,14 @@
updateProductDisplay(index = null) { updateProductDisplay(index = null) {
const indices = index !== null ? [index] : this.items.map((_, i) => i); const indices = index !== null ? [index] : this.items.map((_, i) => i);
indices.forEach(idx => { indices.forEach(idx => {
const item = this.items[idx]; const item = this.items[idx];
const wrapper = document.getElementById(`product-select-wrapper-${idx}`); const wrapper = document.getElementById(`product-select-wrapper-${idx}`);
if (!wrapper) return; if (!wrapper) return;
wrapper.innerHTML = ''; wrapper.innerHTML = '';
if (!item.product_id) { if (!item.product_id) {
wrapper.innerHTML = ` wrapper.innerHTML = `
<div class="luxury-input w-full px-4 py-2.5 bg-slate-50/50 dark:bg-slate-900/50 text-slate-400 text-sm flex items-center"> <div class="luxury-input w-full px-4 py-2.5 bg-slate-50/50 dark:bg-slate-900/50 text-slate-400 text-sm flex items-center">
@ -231,7 +231,7 @@
const productInWarehouse = this.availableProducts.find(p => p.id == item.product_id); const productInWarehouse = this.availableProducts.find(p => p.id == item.product_id);
const productInfo = this.allProducts.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'); const productName = productInfo ? productInfo.name : (productInWarehouse ? productInWarehouse.name : 'Unknown');
// Priority: stock from selected warehouse, otherwise N/A or 0 // Priority: stock from selected warehouse, otherwise N/A or 0
const stock = productInWarehouse ? productInWarehouse.quantity : 0; const stock = productInWarehouse ? productInWarehouse.quantity : 0;
const stockText = this.fromId ? `{{ __('Stock') }}: ${stock}` : '{{ __('Select Warehouse') }}'; const stockText = this.fromId ? `{{ __('Stock') }}: ${stock}` : '{{ __('Select Warehouse') }}';
@ -266,23 +266,23 @@
const form = document.getElementById('replenishmentForm'); const form = document.getElementById('replenishmentForm');
const warehouseId = this.fromId; const warehouseId = this.fromId;
const machineId = form.querySelector('[name="machine_id"]')?.value; const machineId = form.querySelector('[name="machine_id"]')?.value;
if (!warehouseId || warehouseId === ' ') { if (!warehouseId || warehouseId === ' ') {
window.showToast?.('{{ __("Please select source warehouse") }}', 'error'); return; window.showToast?.('{{ __("Please select source warehouse") }}', 'error'); return;
} }
if (!machineId || machineId === ' ') { if (!machineId || machineId === ' ') {
window.showToast?.('{{ __("Please select target machine") }}', 'error'); return; window.showToast?.('{{ __("Please select target machine") }}', 'error'); return;
} }
if (this.items.length === 0) { if (this.items.length === 0) {
window.showToast?.('{{ __("Please add at least one item") }}', 'error'); return; window.showToast?.('{{ __("Please add at least one item") }}', 'error'); return;
} }
for (let i = 0; i < this.items.length; i++) { for (let i = 0; i < this.items.length; i++) {
if (!this.items[i].product_id) { if (!this.items[i].product_id) {
window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i+1), 'error'); return; window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i + 1), 'error'); return;
} }
if (!this.items[i].quantity || this.items[i].quantity < 1) { 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; window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i + 1), 'error'); return;
} }
} }
@ -309,8 +309,8 @@
this.autoInsufficient = data.insufficient || {}; this.autoInsufficient = data.insufficient || {};
this.autoWarehouseStocks = data.warehouse_stocks || {}; this.autoWarehouseStocks = data.warehouse_stocks || {};
this.autoPreviewLoaded = true; this.autoPreviewLoaded = true;
} else { } else {
window.showToast?.(data.message || '{{ __("Failed to load preview") }}', 'error'); window.showToast?.(data.message || '{{ __("Failed to load preview") }}', 'error');
} }
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); } } catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
finally { this.autoLoading = false; } finally { this.autoLoading = false; }
@ -319,7 +319,7 @@
getActualFillable(index) { getActualFillable(index) {
const slot = this.autoSlots[index]; const slot = this.autoSlots[index];
if (!slot) return 0; if (!slot) return 0;
let available = this.autoWarehouseStocks[slot.product_id] || 0; let available = this.autoWarehouseStocks[slot.product_id] || 0;
for (let i = 0; i < index; i++) { for (let i = 0; i < index; i++) {
if (this.autoSlots[i].product_id === slot.product_id) { if (this.autoSlots[i].product_id === slot.product_id) {
@ -337,33 +337,33 @@
if (!this.autoSlots || this.autoSlots.length === 0) { if (!this.autoSlots || this.autoSlots.length === 0) {
window.showToast?.('{{ __("No items to replenish") }}', 'error'); return; window.showToast?.('{{ __("No items to replenish") }}', 'error'); return;
} }
this.autoLoading = true; this.autoLoading = true;
const items = this.autoSlots.map(s => ({ const items = this.autoSlots.map(s => ({
slot_no: s.slot_no, slot_no: s.slot_no,
product_id: s.product_id, product_id: s.product_id,
quantity: s.quantity, quantity: s.quantity,
current_stock: s.current_stock, current_stock: s.current_stock,
max_stock: s.max_stock, max_stock: s.max_stock,
})); }));
try { try {
const res = await fetch('{{ route("admin.warehouses.replenishments.auto") }}', { const res = await fetch('{{ route("admin.warehouses.replenishments.auto") }}', {
method: 'POST', method: 'POST',
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({ body: JSON.stringify({
warehouse_id: this.autoWarehouseId, warehouse_id: this.autoWarehouseId,
machine_id: this.autoMachineId, machine_id: this.autoMachineId,
note: this.autoNote, note: this.autoNote,
items items
}) })
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
this.showAutoModal = false; this.showAutoModal = false;
window.location.reload(); window.location.reload();
} else { } else {
window.showToast?.(data.message || '{{ __("Creation failed") }}', 'error'); window.showToast?.(data.message || '{{ __("Creation failed") }}', 'error');
} }
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); } } catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
finally { this.autoLoading = false; } finally { this.autoLoading = false; }
@ -373,11 +373,11 @@
async advanceStatus(orderId, newStatus) { async advanceStatus(orderId, newStatus) {
this.statusTargetId = orderId; this.statusTargetId = orderId;
this.statusTargetValue = newStatus; this.statusTargetValue = newStatus;
const labels = { const labels = {
prepared: '{{ __("Confirm Prepare") }}', prepared: '{{ __("Confirm Prepare") }}',
delivering: '{{ __("Start Delivery") }}', delivering: '{{ __("Start Delivery") }}',
completed: '{{ __("Confirm Complete") }}' completed: '{{ __("Confirm Complete") }}'
}; };
const messages = { const messages = {
prepared: '{{ __("Are you sure you want to confirm preparation? This will deduct stock from the warehouse.") }}', prepared: '{{ __("Are you sure you want to confirm preparation? This will deduct stock from the warehouse.") }}',
@ -396,11 +396,11 @@
try { try {
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${this.statusTargetId}/status`, { const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${this.statusTargetId}/status`, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json', 'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}' 'X-CSRF-TOKEN': '{{ csrf_token() }}'
}, },
body: JSON.stringify({ status: this.statusTargetValue }) body: JSON.stringify({ status: this.statusTargetValue })
}); });
@ -411,11 +411,11 @@
const data = await res.json(); const data = await res.json();
window.showToast?.(data.message || '{{ __("Operation failed") }}', 'error'); window.showToast?.(data.message || '{{ __("Operation failed") }}', 'error');
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
window.showToast?.('{{ __("System Error") }}', 'error'); window.showToast?.('{{ __("System Error") }}', 'error');
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
@ -483,15 +483,16 @@
} }
</script> </script>
<div class="space-y-4 pb-10" x-data="replenishmentManager()" <div class="space-y-4 pb-10" x-data="replenishmentManager()" @open-details.window="openOrderDetails($event.detail.id)"
@open-details.window="openOrderDetails($event.detail.id)"
@ajax:navigate:replenishments.window.prevent="fetchTabData($event.detail.url)"> @ajax:navigate:replenishments.window.prevent="fetchTabData($event.detail.url)">
{{-- Header --}} {{-- Header --}}
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Replenishment') }}</h1> <h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Create and manage replenishment orders from warehouse to machines') }}</p> __('Machine Replenishment') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Create
and manage replenishment orders from warehouse to machines') }}</p>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button type="button" @click=" <button type="button" @click="
@ -510,11 +511,16 @@
}); });
}); });
" class="btn-luxury-ghost border-cyan-500/30 text-cyan-600 hover:bg-cyan-50 dark:hover:bg-cyan-500/10"> " class="btn-luxury-ghost border-cyan-500/30 text-cyan-600 hover:bg-cyan-50 dark:hover:bg-cyan-500/10">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
<span>{{ __('Auto Replenishment') }}</span> <span>{{ __('Auto Replenishment') }}</span>
</button> </button>
<button type="button" @click="openCreateModal()" class="btn-luxury-primary"> <button type="button" @click="openCreateModal()" class="btn-luxury-primary">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('New Replenishment') }}</span> <span>{{ __('New Replenishment') }}</span>
</button> </button>
</div> </div>
@ -522,29 +528,52 @@
{{-- Filters --}} {{-- Filters --}}
<div class="luxury-card rounded-3xl p-8 animate-luxury-in"> <div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8"> <form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET"
<div class="relative group flex-1 sm:flex-none sm:min-w-[240px]"> class="flex flex-wrap items-center gap-4 mb-8">
<div class="relative group flex-1 sm:flex-none sm:w-80">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"> <span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg> <svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span> </span>
<input type="text" name="search_order" value="{{ request('search_order') }}" @keydown.enter.prevent="fetchTabData()" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}"> <input type="text" name="search_order" value="{{ request('search_order') }}"
@keydown.enter.prevent="fetchTabData()" class="py-2.5 pl-12 pr-6 block w-full luxury-input"
placeholder="{{ __('Search order number...') }}">
</div> </div>
<div class="flex-1 sm:flex-none sm:min-w-[180px]"> <div class="flex-1 sm:flex-none sm:min-w-[180px]">
<x-searchable-select name="status" :selected="request('status')" :placeholder="__('All Statuses')"> <x-searchable-select name="status" :selected="request('status')" :placeholder="__('All Statuses')">
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option> <option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }} data-title="{{ __('Pending') }}">{{ __('Pending') }}</option> <option value="pending" {{ request('status')==='pending' ? 'selected' : '' }}
<option value="prepared" {{ request('status') === 'prepared' ? 'selected' : '' }} data-title="{{ __('Prepared') }}">{{ __('Prepared') }}</option> data-title="{{ __('Pending') }}">{{ __('Pending') }}</option>
<option value="delivering" {{ request('status') === 'delivering' ? 'selected' : '' }} data-title="{{ __('Delivering') }}">{{ __('Delivering') }}</option> <option value="prepared" {{ request('status')==='prepared' ? 'selected' : '' }}
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }} data-title="{{ __('Completed') }}">{{ __('Completed') }}</option> data-title="{{ __('Prepared') }}">{{ __('Prepared') }}</option>
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option> <option value="delivering" {{ request('status')==='delivering' ? 'selected' : '' }}
data-title="{{ __('Delivering') }}">{{ __('Delivering') }}</option>
<option value="completed" {{ request('status')==='completed' ? 'selected' : '' }}
data-title="{{ __('Completed') }}">{{ __('Completed') }}</option>
<option value="cancelled" {{ request('status')==='cancelled' ? 'selected' : '' }}
data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
</x-searchable-select> </x-searchable-select>
</div> </div>
<div class="flex items-center gap-2 flex-none ml-auto sm:ml-0"> <div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}"> <button type="button" @click="fetchTabData()"
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg> class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button> </button>
<button type="button" @click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}"> <button type="button"
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg> @click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button> </button>
</div> </div>
</form> </form>
@ -558,4 +587,4 @@
@include('admin.warehouses.partials.modal-replenishment-assign') @include('admin.warehouses.partials.modal-replenishment-assign')
@include('admin.warehouses.partials.modal-replenishment-status') @include('admin.warehouses.partials.modal-replenishment-status')
</div> </div>
@endsection @endsection

View File

@ -318,9 +318,9 @@
@ajax:navigate:transfers.window.prevent="fetchTabData($event.detail.url)"> @ajax:navigate:transfers.window.prevent="fetchTabData($event.detail.url)">
{{-- Header --}} {{-- Header --}}
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div> <div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Transfer Orders') }}</h1> <h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Transfer Orders') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage stock transfers between warehouses and machine returns') }}</p> <p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage stock transfers between warehouses and machine returns') }}</p>
</div> </div>
<button type="button" @click="openCreateModal()" class="btn-luxury-primary"> <button type="button" @click="openCreateModal()" class="btn-luxury-primary">
@ -350,10 +350,9 @@
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p> <p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div> </div>
<form id="transfer-filter-form" action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8"> <form id="transfer-filter-form" action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8">
<input type="hidden" name="per_page" value="{{ request('per_page', 15) }}">
<div class="relative group flex-1 md:max-w-[240px]"> <div class="relative group flex-1 sm:flex-none sm:w-80">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"> <span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" <svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
@ -363,27 +362,29 @@
</span> </span>
<input type="text" name="search_order" value="{{ request('search_order') }}" <input type="text" name="search_order" value="{{ request('search_order') }}"
@keydown.enter.prevent="$dispatch('ajax:filter')" @keydown.enter.prevent="$dispatch('ajax:filter')"
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}"> class="py-2.5 pl-12 pr-6 block w-full luxury-input text-sm font-bold" placeholder="{{ __('Search order number...') }}">
</div> </div>
<div class="flex-1 md:max-w-[220px]"> <div class="flex-1 sm:flex-none sm:min-w-[220px]">
<x-searchable-select <x-searchable-select
name="type" name="type"
:selected="request('type')" :selected="request('type')"
:placeholder="__('All Types')" :placeholder="__('All Types')"
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))" onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
class="luxury-select-sm"
> >
<option value="" data-title="{{ __('All Types') }}">{{ __('All Types') }}</option> <option value="" data-title="{{ __('All Types') }}">{{ __('All Types') }}</option>
<option value="warehouse_to_warehouse" {{ request('type') === 'warehouse_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Warehouse to Warehouse') }}">{{ __('Warehouse to Warehouse') }}</option> <option value="warehouse_to_warehouse" {{ request('type') === 'warehouse_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Warehouse to Warehouse') }}">{{ __('Warehouse to Warehouse') }}</option>
<option value="machine_to_warehouse" {{ request('type') === 'machine_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Machine to Warehouse') }}">{{ __('Machine to Warehouse') }}</option> <option value="machine_to_warehouse" {{ request('type') === 'machine_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Machine to Warehouse') }}">{{ __('Machine to Warehouse') }}</option>
</x-searchable-select> </x-searchable-select>
</div> </div>
<div class="flex-1 md:max-w-[180px]"> <div class="flex-1 sm:flex-none sm:min-w-[180px]">
<x-searchable-select <x-searchable-select
name="status" name="status"
:selected="request('status')" :selected="request('status')"
:placeholder="__('All Statuses')" :placeholder="__('All Statuses')"
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))" onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
class="luxury-select-sm"
> >
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option> <option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }} data-title="{{ __('Draft') }}">{{ __('Draft') }}</option> <option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }} data-title="{{ __('Draft') }}">{{ __('Draft') }}</option>
@ -393,7 +394,7 @@
</div> </div>
{{-- Date Range --}} {{-- Date Range --}}
<div class="flex-1 md:max-w-[320px]"> <div class="flex-1 sm:flex-none sm:min-w-[320px]">
<div class="relative group" <div class="relative group"
x-data="{ fp: null }" x-data="{ fp: null }"
x-init="fp = flatpickr($refs.dateRange, { x-init="fp = flatpickr($refs.dateRange, {
@ -422,29 +423,33 @@
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}"> <input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
<input type="text" x-ref="dateRange" <input type="text" x-ref="dateRange"
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}" value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer"> placeholder="{{ __('Select Date Range') }}" class="luxury-input luxury-input-sm py-2.5 pl-12 pr-6 block w-full cursor-pointer font-bold">
</div> </div>
</div> </div>
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}"> <div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
</svg> <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</button> </svg>
</button>
<button type="button" <button type="button"
@click=" @click="
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=per_page])').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=per_page])').forEach(i => i.value = '');
$el.closest('form').querySelectorAll('select').forEach(s => { $el.closest('form').querySelectorAll('select').forEach(s => {
s.value = ' '; s.value = ' ';
const instance = window.HSSelect.getInstance(s); const instance = window.HSSelect.getInstance(s);
if (instance) instance.setValue(' '); if (instance) instance.setValue(' ');
}); });
fetchTabData(); fetchTabData();
" "
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}"> class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
</button> </button>
</form> </form>

View File

@ -116,10 +116,21 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
// 6. 銷售管理 // 6. 銷售管理
Route::prefix('sales')->name('sales.')->group(function () { Route::prefix('sales')->name('sales.')->group(function () {
Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index'); 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::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('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders');
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions'); 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'); Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class, 'storeGifts'])->name('store-gifts');
}); });