[FEAT] 新增遠端庫存列表視圖與機台管理租戶公司過濾功能
1. 在遠端管理「庫存與效期」頁面實作列表視圖 (Table View) 與手機版卡片視圖 (Mobile Card View),並支援點擊 Slot 行/卡片彈出編輯 Modal 進行庫存與效期修改。 2. 調整遠端庫存 Table 的 border、hover 背景色、divide 分隔線顏色與 Expiry 日期格式,使其與庫存概覽列表視圖完美對齊。 3. 在機台管理 (Machine Management) 與報修管理 (Maintenance Management) 等頁面新增僅系統管理員可見的 Company Filter 租戶過濾下拉選單。 4. 更新 zh_TW.json、en.json、ja.json 翻譯檔案與相關說明文件。
This commit is contained in:
parent
e414396b0c
commit
857198bbba
@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MaintenanceRecord;
|
||||
use App\Models\System\Company;
|
||||
use App\Traits\ImageHandler;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@ -21,6 +22,9 @@ class MaintenanceController extends Controller
|
||||
{
|
||||
$this->authorize('viewAny', MaintenanceRecord::class);
|
||||
|
||||
$currentUser = auth()->user();
|
||||
$companyId = trim((string) $request->input('company_id', ''));
|
||||
|
||||
$query = MaintenanceRecord::with(['machine', 'user', 'company'])
|
||||
->whereHas('machine') // 確保僅顯示該帳號「看得見」的機台紀錄,避開因權限隔離導致的 null 報錯
|
||||
->latest('maintenance_at');
|
||||
@ -38,9 +42,16 @@ class MaintenanceController extends Controller
|
||||
$query->where('category', $request->category);
|
||||
}
|
||||
|
||||
$records = $query->paginate(15)->withQueryString();
|
||||
if ($currentUser->isSystemAdmin() && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
|
||||
return view('admin.maintenance.index', compact('records'));
|
||||
$records = $query->paginate(15)->withQueryString();
|
||||
$companies = $currentUser->isSystemAdmin()
|
||||
? Company::select('id', 'name', 'code')->orderBy('name')->get()
|
||||
: collect();
|
||||
|
||||
return view('admin.maintenance.index', compact('records', 'companies'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -26,36 +26,54 @@ class WarehouseController extends Controller
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$currentUser = Auth::user();
|
||||
$companyId = trim((string) $request->input('company_id', ''));
|
||||
$search = $request->input('search');
|
||||
$type = $request->input('type');
|
||||
|
||||
$applyWarehouseFilters = function ($query) use ($currentUser, $companyId, $search, $type) {
|
||||
if ($currentUser->isSystemAdmin() && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($type) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
return $query;
|
||||
};
|
||||
|
||||
$query = Warehouse::query()
|
||||
->with(['company'])
|
||||
->withCount('stocks as products_count')
|
||||
->withSum('stocks as total_stock', 'quantity');
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
$applyWarehouseFilters($query);
|
||||
|
||||
$warehouses = $query->orderBy('type')->orderBy('name')
|
||||
->paginate($request->input('per_page', 10))
|
||||
->withQueryString();
|
||||
|
||||
$statsWarehouseQuery = fn() => $applyWarehouseFilters(Warehouse::query());
|
||||
|
||||
$stats = [
|
||||
'total' => Warehouse::count(),
|
||||
'main' => Warehouse::main()->count(),
|
||||
'branch' => Warehouse::branch()->count(),
|
||||
'total' => $statsWarehouseQuery()->count(),
|
||||
'main' => $statsWarehouseQuery()->main()->count(),
|
||||
'branch' => $statsWarehouseQuery()->branch()->count(),
|
||||
'low_stock' => WarehouseStock::lowStock()
|
||||
->whereHas('warehouse', fn($q) => $q)->count(),
|
||||
->whereHas('warehouse', fn($q) => $applyWarehouseFilters($q))
|
||||
->count(),
|
||||
];
|
||||
|
||||
$companies = collect();
|
||||
if (Auth::user()->isSystemAdmin()) {
|
||||
if ($currentUser->isSystemAdmin()) {
|
||||
$companies = \App\Models\System\Company::active()->orderBy('name')->get();
|
||||
}
|
||||
|
||||
@ -149,23 +167,39 @@ class WarehouseController extends Controller
|
||||
public function inventory(Request $request)
|
||||
{
|
||||
$tab = $request->input('tab', 'stock');
|
||||
$warehouses = Warehouse::active()->orderBy('name')->get();
|
||||
$currentUser = Auth::user();
|
||||
$companyId = trim((string) $request->input('company_id', ''));
|
||||
$applyCompanyFilter = fn($query) => $query->when(
|
||||
$currentUser->isSystemAdmin() && $companyId !== '',
|
||||
fn($q) => $q->where('company_id', $companyId)
|
||||
);
|
||||
|
||||
$warehouses = $applyCompanyFilter(Warehouse::active())
|
||||
->orderBy('name')
|
||||
->get();
|
||||
$companies = $currentUser->isSystemAdmin()
|
||||
? \App\Models\System\Company::active()->orderBy('name')->get()
|
||||
: collect();
|
||||
$isAjax = $request->ajax();
|
||||
|
||||
$data = compact('warehouses', 'tab');
|
||||
$data = compact('warehouses', 'companies', 'tab');
|
||||
|
||||
// 1. Current Stocks (stock)
|
||||
if (!$isAjax || $tab === 'stock') {
|
||||
$warehouse_ids = $request->input('warehouse_ids');
|
||||
$inventory_warehouses = Warehouse::active()
|
||||
$inventory_warehouses = $applyCompanyFilter(Warehouse::active())
|
||||
->when($warehouse_ids, fn($q) => $q->whereIn('id', (array)$warehouse_ids))
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'type']);
|
||||
->get(['id', 'name', 'type', 'company_id']);
|
||||
|
||||
$query = Product::with(['stocks' => function($q) {
|
||||
$q->select('id', 'product_id', 'warehouse_id', 'quantity', 'safety_stock');
|
||||
}, 'translations']);
|
||||
|
||||
if ($currentUser->isSystemAdmin() && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
|
||||
if ($warehouse_ids) {
|
||||
$query->whereHas('stocks', fn($q) => $q->whereIn('warehouse_id', (array)$warehouse_ids));
|
||||
}
|
||||
@ -185,6 +219,10 @@ class WarehouseController extends Controller
|
||||
->withCount('items')
|
||||
->latest();
|
||||
|
||||
if ($currentUser->isSystemAdmin() && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
@ -206,7 +244,10 @@ class WarehouseController extends Controller
|
||||
$data['stockInDefaultEnd'] = '';
|
||||
}
|
||||
$data['orders'] = $query->paginate($request->input('per_page', 10), ['*'], 'order_page')->withQueryString();
|
||||
$data['stock_in_products'] = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||
$data['stock_in_products'] = Product::with('translations')
|
||||
->when($currentUser->isSystemAdmin() && $companyId !== '', fn($q) => $q->where('company_id', $companyId))
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||
}
|
||||
|
||||
// 3. Movement History (movements)
|
||||
@ -215,6 +256,10 @@ class WarehouseController extends Controller
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderBy('id', 'desc');
|
||||
|
||||
if ($currentUser->isSystemAdmin() && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
|
||||
if ($request->filled('warehouse_id')) {
|
||||
$query->where('warehouse_id', $request->input('warehouse_id'));
|
||||
}
|
||||
@ -397,10 +442,17 @@ class WarehouseController extends Controller
|
||||
|
||||
public function transfers(Request $request)
|
||||
{
|
||||
$currentUser = Auth::user();
|
||||
$companyId = trim((string) $request->input('company_id', ''));
|
||||
$isSystemAdmin = $currentUser->isSystemAdmin();
|
||||
|
||||
$query = TransferOrder::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'fromMachine:id,name,serial_no', 'creator:id,name'])
|
||||
->withCount('items')
|
||||
->latest();
|
||||
|
||||
if ($isSystemAdmin && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->input('type'));
|
||||
}
|
||||
@ -431,14 +483,22 @@ class WarehouseController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']);
|
||||
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||
$products = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||
$companies = $isSystemAdmin
|
||||
? \App\Models\System\Company::active()->orderBy('name')->get()
|
||||
: collect();
|
||||
$applyCompanyFilter = fn($query) => $query->when(
|
||||
$isSystemAdmin && $companyId !== '',
|
||||
fn($q) => $q->where('company_id', $companyId)
|
||||
);
|
||||
|
||||
$warehouses = $applyCompanyFilter(Warehouse::active())->orderBy('name')->get(['id', 'name']);
|
||||
$machines = $applyCompanyFilter(Machine::query())->orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||
$products = $applyCompanyFilter(Product::with('translations'))->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||
|
||||
$defaultStart = '';
|
||||
$defaultEnd = '';
|
||||
|
||||
return view('admin.warehouses.transfers', compact('orders', 'warehouses', 'machines', 'products', 'defaultStart', 'defaultEnd'));
|
||||
return view('admin.warehouses.transfers', compact('orders', 'warehouses', 'machines', 'products', 'companies', 'defaultStart', 'defaultEnd'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -625,11 +685,19 @@ class WarehouseController extends Controller
|
||||
|
||||
public function machineInventory(Request $request)
|
||||
{
|
||||
$currentUser = Auth::user();
|
||||
$companyId = trim((string) $request->input('company_id', ''));
|
||||
$isSystemAdmin = $currentUser->isSystemAdmin();
|
||||
|
||||
$query = Machine::with(['slots' => fn($q) => $q->with('product:id,name,image_url')->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')])
|
||||
->withCount('slots as total_slots')
|
||||
->withSum('slots as total_stock', 'stock')
|
||||
->withSum('slots as total_capacity', 'max_stock');
|
||||
|
||||
if ($isSystemAdmin && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
@ -650,7 +718,11 @@ class WarehouseController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return view('admin.warehouses.machine-inventory', compact('machines'));
|
||||
$companies = $isSystemAdmin
|
||||
? \App\Models\System\Company::active()->orderBy('name')->get()
|
||||
: collect();
|
||||
|
||||
return view('admin.warehouses.machine-inventory', compact('machines', 'companies'));
|
||||
}
|
||||
|
||||
|
||||
@ -718,10 +790,17 @@ class WarehouseController extends Controller
|
||||
|
||||
public function replenishments(Request $request)
|
||||
{
|
||||
$currentUser = Auth::user();
|
||||
$companyId = trim((string) $request->input('company_id', ''));
|
||||
$isSystemAdmin = $currentUser->isSystemAdmin();
|
||||
|
||||
$query = ReplenishmentOrder::with(['warehouse:id,name', 'machine:id,name,serial_no', 'assignee:id,name', 'creator:id,name'])
|
||||
->withCount('items')
|
||||
->latest();
|
||||
|
||||
if ($isSystemAdmin && $companyId !== '') {
|
||||
$query->where('company_id', $companyId);
|
||||
}
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
@ -749,20 +828,30 @@ class WarehouseController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
$warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']);
|
||||
$machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||
$products = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||
$companies = $isSystemAdmin
|
||||
? \App\Models\System\Company::active()->orderBy('name')->get()
|
||||
: collect();
|
||||
$applyCompanyFilter = fn($query) => $query->when(
|
||||
$isSystemAdmin && $companyId !== '',
|
||||
fn($q) => $q->where('company_id', $companyId)
|
||||
);
|
||||
|
||||
$warehouses = $applyCompanyFilter(Warehouse::active())->orderBy('name')->get(['id', 'name']);
|
||||
$machines = $applyCompanyFilter(Machine::query())->orderBy('name')->get(['id', 'name', 'serial_no']);
|
||||
$products = $applyCompanyFilter(Product::with('translations'))->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||
|
||||
$userQuery = \App\Models\System\User::where('status', 1)->orderBy('name');
|
||||
if (!Auth::user()->isSystemAdmin()) {
|
||||
$userQuery->where('company_id', Auth::user()->company_id);
|
||||
if (!$isSystemAdmin) {
|
||||
$userQuery->where('company_id', $currentUser->company_id);
|
||||
} elseif ($companyId !== '') {
|
||||
$userQuery->where('company_id', $companyId);
|
||||
}
|
||||
$users = $userQuery->get(['id', 'name']);
|
||||
|
||||
$defaultStart = '';
|
||||
$defaultEnd = '';
|
||||
|
||||
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines', 'products', 'users', 'defaultStart', 'defaultEnd'));
|
||||
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines', 'products', 'users', 'companies', 'defaultStart', 'defaultEnd'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1479,4 +1568,3 @@ class WarehouseController extends Controller
|
||||
return view('admin.warehouses.print.replenishment', compact('order', 'items'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1064,6 +1064,7 @@
|
||||
"Machine to Warehouse": "Machine to Warehouse",
|
||||
"Machine to Warehouse Return": "Machine to Warehouse Return",
|
||||
"Machine to warehouse returns": "Machine to warehouse returns",
|
||||
"M2W": "M2W",
|
||||
"Machine updated successfully.": "Machine updated successfully.",
|
||||
"Machine-Specific Webhook": "Machine-Specific Webhook",
|
||||
"Machines": "Machines",
|
||||
@ -1261,6 +1262,7 @@
|
||||
"No pickup codes found": "No pickup codes found",
|
||||
"No product data matching search criteria": "No product data matching search criteria",
|
||||
"No product record found": "No product record found",
|
||||
"No products found": "No products found",
|
||||
"No products found in this warehouse": "No products found in this warehouse",
|
||||
"No products found matching your criteria.": "No products found matching your criteria.",
|
||||
"No records found": "No records found",
|
||||
@ -2227,6 +2229,7 @@
|
||||
"Warehouse to Warehouse": "Warehouse to Warehouse",
|
||||
"Warehouse to Warehouse Transfer": "Warehouse to Warehouse Transfer",
|
||||
"Warehouse to warehouse transfers": "Warehouse to warehouse transfers",
|
||||
"W2W": "W2W",
|
||||
"Warehouse updated successfully": "Warehouse updated successfully",
|
||||
"Warehouse updated successfully.": "Warehouse updated successfully.",
|
||||
"Warning": "Warning",
|
||||
|
||||
@ -1064,6 +1064,7 @@
|
||||
"Machine to Warehouse": "機器から倉庫",
|
||||
"Machine to Warehouse Return": "自販機から倉庫への返品",
|
||||
"Machine to warehouse returns": "機器から倉庫への返却",
|
||||
"M2W": "機器から倉庫",
|
||||
"Machine updated successfully.": "機器が正常に更新されました。",
|
||||
"Machine-Specific Webhook": "機器専用 Webhook",
|
||||
"Machines": "機器管理",
|
||||
@ -1261,6 +1262,7 @@
|
||||
"No pickup codes found": "受取コードが見つかりません",
|
||||
"No product data matching search criteria": "検索条件に一致する商品データはありません",
|
||||
"No product record found": "商品記録が見つかりません",
|
||||
"No products found": "商品が見つかりません",
|
||||
"No products found in this warehouse": "この倉庫に商品は見つかりません",
|
||||
"No products found matching your criteria.": "条件に一致する商品が見つかりません。",
|
||||
"No records found": "記録が見つかりません",
|
||||
@ -2227,6 +2229,7 @@
|
||||
"Warehouse to Warehouse": "倉庫から倉庫",
|
||||
"Warehouse to Warehouse Transfer": "倉庫間移動",
|
||||
"Warehouse to warehouse transfers": "倉庫から倉庫への在庫移動",
|
||||
"W2W": "倉庫間",
|
||||
"Warehouse updated successfully": "倉庫が正常に更新されました",
|
||||
"Warehouse updated successfully.": "倉庫が正常に更新されました。",
|
||||
"Warning": "期限間近",
|
||||
|
||||
@ -1064,6 +1064,7 @@
|
||||
"Machine to Warehouse": "機台對倉庫",
|
||||
"Machine to Warehouse Return": "機台退庫至倉庫",
|
||||
"Machine to warehouse returns": "機台退回倉庫",
|
||||
"M2W": "機對倉",
|
||||
"Machine updated successfully.": "機台更新成功。",
|
||||
"Machine-Specific Webhook": "機台專屬 Webhook",
|
||||
"Machines": "機台管理",
|
||||
@ -1261,6 +1262,7 @@
|
||||
"No pickup codes found": "找不到取貨碼",
|
||||
"No product data matching search criteria": "沒有符合搜尋條件的商品資料",
|
||||
"No product record found": "無商品紀錄",
|
||||
"No products found": "未找到商品",
|
||||
"No products found in this warehouse": "此倉庫目前無商品庫存",
|
||||
"No products found matching your criteria.": "找不到符合條件的商品。",
|
||||
"No records found": "未找到相關紀錄",
|
||||
@ -2227,6 +2229,7 @@
|
||||
"Warehouse to Warehouse": "倉庫對倉庫",
|
||||
"Warehouse to Warehouse Transfer": "倉庫至倉庫調撥",
|
||||
"Warehouse to warehouse transfers": "倉庫對倉庫調撥",
|
||||
"W2W": "倉對倉",
|
||||
"Warehouse updated successfully": "倉庫更新成功",
|
||||
"Warehouse updated successfully.": "倉庫已成功更新。",
|
||||
"Warning": "警告",
|
||||
|
||||
@ -467,7 +467,8 @@
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')" />
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }))" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@ -532,7 +533,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@foreach ($machines as $machine)
|
||||
@forelse ($machines as $machine)
|
||||
<tr
|
||||
class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
|
||||
<td class="px-6 py-6 cursor-pointer group"
|
||||
@ -697,14 +698,21 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@empty
|
||||
<x-empty-state mode="table" :colspan="7" :message="__('No machines found')">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M9.75 17 9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
</x-empty-state>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile/Tablet Card View -->
|
||||
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@foreach ($machines as $machine)
|
||||
@forelse ($machines as $machine)
|
||||
<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">
|
||||
<!-- Card Header -->
|
||||
@ -823,11 +831,27 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@empty
|
||||
<div class="md:col-span-2">
|
||||
<x-empty-state :message="__('No machines found')">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M9.75 17 9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
</x-empty-state>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
@if($machines->total() > 0)
|
||||
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -103,15 +103,7 @@
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: { message: e.message, type: 'error' } }));
|
||||
});
|
||||
}
|
||||
}" @ajax:navigate.window.prevent="fetchPage($event.detail.url)"
|
||||
@submit.prevent="if($event.target.method.toLowerCase() === 'get') {
|
||||
const url = new URL($event.target.action, window.location.origin);
|
||||
new FormData($event.target).forEach((value, key) => {
|
||||
if(value.trim()) url.searchParams.set(key, value);
|
||||
else url.searchParams.delete(key);
|
||||
});
|
||||
fetchPage(url.pathname + url.search);
|
||||
}">
|
||||
}" @ajax:navigate.window.prevent="fetchPage($event.detail.url)">
|
||||
<!-- 1. Header Area -->
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
@ -151,7 +143,15 @@
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex flex-col md:flex-row items-center justify-between mb-8 gap-4">
|
||||
<form method="GET" action="{{ route('admin.machines.permissions') }}"
|
||||
class="flex flex-wrap items-center gap-4 w-full md:w-auto">
|
||||
class="flex flex-wrap items-center gap-4 w-full md:w-auto"
|
||||
@submit.prevent="
|
||||
const url = new URL($el.action, window.location.origin);
|
||||
new FormData($el).forEach((value, key) => {
|
||||
if (value.trim()) url.searchParams.set(key, value);
|
||||
else url.searchParams.delete(key);
|
||||
});
|
||||
fetchPage(url.pathname + url.search);
|
||||
">
|
||||
|
||||
<div class="relative group flex-1 sm:flex-none">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
@ -170,7 +170,8 @@
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')" />
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }))" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@ -186,7 +187,7 @@
|
||||
$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);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
@ -279,12 +280,15 @@
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3 opacity-20">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" stroke-width="1.5" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="m17 8 5 5m0-5-5 5" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -354,17 +358,26 @@
|
||||
</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">
|
||||
<div class="opacity-20 flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75" />
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" stroke-width="1.5" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m17 8 5 5m0-5-5 5" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $users_list->links('vendor.pagination.luxury') }}
|
||||
@if($users_list->total() > 0)
|
||||
{{ $users_list->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -529,4 +542,4 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@ -24,6 +24,14 @@
|
||||
if (newContent) {
|
||||
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
const container = document.querySelector('#ajax-content-container');
|
||||
if (container && window.Alpine) {
|
||||
Alpine.initTree(container);
|
||||
}
|
||||
if (window.HSStaticMethods && typeof window.HSStaticMethods.autoInit === 'function') {
|
||||
setTimeout(() => window.HSStaticMethods.autoInit(), 100);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AJAX Load Failed:', e);
|
||||
@ -80,14 +88,21 @@
|
||||
<div id="ajax-content-container"
|
||||
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
|
||||
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('search=') || $event.target.closest('a').href.includes('category='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('search=') || $event.target.closest('a').href.includes('category=') || $event.target.closest('a').href.includes('company_id='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
|
||||
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Search -->
|
||||
<form method="GET" action="{{ route('admin.maintenance.index') }}" class="flex flex-wrap items-center gap-4"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
@submit.prevent="
|
||||
const url = new URL($el.action, window.location.origin);
|
||||
new FormData($el).forEach((value, key) => {
|
||||
if (value.trim()) url.searchParams.set(key, value);
|
||||
else url.searchParams.delete(key);
|
||||
});
|
||||
fetchPage(url.pathname + url.search);
|
||||
">
|
||||
|
||||
<div class="relative group flex-1 sm:flex-none">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
@ -106,7 +121,7 @@
|
||||
<!-- Category Filter (Custom Select UI) -->
|
||||
<div class="flex items-center gap-3 w-full sm:w-48">
|
||||
<x-searchable-select name="category" :placeholder="__('All Categories')" :selected="request('category')"
|
||||
onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: this.form.action + '?' + new URLSearchParams(new FormData(this.form)).toString() }, bubbles: true }))" :hasSearch="false" class="luxury-select-sm">
|
||||
x-on:change="$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }))" :hasSearch="false" class="luxury-select-sm">
|
||||
|
||||
@foreach(['Repair', 'Installation', 'Removal', 'Maintenance'] as $cat)
|
||||
<option value="{{ $cat }}" {{ request('category') == $cat ? 'selected' : '' }}>
|
||||
@ -116,6 +131,14 @@
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }))" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center gap-2 flex-none 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 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">
|
||||
@ -128,7 +151,7 @@
|
||||
$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);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }));
|
||||
@ -215,8 +238,14 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-20 text-center text-slate-500 dark:text-slate-400 font-bold tracking-widest uppercase italic">
|
||||
{{ __('No maintenance records found') }}
|
||||
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.1-3.1a6 6 0 0 1-7.9 7.9l-5.6 5.6a2.1 2.1 0 0 1-3-3l5.6-5.6a6 6 0 0 1 7.9-7.9l-3.1 3.1Z" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No maintenance records found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@ -304,18 +333,25 @@
|
||||
</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">
|
||||
<div class="opacity-20 flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2v16z" />
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.1-3.1a6 6 0 0 1-7.9 7.9l-5.6 5.6a2.1 2.1 0 0 1-3-3l5.6-5.6a6 6 0 0 1 7.9-7.9l-3.1 3.1Z" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No maintenance records found') }}</p>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No maintenance records found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $records->links('vendor.pagination.luxury') }}
|
||||
@if($records->total() > 0)
|
||||
{{ $records->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
selectedMachine: null,
|
||||
slots: [],
|
||||
viewMode: initialMachineId ? 'detail' : 'history',
|
||||
displayMode: 'table',
|
||||
history: @js($history),
|
||||
loading: false,
|
||||
updating: false,
|
||||
@ -251,17 +252,42 @@
|
||||
},
|
||||
|
||||
getSlotColorClass(slot) {
|
||||
if (!slot.expiry_date) return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50';
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
const expiryStr = slot.expiry_date;
|
||||
if (expiryStr < todayStr) {
|
||||
return 'bg-rose-50/60 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-500/30 shadow-sm shadow-rose-500/5';
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (slot.expiry_date && slot.expiry_date < today) {
|
||||
return 'bg-rose-50/60 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-500/30';
|
||||
}
|
||||
const diffDays = Math.round((new Date(expiryStr) - new Date(todayStr)) / 86400000);
|
||||
if (diffDays <= 7) {
|
||||
return 'bg-amber-50/60 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-500/30 shadow-sm shadow-amber-500/5';
|
||||
if (!slot.product || parseInt(slot.stock) === 0) {
|
||||
return 'bg-slate-50/50 dark:bg-slate-800/50 text-slate-400 border-slate-200/60 dark:border-slate-700/50';
|
||||
}
|
||||
return 'bg-emerald-50/60 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/30 shadow-sm shadow-emerald-500/5';
|
||||
if (slot.max_stock > 0 && parseInt(slot.stock) <= (parseInt(slot.max_stock) * 0.2)) {
|
||||
return 'bg-amber-50/60 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-500/30';
|
||||
}
|
||||
const maxStock = parseInt(slot.max_stock) || 10;
|
||||
if (parseInt(slot.stock) > maxStock / 2) {
|
||||
return 'bg-cyan-50/60 dark:bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-200 dark:border-cyan-500/30';
|
||||
}
|
||||
return 'bg-emerald-50/60 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/30';
|
||||
},
|
||||
|
||||
getSlotCardClass(slot) {
|
||||
const base = 'luxury-card p-6 rounded-[2rem] backdrop-blur-xl group hover:shadow-2xl transition-all duration-500 relative ';
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (slot.expiry_date && slot.expiry_date < today) return base + 'bg-rose-500/[0.06] border-rose-500/20 dark:bg-rose-500/10 dark:border-rose-500/30 hover:shadow-rose-500/10';
|
||||
if (!slot.product || parseInt(slot.stock) === 0) return base + 'bg-slate-500/[0.06] border-slate-500/20 dark:bg-slate-500/10 dark:border-slate-500/30 hover:shadow-slate-500/10';
|
||||
if (slot.max_stock > 0 && parseInt(slot.stock) <= (parseInt(slot.max_stock) * 0.2)) return base + 'bg-amber-500/[0.06] border-amber-500/20 dark:bg-amber-500/10 dark:border-amber-500/30 hover:shadow-amber-500/10';
|
||||
const maxStock = parseInt(slot.max_stock) || 10;
|
||||
if (parseInt(slot.stock) > maxStock / 2) return base + 'bg-cyan-500/[0.06] border-cyan-500/20 dark:bg-cyan-500/10 dark:border-cyan-500/30 hover:shadow-cyan-500/10';
|
||||
return base + 'bg-emerald-500/[0.04] border-emerald-500/10 dark:bg-emerald-500/[0.08] dark:border-emerald-500/25 hover:shadow-emerald-500/10';
|
||||
},
|
||||
|
||||
getSlotProgressClass(slot) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
if (slot.expiry_date && slot.expiry_date < today) return 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.4)]';
|
||||
if (!slot.product || parseInt(slot.stock) === 0) return 'bg-slate-400';
|
||||
if (slot.max_stock > 0 && parseInt(slot.stock) <= (parseInt(slot.max_stock) * 0.2)) return 'bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.4)]';
|
||||
const maxStock = parseInt(slot.max_stock) || 10;
|
||||
if (parseInt(slot.stock) > maxStock / 2) return 'bg-cyan-500 shadow-[0_0_8px_rgba(6,182,212,0.4)]';
|
||||
return 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.4)]';
|
||||
},
|
||||
|
||||
getCommandBadgeClass(status) {
|
||||
@ -546,8 +572,29 @@
|
||||
|
||||
<!-- Cabinet Visualization Grid -->
|
||||
<div class="space-y-6">
|
||||
<!-- Status Legend -->
|
||||
<div class="flex items-center justify-between px-4">
|
||||
<!-- Cabinet Control & Display Toggle -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 px-4">
|
||||
<div class="flex items-center gap-6">
|
||||
<button @click="displayMode = 'table'"
|
||||
:class="displayMode === 'table' ? 'text-cyan-500 bg-cyan-500/10' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-black text-sm uppercase tracking-widest">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
{{ __('Table View') }}
|
||||
</button>
|
||||
<button @click="displayMode = 'grid'"
|
||||
:class="displayMode === 'grid' ? 'text-cyan-500 bg-cyan-500/10' : 'text-slate-400 hover:text-slate-600'"
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all font-black text-sm uppercase tracking-widest">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
{{ __('Grid View') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="w-3.5 h-3.5 rounded-full bg-rose-500 shadow-lg shadow-rose-500/30"></span>
|
||||
@ -558,22 +605,131 @@
|
||||
<span
|
||||
class="w-3.5 h-3.5 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
|
||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{
|
||||
__('Warning') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span
|
||||
class="w-3.5 h-3.5 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
|
||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{
|
||||
__('Normal') }}</span>
|
||||
__('Low Stock') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="luxury-card rounded-[2.5rem] p-8 border 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-[500px]">
|
||||
<!-- Grid View Container -->
|
||||
<div x-show="displayMode === 'grid'" class="animate-luxury-in" x-cloak>
|
||||
<div
|
||||
class="luxury-card rounded-[2.5rem] p-8 border 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-[500px]">
|
||||
<!-- Loading Overlay -->
|
||||
<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">
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<div
|
||||
class="w-16 h-16 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.3em] ml-2 animate-pulse">{{
|
||||
__('Loading Cabinet...') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Decorative Grid -->
|
||||
<div class="absolute inset-0 opacity-[0.05] pointer-events-none"
|
||||
style="background-image: radial-gradient(#00d2ff 1.2px, transparent 1.2px); background-size: 40px 40px;">
|
||||
</div>
|
||||
|
||||
<!-- Slots Grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-6 relative z-10"
|
||||
x-show="!loading">
|
||||
<template x-for="slot in slots" :key="slot.id">
|
||||
<div @click="openEdit(slot)"
|
||||
:class="[getSlotColorClass(slot), slot.is_pending ? 'opacity-50 cursor-not-allowed grayscale-[0.5]' : 'cursor-pointer hover:scale-[1.08] hover:-translate-y-3 hover:shadow-2xl active:scale-[0.98]']"
|
||||
class="min-h-[300px] rounded-[2.5rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-500 group relative">
|
||||
|
||||
<!-- Pending Overlay -->
|
||||
<template x-if="slot.is_pending">
|
||||
<div
|
||||
class="absolute inset-0 z-30 flex items-center justify-center bg-white/10 dark:bg-slate-900/10 backdrop-blur-[1px] rounded-[2.5rem]">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div
|
||||
class="w-8 h-8 border-2 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
</div>
|
||||
<span
|
||||
class="text-[8px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{
|
||||
__('Syncing') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Slot Header (Pinned to top) -->
|
||||
<div class="absolute top-4 left-5 right-5 flex justify-between items-center z-20">
|
||||
<div
|
||||
class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
|
||||
<span
|
||||
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
|
||||
x-text="slot.slot_no"></span>
|
||||
</div>
|
||||
<template x-if="slot.max_stock > 0 && slot.stock <= (slot.max_stock * 0.2)">
|
||||
<div
|
||||
class="px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
||||
{{ __('Low') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Product Image -->
|
||||
<div class="relative w-20 h-20 mb-4 mt-1">
|
||||
<div
|
||||
class="absolute inset-0 rounded-[2rem] bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner group-hover:scale-105 transition-transform duration-500 overflow-hidden">
|
||||
<template x-if="slot.product && slot.product.image_url">
|
||||
<img :src="slot.product.image_url"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
||||
</template>
|
||||
<template x-if="!slot.product">
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<svg class="w-8 h-8 opacity-20" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2.5"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slot Info -->
|
||||
<div class="text-center w-full space-y-3">
|
||||
<template x-if="slot.product">
|
||||
<div class="text-base font-black truncate w-full opacity-90 tracking-tight"
|
||||
x-text="slot.product.name"></div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Stock Level -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-black tracking-tighter leading-none"
|
||||
x-text="slot.stock"></span>
|
||||
<span class="text-xs font-black opacity-30">/</span>
|
||||
<span class="text-sm font-bold opacity-50"
|
||||
x-text="slot.max_stock || 10"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiry Date -->
|
||||
<div class="flex flex-col items-center">
|
||||
<span
|
||||
class="text-base font-black tracking-tight leading-none opacity-80"
|
||||
x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table View Container -->
|
||||
<div x-show="displayMode === 'table'" class="animate-luxury-in relative min-h-[500px]" x-cloak>
|
||||
<!-- Loading Overlay -->
|
||||
<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 rounded-[2.5rem]">
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<div
|
||||
class="w-16 h-16 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
@ -584,102 +740,124 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Decorative Grid -->
|
||||
<div class="absolute inset-0 opacity-[0.05] pointer-events-none"
|
||||
style="background-image: radial-gradient(#00d2ff 1.2px, transparent 1.2px); background-size: 40px 40px;">
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden xl:block 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" x-show="!loading">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 dark:bg-slate-900/50">
|
||||
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Slot') }}</th>
|
||||
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Product') }}</th>
|
||||
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em]">{{ __('Barcode') }}</th>
|
||||
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em] text-center">{{ __('Stock Level') }} / {{ __('Max Stock') }}</th>
|
||||
<th class="px-8 py-5 text-sm font-black text-slate-400 uppercase tracking-[0.2em] text-right">{{ __('Expiry') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/40">
|
||||
<template x-for="slot in slots" :key="slot.id">
|
||||
<tr @click="openEdit(slot)"
|
||||
:class="slot.is_pending ? 'opacity-50 cursor-not-allowed grayscale-[0.5]' : 'cursor-pointer hover:bg-slate-50/50 dark:hover:bg-slate-800/30'"
|
||||
class="transition-colors group">
|
||||
<td class="px-8 py-6">
|
||||
<span class="px-3 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-800 text-sm font-black text-slate-600 dark:text-slate-300"
|
||||
x-text="slot.slot_no"></span>
|
||||
</td>
|
||||
<td class="px-8 py-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 flex items-center justify-center overflow-hidden">
|
||||
<template x-if="slot.product?.image_url">
|
||||
<img :src="slot.product.image_url" class="w-full h-full object-cover">
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-base font-black text-slate-800 dark:text-slate-200"
|
||||
x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></div>
|
||||
<div class="text-sm font-bold text-slate-400 uppercase tracking-widest mt-1"
|
||||
x-text="slot.expiry_date ? '{{ __('Expiry') }}: ' + slot.expiry_date : ''">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-6">
|
||||
<span class="text-sm font-mono font-bold text-slate-400"
|
||||
x-text="slot.product?.barcode || '--'"></span>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<span class="text-lg font-black"
|
||||
:class="(slot.max_stock > 0 && slot.stock <= (slot.max_stock * 0.2)) ? 'text-rose-500' : 'text-slate-700 dark:text-slate-200'"
|
||||
x-text="slot.stock"></span>
|
||||
<span class="text-xs font-bold text-slate-300">/</span>
|
||||
<span class="text-sm font-bold text-slate-400" x-text="slot.max_stock"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-right">
|
||||
<span class="text-sm font-mono font-bold text-slate-500"
|
||||
x-text="slot.expiry_date ? slot.expiry_date : '----/--/--'"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Slots Grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-6 relative z-10"
|
||||
x-show="!loading">
|
||||
<!-- Mobile Card View for Slots -->
|
||||
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6" x-show="!loading">
|
||||
<template x-for="slot in slots" :key="slot.id">
|
||||
<div @click="openEdit(slot)"
|
||||
:class="[getSlotColorClass(slot), slot.is_pending ? 'opacity-50 cursor-not-allowed grayscale-[0.5]' : 'cursor-pointer hover:scale-[1.08] hover:-translate-y-3 hover:shadow-2xl active:scale-[0.98]']"
|
||||
class="min-h-[300px] rounded-[2.5rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-500 group relative">
|
||||
|
||||
<!-- Pending Overlay -->
|
||||
<template x-if="slot.is_pending">
|
||||
<div
|
||||
class="absolute inset-0 z-30 flex items-center justify-center bg-white/10 dark:bg-slate-900/10 backdrop-blur-[1px] rounded-[2.5rem]">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div
|
||||
class="w-8 h-8 border-2 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
</div>
|
||||
<span
|
||||
class="text-[8px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{
|
||||
__('Syncing') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Slot Header (Pinned to top) -->
|
||||
<div class="absolute top-4 left-5 right-5 flex justify-between items-center z-20">
|
||||
<div
|
||||
class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
|
||||
<span
|
||||
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
|
||||
x-text="slot.slot_no"></span>
|
||||
</div>
|
||||
<template x-if="slot.max_stock > 0 && slot.stock <= (slot.max_stock * 0.2)">
|
||||
<div
|
||||
class="px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
||||
{{ __('Low') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Product Image -->
|
||||
<div class="relative w-20 h-20 mb-4 mt-1">
|
||||
<div
|
||||
class="absolute inset-0 rounded-[2rem] bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner group-hover:scale-105 transition-transform duration-500 overflow-hidden">
|
||||
<template x-if="slot.product && slot.product.image_url">
|
||||
<img :src="slot.product.image_url"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
||||
</template>
|
||||
<template x-if="!slot.product">
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<svg class="w-8 h-8 opacity-20" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2.5"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
:class="[getSlotCardClass(slot), slot.is_pending ? 'opacity-50 cursor-not-allowed grayscale-[0.5]' : 'cursor-pointer hover:scale-[1.02] active:scale-[0.98]']"
|
||||
class="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">
|
||||
<template x-if="slot.product?.image_url">
|
||||
<img :src="slot.product.image_url" class="w-full h-full object-cover">
|
||||
</template>
|
||||
<template x-if="!slot.product?.image_url">
|
||||
<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>
|
||||
</template>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800 text-[10px] font-black text-slate-500 dark:text-slate-400 tracking-tighter" x-text="slot.slot_no"></span>
|
||||
<h3 class="text-base font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 transition-colors tracking-tight" x-text="slot.product?.name || '{{ __('Empty Slot') }}'"></h3>
|
||||
</div>
|
||||
</template>
|
||||
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-1" x-text="slot.product?.barcode || '--'"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 text-right">
|
||||
<span class="text-xs font-black px-2.5 py-1.5 rounded-lg bg-slate-900/10 dark:bg-white/10" x-text="slot.slot_no"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slot Info -->
|
||||
<div class="text-center w-full space-y-3">
|
||||
<template x-if="slot.product">
|
||||
<div class="text-base font-black truncate w-full opacity-90 tracking-tight"
|
||||
x-text="slot.product.name"></div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Stock Level -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-black tracking-tighter leading-none"
|
||||
x-text="slot.stock"></span>
|
||||
<span class="text-xs font-black opacity-30">/</span>
|
||||
<span class="text-sm font-bold opacity-50"
|
||||
x-text="slot.max_stock || 10"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-2 border-t border-slate-100 dark:border-slate-800/50 pt-4">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Stock Level') }}</p>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-lg font-black" :class="(slot.max_stock > 0 && slot.stock <= (slot.max_stock * 0.2)) ? 'text-rose-500' : 'text-slate-700 dark:text-slate-200'" x-text="slot.stock"></span>
|
||||
<span class="text-[10px] font-bold text-slate-300">/</span>
|
||||
<span class="text-xs font-bold text-slate-400" x-text="slot.max_stock"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Expiry') }}</p>
|
||||
<p class="text-xs font-bold text-slate-500" x-text="slot.expiry_date || '--'"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiry Date -->
|
||||
<div class="flex flex-col items-center">
|
||||
<span
|
||||
class="text-base font-black tracking-tight leading-none opacity-80"
|
||||
x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'"></span>
|
||||
</div>
|
||||
<!-- Stock Progress Bar in card -->
|
||||
<div class="h-2 w-full bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden border border-slate-200/50 dark:border-slate-700/50 shadow-inner">
|
||||
<div :class="getSlotProgressClass(slot)"
|
||||
class="h-full transition-all duration-1000 ease-out rounded-full"
|
||||
:style="'width: ' + Math.min(100, Math.round((slot.stock / (slot.max_stock || 10)) * 100)) + '%'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -95,6 +95,15 @@
|
||||
const currentContent = document.querySelector('#ajax-content-container');
|
||||
if (newContent && currentContent) {
|
||||
currentContent.innerHTML = newContent.innerHTML;
|
||||
if (window.Alpine) {
|
||||
Alpine.initTree(currentContent);
|
||||
}
|
||||
}
|
||||
|
||||
const newStats = doc.querySelector('#warehouse-stats-container');
|
||||
const currentStats = document.querySelector('#warehouse-stats-container');
|
||||
if (newStats && currentStats) {
|
||||
currentStats.innerHTML = newStats.innerHTML;
|
||||
}
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
@ -148,7 +157,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Stats Cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div id="warehouse-stats-container" class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
|
||||
<p class="text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Total Warehouses') }}</p>
|
||||
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $stats['total'] }}</p>
|
||||
@ -174,7 +183,14 @@
|
||||
{{-- Filters --}}
|
||||
<div class="flex flex-wrap items-center justify-between mb-8 gap-4">
|
||||
<form action="{{ route('admin.warehouses.index') }}" method="GET" class="flex flex-wrap items-center gap-4"
|
||||
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
|
||||
@submit.prevent="
|
||||
const url = new URL($el.action, window.location.origin);
|
||||
new FormData($el).forEach((value, key) => {
|
||||
if (value.trim()) url.searchParams.set(key, value);
|
||||
else url.searchParams.delete(key);
|
||||
});
|
||||
fetchPage(url.pathname + url.search);
|
||||
">
|
||||
<div class="flex items-center gap-3 flex-1 sm:flex-none">
|
||||
<div class="relative group flex-1 sm:flex-none">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
@ -188,8 +204,17 @@
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
class="py-2.5 pl-12 pr-6 block w-full sm:w-64 luxury-input text-sm font-bold" placeholder="{{ __('Search warehouses...') }}">
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
<input type="hidden" name="type" value="{{ request('type') }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="$el.closest('form').dispatchEvent(new Event('submit', { cancelable: true }))" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center gap-2 flex-none">
|
||||
<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">
|
||||
@ -200,6 +225,12 @@
|
||||
<button type="button"
|
||||
@click="
|
||||
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('input[type=hidden][name=type]').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 Event('submit', { cancelable: true }));
|
||||
"
|
||||
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') }}">
|
||||
@ -213,16 +244,16 @@
|
||||
|
||||
<div
|
||||
class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50">
|
||||
<a href="{{ route('admin.warehouses.index') }}" @click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
<a href="{{ route('admin.warehouses.index', array_filter(['search' => request('search'), 'company_id' => request('company_id'), 'per_page' => request('per_page')])) }}" @click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('type') && !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300' }}">
|
||||
{{ __('All') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.warehouses.index', ['type' => 'main']) }}"
|
||||
<a href="{{ route('admin.warehouses.index', array_filter(['type' => 'main', 'search' => request('search'), 'company_id' => request('company_id'), 'per_page' => request('per_page')])) }}"
|
||||
@click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('type') === 'main' ? 'bg-white dark:bg-cyan-500/10 text-cyan-500 shadow-lg shadow-cyan-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-cyan-500/80' }}">
|
||||
{{ __('Main') }}
|
||||
</a>
|
||||
<a href="{{ route('admin.warehouses.index', ['type' => 'branch']) }}"
|
||||
<a href="{{ route('admin.warehouses.index', array_filter(['type' => 'branch', 'search' => request('search'), 'company_id' => request('company_id'), 'per_page' => request('per_page')])) }}"
|
||||
@click.prevent="fetchPage($el.getAttribute('href'))"
|
||||
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('type') === 'branch' ? 'bg-white dark:bg-indigo-500/10 text-indigo-500 shadow-lg shadow-indigo-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-indigo-500/80' }}">
|
||||
{{ __('Branch') }}
|
||||
@ -396,15 +427,12 @@
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 mb-4">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold">{{ __('No warehouses found') }}</p>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No warehouses found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -545,11 +573,11 @@
|
||||
</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">
|
||||
<div class="opacity-20 flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819" />
|
||||
</svg>
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No warehouses found') }}</p>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No warehouses found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
@ -821,4 +849,4 @@
|
||||
__('Loading Data') }}...</p>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@ -117,6 +117,7 @@
|
||||
if (container) {
|
||||
container.innerHTML = data.html;
|
||||
this.$nextTick(() => {
|
||||
if (window.Alpine) Alpine.initTree(container);
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
}
|
||||
@ -759,4 +760,4 @@
|
||||
:confirm-text="__('Delete Permanently')" />
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@ -45,6 +45,18 @@
|
||||
placeholder="{{ __('Search machines...') }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="$el.closest('form').dispatchEvent(new Event('submit'))"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center gap-2 flex-none 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 transition-all active:scale-95"
|
||||
@ -58,6 +70,11 @@
|
||||
|
||||
<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 instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new Event('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 transition-all active:scale-95"
|
||||
@ -1004,6 +1021,7 @@ document.addEventListener('alpine:init', () => {
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (window.Alpine) Alpine.initTree(this.$refs.listContent);
|
||||
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||
window.HSStaticMethods.autoInit();
|
||||
}
|
||||
|
||||
@ -91,7 +91,17 @@
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<x-empty-state mode="table" :colspan="6" :message="__('No machines found')" />
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M9.75 17 9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No machines found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@ -163,13 +173,26 @@
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<x-empty-state :message="__('No machines found')" />
|
||||
<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">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M9.75 17 9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No machines found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $machines->links('vendor.pagination.luxury') }}
|
||||
@if($machines->total() > 0)
|
||||
{{ $machines->links('vendor.pagination.luxury') }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,26 @@
|
||||
@submit.prevent="handleFilterSubmit('movements')">
|
||||
<input type="hidden" name="tab" value="movements">
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="
|
||||
const warehouseSelect = $el.closest('form').querySelector('select[name=warehouse_id]');
|
||||
if (warehouseSelect) {
|
||||
warehouseSelect.value = ' ';
|
||||
const warehouseInstance = window.HSSelect?.getInstance(warehouseSelect);
|
||||
if (warehouseInstance) warehouseInstance.setValue(' ');
|
||||
}
|
||||
handleFilterSubmit('movements');
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 倉庫篩選 --}}
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
@ -88,7 +108,7 @@
|
||||
});
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
|
||||
@ -167,12 +187,10 @@
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<div class="p-4 rounded-3xl bg-slate-50 dark:bg-slate-800/50">
|
||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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-400 font-bold uppercase tracking-widest text-xs">{{ __('No movement records found') }}</p>
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No movement records found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -229,12 +247,24 @@
|
||||
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<x-empty-state :message="__('No movement records found')" />
|
||||
<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">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No movement records found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $movements->links('vendor.pagination.luxury') }}
|
||||
@if($movements->total() > 0)
|
||||
{{ $movements->links('vendor.pagination.luxury') }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -80,6 +80,18 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="handleFilterSubmit('stock-in')"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 搜尋 + 重置按鈕 --}}
|
||||
<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 transition-all active:scale-95" title="{{ __('Search') }}">
|
||||
@ -98,7 +110,7 @@
|
||||
});
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
handleFilterSubmit('stock-in');
|
||||
@ -234,12 +246,10 @@
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<div class="p-4 rounded-3xl bg-slate-50 dark:bg-slate-800/50">
|
||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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-400 font-bold uppercase tracking-widest text-xs">{{ __('No stock-in orders found') }}</p>
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No stock-in orders found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -318,13 +328,25 @@
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<x-empty-state :message="__('No stock-in orders found')" />
|
||||
<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">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No stock-in orders found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<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']) }}
|
||||
@if($orders->total() > 0)
|
||||
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:stock-in']) }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,6 +22,22 @@
|
||||
:placeholder="__('Filter by Warehouse Presence')" />
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
x-on:change="
|
||||
$el.closest('form').querySelectorAll('input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
|
||||
handleFilterSubmit('stock');
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<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 transition-all active:scale-95"
|
||||
@ -36,7 +52,7 @@
|
||||
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
|
||||
@ -144,17 +160,13 @@
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($inventory_warehouses) + 2 }}" class="px-6 py-20 text-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">
|
||||
<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 class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path 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"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No products found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -253,12 +265,26 @@
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<x-empty-state :message="__('No products found')" />
|
||||
<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">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path 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"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('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>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
@if($products->total() > 0)
|
||||
{{ $products->links('vendor.pagination.luxury') }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -115,7 +115,16 @@
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="8" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No replenishment orders found') }}</td></tr>
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No replenishment orders found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@ -195,12 +204,24 @@
|
||||
</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 replenishment orders found') }}</p>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No replenishment orders found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:replenishments']) }}
|
||||
@if($orders->total() > 0)
|
||||
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:replenishments']) }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,7 +88,16 @@
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold uppercase tracking-widest">{{ __('No transfer orders found') }}</td></tr>
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No transfer orders found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@ -173,12 +182,24 @@
|
||||
</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 transfer orders found') }}</p>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<svg class="w-16 h-16 text-slate-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<p class="text-slate-400 dark:text-slate-500 font-extrabold tracking-widest uppercase text-xs">{{ __('No transfer orders found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:transfers']) }}
|
||||
@if($orders->total() > 0)
|
||||
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:transfers']) }}
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-4 text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
<span>{{ __('No records found') }}</span>
|
||||
<span>0 / 0</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -517,6 +517,8 @@
|
||||
if (data.success && container) {
|
||||
container.outerHTML = data.html;
|
||||
this.$nextTick(() => {
|
||||
const newContainer = document.getElementById('replenishments-table-container');
|
||||
if (newContainer && window.Alpine) Alpine.initTree(newContainer);
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
window.history.pushState({}, '', targetUrl);
|
||||
@ -533,6 +535,7 @@
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 pb-10" x-data="replenishmentManager()" @open-details.window="openOrderDetails($event.detail.id)"
|
||||
@ajax:filter.window="fetchTabData()"
|
||||
@ajax:navigate:replenishments.window.prevent="fetchTabData($event.detail.url)">
|
||||
|
||||
{{-- Header --}}
|
||||
@ -625,6 +628,18 @@
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 開始時間 --}}
|
||||
<div class="relative group w-full sm:w-56 sm:flex-none"
|
||||
x-data="{ fp: null }"
|
||||
@ -688,7 +703,7 @@
|
||||
});
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
fetchTabData();
|
||||
@ -716,4 +731,4 @@
|
||||
@include('admin.warehouses.partials.modal-replenishment-assign')
|
||||
@include('admin.warehouses.partials.modal-replenishment-status')
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
|
||||
@ -109,6 +109,8 @@
|
||||
container.outerHTML = data.html;
|
||||
// Re-init HSSelect or other Preline components if needed
|
||||
this.$nextTick(() => {
|
||||
const newContainer = document.getElementById('transfers-table-container');
|
||||
if (newContainer && window.Alpine) Alpine.initTree(newContainer);
|
||||
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
||||
});
|
||||
|
||||
@ -392,6 +394,18 @@
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="w-full sm:w-72">
|
||||
<x-searchable-select
|
||||
name="company_id"
|
||||
:options="$companies"
|
||||
:selected="request('company_id')"
|
||||
:placeholder="__('All Companies')"
|
||||
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 開始時間 --}}
|
||||
<div class="relative group w-full sm:w-56 sm:flex-none"
|
||||
x-data="{ fp: null }"
|
||||
@ -455,7 +469,7 @@
|
||||
});
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
const instance = window.HSSelect?.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
fetchTabData();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user