[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:
parent
de538a0162
commit
9d7fdc9c7f
@ -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
|
||||
|
||||
12
lang/ja.json
12
lang/ja.json
@ -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": "商品名"
|
||||
}
|
||||
@ -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": "商品名稱"
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)">
|
||||
|
||||
@ -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')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user