[STYLE] 優化維修紀錄與倉儲管理模組 RWD 響應式佈局

1. 維修紀錄 (Maintenance Records):優化標題區域與按鈕在行動裝置上的顯示,新增「系統 (System)」維修類型支援。
2. 倉儲管理 (Warehouse Management):全面優化總覽、庫存、調撥與補貨介面,確保在手機版自動切換為優化的卡片或折疊佈局。
3. 機台庫存 (Machine Inventory):修正表格寬度與字體大小,提升小螢幕下的閱讀性。
4. 補貨流程 (Replenishment Workflow):優化一鍵補貨 Modal 與詳情面板的 RWD 表現。
5. 全站佈局:調整 admin.blade.php 與主容器間距,修復部分區域在窄螢幕下的破圖問題。
This commit is contained in:
sky121113 2026-04-27 14:24:37 +08:00
parent de538a0162
commit 9d7fdc9c7f
18 changed files with 1481 additions and 630 deletions

View File

@ -594,7 +594,7 @@ class WarehouseController extends Controller
public function machineSlots(Machine $machine)
{
$slots = $machine->slots()
->with(['product:id,name,image_url,barcode'])
->with(['product:id,name,image_url,barcode,price'])
->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')
->get();
@ -703,16 +703,9 @@ class WarehouseController extends Controller
ReplenishmentOrderItem::insert($itemsData);
});
if ($request->ajax()) {
session()->flash('success', __('Replenishment order created successfully'));
return response()->json([
'success' => true,
'message' => __('Replenishment order created successfully')
]);
}
session()->flash('success', __('Replenishment order created successfully'));
return redirect()->route('admin.warehouses.replenishments')
->with('success', __('Replenishment order created successfully'));
return redirect()->route('admin.warehouses.replenishments');
}
/**
@ -865,14 +858,18 @@ class WarehouseController extends Controller
// pending → prepared扣減倉庫庫存
if ($newStatus === ReplenishmentOrder::STATUS_PREPARED) {
foreach ($replenishmentOrder->items as $item) {
$stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id)
->where('product_id', $item->product_id)
->lockForUpdate()
->first();
$stock = WarehouseStock::firstOrCreate(
[
'warehouse_id' => $replenishmentOrder->warehouse_id,
'product_id' => $item->product_id,
],
[
'company_id' => $replenishmentOrder->company_id,
'quantity' => 0,
]
);
if (!$stock || $stock->quantity < $item->quantity) {
throw new \Exception(__('Warehouse stock insufficient') . " ({$item->product?->name})");
}
$stock->lockForUpdate();
$before = $stock->quantity;
$stock->decrement('quantity', $item->quantity);
@ -914,6 +911,14 @@ class WarehouseController extends Controller
'completed' => __('Confirm Complete'),
];
$statusMessages = [
'prepared' => __('Order prepared successfully, stock deducted'),
'delivering' => __('Order is now in delivery'),
'completed' => __('Order completed and stock updated'),
];
session()->flash('success', $statusMessages[$newStatus] ?? __('Status updated successfully'));
return response()->json([
'success' => true,
'message' => __('Status updated successfully')
@ -938,29 +943,35 @@ class WarehouseController extends Controller
$replenishmentOrder->loadMissing('items');
foreach ($replenishmentOrder->items as $item) {
$stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id)
->where('product_id', $item->product_id)
->lockForUpdate()
->first();
if ($stock) {
$before = $stock->quantity;
$stock->increment('quantity', $item->quantity);
StockMovement::create([
'company_id' => $replenishmentOrder->company_id,
$stock = WarehouseStock::firstOrCreate(
[
'warehouse_id' => $replenishmentOrder->warehouse_id,
'product_id' => $item->product_id,
'type' => 'in',
'quantity' => $item->quantity,
'before_qty' => $before,
'after_qty' => $before + $item->quantity,
'reference_type' => ReplenishmentOrder::class,
'reference_id' => $replenishmentOrder->id,
'note' => __('Replenishment cancelled, stock returned') . ' #' . $replenishmentOrder->order_no,
'created_by' => Auth::id(),
]);
}
],
[
'company_id' => $replenishmentOrder->company_id,
'quantity' => 0,
]
);
$stock->lockForUpdate();
$before = $stock->quantity;
$stock->increment('quantity', $item->quantity);
StockMovement::create([
'company_id' => $replenishmentOrder->company_id,
'warehouse_id' => $replenishmentOrder->warehouse_id,
'product_id' => $item->product_id,
'type' => 'in',
'quantity' => $item->quantity,
'before_qty' => $before,
'after_qty' => $before + $item->quantity,
'reference_type' => ReplenishmentOrder::class,
'reference_id' => $replenishmentOrder->id,
'note' => __('Replenishment cancelled, stock returned') . ' #' . $replenishmentOrder->order_no,
'created_by' => Auth::id(),
]);
}
}
@ -969,10 +980,16 @@ class WarehouseController extends Controller
]);
});
$msg = __('Order has been cancelled');
if (in_array($replenishmentOrder->getOriginal('status'), [ReplenishmentOrder::STATUS_PREPARED, ReplenishmentOrder::STATUS_DELIVERING])) {
$msg .= ' — ' . __('Stock returned to warehouse');
}
session()->flash('success', $msg);
return response()->json([
'success' => true,
'message' => __('Order has been cancelled'),
'stock_returned' => in_array($replenishmentOrder->getOriginal('status'), [ReplenishmentOrder::STATUS_PREPARED, ReplenishmentOrder::STATUS_DELIVERING]),
'message' => $msg
]);
}
@ -999,10 +1016,11 @@ class WarehouseController extends Controller
$replenishmentOrder->update(['assigned_to' => $user->id]);
session()->flash('success', __('Personnel assigned successfully'));
return response()->json([
'success' => true,
'message' => __('Personnel assigned successfully'),
'assignee_name' => $user->name,
'message' => __('Personnel assigned successfully')
]);
}
@ -1101,16 +1119,23 @@ class WarehouseController extends Controller
if ($warehouseId) {
// Warehouse uses TenantScoped, findOrFail will respect it
$warehouse = Warehouse::findOrFail($warehouseId);
$stocks = WarehouseStock::with(['product' => fn($q) => $q->select('id', 'name', 'image_url', 'name_dictionary_key')->with('translations')])
->where('warehouse_id', $warehouse->id)
->where('quantity', '>', 0)
// 獲取公司所有啟動中的商品
$products = Product::where('company_id', $warehouse->company_id)
->active()
->select('id', 'name', 'image_url', 'name_dictionary_key')
->with('translations')
->get();
// 獲取目前的庫存量(用於顯示,不限制選擇)
$stocks = WarehouseStock::where('warehouse_id', $warehouse->id)
->pluck('quantity', 'product_id');
$data = $stocks->map(fn($s) => [
'id' => $s->product_id,
'name' => $s->product?->localized_name ?? 'Unknown',
'quantity' => $s->quantity,
'image' => $s->product?->image_url
$data = $products->map(fn($p) => [
'id' => $p->id,
'name' => $p->localized_name ?? 'Unknown',
'quantity' => $stocks[$p->id] ?? 0,
'image' => $p->image_url
]);
} elseif ($machineId) {
// Machine uses TenantScoped and machine_access scope

View File

@ -46,6 +46,7 @@
"Confirm Transfer Order": "転送伝票を確認",
"Create and manage replenishment orders from warehouse to machines": "倉庫から機台への補充伝票の作成と管理",
"Create Replenishment Order": "補充伝票を作成",
"Create stock replenishment for specific machines": "指定マシンの在庫補充伝票を作成",
"CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "倉庫間の転送および機台からの返品伝票を作成",
"Create stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品伝票を作成",
"Create Transfer Order": "転送伝票を作成",
@ -88,9 +89,11 @@
"Loading...": "読み込み中...",
"Low Stock": "在庫不足",
"Low Stock Alerts": "低在庫アラート",
"Machine": "マシン",
"Machine Distribution": "機器分布",
"Machine Distribution Map": "機器分布マップ",
"Machine Inventory Overview": "機台在庫概要",
"Machine Stock": "マシン在庫",
"Machine Replenishment": "機台補充",
"Machine Return": "機台返品",
"Machine to Warehouse": "機台から倉庫",
@ -131,6 +134,7 @@
"Please enter configuration name": "設定名を入力してください",
"Please select a company": "会社を選択してください",
"Please select warehouse and machine": "倉庫とマシンを選択してください",
"Please Select Slot first": "先にスロットを選択してください",
"Points": "ポイント",
"Prepared": "準備済み",
"Preparing": "準備中",
@ -147,6 +151,7 @@
"Quick Replenish": "クイック補充",
"Real-time slot-level inventory across all machines": "すべての機台におけるリアルタイムの貨道別在庫",
"Replenishment cancelled, stock returned": "補充キャンセル、在庫返却",
"Replenishment Details": "機台補充伝票詳細",
"Replenishment Items": "補充項目",
"Replenishment order confirmed and inventory updated": "補充伝票が確定され、在庫が更新されました",
"Replenishment order created successfully": "補充伝票が正常に作成されました",
@ -242,5 +247,10 @@
"Welcome Gift Feature": "来店特典機能",
"Stock / Capacity": "在庫 / 上限",
"Warehouse Stock": "倉庫在庫",
"Recalculate": "再計算"
"Recalculate": "再計算",
"Work Content": "作業内容",
"No content provided": "内容は提供されていません",
"Maintenance Photos": "メンテナンス写真",
"Execution Time": "実行時間",
"Product Name": "商品名"
}

View File

@ -1,4 +1,6 @@
{
"Start Time": "開始時間",
"End Time": "結束時間",
"15 Seconds": "15 秒",
"30 Seconds": "30 秒",
"60 Seconds": "60 秒",
@ -258,6 +260,9 @@
"Confirm this transfer?": "確定要確認此調撥作業嗎?",
"Confirm Transfer": "確認調撥",
"Confirm Transfer Order": "確認調撥單",
"Are you sure you want to confirm preparation? This will deduct stock from the warehouse.": "確定要確認備貨嗎?此操作將會扣除倉庫庫存。",
"Are you sure you want to start delivery?": "確定要開始配送嗎?",
"Are you sure you want to confirm completion? This will update the machine stock.": "確定要確認完成嗎?此操作將會更新機台貨道庫存。",
"Connected": "通訊中",
"Connecting...": "連線中",
"Connection lost (LWT)": "連線中斷",
@ -292,6 +297,7 @@
"Create Product": "建立商品",
"Create Replenishment Order": "建立補貨單",
"Create replenishment orders and manage restocking from warehouse to machines": "建立補貨單,管理從倉庫到機台的補貨流程",
"Create stock replenishment for specific machines": "為指定機台建立庫存補貨單",
"Create Role": "建立角色",
"CREATE STOCK TRANSFERS BETWEEN WAREHOUSES AND MACHINE RETURNS": "建立倉庫間調撥或機台退庫單",
"Create stock transfers between warehouses and machine returns": "建立倉庫間調撥或機台退庫單",
@ -624,6 +630,7 @@
"Low stock and out-of-stock alerts": "低庫存與缺貨警示",
"Loyalty & Features": "行銷與點數",
"Machine Advertisement Settings": "機台廣告設置",
"Machine": "機台",
"Machine Count": "機台數量",
"Machine created successfully.": "機台已成功建立。",
"Machine Details": "機台詳情",
@ -634,6 +641,7 @@
"Machine Info": "機台資訊",
"Machine Information": "機台資訊",
"Machine Inventory": "機台庫存",
"Machine Stock": "機台庫存",
"Machine Inventory Overview": "機台庫存概覽",
"Machine is heartbeat normal": "機台通訊正常",
"Machine List": "機台列表",
@ -887,10 +895,13 @@
"Optional remarks...": "選填備註...",
"Order already completed": "訂單已完成",
"Order already processed": "訂單已處理",
"Order completed and stock updated": "訂單已完成,機台庫存已更新",
"Order Details": "進貨單詳情",
"Order has been cancelled": "訂單已取消",
"Order Info": "單據資訊",
"Order is now in delivery": "訂單配送中",
"Order Management": "訂單管理",
"Order prepared successfully, stock deducted": "訂單已成功備貨,庫存已扣除",
"Order No.": "單號",
"Orders": "購買單",
"Original": "原始",
@ -977,6 +988,7 @@
"Please select a material": "請選擇素材",
"Please select a slot": "請選擇貨道",
"Please select warehouse and machine": "請選擇倉庫和機台",
"Please Select Slot first": "請先選擇貨道",
"PNG, JPG up to 2MB": "支援 PNG, JPG (最大 2MB)",
"PNG, JPG, WEBP up to 10MB": "支援 PNG, JPG, WEBP 格式,且不超過 10MB",
"Point Rules": "點數規則",
@ -1071,6 +1083,7 @@
"Repair": "維修",
"Replenishment": "補貨",
"Replenishment Audit": "補貨單",
"Replenishment Details": "補貨單詳情",
"Replenishment cancelled, stock returned": "補貨取消,庫存退回",
"Replenishment completed": "補貨完成",
"Replenishment history": "補貨歷史紀錄",
@ -1508,5 +1521,7 @@
"待填寫": "待填寫",
"Stock / Capacity": "庫存/上限",
"Warehouse Stock": "倉庫數量",
"Recalculate": "重新計算"
"Recalculate": "重新計算",
"Work Content": "作業內容",
"Product Name": "商品名稱"
}

View File

@ -172,7 +172,7 @@
@layer components {
.luxury-nav-item {
@apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-semibold rounded-xl transition-all duration-200;
@apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-semibold rounded-xl transition-all duration-200 overflow-hidden;
@apply text-slate-600 hover:text-slate-900 hover:bg-slate-100;
@apply dark:text-slate-300 dark:hover:text-white dark:hover:bg-white/5;
}

View File

@ -8,7 +8,8 @@
<div class="luxury-card rounded-2xl p-8 animate-luxury-in flex flex-col">
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Connectivity Status') }}</h3>
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
__('Connectivity Status') }}</h3>
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time status monitoring') }}</p>
</div>
<div
@ -22,39 +23,43 @@
</div>
</div>
<div class="flex-1 flex items-center">
<div class="flex-1 flex flex-col sm:flex-row items-center gap-y-8 sm:gap-y-0">
<!-- Left: Stats List -->
<div class="flex-1 space-y-6">
<div class="flex items-center justify-between pr-10">
<div class="w-full sm:flex-1 space-y-6">
<div class="flex items-center justify-between sm:pr-10">
<div class="flex items-center gap-x-4">
<div class="w-2 h-2 rounded-full bg-cyan-500 shadow-[0_0_10px_rgba(6,182,212,0.6)]"></div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Online Machines') }}</span>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Online Machines')
}}</span>
</div>
<span class="text-2xl font-black text-slate-900 dark:text-white">{{ $activeMachines }}</span>
</div>
<div class="flex items-center justify-between pr-10">
<div class="flex items-center justify-between sm:pr-10">
<div class="flex items-center gap-x-4">
<div class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.6)]"></div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Offline Machines') }}</span>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Offline Machines')
}}</span>
</div>
<span class="text-2xl font-black text-rose-500">{{ $offlineMachines }}</span>
</div>
<div class="flex items-center justify-between pr-10">
<div class="flex items-center justify-between sm:pr-10">
<div class="flex items-center gap-x-4">
<div class="w-2 h-2 rounded-full bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.6)]"></div>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Alerts Pending') }}</span>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ __('Alerts Pending')
}}</span>
</div>
<span class="text-2xl font-black text-slate-900 dark:text-white">{{ $alertsPending }}</span>
</div>
</div>
<!-- Divider -->
<div class="w-px h-32 bg-slate-100 dark:bg-slate-800 mx-2"></div>
<!-- Divider (Hidden on Mobile) -->
<div class="hidden sm:block w-px h-32 bg-slate-100 dark:bg-slate-800 mx-2"></div>
<!-- Right: Big Total -->
<div class="w-40 text-center">
<div
class="w-full sm:w-40 text-center pt-6 sm:pt-0 border-t sm:border-t-0 border-slate-100 dark:border-slate-800/50">
<p
class="text-7xl font-black text-cyan-500 drop-shadow-[0_0_20px_rgba(6,182,212,0.3)] leading-none">
class="text-6xl sm:text-7xl font-black text-cyan-500 drop-shadow-[0_0_20px_rgba(6,182,212,0.3)] leading-none">
{{ $activeMachines }}</p>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em] mt-4">
{{ __('Total Connected') }}</p>
@ -66,7 +71,8 @@
<div class="luxury-card rounded-2xl p-8 animate-luxury-in flex flex-col" style="animation-delay: 100ms">
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Monthly Transactions') }}</h3>
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
__('Monthly Transactions') }}</h3>
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Monthly cumulative revenue overview') }}</p>
</div>
<div
@ -92,7 +98,7 @@
</svg>
</div>
<div>
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Today's Transactions") }}</p>
<p class="text-xs font-bold text-slate-500 dark:text-slate-400">{{ __("Today Cumulative Sales") }}</p>
<p
class="text-4xl font-black text-slate-900 dark:text-white mt-1 tracking-tight drop-shadow-sm">
${{ number_format($totalRevenue / 30, 0) }}</p>
@ -101,7 +107,8 @@
<div class="flex flex-col items-end gap-y-1">
<span
class="text-[10px] font-black text-emerald-500 bg-emerald-500/10 px-2.5 py-0.5 rounded-full">+12.5%</span>
<p class="text-[9px] font-bold text-slate-300 dark:text-slate-500 uppercase tracking-tighter">{{ __('vs Yesterday') }}</p>
<p class="text-[9px] font-bold text-slate-300 dark:text-slate-500 uppercase tracking-tighter">{{
__('vs Yesterday') }}</p>
</div>
</div>
@ -148,11 +155,12 @@
</div>
<!-- Bottom Section: Status List -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
<div class="luxury-card rounded-3xl p-8 border border-slate-100 dark:border-white/5 animate-luxury-in" style="animation-delay: 200ms">
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
<div>
<div class="flex items-center gap-x-3">
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Status List') }}</h2>
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
__('Machine Status List') }}</h2>
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-tighter">
{{ __('Total items', ['count' => $machines->total()]) }}
@ -161,8 +169,9 @@
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time monitoring across all machines') }}</p>
</div>
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex flex-wrap items-center gap-4">
<div class="relative group">
<form action="{{ route('admin.dashboard') }}" method="GET"
class="flex flex-col sm:flex-row items-stretch sm:items-center gap-4 w-full md:w-auto">
<div class="relative group w-full sm:w-64">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
@ -172,38 +181,24 @@
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none"
class="py-3 pl-12 pr-6 block w-full border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none"
placeholder="{{ __('Quick search...') }}">
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
</div>
</form>
</div>
<div class="overflow-x-auto">
<!-- Desktop Table View (xl and above) -->
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">
{{ __('Machine Info') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Running Status') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Today Cumulative Sales') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Current Stock') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Last Signal') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Alert Summary') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Actions') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Signal') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@ -211,56 +206,36 @@
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6">
<div class="flex items-center gap-x-5">
<div
class="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-300 group-hover:bg-cyan-500 group-hover:text-white transition-all shadow-sm">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2">
<path
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
<div class="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-300 group-hover:bg-cyan-500 group-hover:text-white transition-all shadow-sm">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<div class="flex flex-col">
<span
class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{
$machine->name }}</span>
<span
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN:
{{ $machine->serial_no }})</span>
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $machine->serial_no }})</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
@php
$cStatus = $machine->calculated_status;
@endphp
@php $cStatus = $machine->calculated_status; @endphp
@if($cStatus === 'online')
<div
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
<div class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</div>
<span
class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
__('Online') }}</span>
<span class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{ __('Online') }}</span>
</div>
@elseif($cStatus === 'error')
<div
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
<span
class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
__('Error') }}</span>
<span class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{ __('Error') }}</span>
</div>
@else
<div
class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
<span
class="text-[10px] font-black text-slate-600 dark:text-slate-400 tracking-widest uppercase">{{
__('Offline') }}</span>
<span class="text-[10px] font-black text-slate-600 dark:text-slate-400 tracking-widest uppercase">{{ __('Offline') }}</span>
</div>
@endif
</td>
@ -269,53 +244,32 @@
</td>
<td class="px-6 py-6">
@php
$stockSum = (float)($machine->slots_sum_stock ?? 0);
$maxStockSum = (float)($machine->slots_sum_max_stock ?? 0);
$stockPercentage = $maxStockSum > 0 ? round(($stockSum / $maxStockSum) * 100, 1) : 0;
// 顏色與標籤判定 (Color & Label Determination)
$barColor = 'bg-emerald-500';
$shadowColor = 'shadow-[0_0_8px_rgba(16,185,129,0.4)]';
$textColor = 'text-emerald-500';
$statusLabel = __('Healthy');
if ($stockPercentage < 20) {
$barColor = 'bg-rose-500';
$shadowColor = 'shadow-[0_0_8px_rgba(244,63,94,0.4)]';
$textColor = 'text-rose-500';
$statusLabel = __('Low Stock');
} elseif ($stockPercentage < 50) {
$barColor = 'bg-amber-500';
$shadowColor = 'shadow-[0_0_8px_rgba(245,158,11,0.4)]';
$textColor = 'text-amber-500';
$statusLabel = __('Warning');
}
$stockSum = (float)($machine->slots_sum_stock ?? 0);
$maxStockSum = (float)($machine->slots_sum_max_stock ?? 0);
$stockPercentage = $maxStockSum > 0 ? round(($stockSum / $maxStockSum) * 100, 1) : 0;
$barColor = $stockPercentage < 20 ? 'bg-rose-500' : ($stockPercentage < 50 ? 'bg-amber-500' : 'bg-emerald-500');
$textColor = str_replace('bg-', 'text-', $barColor);
@endphp
<div class="flex flex-col items-center gap-y-2.5">
<div class="w-32 h-2 bg-slate-100 dark:bg-slate-700/40 border border-slate-200/60 dark:border-slate-600/30 rounded-full overflow-hidden shadow-inner">
<div class="h-full {{ $barColor }} rounded-full {{ $shadowColor }}"
style="width: {{ $stockPercentage }}%"></div>
<div class="w-32 h-2 bg-slate-100 dark:bg-white/10 border border-slate-200/60 dark:border-slate-700/30 rounded-full overflow-hidden shadow-inner">
<div class="h-full {{ $barColor }} rounded-full" style="width: {{ $stockPercentage }}%"></div>
</div>
<span class="text-sm font-black {{ $textColor }} uppercase tracking-[0.2em]">{{ $stockPercentage }}%</span>
</div>
</td>
<td class="px-6 py-6 text-center">
<div
class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') :
'---' }}
<div class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y-m-d H:i:s') : '---' }}
</div>
</td>
<td class="px-6 py-6 text-right">
<span
class="text-[11px] font-bold text-slate-400/30 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-400 transition-colors">{{
__('No alert summary') }}</span>
</td>
<td class="px-6 py-6 text-right text-nowrap">
<a href="{{ route('admin.warehouses.machine-inventory', ['machine_id' => $machine->id]) }}"
class="px-3 py-1.5 rounded-lg bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all text-[11px] font-black uppercase tracking-widest active:scale-95 shadow-sm"
class="p-2.5 inline-flex items-center justify-center rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all shadow-sm"
title="{{ __('View Inventory Overview') }}">
{{ __('Inventory Overview') }}
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.67 8.5 7.652 6 12 6c4.348 0 8.332 2.5 9.964 5.678a1.012 1.012 0 0 1 0 .644C20.33 15.5 16.348 18 12 18c-4.348 0-8.332-2.5-9.964-5.678z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
</svg>
</a>
</td>
</tr>
@ -328,9 +282,110 @@
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
<!-- Mobile/Tablet Card View (xl:hidden) -->
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@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 hover:bg-white dark:hover:bg-slate-800/80 transition-all duration-300 group relative overflow-hidden">
<!-- Card Header -->
<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">
@if(isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<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="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
@endif
</div>
<div class="min-w-0">
<h3 class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $machine->name }}
</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
SN: {{ $machine->serial_no ?: '--' }}
</p>
</div>
</div>
@php $cStatus = $machine->calculated_status; @endphp
<div class="flex items-center">
@if($cStatus === 'online')
<div class="flex h-2.5 w-2.5 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</div>
@elseif($cStatus === 'error')
<div class="h-2.5 w-2.5 rounded-full bg-rose-500 animate-pulse"></div>
@else
<div class="h-2.5 w-2.5 rounded-full bg-slate-400"></div>
@endif
</div>
</div>
<!-- Info Grid (Content from Dashboard) -->
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Today Cumulative Sales') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">$ {{ number_format($machine->today_sales_sum ?? 0, 0) }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Status') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 capitalize">
@if($cStatus === 'online')
<span class="text-emerald-500">{{ __('Online') }}</span>
@elseif($cStatus === 'error')
<span class="text-rose-500">{{ __('Error') }}</span>
@else
<span class="text-slate-400">{{ __('Offline') }}</span>
@endif
</p>
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Last Signal') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y-m-d H:i:s') : '---' }}
</p>
</div>
</div>
<!-- Stock Bar Section (Essential for Dashboard) -->
<div class="mb-6">
@php
$stockSum = (float)($machine->slots_sum_stock ?? 0);
$maxStockSum = (float)($machine->slots_sum_max_stock ?? 0);
$stockPercentage = $maxStockSum > 0 ? round(($stockSum / $maxStockSum) * 100, 1) : 0;
$barColor = $stockPercentage < 20 ? 'bg-rose-500' : ($stockPercentage < 50 ? 'bg-amber-500' : 'bg-emerald-500');
@endphp
<div class="flex justify-between items-end mb-2">
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Current Stock') }}</p>
<span class="text-xs font-black {{ str_replace('bg-', 'text-', $barColor) }}">{{ $stockPercentage }}%</span>
</div>
<div class="w-full h-2 bg-slate-100 dark:bg-white/10 rounded-full overflow-hidden shadow-inner">
<div class="h-full {{ $barColor }} transition-all duration-500" style="width: {{ $stockPercentage }}%"></div>
</div>
</div>
<!-- Action Button -->
<a href="{{ route('admin.warehouses.machine-inventory', ['machine_id' => $machine->id]) }}"
class="flex items-center justify-center gap-2 w-full py-3.5 rounded-2xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50 group/btn">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.67 8.5 7.652 6 12 6c4.348 0 8.332 2.5 9.964 5.678a1.012 1.012 0 0 1 0 .644C20.33 15.5 16.348 18 12 18c-4.348 0-8.332-2.5-9.964-5.678z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
</svg>
{{ __('Inventory Overview') }}
</a>
</div>
@empty
<div class="col-span-full py-20 text-center text-slate-400 font-bold border-2 border-dashed border-slate-100 dark:border-slate-800 rounded-3xl">
{{ __('No data available') }}
</div>
@endforelse
</div>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
</div>
</div>
</div>
@endsection

View File

@ -156,7 +156,7 @@
};
</script>
<div class="space-y-2 pb-20" x-data="machineApp()"
<div class="space-y-4 pb-20" x-data="machineApp()"
@keydown.escape.window="showLogPanel = false; showInventoryPanel = false"
@ajax:navigate.window.prevent="fetchPage($event.detail.url)"
@submit.prevent="if($event.target.method.toLowerCase() === 'get') {
@ -168,11 +168,11 @@
fetchPage(url.pathname + url.search);
}">
<!-- Top Header & Actions -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div>
<h1
class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
{{ __('Machine List') }}
</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
@ -210,8 +210,8 @@
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }">
<!-- Filters Area -->
<div class="flex items-center justify-between mb-8">
<form method="GET" action="{{ route('admin.machines.index') }}" class="flex items-center gap-4"
<div class="mb-8">
<form method="GET" action="{{ route('admin.machines.index') }}" class="flex flex-wrap items-center gap-3 sm:gap-4"
@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);
@ -220,39 +220,44 @@
fetchPage(url.pathname + url.search);">
<input type="hidden" name="tab" value="list">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search machines...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-72">
<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">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search machines...') }}" class="luxury-input py-2.5 pl-11 pr-4 block w-full sm:w-72 text-sm">
</div>
<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 transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
$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 transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
$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" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</form>
</div>
<div class="overflow-x-auto pb-4">
<!-- Desktop Table View -->
<div class="hidden xl:block overflow-x-auto pb-4">
<table class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
@ -409,6 +414,105 @@
</table>
</div>
<!-- Mobile/Tablet Card View -->
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach ($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 -->
<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">
@if(isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
@endif
</div>
<div class="min-w-0">
<h3 class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $machine->name }}
</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate">
{{ $machine->serial_no ?: '--' }}
</p>
</div>
</div>
@php
$cStatus = $machine->calculated_status;
@endphp
@if($cStatus === 'online')
<div class="flex h-2.5 w-2.5 relative">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</div>
@elseif($cStatus === 'error')
<div class="h-2.5 w-2.5 rounded-full bg-rose-500 animate-pulse"></div>
@else
<div class="h-2.5 w-2.5 rounded-full bg-slate-400"></div>
@endif
</div>
<!-- Info Grid -->
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Temperature') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $machine->temperature ?? '--' }}°C</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('APP Version') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $machine->firmware_version ?: '-' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Last Page') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $machine->current_page_label ?: '-' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Status') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
@if($cStatus === 'online')
<span class="text-emerald-500">{{ __('Online') }}</span>
@elseif($cStatus === 'error')
<span class="text-rose-500">{{ __('Error') }}</span>
@else
<span class="text-slate-400">{{ __('Offline') }}</span>
@endif
</p>
</div>
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Last Heartbeat') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y-m-d H:i:s') : '-' }}
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-3">
<button type="button"
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
{{ __('Edit') }}
</button>
<button type="button"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold text-xs hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.67 8.5 7.652 6 12 6c4.348 0 8.332 2.5 9.964 5.678a1.012 1.012 0 0 1 0 .644C20.33 15.5 16.348 18 12 18c-4.348 0-8.332-2.5-9.964-5.678z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
</svg>
{{ __('Logs') }}
</button>
</div>
</div>
@endforeach
</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') }}
</div>
@ -476,47 +580,54 @@
</div>
<!-- Responsive Search within Panel -->
<div class="mt-6 flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6">
<div class="mt-2 flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6">
<div class="grid grid-cols-2 sm:flex sm:items-center gap-3 sm:gap-4 flex-1">
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
<label
class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
__('From') }}</label>
<input type="text" x-ref="startDatePicker" x-model="startDate"
x-init="flatpickr($refs.startDatePicker, {
enableTime: true,
dateFormat: 'Y/m/d H:i',
time_24hr: true,
locale: window.flatpickrLocale,
defaultDate: startDate,
onClose: (selectedDates, dateStr) => { startDate = dateStr; fetchLogs(1); }
})"
class="luxury-input text-[12px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2 group">
<label class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{ __('Start Time') }}</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</span>
<input type="text" x-ref="startDatePicker" x-model="startDate"
x-init="flatpickr($refs.startDatePicker, {
enableTime: true,
dateFormat: 'Y/m/d H:i',
time_24hr: true,
locale: 'zh_tw',
defaultDate: startDate,
onClose: (selectedDates, dateStr) => { startDate = dateStr; fetchLogs(1); }
})"
class="luxury-input text-[12px] h-9 py-0 pl-9 pr-3 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 cursor-pointer">
</div>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
<label
class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
__('To') }}</label>
<input type="text" x-ref="endDatePicker" x-model="endDate"
x-init="flatpickr($refs.endDatePicker, {
enableTime: true,
dateFormat: 'Y/m/d H:i',
time_24hr: true,
locale: window.flatpickrLocale,
defaultDate: endDate,
onClose: (selectedDates, dateStr) => { endDate = dateStr; fetchLogs(1); }
})"
class="luxury-input text-[12px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2 group">
<label class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{ __('End Time') }}</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</span>
<input type="text" x-ref="endDatePicker" x-model="endDate"
x-init="flatpickr($refs.endDatePicker, {
enableTime: true,
dateFormat: 'Y/m/d H:i',
time_24hr: true,
locale: 'zh_tw',
defaultDate: endDate,
onClose: (selectedDates, dateStr) => { endDate = dateStr; fetchLogs(1); }
})"
class="luxury-input text-[12px] h-9 py-0 pl-9 pr-3 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 cursor-pointer">
</div>
</div>
</div>
<div class="flex justify-start sm:justify-end">
<button @click="startDate = ''; endDate = ''; fetchLogs(1)"
class="text-[12px] font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:text-cyan-500 transition-colors flex items-center gap-1.5">
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5">
<path d="M18 13.5V19a2 2 0 0 1-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h5.5l7.5 7.5z" />
<path d="m14 2 2 2 2 2" />
<path d="m18 2-6 6" />
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ __('Clear Filter') }}
</button>
@ -606,8 +717,11 @@
<tr
class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-all">
<td class="px-6 py-4">
<div class="text-[12px] font-bold text-slate-600 dark:text-slate-300"
x-text="formatDateTime(log.created_at)">
<div class="flex flex-col">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest"
x-text="formatDateTime(log.created_at).split(' ')[0]"></span>
<span class="text-[14px] font-bold text-slate-600 dark:text-slate-200 mt-0.5"
x-text="formatDateTime(log.created_at).split(' ')[1]"></span>
</div>
</td>
<td class="px-6 py-4 text-center">

View File

@ -1,7 +1,7 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-2 pb-20" x-data="{
<div class="space-y-4 pb-20" x-data="{
permissionSearchQuery: '',
showPermissionModal: false,
isPermissionsLoading: false,
@ -113,9 +113,9 @@
fetchPage(url.pathname + url.search);
}">
<!-- 1. Header Area -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Machine Permissions') }}</h1>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Machine Permissions') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{
__('Manage machine access permissions') }}</p>
</div>
@ -153,7 +153,7 @@
<form method="GET" action="{{ route('admin.machines.permissions') }}"
class="flex flex-wrap items-center gap-4 w-full md:w-auto">
<div class="relative group">
<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">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
@ -164,41 +164,43 @@
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search accounts...') }}"
class="luxury-input py-2.5 pl-12 pr-6 block w-64 text-sm font-bold">
class="luxury-input py-2.5 pl-12 pr-6 block w-full sm:w-64 text-sm font-bold">
</div>
@if(auth()->user()->isSystemAdmin())
<div class="w-72">
<div class="w-full sm:w-72">
<x-searchable-select name="company_id" :options="$companies" :selected="request('company_id')"
:placeholder="__('All Companies')" />
</div>
@endif
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button 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', { 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" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button 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', { 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') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
</div>
<div class="overflow-x-auto">
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
@ -222,7 +224,7 @@
<td class="px-6 py-6 font-display">
<div class="flex items-center gap-4">
<div
class="w-10 h-10 rounded-xl 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 shadow-sm overflow-hidden">
class="w-10 h-10 rounded-xl 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 shadow-sm overflow-hidden shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
@ -282,8 +284,7 @@
<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" />
</svg>
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No
accounts found') }}</p>
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
</div>
</td>
</tr>
@ -291,6 +292,77 @@
</tbody>
</table>
</div>
<!-- Mobile & Tablet Card View -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 xl:hidden">
@forelse($users_list as $user)
<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 -->
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
</div>
<div class="min-w-0">
<h3 class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $user->name }}
</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate mt-0.5">
{{ $user->username }}
</p>
</div>
</div>
<div class="shrink-0">
<span class="inline-flex px-2.5 py-1 rounded-lg text-[10px] font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest uppercase">
{{ $user->company->name ?? __('System') }}
</span>
</div>
</div>
<!-- Info Grid (Authorized Machines) -->
<div class="mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div class="flex items-center justify-between mb-3">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Authorized Machines') }}</span>
<span class="text-[10px] font-bold text-cyan-500 bg-cyan-50 dark:bg-cyan-500/10 px-2 py-0.5 rounded-full">{{ $user->machines->count() }}</span>
</div>
<div class="flex flex-wrap gap-2 max-h-[120px] overflow-y-auto pr-2 custom-scrollbar">
@forelse($user->machines as $m)
<div class="px-2.5 py-1.5 rounded-lg bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
<span class="text-xs font-bold text-slate-700 dark:text-slate-300 leading-none">{{ $m->name }}</span>
</div>
@empty
<div class="w-full flex items-center justify-center py-4">
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest opacity-40 italic">-- {{ __('None') }} --</span>
</div>
@endforelse
</div>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-3">
<button type="button" @click="openPermissionModal({{ json_encode(['id' => $user->id, 'name' => $user->name]) }})"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 font-black text-xs uppercase tracking-widest hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 00-2 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
{{ __('Authorize') }}
</button>
</div>
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<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" />
</svg>
<p class="text-slate-400 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') }}
</div>

View File

@ -176,7 +176,7 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
}
</script>
<div class="space-y-2" x-data="utilizationDashboard(@js($compactMachines), @js($fleetStats))">
<div class="space-y-4" x-data="utilizationDashboard(@js($compactMachines), @js($fleetStats))">
<!-- Page Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>

View File

@ -1,7 +1,7 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-2 pb-20" x-data="{
<div class="space-y-4 pb-20" x-data="{
showDetailDrawer: false,
currentRecord: null,
isLoadingTable: false,
@ -9,7 +9,8 @@
'Repair': '{{ __('Repair') }}',
'Installation': '{{ __('Installation') }}',
'Removal': '{{ __('Removal') }}',
'Maintenance': '{{ __('Maintenance') }}'
'Maintenance': '{{ __('Maintenance') }}',
'System': '{{ __('System') }}'
},
async fetchPage(url) {
if (!url || this.isLoadingTable) return;
@ -37,9 +38,9 @@
enlargedImage: null
}">
<!-- 1. Header Area -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Maintenance Records') }}</h1>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Maintenance Records') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Track device health and maintenance history') }}</p>
</div>
<div class="flex items-center gap-3">
@ -47,7 +48,7 @@
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('New Record') }}</span>
<span class="text-sm sm:text-base">{{ __('New Record') }}</span>
</a>
</div>
</div>
@ -85,47 +86,73 @@
<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 items-center gap-4"
<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())">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search serial or machine...') }}"
class="luxury-input luxury-input-sm pl-12 pr-6 block w-72 font-bold">
</div>
<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">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
placeholder="{{ __('Search serial or machine...') }}"
class="luxury-input luxury-input-sm pl-12 pr-6 block w-full sm:w-72 font-bold">
</div>
<!-- Category Filter (Custom Select UI) -->
<div class="flex items-center gap-3 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">
<!-- 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">
@foreach(['Repair', 'Installation', 'Removal', 'Maintenance'] as $cat)
<option value="{{ $cat }}" {{ request('category') == $cat ? 'selected' : '' }}>
{{ __($cat) }}
</option>
@endforeach
</x-searchable-select>
</div>
@foreach(['Repair', 'Installation', 'Removal', 'Maintenance'] as $cat)
<option value="{{ $cat }}" {{ request('category') == $cat ? 'selected' : '' }}>
{{ __($cat) }}
</option>
@endforeach
</x-searchable-select>
</div>
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button 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', { 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') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="overflow-x-auto">
<!-- Table (Desktop) -->
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Time') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine Information') }}</th>
@if(auth()->user()->isSystemAdmin())
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Company') }}</th>
@endif
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Category') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Engineer') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
@ -143,12 +170,14 @@
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest group-hover/cell:text-cyan-500/60 transition-colors">{{ $record->machine->serial_no ?? 'N/A' }}</span>
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
<div class="flex flex-col">
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $record->company->name ?? 'N/A' }}</span>
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $record->company->name ?? __('System') }}</span>
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ $record->company->company_code ?? '' }}</span>
</div>
</td>
@endif
<td class="px-6 py-6">
@php
$color = match($record->category) {
@ -195,12 +224,102 @@
</table>
</div>
<!-- Mobile & Tablet Card View -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 xl:hidden">
@forelse($records as $record)
<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 -->
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.65 2.65 0 0021 17.25l-5.83-5.83M11.42 15.17l2.42-2.42a2.65 2.65 0 000-3.75l-.6-.6a2.65 2.65 0 00-3.75 0l-2.42 2.42M11.42 15.17L4.41 8.16a2.12 2.12 0 010-3l.6-.6a2.12 2.12 0 013 0l7.01 7.01" />
</svg>
</div>
<div class="min-w-0">
<h3 class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $record->machine->name ?? 'N/A' }}
</h3>
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate mt-0.5">
{{ $record->machine->serial_no ?? 'N/A' }}
</p>
</div>
</div>
@php
$color = match($record->category) {
'Repair' => 'rose',
'Installation' => 'emerald',
'Removal' => 'amber',
'Maintenance' => 'cyan',
default => 'slate'
};
@endphp
<div class="shrink-0">
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-{{ $color }}-500/10 border border-{{ $color }}-500/20 w-fit">
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-{{ $color }}-500"></span>
<span class="text-[10px] font-black text-{{ $color }}-600 dark:text-{{ $color }}-400 tracking-widest uppercase">
{{ __($record->category) }}
</span>
</div>
</div>
</div>
<!-- Info Grid -->
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Execution Time') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">{{ $record->maintenance_at->format('Y-m-d H:i') }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Engineer') }}</p>
<div class="flex items-center gap-2">
<div class="w-5 h-5 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-[8px] font-bold text-slate-500 border border-slate-200 dark:border-slate-700">
{{ mb_substr($record->user->name, 0, 1) }}
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $record->user->name }}</span>
</div>
</div>
@if(auth()->user()->isSystemAdmin())
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Company') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">
{{ $record->company->name ?? __('System') }} <span class="text-[10px] text-slate-400 opacity-60">({{ $record->company->company_code ?? '' }})</span>
</p>
</div>
@endif
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-3">
<button type="button" @click="openDetail({{ $record->load('machine', 'user', 'company')->toJson() }})"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 font-black text-xs uppercase tracking-widest hover:bg-cyan-500 hover:text-white transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
{{ __('View Details') }}
</button>
</div>
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<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" />
</svg>
<p class="text-slate-400 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') }}
</div>
</div>
</div>
</div>
<!-- 1. Maintenance Detail Drawer -->
@ -242,10 +361,12 @@
<div class="text-xs font-black text-slate-700 dark:text-slate-100" x-text="currentRecord?.user?.name"></div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-800/40 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/80">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1">{{ __('Company') }}</span>
<div class="text-xs font-black text-slate-700 dark:text-slate-100" x-text="currentRecord?.company?.name || 'N/A'"></div>
</div>
<template x-if="@js(auth()->user()->isSystemAdmin())">
<div class="bg-slate-50 dark:bg-slate-800/40 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/80">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1">{{ __('Company') }}</span>
<div class="text-xs font-black text-slate-700 dark:text-slate-100" x-text="currentRecord?.company?.name || translations['System']"></div>
</div>
</template>
<div class="bg-slate-50 dark:bg-slate-800/40 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/80">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1">{{ __('Execution Time') }}</span>
<div class="text-xs font-black text-slate-700 dark:text-slate-100" x-text="currentRecord ? new Date(currentRecord.maintenance_at).toLocaleString() : ''"></div>
@ -289,7 +410,7 @@
</div>
</template>
</template>
<!-- Image Lightbox Overlay -->
<template x-teleport="body">
<div x-show="enlargedImage"

View File

@ -1,7 +1,7 @@
@extends('layouts.admin')
@section('content')
<div class="relative space-y-3 pb-20" x-data="{
<div class="relative space-y-4 pb-20" x-data="{
showModal: false,
editing: false,
isDeleteConfirmOpen: false,
@ -132,37 +132,18 @@
finally { this.inventoryLoading = false; }
}
}" @ajax:navigate.window.prevent="fetchPage($event.detail.url); $event.stopImmediatePropagation();">
<!-- Global Loading Overlay -->
<div x-show="isLoading"
class="absolute inset-0 z-[60] flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] rounded-3xl"
x-cloak>
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
<div
class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin">
</div>
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin"
style="animation-duration: 3s; direction: reverse;"></div>
<div class="relative w-8 h-8 flex items-center justify-center">
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
</div>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{
__('Loading Data') }}...</p>
</div>
{{-- Header --}}
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Warehouse Overview') }}</h1>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Warehouse Overview') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage your warehouses, including main and branch warehouses') }}</p>
</div>
<button @click="openCreateModal()" class="btn-luxury-primary">
<button @click="openCreateModal()" class="btn-luxury-primary flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('Add Warehouse') }}</span>
<span class="text-sm sm:text-base">{{ __('Add Warehouse') }}</span>
</button>
</div>
@ -191,39 +172,43 @@
id="ajax-content-container"
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoading }">
{{-- Filters --}}
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
<form action="{{ route('admin.warehouses.index') }}" method="GET" class="flex items-center gap-4"
<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())">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search warehouses...') }}">
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
<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">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}"
class="py-2.5 pl-12 pr-6 block w-full sm:w-64 luxury-input text-sm font-bold" placeholder="{{ __('Search warehouses...') }}">
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
</div>
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
$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') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
</div>
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
$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" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</form>
<div
@ -245,8 +230,8 @@
</div>
</div>
{{-- Table --}}
<div class="overflow-x-auto">
{{-- Table View (Desktop) --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
@ -291,7 +276,7 @@
$warehouse->name }}</span>
@if($warehouse->address)
<span
class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-wide">{{
class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-wide line-clamp-1">{{
$warehouse->address }}</span>
@endif
</div>
@ -428,6 +413,148 @@
</table>
</div>
{{-- Card View (Mobile/Tablet) --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@forelse($warehouses as $warehouse)
<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 relative">
<!-- Card Header -->
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" 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>
<div class="min-w-0">
<h3 class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
{{ $warehouse->name }}
</h3>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate mt-0.5">
{{ $warehouse->address ?: __('No address provided') }}
</p>
</div>
</div>
<div class="shrink-0">
@if($warehouse->type === 'main')
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/20 w-fit">
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-cyan-500"></span>
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 tracking-widest uppercase">
{{ __('Main') }}
</span>
</div>
@else
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-indigo-500/10 border border-indigo-500/20 w-fit">
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-indigo-500"></span>
<span class="text-[10px] font-black text-indigo-600 dark:text-indigo-400 tracking-widest uppercase">
{{ __('Branch') }}
</span>
</div>
@endif
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Products') }}</p>
<p class="text-xl font-black text-slate-800 dark:text-white">{{ $warehouse->products_count ?? 0 }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Stock') }}</p>
<p class="text-xl font-black text-slate-800 dark:text-white">{{ (int)($warehouse->total_stock ?? 0) }}</p>
</div>
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Status') }}</p>
@if($warehouse->is_active)
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/10 border border-emerald-500/20 w-fit">
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500 animate-pulse"></span>
<span class="text-[10px] font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">
{{ __('Active') }}
</span>
</div>
@else
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-500/10 border border-slate-500/20 w-fit">
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-slate-400"></span>
<span class="text-[10px] font-black text-slate-500 dark:text-slate-400 tracking-widest uppercase">
{{ __('Disabled') }}
</span>
</div>
@endif
</div>
@if(auth()->user()->isSystemAdmin())
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">{{ __('Company') }}</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 leading-tight">
{{ $warehouse->company->name ?? __('System') }}
</p>
</div>
@endif
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2">
<!-- Status Toggle -->
@if($warehouse->is_active)
<button type="button"
@click="toggleFormAction = '{{ route('admin.warehouses.toggle-status', $warehouse->id) }}'; isStatusConfirmOpen = true"
class="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-amber-500 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm"
title="{{ __('Disable') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
</button>
@else
<button type="button"
@click="toggleFormAction = '{{ route('admin.warehouses.toggle-status', $warehouse->id) }}'; $nextTick(() => $refs.statusToggleForm.submit())"
class="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm"
title="{{ __('Enable') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 0 1 0 1.971l-11.54 6.347c-.75.412-1.667-.13-1.667-.986V5.653z" />
</svg>
</button>
@endif
<!-- Edit -->
<button @click="openEditModal({{ json_encode($warehouse) }})"
class="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
<!-- Delete -->
<button type="button"
@click="deleteFormAction = '{{ route('admin.warehouses.destroy', $warehouse->id) }}'; isDeleteConfirmOpen = true"
class="p-3 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 transition-all border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
<!-- View Inventory (Primary Action) -->
<button @click="openInventoryPanel({ id: '{{ $warehouse->id }}', name: '{{ addslashes($warehouse->name) }}' })"
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-cyan-500 text-white font-black text-xs uppercase tracking-widest hover:bg-cyan-600 transition-all duration-300 shadow-lg shadow-cyan-500/25">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
{{ __('View Inventory') }}
</button>
</div>
</div>
@empty
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
<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="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>
</div>
</div>
@endforelse
</div>
{{-- Pagination --}}
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">
{{ $warehouses->links('vendor.pagination.luxury') }}
@ -673,5 +800,25 @@
</div>
</div>
</div>
<!-- Global Loading Overlay -->
<div x-show="isLoading"
class="absolute inset-0 z-[60] flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] rounded-3xl"
x-cloak>
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
<div
class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin">
</div>
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin"
style="animation-duration: 3s; direction: reverse;"></div>
<div class="relative w-8 h-8 flex items-center justify-center">
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
</div>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{
__('Loading Data') }}...</p>
</div>
</div>
@endsection

View File

@ -85,39 +85,47 @@
</template>
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Inventory Overview') }}</h1>
<p class="text-base font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p>
</div>
</div>
{{-- List Mode --}}
<div x-show="viewMode === 'list'" class="space-y-6 animate-luxury-in" x-cloak>
<div class="luxury-card rounded-3xl p-8">
<div class="luxury-card rounded-3xl p-8 hover:translate-y-0 transition-transform duration-300">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET" class="flex items-center gap-4">
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET"
class="flex items-center gap-4">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-72 luxury-input" placeholder="{{ __('Search machines...') }}">
<input type="text" name="search" value="{{ request('search') }}"
class="py-2.5 pl-12 pr-6 block w-72 luxury-input"
placeholder="{{ __('Search machines...') }}">
</div>
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
<button type="submit"
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group"
title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
<button type="button"
@click="
<button type="button" @click="
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
$el.closest('form').submit();
"
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group"
title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</form>
@ -127,80 +135,113 @@
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Serial No.') }}</th>
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Slots') }}</th>
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Total Stock') }}</th>
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Stock Rate') }}</th>
<th class="px-6 py-4 text-sm font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Machine') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Serial No.') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Slots') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Total Stock') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Stock Rate') }}</th>
<th
class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($machines as $machine)
@php
$maxStock = $machine->slots->sum('max_stock') ?: 1;
$currentStock = (int)($machine->total_stock ?? 0);
$fillRate = round(($currentStock / $maxStock) * 100);
$fillColor = $fillRate >= 50 ? 'emerald' : ($fillRate >= 20 ? 'amber' : 'rose');
$maxStock = $machine->slots->sum('max_stock') ?: 1;
$currentStock = (int)($machine->total_stock ?? 0);
$fillRate = round(($currentStock / $maxStock) * 100);
$fillColor = $fillRate >= 50 ? 'emerald' : ($fillRate >= 20 ? 'amber' : 'rose');
@endphp
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300 cursor-pointer"
@click="viewSlots({{ json_encode($machine->only(['id', 'name', 'serial_no'])) }})">
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm border border-slate-200 dark:border-slate-700">
<div
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all overflow-hidden shadow-sm border border-slate-200 dark:border-slate-700">
@if($machine->image_urls && isset($machine->image_urls[0]))
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
@else
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
@endif
</div>
<span class="font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</span>
<span
class="font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{
$machine->name }}</span>
</div>
</td>
<td class="px-6 py-5">
<span class="font-mono font-bold text-slate-500 uppercase tracking-widest text-sm">{{ $machine->serial_no }}</span>
<span class="font-mono font-bold text-slate-500 uppercase tracking-widest text-sm">{{
$machine->serial_no }}</span>
</td>
<td class="px-6 py-5 text-center">
<span class="font-black text-slate-800 dark:text-white">{{ $machine->total_slots ?? 0 }}</span>
<span class="font-black text-slate-800 dark:text-white">{{ $machine->total_slots ?? 0
}}</span>
</td>
<td class="px-6 py-5 text-center">
<span class="font-black text-slate-800 dark:text-white">{{ $currentStock }}</span>
</td>
<td class="px-6 py-5 text-center">
<div class="flex items-center justify-center gap-3">
<div class="w-24 h-2 bg-slate-100 dark:bg-slate-700/50 rounded-full overflow-hidden shadow-inner">
<div class="h-full bg-{{ $fillColor }}-500 rounded-full transition-all duration-500" style="width: {{ $fillRate }}%;"></div>
</div>
<span class="text-sm font-black text-{{ $fillColor }}-500 w-8">{{ $fillRate }}%</span>
</div>
<div
class="w-24 h-2 bg-slate-100 dark:bg-slate-700/50 rounded-full overflow-hidden shadow-inner">
<div class="h-full bg-{{ $fillColor }}-500 rounded-full transition-all duration-500"
style="width: {{ $fillRate }}%;"></div>
</div>
<span class="text-sm font-black text-{{ $fillColor }}-500 w-8">{{ $fillRate
}}%</span>
</div>
</td>
<td class="px-6 py-5 text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ route('admin.warehouses.replenishments') }}?auto_machine_id={{ $machine->id }}"
@click.stop
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
title="{{ __('Quick Replenish') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
title="{{ __('Auto Replenishment') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
</a>
<button class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
<button
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
title="{{ __('View Slots') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
</td>
</tr>
@empty
<tr><td colspan="6" class="px-6 py-24 text-center text-slate-400 font-bold italic">{{ __('No machines found') }}</td></tr>
<tr>
<td colspan="6" class="px-6 py-24 text-center text-slate-400 font-bold italic">{{ __('No
machines found') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6 pt-6 border-t border-slate-50 dark:border-slate-800/50">{{ $machines->links('vendor.pagination.luxury') }}</div>
<div class="mt-6 pt-6 border-t border-slate-50 dark:border-slate-800/50">{{
$machines->links('vendor.pagination.luxury') }}</div>
</div>
</div>
@ -209,22 +250,32 @@
<!-- Statistics Dashboard -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-stretch">
<!-- Left: Overall Progress -->
<div class="lg:col-span-5 luxury-card rounded-[2.5rem] p-10 flex items-center justify-center relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-700"></div>
<div
class="lg:col-span-5 luxury-card rounded-[2.5rem] p-10 flex items-center justify-center relative overflow-hidden group">
<div
class="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-700">
</div>
<div class="relative flex flex-col items-center">
<div class="relative w-48 h-48">
<!-- SVG Circle Progress -->
<svg class="w-full h-full -rotate-90 transform" viewBox="0 0 100 100">
<circle class="text-slate-100 dark:text-slate-700/50 stroke-current" stroke-width="10" fill="transparent" r="40" cx="50" cy="50"></circle>
<circle class="text-emerald-500 stroke-current transition-all duration-1000 ease-out" stroke-width="10" stroke-dasharray="251.2" :stroke-dashoffset="251.2 - (251.2 * Math.min(overallFillRate, 100)) / 100" stroke-linecap="round" fill="transparent" r="40" cx="50" cy="50"></circle>
<circle class="text-slate-100 dark:text-slate-700/50 stroke-current" stroke-width="10"
fill="transparent" r="40" cx="50" cy="50"></circle>
<circle class="text-emerald-500 stroke-current transition-all duration-1000 ease-out"
stroke-width="10" stroke-dasharray="251.2"
:stroke-dashoffset="251.2 - (251.2 * Math.min(overallFillRate, 100)) / 100"
stroke-linecap="round" fill="transparent" r="40" cx="50" cy="50"></circle>
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-5xl font-black text-slate-800 dark:text-white tracking-tighter" x-text="overallFillRate + '%'"></span>
<span class="text-sm font-black text-slate-400 uppercase tracking-[0.2em] mt-1" x-text="totalStock + ' / ' + totalCapacity"></span>
<span class="text-5xl font-black text-slate-800 dark:text-white tracking-tighter"
x-text="overallFillRate + '%'"></span>
<span class="text-sm font-black text-slate-400 uppercase tracking-[0.2em] mt-1"
x-text="totalStock + ' / ' + totalCapacity"></span>
</div>
</div>
<div class="mt-6 text-center">
<span class="text-base font-black text-slate-500 uppercase tracking-[0.3em]">{{ __('總庫存量') }}</span>
<span class="text-base font-black text-slate-500 uppercase tracking-[0.3em]">{{ __('總庫存量')
}}</span>
</div>
</div>
</div>
@ -232,66 +283,98 @@
<!-- Right: Stats Cards -->
<div class="lg:col-span-7 grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- Machine Info Card -->
<div class="sm:col-span-2 luxury-card rounded-[2rem] p-6 border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between">
<div
class="sm:col-span-2 luxury-card rounded-[2rem] p-6 border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-6">
<div class="w-16 h-16 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 overflow-hidden">
<div
class="w-16 h-16 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 overflow-hidden">
<template x-if="selectedMachine?.image_urls && selectedMachine?.image_urls[0]">
<img :src="selectedMachine.image_urls[0]" class="w-full h-full object-cover">
</template>
<template x-if="!selectedMachine?.image_urls || !selectedMachine?.image_urls[0]">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</template>
</div>
<div>
<h2 x-text="selectedMachine?.name" class="text-2xl font-black text-slate-800 dark:text-white tracking-tight leading-tight"></h2>
<span x-text="selectedMachine?.serial_no" class="text-sm font-mono font-bold text-slate-400 uppercase tracking-widest mt-1 block"></span>
<h2 x-text="selectedMachine?.name"
class="text-2xl font-black text-slate-800 dark:text-white tracking-tight leading-tight">
</h2>
<span x-text="selectedMachine?.serial_no"
class="text-sm font-mono font-bold text-slate-400 uppercase tracking-widest mt-1 block"></span>
</div>
</div>
<div class="hidden sm:flex items-center gap-4">
<div class="text-right">
<span class="text-sm font-black text-slate-400 uppercase tracking-widest block mb-1">{{ __('Location') }}</span>
<span x-text="selectedMachine?.location || '{{ __('No Location') }}'" class="text-base font-bold text-slate-600 dark:text-slate-300"></span>
<span class="text-sm font-black text-slate-400 uppercase tracking-widest block mb-1">{{
__('Location') }}</span>
<span x-text="selectedMachine?.location || '{{ __('No Location') }}'"
class="text-base font-bold text-slate-600 dark:text-slate-300"></span>
</div>
<a :href="'{{ route('admin.warehouses.replenishments') }}?auto_machine_id=' + selectedMachine?.id"
class="px-4 py-2.5 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all flex items-center gap-2 uppercase tracking-widest">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg>
{{ __('Quick Replenish') }}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
</svg>
{{ __('Auto Replenishment') }}
</a>
</div>
</div>
<!-- Active Slots -->
<div class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02]">
<div class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02] hover:translate-y-0 transition-transform duration-300">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<div
class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span class="text-2xl font-black text-emerald-600 dark:text-emerald-400" x-text="activeSlotsCount"></span>
<span class="text-2xl font-black text-emerald-600 dark:text-emerald-400"
x-text="activeSlotsCount"></span>
</div>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Active Slots') }}</span>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Active Slots')
}}</span>
</div>
<!-- Empty Slots -->
<div class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02]">
<div class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02] hover:translate-y-0 transition-transform duration-300">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-500/10 flex items-center justify-center text-slate-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
<div
class="w-10 h-10 rounded-xl bg-slate-500/10 flex items-center justify-center text-slate-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<span class="text-2xl font-black text-slate-600 dark:text-slate-400" x-text="emptySlotsCount"></span>
<span class="text-2xl font-black text-slate-600 dark:text-slate-400"
x-text="emptySlotsCount"></span>
</div>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Empty Slots') }}</span>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Empty Slots')
}}</span>
</div>
<!-- High Stock -->
<div class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2">
<div class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2 hover:translate-y-0 transition-transform duration-300">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" /></svg>
<div
class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Stock Level > 50%') }}</span>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">{{ __('Stock Level
> 50%') }}</span>
</div>
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400" x-text="highStockSlotsCount"></span>
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400"
x-text="highStockSlotsCount"></span>
</div>
</div>
</div>
@ -303,47 +386,60 @@
<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>
<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>
<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-4">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span>
<span class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Expired') }}</span>
</div>
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-amber-500"></span>
<span class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Low Stock') }}</span>
<span class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Low Stock')
}}</span>
</div>
</div>
</div>
<!-- Grid View -->
<div x-show="displayMode === 'grid'" class="space-y-6">
<div class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px]">
<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="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
<div class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px] hover:translate-y-0 transition-transform duration-300">
<div x-show="loading"
class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500">
<div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
</div>
</div>
<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">
<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 :class="getSlotColorClass(slot)"
class="min-h-[260px] rounded-[2.5rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-500 group relative hover:scale-105 hover:-translate-y-1 hover:shadow-xl">
<div class="absolute top-4 left-5 px-3 py-1 rounded-xl bg-slate-900/5 dark:bg-white/10 backdrop-blur-md">
<span class="text-sm font-black tracking-tighter text-slate-800 dark:text-white" x-text="slot.slot_no"></span>
<div
class="absolute top-4 left-5 px-3 py-1 rounded-xl bg-slate-900/5 dark:bg-white/10 backdrop-blur-md">
<span class="text-sm font-black tracking-tighter text-slate-800 dark:text-white"
x-text="slot.slot_no"></span>
</div>
<div class="relative w-16 h-16 mb-4 mt-2">
<div class="absolute inset-0 rounded-2xl bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 overflow-hidden shadow-inner">
<div
class="absolute inset-0 rounded-2xl bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 overflow-hidden shadow-inner">
<template x-if="slot.product && slot.product.image_url">
<img :src="slot.product.image_url" class="w-full h-full object-cover">
</template>
@ -351,13 +447,15 @@
</div>
<div class="text-center w-full">
<div class="text-base font-black truncate w-full opacity-90 tracking-tight" x-text="slot.product?.name || '{{ __('Empty') }}'"></div>
<div class="text-base font-black truncate w-full opacity-90 tracking-tight"
x-text="slot.product?.name || '{{ __('Empty') }}'"></div>
<div class="flex items-baseline justify-center gap-1 mt-2">
<span class="text-2xl font-black tracking-tighter" x-text="slot.stock"></span>
<span class="text-sm font-black opacity-30">/</span>
<span class="text-sm font-bold opacity-50" x-text="slot.max_stock"></span>
</div>
<div class="text-sm font-black tracking-widest opacity-40 uppercase mt-2" x-text="slot.expiry_date || '----/--/--'"></div>
<div class="text-sm font-black tracking-widest opacity-40 uppercase mt-2"
x-text="slot.expiry_date || '----/--/--'"></div>
</div>
</div>
</template>
@ -367,48 +465,65 @@
<!-- Table View -->
<div x-show="displayMode === 'table'" class="animate-luxury-in" x-cloak>
<div class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl">
<div class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl hover:translate-y-0 transition-transform duration-300">
<table class="w-full text-left">
<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">{{ __('Sale Price') }}</th>
<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">
{{ __('Sale Price') }}</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 class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
<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>
<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">
<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 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>
<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.stock <= 2 ? 'text-rose-500' : 'text-slate-700 dark:text-slate-200'" x-text="slot.stock"></span>
<span class="text-lg font-black"
:class="slot.stock <= 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-base font-black text-emerald-500" x-text="'$' + (slot.price || 0)"></span>
<span class="text-base font-black text-emerald-500"
x-text="'$' + Math.floor(slot.product?.price || 0)"></span>
</td>
</tr>
</template>
@ -418,4 +533,4 @@
</div>
</div>
</div>
@endsection
@endsection

View File

@ -88,10 +88,12 @@
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate max-w-[150px]" x-text="slot.product_name"></span>
</div>
</td>
<td class="px-4 py-2.5 text-center font-mono font-bold text-slate-500 whitespace-nowrap">
<span class="text-slate-700 dark:text-slate-300" x-text="slot.current_stock"></span>
<span class="text-slate-300 dark:text-slate-600 mx-0.5">/</span>
<span class="text-slate-400" x-text="slot.max_stock"></span>
<td class="px-4 py-2.5 text-center whitespace-nowrap">
<div class="flex items-baseline justify-center gap-0.5">
<span class="text-base font-black text-slate-800 dark:text-slate-100 tracking-tighter" x-text="slot.current_stock"></span>
<span class="text-[10px] font-bold text-slate-400 opacity-50">/</span>
<span class="text-xs font-bold text-slate-500 opacity-80" x-text="slot.max_stock"></span>
</div>
</td>
<td class="px-4 py-2.5 text-center flex justify-center">
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50 w-fit">
@ -146,10 +148,10 @@
{{-- 庫存 / 上限 --}}
<div class="text-center bg-white/60 dark:bg-slate-900/40 rounded-xl py-2 px-1">
<div class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Stock / Capacity') }}</div>
<div class="font-mono font-bold text-sm">
<span class="text-slate-700 dark:text-slate-300" x-text="slot.current_stock"></span>
<span class="text-slate-300 dark:text-slate-600">/</span>
<span class="text-slate-400" x-text="slot.max_stock"></span>
<div class="flex items-baseline justify-center gap-0.5">
<span class="text-base font-black text-slate-800 dark:text-slate-100 tracking-tighter" x-text="slot.current_stock"></span>
<span class="text-[10px] font-bold text-slate-400 opacity-50">/</span>
<span class="text-xs font-bold text-slate-500 opacity-80" x-text="slot.max_stock"></span>
</div>
</div>

View File

@ -23,7 +23,7 @@
</div>
<div class="space-y-3">
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Target Machine') }} <span class="text-rose-500">*</span></label>
<x-searchable-select name="machine_id" :options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])" :placeholder="__('Select Machine')" class="w-full" required />
<x-searchable-select name="machine_id" :options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])" :placeholder="__('Select Machine')" class="w-full" required @change="targetMachineId = $event.target.value; fetchMachineSlots()" />
</div>
</div>
<div class="space-y-3">
@ -39,18 +39,17 @@
</button>
</div>
<div class="space-y-3">
<template x-for="(item, index) in items" :key="index">
<template x-for="(item, index) in items" :key="modalOpenCount + '-' + index">
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all">
<div class="w-20">
<input type="number" :name="'items['+index+'][slot_no]'" x-model.number="item.slot_no" min="1" required class="luxury-input w-full px-3 py-2 text-center text-sm font-mono font-bold [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" placeholder="{{ __('Slot') }}">
</div>
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
<div class="flex-1 min-w-[200px] relative" :id="'product-select-wrapper-' + index" x-init="$nextTick(() => updateProductSelects())">
<div x-show="isLoadingProducts" class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 flex items-center justify-center z-10 rounded-xl">
<div class="w-32 relative" :id="'slot-select-wrapper-' + index" x-init="$nextTick(() => updateSlotSelects(index))">
<div x-show="isLoadingSlots" class="absolute inset-0 bg-white/50 dark:bg-slate-900/50 flex items-center justify-center z-10 rounded-xl">
<div class="w-4 h-4 border-2 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
</div>
</div>
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
<div class="flex-1 min-w-[200px] relative" :id="'product-select-wrapper-' + index" x-init="$nextTick(() => updateProductDisplay(index))">
</div>
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
<div class="flex items-center gap-1 bg-slate-100/50 dark:bg-slate-900/50 p-1 rounded-xl border border-slate-200/50 dark:border-slate-800/50">
<button type="button" @click="item.quantity > 1 ? item.quantity-- : null" class="p-1.5 text-slate-400 hover:text-cyan-500 transition-colors">
<svg class="w-3 h-3 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" /></svg>

View File

@ -3,26 +3,21 @@
<div class="absolute inset-0 overflow-hidden">
<div x-show="showOrderDetails" x-transition:enter="ease-in-out duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in-out duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showOrderDetails = false"></div>
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<div x-show="showOrderDetails" x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" class="pointer-events-auto w-screen max-w-md">
<div x-show="showOrderDetails" x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" class="pointer-events-auto w-screen max-w-2xl">
<div class="flex h-full flex-col overflow-y-scroll bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
{{-- Header --}}
<div class="bg-slate-50/50 dark:bg-slate-900/50 px-6 py-8 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-start justify-between">
<h2 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Replenishment Details') }}</h2>
<button type="button" @click="showOrderDetails = false" class="rounded-full bg-white dark:bg-slate-800 p-2 text-slate-400 hover:text-slate-500 border border-slate-100 dark:border-slate-700 shadow-sm transition-all">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<div class="px-8 py-8 border-b border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-3">{{ __('Replenishment Details') }}</h3>
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
<template x-if="activeOrder">
<span class="text-cyan-600 dark:text-cyan-400" x-text="activeOrder.order_no"></span>
</template>
</p>
</div>
<template x-if="activeOrder">
<div class="mt-4 flex flex-col gap-1">
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="activeOrder.order_no"></span>
<div class="flex items-center gap-2">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]" x-text="activeOrder.warehouse_name"></span>
<svg class="w-3 h-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em]" x-text="activeOrder.machine_name"></span>
</div>
</div>
</template>
<button type="button" @click="showOrderDetails = false" class="p-2.5 rounded-full hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-400 transition-colors border border-slate-100 dark:border-slate-800 shadow-sm">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
{{-- Content --}}
@ -35,8 +30,8 @@
{{-- Info Grid --}}
<div class="grid grid-cols-2 gap-6 p-6 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
<div class="space-y-1">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Status') }}</p>
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-widest"
<p class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Status') }}</p>
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-black uppercase tracking-widest"
:class="{
'bg-amber-500/10 text-amber-500 border border-amber-500/20': activeOrder.status === 'pending',
'bg-cyan-500/10 text-cyan-500 border border-cyan-500/20': activeOrder.status === 'prepared',
@ -47,27 +42,27 @@
x-text="{'pending':'{{ __("Pending") }}','prepared':'{{ __("Prepared") }}','delivering':'{{ __("Delivering") }}','completed':'{{ __("Completed") }}','cancelled':'{{ __("Cancelled") }}'}[activeOrder.status]"></span>
</div>
<div class="space-y-1">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Assigned To') }}</p>
<p class="text-xs font-bold" :class="activeOrder.assignee_name ? 'text-slate-700 dark:text-slate-300' : 'text-slate-400'" x-text="activeOrder.assignee_name || '{{ __("Unassigned") }}'"></p>
<p class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Assigned To') }}</p>
<p class="text-base font-bold" :class="activeOrder.assignee_name ? 'text-slate-700 dark:text-slate-300' : 'text-slate-400'" x-text="activeOrder.assignee_name || '{{ __("Unassigned") }}'"></p>
</div>
<div class="space-y-1">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
<p class="text-xs font-bold text-slate-700 dark:text-slate-300" x-text="activeOrder.creator_name"></p>
<p class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
<p class="text-base font-bold text-slate-700 dark:text-slate-300" x-text="activeOrder.creator_name"></p>
</div>
<div class="space-y-1">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created At') }}</p>
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="activeOrder.created_at"></p>
<p class="text-sm font-black text-slate-400 uppercase tracking-widest">{{ __('Created At') }}</p>
<p class="text-sm font-mono font-bold text-slate-600 dark:text-slate-400" x-text="activeOrder.created_at"></p>
</div>
<template x-if="activeOrder.completed_at">
<div class="space-y-1">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Completed At') }}</p>
<p class="text-[11px] font-mono font-bold text-emerald-600 dark:text-emerald-400" x-text="activeOrder.completed_at"></p>
<p class="text-xs font-black text-slate-400 uppercase tracking-widest">{{ __('Completed At') }}</p>
<p class="text-xs font-mono font-bold text-emerald-600 dark:text-emerald-400" x-text="activeOrder.completed_at"></p>
</div>
</template>
<template x-if="activeOrder.note">
<div class="col-span-2 space-y-1 pt-2 border-t border-slate-200/50 dark:border-slate-700/50">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Note') }}</p>
<p class="text-xs font-bold text-slate-600 dark:text-slate-400 italic" x-text="activeOrder.note"></p>
<p class="text-xs font-black text-slate-400 uppercase tracking-widest">{{ __('Note') }}</p>
<p class="text-sm font-bold text-slate-600 dark:text-slate-400 italic" x-text="activeOrder.note"></p>
</div>
</template>
</div>
@ -95,7 +90,7 @@
<div class="space-y-5">
<div class="flex items-center justify-between px-1">
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Replenishment Items') }}</h3>
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="activeItems.length + ' {{ __("Items") }}'"></span>
<span class="text-xs font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="activeItems.length + ' {{ __("Items") }}'"></span>
</div>
<div class="space-y-3">
<template x-for="(item, idx) in activeItems" :key="idx">
@ -108,7 +103,7 @@
<div class="flex-1 min-w-0">
<p class="text-sm font-black text-slate-800 dark:text-slate-100 truncate tracking-tight" x-text="item.product_name"></p>
<template x-if="item.current_stock !== null && item.max_stock">
<p class="text-[10px] font-bold text-slate-400 mt-0.5">
<p class="text-xs font-bold text-slate-400 mt-0.5">
{{ __('Stock') }}: <span class="text-slate-600 dark:text-slate-300 font-mono" x-text="item.current_stock"></span>
/ <span class="font-mono" x-text="item.max_stock"></span>
</p>

View File

@ -17,10 +17,21 @@
@forelse($orders as $order)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
{{-- 訂單編號 --}}
<td class="px-6 py-3.5">
<div class="flex flex-col cursor-pointer" @click="openOrderDetails('{{ $order->id }}')">
<span class="font-mono font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 transition-colors">{{ $order->order_no }}</span>
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{{ $order->created_at->format('Y-m-d H:i:s') }}</span>
<td class="px-6 py-5 cursor-pointer" @click="openOrderDetails('{{ $order->id }}')">
<div class="flex items-center gap-x-4">
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<div class="flex flex-col group/no">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover/no:text-cyan-600 dark:group-hover/no:text-cyan-400 transition-colors">
{{ $order->order_no }}
</span>
<div class="flex items-center gap-1.5 mt-1">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ $order->created_at->format('Y-m-d H:i:s') }}</span>
</div>
</div>
</div>
</td>
{{-- 機台 --}}
@ -54,8 +65,8 @@
<td class="px-6 py-3.5 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-2">
{{-- 查看詳情 --}}
<button type="button" @click="openOrderDetails('{{ $order->id }}')"
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-50 dark:hover:bg-cyan-500/10 transition-all border border-slate-100 dark:border-slate-700 shadow-sm group/btn"
<button type="button" @click="openOrderDetails('{{ $order->id }}')"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 shadow-sm group/btn"
title="{{ __('View Details') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.644C3.399 8.049 7.21 5 12 5c4.79 0 8.601 3.049 9.964 6.678.045.133.045.278 0 .412C20.601 15.951 16.79 19 12 19c-4.79 0-8.601-3.049-9.964-6.678z" />
@ -63,35 +74,40 @@
</svg>
</button>
{{-- 狀態推進按鈕(依當前狀態顯示) --}}
{{-- 狀態推進按鈕(依當前狀態顯示) --}}
@if($order->status === 'pending')
<button type="button"
@click.stop="advanceStatus('{{ $order->id }}', 'prepared')"
class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all shadow-sm">
{{ __('Confirm Prepare') }}
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'prepared')"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
title="{{ __('Confirm Prepare') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</button>
@elseif($order->status === 'prepared')
<button type="button"
@click.stop="advanceStatus('{{ $order->id }}', 'delivering')"
class="px-4 py-2 rounded-xl text-xs font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 hover:bg-indigo-500 hover:text-white transition-all shadow-sm">
{{ __('Start Delivery') }}
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'delivering')"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 transition-all border border-transparent hover:border-indigo-500/20"
title="{{ __('Start Delivery') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</button>
@elseif($order->status === 'delivering')
<button type="button"
@click.stop="advanceStatus('{{ $order->id }}', 'completed')"
class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all shadow-sm">
{{ __('Confirm Complete') }}
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'completed')"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
title="{{ __('Confirm Complete') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
@endif
{{-- 取消按鈕(非完成/非取消狀態顯示) --}}
@if(in_array($order->status, ['pending', 'prepared', 'delivering']))
<button type="button"
@click.stop="confirmCancel('{{ $order->id }}')"
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-all border border-slate-100 dark:border-slate-700 shadow-sm"
<button type="button" @click.stop="confirmCancel('{{ $order->id }}')"
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20"
title="{{ __('Cancel Order') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
@endif

View File

@ -8,6 +8,7 @@
showAutoModal: false,
showCancelModal: false,
showAssignModal: false,
showStatusModal: false,
fromId: '',
availableProducts: [],
allProducts: @json($products ?? []),
@ -17,6 +18,10 @@
cancelOrderId: null,
assignOrderId: null,
assignUserId: '',
statusTargetId: null,
statusTargetValue: null,
statusModalTitle: '',
statusModalMessage: '',
// Auto replenishment
autoWarehouseId: '',
@ -35,6 +40,12 @@
activeOrder: null,
activeItems: [],
// Machine Slots for create modal
targetMachineId: '',
machineSlots: [],
isLoadingSlots: false,
modalOpenCount: 0,
init() {
// 偵測 URL 參數自動開啟一鍵補貨 Modal
const params = new URLSearchParams(window.location.search);
@ -79,74 +90,178 @@
},
openCreateModal() {
this.items = [{ product_id: '', slot_no: '', quantity: 1, current_stock: 0, max_stock: 0 }];
// 遞增計數器,強制 x-for 重建 DOM 元素並觸發 x-init
this.modalOpenCount++;
this.showReplenishmentModal = true;
this.$nextTick(() => { if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select'); });
},
addItem() {
this.items.push({ product_id: '', slot_no: '', quantity: 1, current_stock: 0, max_stock: 0 });
this.$nextTick(() => this.updateProductSelects());
const lastIdx = this.items.length - 1;
this.$nextTick(() => {
this.updateProductDisplay(lastIdx);
this.updateSlotSelects(lastIdx);
});
},
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
async fetchStock() {
this.availableProducts = [];
if (!this.fromId) { this.$nextTick(() => this.updateProductSelects()); return; }
if (!this.fromId) { this.updateProductDisplay(); return; }
this.isLoadingProducts = true;
try {
const res = await fetch('{{ route("admin.warehouses.ajax.stock") }}?warehouse_id=' + this.fromId);
const json = await res.json();
if (json.success) { this.availableProducts = json.data; this.$nextTick(() => this.updateProductSelects()); }
if (json.success) {
this.availableProducts = json.data;
this.updateProductDisplay();
}
} catch (e) { console.error(e); }
finally { this.isLoadingProducts = false; }
},
updateProductSelects() {
this.items.forEach((item, index) => {
const wrapper = document.getElementById(`product-select-wrapper-${index}`);
async fetchMachineSlots() {
this.machineSlots = [];
if (!this.targetMachineId) {
this.items.forEach((_, idx) => this.updateSlotSelects(idx));
return;
}
this.isLoadingSlots = true;
try {
const res = await fetch(`/admin/machines/${this.targetMachineId}/slots-ajax`, {
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
});
const json = await res.json();
if (json.success) {
this.machineSlots = json.slots;
this.items.forEach((_, idx) => this.updateSlotSelects(idx));
}
} catch (e) { console.error(e); }
finally { this.isLoadingSlots = false; }
},
updateSlotSelects(index = null) {
const indices = index !== null ? [index] : this.items.map((_, i) => i);
indices.forEach(idx => {
const item = this.items[idx];
const wrapper = document.getElementById(`slot-select-wrapper-${idx}`);
if (!wrapper) return;
const oldSelect = wrapper.querySelector('select');
if (oldSelect && window.HSSelect?.getInstance(oldSelect)) {
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) {}
}
wrapper.innerHTML = '';
const selectEl = document.createElement('select');
selectEl.id = `product-select-${index}-${Date.now()}`;
selectEl.name = `items[${index}][product_id]`;
selectEl.id = `slot-select-${idx}-${Date.now()}`;
selectEl.name = `items[${idx}][slot_no]`;
selectEl.required = true;
selectEl.className = 'hidden';
const products = this.fromId ? this.availableProducts : this.allProducts;
const placeholderOpt = document.createElement('option');
placeholderOpt.value = '';
placeholderOpt.textContent = '{{ __("Select Product") }}';
placeholderOpt.textContent = '{{ __("Slot") }}';
selectEl.appendChild(placeholderOpt);
products.forEach(p => {
this.machineSlots.forEach(s => {
const opt = document.createElement('option');
opt.value = p.id;
const stockText = p.quantity !== undefined ? ` (${'{{ __("Stock") }}'}: ${p.quantity})` : '';
opt.textContent = p.name + stockText;
opt.setAttribute('data-title', p.name + stockText);
if (item.product_id == p.id) opt.selected = true;
opt.value = s.slot_no;
const stockInfo = ` [${s.stock || 0}/${s.max_stock || 0}]`;
opt.textContent = s.slot_no + (s.product ? ` (${s.product.name})` : '') + stockInfo;
// Enhanced style for HSSelect
opt.setAttribute('data-title', s.slot_no + (s.product ? ` (${s.product.name})` : ''));
const stockHtml = `<div class="flex items-baseline gap-0.5"><span class="text-xs font-black text-slate-800 dark:text-slate-100 tracking-tighter">${s.stock || 0}</span><span class="text-[9px] font-bold text-slate-400 opacity-50">/</span><span class="text-[10px] font-bold text-slate-500 opacity-80">${s.max_stock || 0}</span></div>`;
opt.setAttribute('data-description', stockHtml);
if (item.slot_no == s.slot_no) opt.selected = true;
selectEl.appendChild(opt);
});
selectEl.setAttribute('data-hs-select', JSON.stringify({
"placeholder": "{{ __('Select Product') }}",
"toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left",
"toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-4 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>",
"dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl mt-2 z-[150] max-h-48 overflow-y-auto custom-scrollbar-thin",
"optionClasses": "hs-select-option py-2 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 rounded-lg",
"hasSearch": true, "searchPlaceholder": "{{ __('Search Product') }}",
"searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200",
"searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10",
"placeholder": "{{ __('Slot') }}",
"toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left text-xs font-mono font-bold",
"toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-3 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>",
"dropdownClasses": "hs-select-menu w-64 bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl mt-2 z-[150] max-h-48 overflow-y-auto custom-scrollbar-thin",
"optionClasses": "hs-select-option py-2 px-3 text-xs text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 rounded-lg",
"optionTemplate": "<div class=\"flex items-center justify-between w-full\"><span class=\"truncate max-w-[140px]\" data-title></span><div data-description></div></div>",
"hasSearch": false,
"strategy": "fixed"
}));
wrapper.appendChild(selectEl);
selectEl.addEventListener('change', (e) => { item.product_id = e.target.value; });
selectEl.addEventListener('change', (e) => {
item.slot_no = e.target.value;
// Auto-select product based on slot
const slotData = this.machineSlots.find(s => s.slot_no == item.slot_no);
if (slotData) {
item.product_id = slotData.product_id;
item.current_stock = slotData.stock || 0;
item.max_stock = slotData.max_stock || 0;
// Update the product display for this row
this.updateProductDisplay(idx);
}
});
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
});
},
updateProductDisplay(index = null) {
const indices = index !== null ? [index] : this.items.map((_, i) => i);
indices.forEach(idx => {
const item = this.items[idx];
const wrapper = document.getElementById(`product-select-wrapper-${idx}`);
if (!wrapper) return;
wrapper.innerHTML = '';
if (!item.product_id) {
wrapper.innerHTML = `
<div class="luxury-input w-full px-4 py-2.5 bg-slate-50/50 dark:bg-slate-900/50 text-slate-400 text-sm flex items-center">
<span class="opacity-50 italic">{{ __('Please Select Slot first') }}</span>
</div>
`;
return;
}
const productInWarehouse = this.availableProducts.find(p => p.id == item.product_id);
const productInfo = this.allProducts.find(p => p.id == item.product_id);
const productName = productInfo ? productInfo.name : (productInWarehouse ? productInWarehouse.name : 'Unknown');
// Priority: stock from selected warehouse, otherwise N/A or 0
const stock = productInWarehouse ? productInWarehouse.quantity : 0;
const stockText = this.fromId ? `{{ __('Stock') }}: ${stock}` : '{{ __('Select Warehouse') }}';
wrapper.innerHTML = `
<div class="luxury-input w-full px-6 py-2.5 bg-slate-50/50 dark:bg-slate-900/30 flex items-center justify-between border-dashed border-slate-200 dark:border-slate-700">
<div class="flex items-center gap-8">
<div class="flex flex-col min-w-0">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-0.5">{{ __('Product Name') }}</span>
<span class="text-sm font-bold text-slate-800 dark:text-slate-200 truncate max-w-[180px]">${productName}</span>
</div>
<div class="flex flex-col">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-0.5">{{ __('Machine Stock') }}</span>
<div class="flex items-baseline gap-1">
<span class="text-xl font-black text-slate-800 dark:text-white tracking-tighter leading-none">${item.current_stock}</span>
<span class="text-xs font-black text-slate-400 opacity-30">/</span>
<span class="text-[11px] font-bold text-slate-500 opacity-50">${item.max_stock}</span>
</div>
</div>
</div>
<div class="flex flex-col items-end shrink-0">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-0.5">{{ __('Source') }}</span>
<span class="text-xs font-mono font-bold ${stock <= 0 ? 'text-rose-500' : 'text-cyan-500'} tracking-tighter">${stockText}</span>
</div>
<input type="hidden" name="items[${idx}][product_id]" value="${item.product_id}">
</div>
`;
});
},
async submitReplenishment() {
const form = document.getElementById('replenishmentForm');
const warehouseId = this.fromId;
@ -172,23 +287,7 @@
}
this.loading = true;
try {
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST', body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const result = await response.json();
if (response.ok && result.success) {
this.showReplenishmentModal = false;
window.showToast?.(result.message, 'success');
this.fetchTabData();
} else if (response.status === 422) {
const firstError = Object.values(result.errors || {})[0]?.[0] || '{{ __("Validation failed") }}';
window.showToast?.(firstError, 'error');
} else { window.showToast?.(result.message || '{{ __("System error") }}', 'error'); }
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
finally { this.loading = false; }
form.submit();
},
// 一鍵補貨預覽
@ -227,7 +326,7 @@
available -= this.autoSlots[i].quantity;
}
}
return Math.max(0, Math.min(slot.quantity, available));
return available;
},
removeAutoSlot(idx) {
@ -262,10 +361,7 @@
const data = await res.json();
if (data.success) {
this.showAutoModal = false;
this.autoPreviewLoaded = false;
this.autoSlots = [];
window.showToast?.(data.message, 'success');
this.fetchTabData();
window.location.reload();
} else {
window.showToast?.(data.message || '{{ __("Creation failed") }}', 'error');
}
@ -273,22 +369,54 @@
finally { this.autoLoading = false; }
},
// 狀態推進
// 狀態推進 (顯示 Modal)
async advanceStatus(orderId, newStatus) {
const labels = { prepared: '{{ __("Confirm Prepare") }}', delivering: '{{ __("Start Delivery") }}', completed: '{{ __("Confirm Complete") }}' };
if (!confirm(labels[newStatus] + '?')) return;
this.statusTargetId = orderId;
this.statusTargetValue = newStatus;
const labels = {
prepared: '{{ __("Confirm Prepare") }}',
delivering: '{{ __("Start Delivery") }}',
completed: '{{ __("Confirm Complete") }}'
};
const messages = {
prepared: '{{ __("Are you sure you want to confirm preparation? This will deduct stock from the warehouse.") }}',
delivering: '{{ __("Are you sure you want to start delivery?") }}',
completed: '{{ __("Are you sure you want to confirm completion? This will update the machine stock.") }}'
};
this.statusModalTitle = labels[newStatus];
this.statusModalMessage = messages[newStatus];
this.showStatusModal = true;
},
// 執行狀態更新
async executeStatusUpdate() {
this.loading = true;
try {
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${orderId}/status`, {
const res = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${this.statusTargetId}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: JSON.stringify({ status: newStatus })
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ status: this.statusTargetValue })
});
const data = await res.json();
if (data.success) { window.showToast?.(data.message, 'success'); this.fetchTabData(); }
else { window.showToast?.(data.message || '{{ __("Operation failed") }}', 'error'); }
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
finally { this.loading = false; }
if (res.ok) {
this.showStatusModal = false;
window.location.reload();
} else {
const data = await res.json();
window.showToast?.(data.message || '{{ __("Operation failed") }}', 'error');
}
} catch (e) {
console.error(e);
window.showToast?.('{{ __("System Error") }}', 'error');
} finally {
this.loading = false;
}
},
// 取消
@ -300,14 +428,13 @@
method: 'PATCH',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const data = await res.json();
if (data.success) {
if (res.ok) {
this.showCancelModal = false;
let msg = data.message;
if (data.stock_returned) msg += ' — {{ __("Stock returned to warehouse") }}';
window.showToast?.(msg, 'success');
this.fetchTabData();
} else { window.showToast?.(data.message, 'error'); }
window.location.reload();
} else {
const data = await res.json();
window.showToast?.(data.message, 'error');
}
} catch (e) { console.error(e); window.showToast?.('{{ __("System Error") }}', 'error'); }
finally { this.loading = false; }
},
@ -323,9 +450,13 @@
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: JSON.stringify({ assigned_to: this.assignUserId })
});
const data = await res.json();
if (data.success) { this.showAssignModal = false; window.showToast?.(data.message, 'success'); this.fetchTabData(); }
else { window.showToast?.(data.message, 'error'); }
if (res.ok) {
this.showAssignModal = false;
window.location.reload();
} else {
const data = await res.json();
window.showToast?.(data.message, 'error');
}
} catch (e) { console.error(e); }
finally { this.loading = false; }
},
@ -352,14 +483,14 @@
}
</script>
<div class="space-y-3 pb-10" x-data="replenishmentManager()"
<div class="space-y-4 pb-10" x-data="replenishmentManager()"
@open-details.window="openOrderDetails($event.detail.id)"
@ajax:navigate:replenishments.window.prevent="fetchTabData($event.detail.url)">
{{-- Header --}}
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Replenishment') }}</h1>
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Replenishment') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Create and manage replenishment orders from warehouse to machines') }}</p>
</div>
<div class="flex items-center gap-3">
@ -391,14 +522,14 @@
{{-- Filters --}}
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
<div class="relative group flex-1 md:max-w-[240px]">
<form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8">
<div class="relative group flex-1 sm:flex-none sm:min-w-[240px]">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</span>
<input type="text" name="search_order" value="{{ request('search_order') }}" @keydown.enter.prevent="fetchTabData()" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
</div>
<div class="flex-1 md:max-w-[180px]">
<div class="flex-1 sm:flex-none sm:min-w-[180px]">
<x-searchable-select name="status" :selected="request('status')" :placeholder="__('All Statuses')">
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }} data-title="{{ __('Pending') }}">{{ __('Pending') }}</option>
@ -408,12 +539,14 @@
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
</x-searchable-select>
</div>
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</button>
<button type="button" @click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
</button>
<div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</button>
<button type="button" @click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
</button>
</div>
</form>
@include('admin.warehouses.partials.table-replenishments')
</div>
@ -423,5 +556,6 @@
@include('admin.warehouses.partials.slideover-replenishment-details')
@include('admin.warehouses.partials.modal-replenishment-cancel')
@include('admin.warehouses.partials.modal-replenishment-assign')
@include('admin.warehouses.partials.modal-replenishment-status')
</div>
@endsection

View File

@ -312,7 +312,7 @@
}
</script>
<div class="space-y-3 pb-10" x-data="transferManager()"
<div class="space-y-4 pb-10" x-data="transferManager()"
@open-details.window="openOrderDetails($event.detail.id)"
@ajax:filter.window="fetchTabData()"
@ajax:navigate:transfers.window.prevent="fetchTabData($event.detail.url)">

View File

@ -24,7 +24,13 @@
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50 dark:bg-[#0f172a] antialiased font-sans h-full selection:bg-indigo-100 dark:selection:bg-indigo-900/40" x-data="{ sidebarOpen: false, userDropdownOpen: false }">
<body class="bg-gray-50 dark:bg-[#0f172a] antialiased font-sans h-full selection:bg-indigo-100 dark:selection:bg-indigo-900/40"
x-data="{
sidebarOpen: false,
sidebarCollapsed: localStorage.getItem('sidebarCollapsed') === 'true',
userDropdownOpen: false
}"
x-init="$watch('sidebarCollapsed', value => localStorage.setItem('sidebarCollapsed', value))">
<!-- Option A: Loading Screen -->
<x-loading-screen />
@ -55,7 +61,8 @@
x-cloak></div>
<!-- ========== HEADER ========== -->
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white/80 backdrop-blur-md border-b border-slate-200/50 text-sm py-2.5 sm:py-4 lg:pl-72 dark:bg-[#0f172a]/80 dark:border-slate-800/50 shadow-sm">
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white/80 backdrop-blur-md border-b border-slate-200/50 text-sm py-2.5 sm:py-4 lg:pl-[256px] dark:bg-[#0f172a]/80 dark:border-slate-800/50 shadow-sm transition-all duration-300"
:class="sidebarCollapsed ? 'lg:pl-24' : 'lg:pl-[256px]'">
<nav class="flex basis-full items-center w-full mx-auto px-4 sm:px-6 md:px-8" aria-label="Global">
<div class="mr-5 lg:mr-0 lg:hidden text-center">
<a class="flex items-center gap-x-2 flex-none text-xl font-bold dark:text-white font-display tracking-tight" href="{{ route('admin.dashboard') }}" aria-label="Brand">
@ -210,8 +217,12 @@
<!-- Sidebar -->
<div id="application-sidebar"
class="fixed top-0 left-0 bottom-0 z-[60] w-64 bg-white dark:bg-[#0f172a] pt-5 pb-10 border-e border-slate-200 dark:border-none overflow-y-auto transition-transform duration-300 transform lg:translate-x-0 shadow-xl dark:shadow-2xl"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'">
class="fixed top-0 left-0 bottom-0 z-[60] bg-white dark:bg-[#0f172a] pt-5 pb-10 border-e border-slate-200 dark:border-none overflow-y-auto overflow-x-hidden transition-all duration-300 transform lg:translate-x-0 shadow-xl dark:shadow-2xl flex flex-col"
:class="[
sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
sidebarCollapsed ? 'w-[60px]' : 'w-56'
]">
<!-- Close Button (Mobile) -->
<button type="button" @click="sidebarOpen = false" class="absolute top-4 right-4 text-slate-500 hover:text-slate-800 lg:hidden dark:text-slate-400 dark:hover:text-slate-200">
<span class="sr-only">Close sidebar</span>
@ -220,14 +231,33 @@
</svg>
</button>
<div class="px-6 mb-8 mt-2">
<a class="flex items-center gap-x-2 text-2xl font-bold text-slate-900 dark:text-white font-display tracking-tight whitespace-nowrap" href="{{ route('admin.dashboard') }}" aria-label="Brand">
<img src="{{ asset('S1_cropped_transparent.png') }}" alt="{{ config('app.name') }} Logo" class="w-8 h-8 object-contain">
<!-- Sidebar Header (Logo & Toggle) -->
<div class="mb-8 mt-2 flex items-center h-8 transition-all duration-300 shrink-0 w-full"
:class="sidebarCollapsed ? 'px-[14px]' : 'px-4'">
<!-- Logo (Hidden when collapsed to save space) -->
<a class="flex items-center gap-x-2 text-xl font-bold text-slate-900 dark:text-white font-display tracking-tight whitespace-nowrap overflow-hidden transition-all duration-300"
href="{{ route('admin.dashboard') }}" aria-label="Brand"
:class="sidebarCollapsed ? 'max-w-0 opacity-0' : 'max-w-[200px] opacity-100'">
<img src="{{ asset('S1_cropped_transparent.png') }}" alt="{{ config('app.name') }} Logo" class="w-8 h-8 object-contain shrink-0">
<span>Star<span class="text-cyan-500">Cloud</span></span>
</a>
<div class="flex-1 transition-all duration-300"></div>
<!-- Unified Toggle Button (Inside Sidebar) -->
<button type="button" @click="sidebarCollapsed = !sidebarCollapsed"
x-cloak
class="hidden lg:flex items-center justify-center size-8 rounded-xl bg-slate-50 dark:bg-slate-800/80 text-slate-400 hover:bg-cyan-500 hover:text-white border border-slate-200 dark:border-slate-700 transition-all shrink-0">
<svg class="size-4.5 transition-transform duration-500"
:class="sidebarCollapsed ? 'rotate-180' : ''"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5"/>
</svg>
</button>
</div>
<nav class="px-6 py-2 w-full flex flex-col flex-wrap">
<nav class="px-4 py-2 w-full flex flex-col flex-wrap transition-all duration-300 flex-1" :class="sidebarCollapsed ? 'px-2' : 'px-5'">
<ul class="space-y-1.5">
@include('layouts.partials.sidebar-menu')
</ul>
@ -236,7 +266,8 @@
<!-- End Sidebar -->
<!-- Content -->
<div class="w-full pt-4 lg:pt-5 pb-12 px-4 sm:px-6 md:px-8 lg:pl-72">
<div class="w-full pt-4 lg:pt-5 pb-12 px-4 sm:px-6 md:px-8 transition-all duration-300"
:class="sidebarCollapsed ? 'lg:pl-24' : 'lg:pl-[256px]'">
<x-breadcrumbs class="mb-4 hidden lg:flex" />
<main class="animate-fade-up">
@yield('content')