[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:
sky121113 2026-06-05 14:20:35 +08:00
parent e414396b0c
commit 857198bbba
20 changed files with 820 additions and 242 deletions

View File

@ -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'));
}
/**

View File

@ -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'));
}
}

View File

@ -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",

View File

@ -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": "期限間近",

View File

@ -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": "警告",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -117,6 +117,7 @@
if (container) {
container.innerHTML = data.html;
this.$nextTick(() => {
if (window.Alpine) Alpine.initTree(container);
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
});
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 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>

View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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();