[FEAT] 遠端指令中心 AJAX 化與介面標準化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 2m10s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 2m10s
1. 將遠端指令中心 (Remote Command Center) 兩大分頁 (操作紀錄、新增指令) 改為 AJAX 異步載入,提升切換速度。 2. 建立抽離的 Blade Partials 結構 (partials/tab-history-index.blade.php, tab-machines-index.blade.php) 以利維護。 3. 實作全域 Loading Bar 與 Luxury Spinner 視覺回饋,確保 AJAX 過程中有明確狀態。 4. 修正庫存管理與指令中心在機台圖片不存在時的 `Undefined array key 0` 錯誤。 5. 標準化操作紀錄搜尋行為:文字搜尋改為 Enter 觸發,日期範圍改為手動按下搜尋按鈕觸發,並新增「重設」功能。 6. 設定 Flatpickr 日期時間選擇器預設時間為 `00:00`。 7. 修正 `stock.blade.php` 中的 PHP 語法錯誤 (括號未閉合)。 8. 同步更新多語系翻譯檔案 (zh_TW, en, ja)。
This commit is contained in:
@@ -15,13 +15,26 @@ class RemoteController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$machines = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get();
|
|
||||||
$selectedMachine = null;
|
$selectedMachine = null;
|
||||||
|
|
||||||
|
// --- 1. 機台列表處理 (New Command Tab) ---
|
||||||
|
$machineQuery = Machine::withCount(['slots'])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc');
|
||||||
|
|
||||||
|
if ($request->filled('search') && $request->input('tab') === 'list') {
|
||||||
|
$search = $request->input('search');
|
||||||
|
$machineQuery->where(function ($q) use ($search) {
|
||||||
|
$q->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('serial_no', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$machines = $machineQuery->paginate($request->input('per_page', 10), ['*'], 'machine_page');
|
||||||
|
|
||||||
|
// --- 2. 歷史紀錄處理 (Operation Records Tab) ---
|
||||||
$historyQuery = RemoteCommand::where('command_type', '!=', 'reload_stock')
|
$historyQuery = RemoteCommand::where('command_type', '!=', 'reload_stock')
|
||||||
->with(['machine', 'user']);
|
->with(['machine', 'user']);
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search') && ($request->input('tab') === 'history' || !$request->has('tab'))) {
|
||||||
$search = $request->input('search');
|
$search = $request->input('search');
|
||||||
$historyQuery->where(function ($q) use ($search) {
|
$historyQuery->where(function ($q) use ($search) {
|
||||||
$q->whereHas('machine', function ($mq) use ($search) {
|
$q->whereHas('machine', function ($mq) use ($search) {
|
||||||
@@ -59,17 +72,35 @@ class RemoteController extends Controller
|
|||||||
$historyQuery->where('status', $request->input('status'));
|
$historyQuery->where('status', $request->input('status'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$history = $historyQuery->latest()->paginate($request->input('per_page', 10));
|
$history = $historyQuery->latest()->paginate($request->input('per_page', 10), ['*'], 'history_page');
|
||||||
|
|
||||||
|
// --- 3. 特定機台詳情處理 ---
|
||||||
if ($request->has('machine_id')) {
|
if ($request->has('machine_id')) {
|
||||||
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
|
$selectedMachine = Machine::with([
|
||||||
|
'slots.product',
|
||||||
|
'commands' => function ($query) {
|
||||||
$query->where('command_type', '!=', 'reload_stock')
|
$query->where('command_type', '!=', 'reload_stock')
|
||||||
->latest()
|
->latest()
|
||||||
->limit(5);
|
->limit(5);
|
||||||
}])->find($request->machine_id);
|
}
|
||||||
|
])->find($request->machine_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 4. AJAX 回應處理 ---
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
|
if ($request->has('tab')) {
|
||||||
|
$tab = $request->input('tab');
|
||||||
|
$viewPath = $tab === 'list' ? 'admin.remote.partials.tab-machines-index' : 'admin.remote.partials.tab-history-index';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'html' => view($viewPath, [
|
||||||
|
'machines' => $machines,
|
||||||
|
'history' => $history,
|
||||||
|
])->render()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'machine' => $selectedMachine,
|
'machine' => $selectedMachine,
|
||||||
@@ -142,7 +173,8 @@ class RemoteController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function stock(Request $request)
|
public function stock(Request $request)
|
||||||
{
|
{
|
||||||
$machines = Machine::withCount([
|
// 1. 機台查詢與分頁
|
||||||
|
$machineQuery = Machine::withCount([
|
||||||
'slots as slots_count',
|
'slots as slots_count',
|
||||||
'slots as low_stock_count' => function ($query) {
|
'slots as low_stock_count' => function ($query) {
|
||||||
$query->where('stock', '<=', 5);
|
$query->where('stock', '<=', 5);
|
||||||
@@ -152,10 +184,22 @@ class RemoteController extends Controller
|
|||||||
->where('expiry_date', '<=', now()->addDays(7))
|
->where('expiry_date', '<=', now()->addDays(7))
|
||||||
->where('expiry_date', '>=', now()->startOfDay());
|
->where('expiry_date', '>=', now()->startOfDay());
|
||||||
}
|
}
|
||||||
])->orderBy('last_heartbeat_at', 'desc')->orderBy('id', 'desc')->get();
|
]);
|
||||||
|
|
||||||
|
if ($request->filled('machine_search')) {
|
||||||
|
$ms = $request->input('machine_search');
|
||||||
|
$machineQuery->where(function ($q) use ($ms) {
|
||||||
|
$q->where('name', 'like', "%{$ms}%")
|
||||||
|
->orWhere('serial_no', 'like', "%{$ms}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$machines = $machineQuery->orderBy('last_heartbeat_at', 'desc')
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->paginate($request->input('per_page', 10), ['*'], 'machine_page');
|
||||||
|
|
||||||
|
// 2. 歷史紀錄查詢與分頁
|
||||||
$historyQuery = RemoteCommand::with(['machine', 'user']);
|
$historyQuery = RemoteCommand::with(['machine', 'user']);
|
||||||
|
|
||||||
$historyQuery->where('command_type', 'reload_stock');
|
$historyQuery->where('command_type', 'reload_stock');
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
@@ -191,15 +235,31 @@ class RemoteController extends Controller
|
|||||||
$historyQuery->where('status', $request->input('status'));
|
$historyQuery->where('status', $request->input('status'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$history = $historyQuery->latest()->paginate($request->input('per_page', 10));
|
$history = $historyQuery->latest()->paginate($request->input('per_page', 10), ['*'], 'history_page');
|
||||||
|
|
||||||
|
// 3. AJAX 回傳處理
|
||||||
|
if ($request->boolean('_ajax')) {
|
||||||
|
$tab = $request->input('tab', 'history');
|
||||||
|
if ($tab === 'machines') {
|
||||||
|
return response()->view('admin.remote.partials.tab-machines', [
|
||||||
|
'machines' => $machines,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return response()->view('admin.remote.partials.tab-history', [
|
||||||
|
'history' => $history,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$selectedMachine = null;
|
$selectedMachine = null;
|
||||||
if ($request->has('machine_id')) {
|
if ($request->has('machine_id')) {
|
||||||
$selectedMachine = Machine::with(['slots.product', 'commands' => function($query) {
|
$selectedMachine = Machine::with([
|
||||||
|
'slots.product',
|
||||||
|
'commands' => function ($query) {
|
||||||
$query->where('command_type', 'reload_stock')
|
$query->where('command_type', 'reload_stock')
|
||||||
->latest()
|
->latest()
|
||||||
->limit(50);
|
->limit(50);
|
||||||
}])->find($request->machine_id);
|
}
|
||||||
|
])->find($request->machine_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('admin.remote.stock', [
|
return view('admin.remote.stock', [
|
||||||
|
|||||||
@@ -1154,5 +1154,6 @@
|
|||||||
"Publish Time": "Publish Time",
|
"Publish Time": "Publish Time",
|
||||||
"Expired Time": "Expired Time",
|
"Expired Time": "Expired Time",
|
||||||
"Inventory synced with machine": "Inventory synced with machine",
|
"Inventory synced with machine": "Inventory synced with machine",
|
||||||
"Failed to load tab content": "Failed to load tab content"
|
"Failed to load tab content": "Failed to load tab content",
|
||||||
|
"No machines found": "No machines found"
|
||||||
}
|
}
|
||||||
@@ -1153,5 +1153,6 @@
|
|||||||
"Publish Time": "公開時間",
|
"Publish Time": "公開時間",
|
||||||
"Expired Time": "終了時間",
|
"Expired Time": "終了時間",
|
||||||
"Inventory synced with machine": "在庫が機体と同期されました",
|
"Inventory synced with machine": "在庫が機体と同期されました",
|
||||||
"Failed to load tab content": "タブコンテンツの読み込みに失敗しました"
|
"Failed to load tab content": "タブコンテンツの読み込みに失敗しました",
|
||||||
|
"No machines found": "マシンが見つかりません"
|
||||||
}
|
}
|
||||||
@@ -1159,5 +1159,6 @@
|
|||||||
"Publish Time": "發布時間",
|
"Publish Time": "發布時間",
|
||||||
"Expired Time": "下架時間",
|
"Expired Time": "下架時間",
|
||||||
"Inventory synced with machine": "庫存已與機台同步",
|
"Inventory synced with machine": "庫存已與機台同步",
|
||||||
"Failed to load tab content": "載入分頁內容失敗"
|
"Failed to load tab content": "載入分頁內容失敗",
|
||||||
|
"No machines found": "未找到機台"
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
// 預設為 history,篩選條件存在時也維持 history
|
// 預設為 history,篩選條件存在時也維持 history
|
||||||
history: @js($history),
|
history: @js($history),
|
||||||
loading: false,
|
loading: false,
|
||||||
|
tabLoading: null,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
|
||||||
// App Config & Meta
|
// App Config & Meta
|
||||||
@@ -67,17 +68,153 @@
|
|||||||
note: '',
|
note: '',
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (initialMachineId) {
|
|
||||||
const machine = this.machines.find(m => m.id == initialMachineId);
|
|
||||||
if (machine) {
|
|
||||||
await this.selectMachine(machine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for machine data changes to rebuild slot select
|
// Watch for machine data changes to rebuild slot select
|
||||||
this.$watch('selectedMachine.slots', () => {
|
this.$watch('selectedMachine.slots', () => {
|
||||||
this.$nextTick(() => this.updateSlotSelect());
|
this.$nextTick(() => this.updateSlotSelect());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 首次載入時綁定分頁連結
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bindPaginationLinks(this.$refs.historyContent, 'history');
|
||||||
|
this.bindPaginationLinks(this.$refs.listContent, 'list');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同步頂部進度條
|
||||||
|
this.$watch('tabLoading', (val) => {
|
||||||
|
const bar = document.getElementById('top-loading-bar');
|
||||||
|
if (bar) {
|
||||||
|
if (val) bar.classList.add('loading');
|
||||||
|
else bar.classList.remove('loading');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initialMachineId) {
|
||||||
|
const machine = this.machines.data.find(m => m.id == initialMachineId);
|
||||||
|
if (machine) {
|
||||||
|
await this.selectMachine(machine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 攔截分頁連結與下拉切換 (與庫存模組一致的強健版本)
|
||||||
|
bindPaginationLinks(container, tab) {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 處理連結
|
||||||
|
container.querySelectorAll('a[href]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href');
|
||||||
|
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(href, window.location.origin);
|
||||||
|
const pageKey = (tab === 'list') ? 'machine_page' : 'history_page';
|
||||||
|
|
||||||
|
// 確保只有分頁連結被攔截
|
||||||
|
if (!url.searchParams.has(pageKey) || a.closest('td.px-6')) return;
|
||||||
|
|
||||||
|
a.addEventListener('click', (e) => {
|
||||||
|
if (a.title) return; // 略過有 title 的連結 (可能是其他功能)
|
||||||
|
e.preventDefault();
|
||||||
|
const page = url.searchParams.get(pageKey) || 1;
|
||||||
|
const perPage = url.searchParams.get('per_page') || '';
|
||||||
|
let extra = `&${pageKey}=${page}`;
|
||||||
|
if (perPage) extra += `&per_page=${perPage}`;
|
||||||
|
|
||||||
|
const newUrl = new URL(this.appConfig.indexUrl);
|
||||||
|
newUrl.searchParams.set('tab', tab);
|
||||||
|
newUrl.searchParams.set('_ajax', '1');
|
||||||
|
extra.split('&').filter(Boolean).forEach(p => {
|
||||||
|
const [k, v] = p.split('=');
|
||||||
|
newUrl.searchParams.set(k, v);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchTabData(tab, newUrl.toString());
|
||||||
|
});
|
||||||
|
} catch (err) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 處理下拉切換 (每頁筆數或跳頁)
|
||||||
|
container.querySelectorAll('select[onchange]').forEach(sel => {
|
||||||
|
const origOnchange = sel.getAttribute('onchange');
|
||||||
|
sel.removeAttribute('onchange');
|
||||||
|
sel.addEventListener('change', () => {
|
||||||
|
const val = sel.value;
|
||||||
|
const pageKey = (tab === 'list') ? 'machine_page' : 'history_page';
|
||||||
|
try {
|
||||||
|
if (val.startsWith('http') || val.startsWith('/')) {
|
||||||
|
const url = new URL(val, window.location.origin);
|
||||||
|
const page = url.searchParams.get(pageKey) || 1;
|
||||||
|
const perPage = url.searchParams.get('per_page') || '';
|
||||||
|
let extra = `&${pageKey}=${page}`;
|
||||||
|
if (perPage) extra += `&per_page=${perPage}`;
|
||||||
|
|
||||||
|
const newUrl = new URL(this.appConfig.indexUrl);
|
||||||
|
newUrl.searchParams.set('tab', tab);
|
||||||
|
newUrl.searchParams.set('_ajax', '1');
|
||||||
|
extra.split('&').filter(Boolean).forEach(p => {
|
||||||
|
const [k, v] = p.split('=');
|
||||||
|
newUrl.searchParams.set(k, v);
|
||||||
|
});
|
||||||
|
this.fetchTabData(tab, newUrl.toString());
|
||||||
|
} else if (origOnchange && origOnchange.includes('per_page')) {
|
||||||
|
const newUrl = new URL(this.appConfig.indexUrl);
|
||||||
|
newUrl.searchParams.set('tab', tab);
|
||||||
|
newUrl.searchParams.set('_ajax', '1');
|
||||||
|
newUrl.searchParams.set('per_page', val);
|
||||||
|
this.fetchTabData(tab, newUrl.toString());
|
||||||
|
}
|
||||||
|
} catch (err) { }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async searchInTab(tab, clear = false) {
|
||||||
|
const form = event?.target?.closest('form');
|
||||||
|
const url = new URL(this.appConfig.indexUrl);
|
||||||
|
url.searchParams.set('tab', tab);
|
||||||
|
url.searchParams.set('_ajax', '1');
|
||||||
|
|
||||||
|
if (!clear && form) {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
if (value) url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fetchTabData(tab, url.toString());
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchTabData(tab, url) {
|
||||||
|
this.tabLoading = tab;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
const ref = (tab === 'history') ? this.$refs.historyContent : this.$refs.listContent;
|
||||||
|
if (ref) {
|
||||||
|
ref.innerHTML = data.html;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
Alpine.initTree(ref);
|
||||||
|
this.bindPaginationLinks(ref, tab);
|
||||||
|
if (window.HSStaticMethods && window.HSStaticMethods.autoInit) {
|
||||||
|
setTimeout(() => window.HSStaticMethods.autoInit(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update browser URL
|
||||||
|
const browserUrl = new URL(url);
|
||||||
|
browserUrl.searchParams.delete('_ajax');
|
||||||
|
window.history.pushState({}, '', browserUrl.toString());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fetch error:', e);
|
||||||
|
} finally {
|
||||||
|
this.tabLoading = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSlotSelect() {
|
updateSlotSelect() {
|
||||||
@@ -400,244 +537,9 @@
|
|||||||
<!-- History View: Operation Records -->
|
<!-- History View: Operation Records -->
|
||||||
<div x-show="viewMode === 'history'" x-cloak>
|
<div x-show="viewMode === 'history'" x-cloak>
|
||||||
<div class="space-y-6 animate-luxury-in">
|
<div class="space-y-6 animate-luxury-in">
|
||||||
<div class="luxury-card rounded-3xl p-8 overflow-hidden">
|
<div class="luxury-card rounded-3xl p-8 overflow-hidden relative">
|
||||||
<!-- Filters Area -->
|
<div x-ref="historyContent">
|
||||||
<div class="mb-8">
|
@include('admin.remote.partials.tab-history-index')
|
||||||
<form method="GET" action="{{ route('admin.remote.index') }}"
|
|
||||||
class="flex flex-wrap items-center gap-4">
|
|
||||||
<!-- Search Box -->
|
|
||||||
<div class="relative group flex-[1.5] min-w-[200px]">
|
|
||||||
<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-full">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Date Range -->
|
|
||||||
<div class="relative group flex-[2] min-w-[340px]" x-data="{
|
|
||||||
fp: null,
|
|
||||||
startDate: '{{ request('start_date') }}',
|
|
||||||
endDate: '{{ request('end_date') }}'
|
|
||||||
}" x-init="fp = flatpickr($refs.dateRange, {
|
|
||||||
mode: 'range',
|
|
||||||
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
|
|
||||||
locale: 'zh_tw',
|
|
||||||
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
|
|
||||||
onChange: function(selectedDates, dateStr, instance) {
|
|
||||||
if (selectedDates.length === 2) {
|
|
||||||
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
|
||||||
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
|
||||||
} else if (selectedDates.length === 0) {
|
|
||||||
$refs.startDate.value = '';
|
|
||||||
$refs.endDate.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})">
|
|
||||||
<span
|
|
||||||
class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
|
||||||
<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"
|
|
||||||
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="hidden" name="start_date" x-ref="startDate"
|
|
||||||
value="{{ request('start_date') }}">
|
|
||||||
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
|
||||||
<input type="text" x-ref="dateRange"
|
|
||||||
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
|
||||||
placeholder="{{ __('Select Date Range') }}"
|
|
||||||
class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Command Type -->
|
|
||||||
<div class="flex-1 min-w-[160px]">
|
|
||||||
<x-searchable-select name="command_type" :options="[
|
|
||||||
'reboot' => __('Machine Reboot'),
|
|
||||||
'reboot_card' => __('Card Reader Reboot'),
|
|
||||||
'checkout' => __('Remote Settlement'),
|
|
||||||
'lock' => __('Lock Page Lock'),
|
|
||||||
'unlock' => __('Lock Page Unlock'),
|
|
||||||
'change' => __('Remote Change'),
|
|
||||||
'dispense' => __('Remote Dispense'),
|
|
||||||
]" :selected="request('command_type')" :placeholder="__('All Command Types')"
|
|
||||||
:hasSearch="false"
|
|
||||||
onchange="this.form.submit()" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status -->
|
|
||||||
<div class="flex-1 min-w-[160px]">
|
|
||||||
<x-searchable-select name="status" :options="[
|
|
||||||
'pending' => __('Pending'),
|
|
||||||
'sent' => __('Sent'),
|
|
||||||
'success' => __('Success'),
|
|
||||||
'failed' => __('Failed'),
|
|
||||||
'superseded' => __('Superseded'),
|
|
||||||
]" :selected="request('status')" :placeholder="__('All Status')" :hasSearch="false" onchange="this.form.submit()" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button type="submit"
|
|
||||||
class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
|
|
||||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<a href="{{ route('admin.remote.index') }}"
|
|
||||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
|
||||||
<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="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>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
|
|
||||||
<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">
|
|
||||||
{{ __('Machine Information') }}</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-center whitespace-nowrap">
|
|
||||||
{{ __('Creation 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 text-center whitespace-nowrap">
|
|
||||||
{{ __('Picked up 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">
|
|
||||||
{{ __('Command Type') }}</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">
|
|
||||||
{{ __('Operator') }}</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-center">
|
|
||||||
{{ __('Status') }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
||||||
@foreach ($history as $item)
|
|
||||||
<tr
|
|
||||||
class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(@js($item->machine))">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 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 shadow-sm overflow-hidden">
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
|
|
||||||
{{ $item->machine->name }}</div>
|
|
||||||
<div
|
|
||||||
class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
|
|
||||||
{{ $item->machine->serial_no }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span>{{ $item->created_at->format('Y/m/d') }}</span>
|
|
||||||
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s')
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
|
||||||
@if($item->executed_at)
|
|
||||||
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
|
|
||||||
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
|
|
||||||
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s')
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<span class="text-slate-300 dark:text-slate-700">-</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6">
|
|
||||||
<div class="flex flex-col min-w-[200px]">
|
|
||||||
<span
|
|
||||||
class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight"
|
|
||||||
x-text="getCommandName(@js($item->command_type))"></span>
|
|
||||||
<div class="flex flex-col gap-0.5 mt-1">
|
|
||||||
<span x-show="getPayloadDetails(@js($item))"
|
|
||||||
class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
|
|
||||||
x-text="getPayloadDetails(@js($item))"></span>
|
|
||||||
@if($item->note)
|
|
||||||
<span class="text-[10px] text-slate-400 italic pl-1"
|
|
||||||
x-text="translateNote(@js($item->note))"></span>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 whitespace-nowrap">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
|
||||||
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{
|
|
||||||
$item->user ? $item->user->name : __('System') }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex flex-col items-center gap-1.5">
|
|
||||||
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
|
|
||||||
:class="getCommandBadgeClass(@js($item->status))">
|
|
||||||
<div class="w-1.5 h-1.5 rounded-full mr-2" :class="{
|
|
||||||
'bg-amber-500 animate-pulse': @js($item->status) === 'pending',
|
|
||||||
'bg-cyan-500': @js($item->status) === 'sent',
|
|
||||||
'bg-emerald-500': @js($item->status) === 'success',
|
|
||||||
'bg-rose-500': @js($item->status) === 'failed',
|
|
||||||
'bg-slate-400': @js($item->status) === 'superseded'
|
|
||||||
}"></div>
|
|
||||||
<span x-text="getCommandStatus(@js($item->status))"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
@if($history->isEmpty())
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="px-6 py-20 text-center">
|
|
||||||
<div class="flex flex-col items-center gap-3">
|
|
||||||
<div
|
|
||||||
class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
|
|
||||||
<svg class="w-8 h-8" fill="none" stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{
|
|
||||||
__('No records found') }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@endif
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination Area -->
|
|
||||||
<div class="mt-8">
|
|
||||||
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -647,138 +549,9 @@
|
|||||||
<!-- Master View: Machine List -->
|
<!-- Master View: Machine List -->
|
||||||
<div x-show="viewMode === 'list'" x-cloak>
|
<div x-show="viewMode === 'list'" x-cloak>
|
||||||
<div class="space-y-6 animate-luxury-in">
|
<div class="space-y-6 animate-luxury-in">
|
||||||
<div class="luxury-card rounded-3xl p-8 overflow-hidden">
|
<div class="luxury-card rounded-3xl p-8 overflow-hidden relative">
|
||||||
<!-- Filters Area -->
|
<div x-ref="listContent">
|
||||||
<div class="flex items-center justify-between mb-8">
|
@include('admin.remote.partials.tab-machines-index')
|
||||||
<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" x-model="searchQuery" placeholder="{{ __('Search...') }}"
|
|
||||||
class="luxury-input py-2.5 pl-12 pr-6 block w-72">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="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">
|
|
||||||
<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>
|
|
||||||
<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-center">
|
|
||||||
{{ __('Status') }}</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-center">
|
|
||||||
{{ __('Last Communication') }}</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>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
||||||
<template x-for="machine in machines.filter(m =>
|
|
||||||
m.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
m.serial_no.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
)" :key="machine.id">
|
|
||||||
<tr
|
|
||||||
class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(machine)">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
|
|
||||||
<template x-if="machine.image_urls && machine.image_urls[0]">
|
|
||||||
<img :src="machine.image_urls[0]"
|
|
||||||
class="w-full h-full object-cover">
|
|
||||||
</template>
|
|
||||||
<template x-if="!machine.image_urls || !machine.image_urls[0]">
|
|
||||||
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24" stroke-width="1.5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight"
|
|
||||||
x-text="machine.name"></div>
|
|
||||||
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5"
|
|
||||||
x-text="machine.serial_no"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<template x-if="machine.status === 'online' || !machine.status">
|
|
||||||
<div
|
|
||||||
class="flex items-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="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-[0.1em] uppercase">{{
|
|
||||||
__('Online') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="machine.status === 'offline'">
|
|
||||||
<div
|
|
||||||
class="flex items-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-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{
|
|
||||||
__('Offline') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
x-if="machine.status && machine.status !== 'online' && machine.status !== 'offline'">
|
|
||||||
<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="relative flex h-2 w-2">
|
|
||||||
<span
|
|
||||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
|
|
||||||
<span
|
|
||||||
class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{
|
|
||||||
__('Abnormal') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
|
|
||||||
x-text="formatTime(machine.last_heartbeat_at)"></span>
|
|
||||||
<span
|
|
||||||
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5"
|
|
||||||
x-text="machine.last_heartbeat_at ? machine.last_heartbeat_at.split('T')[0] : '--'"></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-right">
|
|
||||||
<button @click="selectMachine(machine)"
|
|
||||||
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="{{ __('Manage') }}">
|
|
||||||
<svg class="size-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>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div x-show="tabLoading === 'history'" x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center"
|
||||||
|
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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">
|
||||||
|
{{ __('Loading Data') }}...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Area -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<form @submit.prevent="searchInTab('history')" class="flex flex-wrap items-center gap-4">
|
||||||
|
<!-- Search Box -->
|
||||||
|
<div class="relative group flex-[1.5] min-w-[200px]">
|
||||||
|
<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-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range -->
|
||||||
|
<div class="relative group flex-[2] min-w-[340px]" x-data="{
|
||||||
|
fp: null,
|
||||||
|
startDate: '{{ request('start_date') }}',
|
||||||
|
endDate: '{{ request('end_date') }}'
|
||||||
|
}" x-init="fp = flatpickr($refs.dateRange, {
|
||||||
|
mode: 'range',
|
||||||
|
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
|
||||||
|
defaultHour: 0,
|
||||||
|
defaultMinute: 0,
|
||||||
|
locale: 'zh_tw',
|
||||||
|
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
|
||||||
|
onChange: function(selectedDates, dateStr, instance) {
|
||||||
|
if (selectedDates.length === 2) {
|
||||||
|
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
||||||
|
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
||||||
|
} else if (selectedDates.length === 0) {
|
||||||
|
$refs.startDate.value = '';
|
||||||
|
$refs.endDate.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})">
|
||||||
|
<span
|
||||||
|
class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||||
|
<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"
|
||||||
|
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="hidden" name="start_date" x-ref="startDate"
|
||||||
|
value="{{ request('start_date') }}">
|
||||||
|
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
||||||
|
<input type="text" x-ref="dateRange"
|
||||||
|
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||||
|
placeholder="{{ __('Select Date Range') }}"
|
||||||
|
class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Command Type -->
|
||||||
|
<div class="flex-[0.8] min-w-[160px]">
|
||||||
|
<x-searchable-select name="command_type" :options="[
|
||||||
|
'reboot' => __('Machine Reboot'),
|
||||||
|
'reboot_card' => __('Card Reader Reboot'),
|
||||||
|
'checkout' => __('Remote Settlement'),
|
||||||
|
'lock' => __('Lock Page Lock'),
|
||||||
|
'unlock' => __('Lock Page Unlock'),
|
||||||
|
'change' => __('Remote Change'),
|
||||||
|
'dispense' => __('Remote Dispense'),
|
||||||
|
]" :selected="request('command_type')" :placeholder="__('All Command Types')"
|
||||||
|
:hasSearch="false"
|
||||||
|
@change="searchInTab('history')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="flex-[0.8] min-w-[160px]">
|
||||||
|
<x-searchable-select name="status" :options="[
|
||||||
|
'pending' => __('Pending'),
|
||||||
|
'sent' => __('Sent'),
|
||||||
|
'success' => __('Success'),
|
||||||
|
'failed' => __('Failed'),
|
||||||
|
'superseded' => __('Superseded'),
|
||||||
|
]" :selected="request('status')" :placeholder="__('All Status')" :hasSearch="false" @change="searchInTab('history')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="searchInTab('history', true)" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
||||||
|
<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="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">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
|
||||||
|
<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">
|
||||||
|
{{ __('Machine Information') }}</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-center whitespace-nowrap">
|
||||||
|
{{ __('Creation 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 text-center whitespace-nowrap">
|
||||||
|
{{ __('Picked up 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">
|
||||||
|
{{ __('Command Type') }}</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">
|
||||||
|
{{ __('Operator') }}</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-center">
|
||||||
|
{{ __('Status') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||||
|
@foreach ($history as $item)
|
||||||
|
<tr
|
||||||
|
class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
<td class="px-6 py-6 cursor-pointer" @click="selectMachine({{ Js::from($item->machine) }})">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 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 shadow-sm overflow-hidden shrink-0">
|
||||||
|
<template x-if="{{ Js::from(!empty($item->machine->image_urls) && isset($item->machine->image_urls[0])) }}">
|
||||||
|
<img src="{{ !empty($item->machine->image_urls) ? $item->machine->image_urls[0] : '' }}"
|
||||||
|
class="w-full h-full object-cover">
|
||||||
|
</template>
|
||||||
|
<template x-if="{{ Js::from(empty($item->machine->image_urls) || !isset($item->machine->image_urls[0])) }}">
|
||||||
|
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
|
||||||
|
{{ $item->machine->name }}</div>
|
||||||
|
<div
|
||||||
|
class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
|
||||||
|
{{ $item->machine->serial_no }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{{ $item->created_at->format('Y/m/d') }}</span>
|
||||||
|
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
||||||
|
@if($item->executed_at)
|
||||||
|
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
|
||||||
|
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
|
||||||
|
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="text-slate-300 dark:text-slate-700">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6">
|
||||||
|
<div class="flex flex-col min-w-[200px]">
|
||||||
|
<span
|
||||||
|
class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight"
|
||||||
|
x-text="getCommandName({{ Js::from($item->command_type) }})"></span>
|
||||||
|
<div class="flex flex-col gap-0.5 mt-1">
|
||||||
|
<span x-show="getPayloadDetails({{ Js::from($item) }})"
|
||||||
|
class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit"
|
||||||
|
x-text="getPayloadDetails({{ Js::from($item) }})"></span>
|
||||||
|
@if($item->note)
|
||||||
|
<span class="text-[10px] text-slate-400 italic pl-1"
|
||||||
|
x-text="translateNote({{ Js::from($item->note) }})"></span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||||
|
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{
|
||||||
|
$item->user ? $item->user->name : __('System') }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-1.5">
|
||||||
|
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
|
||||||
|
:class="getCommandBadgeClass({{ Js::from($item->status) }})">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full mr-2" :class="{
|
||||||
|
'bg-amber-500 animate-pulse': {{ Js::from($item->status) }} === 'pending',
|
||||||
|
'bg-cyan-500': {{ Js::from($item->status) }} === 'sent',
|
||||||
|
'bg-emerald-500': {{ Js::from($item->status) }} === 'success',
|
||||||
|
'bg-rose-500': {{ Js::from($item->status) }} === 'failed',
|
||||||
|
'bg-slate-400': {{ Js::from($item->status) }} === 'superseded'
|
||||||
|
}"></div>
|
||||||
|
<span x-text="getCommandStatus({{ Js::from($item->status) }})"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@if($history->isEmpty())
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="px-6 py-20 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
|
||||||
|
<svg class="w-8 h-8" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{
|
||||||
|
__('No records found') }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Area -->
|
||||||
|
<div class="mt-8">
|
||||||
|
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||||
|
</div>
|
||||||
190
resources/views/admin/remote/partials/tab-history.blade.php
Normal file
190
resources/views/admin/remote/partials/tab-history.blade.php
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
{{-- 庫存操作紀錄 Partial (AJAX 可替換) --}}
|
||||||
|
<!-- Filters Area -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<form method="GET" action="{{ route('admin.remote.stock') }}" class="flex flex-wrap items-center gap-4" @submit.prevent="searchInTab()">
|
||||||
|
<!-- Search Box -->
|
||||||
|
<div class="relative group flex-[1.5] min-w-[200px]">
|
||||||
|
<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') }}" x-model="historySearch"
|
||||||
|
placeholder="{{ __('Search machines...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range -->
|
||||||
|
<div class="relative group flex-[2] min-w-[340px]"
|
||||||
|
x-data="{
|
||||||
|
fp: null,
|
||||||
|
startDate: '{{ request('start_date') }}',
|
||||||
|
endDate: '{{ request('end_date') }}'
|
||||||
|
}"
|
||||||
|
x-init="fp = flatpickr($refs.dateRange, {
|
||||||
|
mode: 'range',
|
||||||
|
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
|
||||||
|
defaultHour: 0,
|
||||||
|
defaultMinute: 0,
|
||||||
|
locale: 'zh_tw',
|
||||||
|
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
|
||||||
|
onChange: function(selectedDates, dateStr, instance) {
|
||||||
|
if (selectedDates.length === 2) {
|
||||||
|
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
||||||
|
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
||||||
|
historyStartDate = $refs.startDate.value;
|
||||||
|
historyEndDate = $refs.endDate.value;
|
||||||
|
} else if (selectedDates.length === 0) {
|
||||||
|
$refs.startDate.value = '';
|
||||||
|
$refs.endDate.value = '';
|
||||||
|
historyStartDate = '';
|
||||||
|
historyEndDate = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})">
|
||||||
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
||||||
|
<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" 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="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
|
||||||
|
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
||||||
|
<input type="text" x-ref="dateRange"
|
||||||
|
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||||
|
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="flex-1 min-w-[160px]" @change="
|
||||||
|
historyStatus = $event.target.value === ' ' ? '' : $event.target.value;
|
||||||
|
searchInTab();
|
||||||
|
">
|
||||||
|
<x-searchable-select
|
||||||
|
name="status"
|
||||||
|
:options="[
|
||||||
|
'pending' => __('Pending'),
|
||||||
|
'sent' => __('Sent'),
|
||||||
|
'success' => __('Success'),
|
||||||
|
'failed' => __('Failed'),
|
||||||
|
'superseded' => __('Superseded'),
|
||||||
|
]"
|
||||||
|
:selected="request('status')"
|
||||||
|
:placeholder="__('All Status')"
|
||||||
|
:hasSearch="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
|
||||||
|
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="historySearch = ''; historyStartDate = ''; historyEndDate = ''; historyStatus = ''; searchInTab()" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
||||||
|
<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="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">
|
||||||
|
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
|
||||||
|
<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">{{ __('Machine Information') }}</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-center whitespace-nowrap">{{ __('Creation 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 text-center whitespace-nowrap">{{ __('Picked up 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">{{ __('Command Type') }}</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">{{ __('Operator') }}</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-center">{{ __('Status') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||||
|
@foreach ($history as $item)
|
||||||
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(@js($item->machine))">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 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 shadow-sm overflow-hidden">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">{{ $item->machine->name }}</div>
|
||||||
|
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $item->machine->serial_no }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{{ $item->created_at->format('Y/m/d') }}</span>
|
||||||
|
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s') }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
||||||
|
@if($item->executed_at)
|
||||||
|
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
|
||||||
|
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
|
||||||
|
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s') }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="text-slate-300 dark:text-slate-700">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6">
|
||||||
|
<div class="flex flex-col min-w-[200px]">
|
||||||
|
<span class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight" x-text="getCommandName(@js($item->command_type))"></span>
|
||||||
|
<div class="flex flex-col gap-0.5 mt-1">
|
||||||
|
<span x-show="getPayloadDetails(@js($item))" class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" x-text="getPayloadDetails(@js($item))"></span>
|
||||||
|
@if($item->note)
|
||||||
|
<span class="text-[10px] text-slate-400 italic pl-1" x-text="translateNote(@js($item->note))"></span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 whitespace-nowrap">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||||
|
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $item->user ? $item->user->name : __('System') }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-1.5">
|
||||||
|
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
|
||||||
|
:class="getCommandBadgeClass(@js($item->status))">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full mr-2"
|
||||||
|
:class="{
|
||||||
|
'bg-amber-500 animate-pulse': @js($item->status) === 'pending',
|
||||||
|
'bg-cyan-500': @js($item->status) === 'sent',
|
||||||
|
'bg-emerald-500': @js($item->status) === 'success',
|
||||||
|
'bg-rose-500': @js($item->status) === 'failed',
|
||||||
|
'bg-slate-400': @js($item->status) === 'superseded'
|
||||||
|
}"></div>
|
||||||
|
<span x-text="getCommandStatus(@js($item->status))"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@if($history->isEmpty())
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="px-6 py-20 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<div class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
|
||||||
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{ __('No records found') }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Area -->
|
||||||
|
<div class="mt-8">
|
||||||
|
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div x-show="tabLoading === 'list'" x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center"
|
||||||
|
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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">
|
||||||
|
{{ __('Loading Data') }}...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Area -->
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<form @submit.prevent="searchInTab('list')" 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">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="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">
|
||||||
|
<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>
|
||||||
|
<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-center">
|
||||||
|
{{ __('Status') }}</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-center">
|
||||||
|
{{ __('Last Communication') }}</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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||||
|
@foreach($machines as $machine)
|
||||||
|
<tr
|
||||||
|
class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
<td class="px-6 py-6 cursor-pointer" @click="selectMachine({{ Js::from($machine) }})">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 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(!empty($machine->image_urls) && isset($machine->image_urls[0]))
|
||||||
|
<img src="{{ $machine->image_urls[0] }}"
|
||||||
|
class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<svg class="w-6 h-6 shrink-0" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
||||||
|
</svg>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
|
||||||
|
{{ $machine->name }}</div>
|
||||||
|
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
|
||||||
|
{{ $machine->serial_no }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
@if($machine->status === 'online' || !$machine->status)
|
||||||
|
<div
|
||||||
|
class="flex items-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="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-[0.1em] uppercase">{{
|
||||||
|
__('Online') }}</span>
|
||||||
|
</div>
|
||||||
|
@elseif($machine->status === 'offline')
|
||||||
|
<div
|
||||||
|
class="flex items-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-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{
|
||||||
|
__('Offline') }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<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="relative flex h-2 w-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
|
||||||
|
<span
|
||||||
|
class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{
|
||||||
|
__('Abnormal') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<template x-data="{ heartbeat: {{ Js::from($machine->last_heartbeat_at) }} }">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
|
||||||
|
x-text="formatTime(heartbeat)"></span>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5"
|
||||||
|
x-text="heartbeat ? heartbeat.split('T')[0] : '--'"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-right">
|
||||||
|
<button @click="selectMachine({{ Js::from($machine) }})"
|
||||||
|
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="{{ __('Manage') }}">
|
||||||
|
<svg class="size-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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Area -->
|
||||||
|
<div class="mt-8">
|
||||||
|
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||||
|
</div>
|
||||||
124
resources/views/admin/remote/partials/tab-machines.blade.php
Normal file
124
resources/views/admin/remote/partials/tab-machines.blade.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<div class="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">
|
||||||
|
<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>
|
||||||
|
<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-center">
|
||||||
|
{{ __('Status') }}</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-center">
|
||||||
|
{{ __('Alerts') }}</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-center">
|
||||||
|
{{ __('Last Sync') }}</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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||||
|
@forelse($machines as $machine)
|
||||||
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||||
|
<td class="px-6 py-6 cursor-pointer" @click="selectMachine({{ Js::from($machine) }})">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 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">
|
||||||
|
@if($machine->image_urls && isset($machine->image_urls[0]))
|
||||||
|
<img src="{{ $machine->image_urls[0] }}" class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
||||||
|
</svg>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
|
||||||
|
{{ $machine->name }}</div>
|
||||||
|
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
|
||||||
|
{{ $machine->serial_no }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
@if($machine->status === 'online' || !$machine->status)
|
||||||
|
<div class="flex items-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="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-[0.1em] uppercase">{{ __('Online') }}</span>
|
||||||
|
</div>
|
||||||
|
@elseif($machine->status === 'offline')
|
||||||
|
<div class="flex items-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-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{ __('Offline') }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
|
||||||
|
<div class="relative flex h-2 w-2">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{ __('Abnormal') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-1.5">
|
||||||
|
@if($machine->low_stock_count > 0)
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-500/10 text-rose-500 text-[10px] font-black border border-rose-500/20 uppercase tracking-widest leading-none shadow-sm shadow-rose-500/5">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
|
||||||
|
{{ $machine->low_stock_count }} {{ __('Low') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if($machine->expiring_soon_count > 0)
|
||||||
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-amber-500/10 text-amber-500 text-[10px] font-black border border-amber-500/20 uppercase tracking-widest leading-none shadow-sm shadow-amber-500/5">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
|
||||||
|
{{ $machine->expiring_soon_count }} {{ __('Expiring') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if(!$machine->low_stock_count && !$machine->expiring_soon_count)
|
||||||
|
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-600 uppercase tracking-[0.1em]">{{ __('All Stable') }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span class="text-sm font-black text-slate-700 dark:text-slate-200"
|
||||||
|
x-text="formatTime({{ Js::from($machine->last_heartbeat_at) }})"></span>
|
||||||
|
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">
|
||||||
|
{{ $machine->last_heartbeat_at ? \Illuminate\Support\Carbon::parse($machine->last_heartbeat_at)->format('Y-m-d') : '--' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-6 text-right">
|
||||||
|
<button @click="selectMachine({{ Js::from($machine) }})"
|
||||||
|
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="{{ __('Manage') }}">
|
||||||
|
<svg class="size-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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="px-6 py-20 text-center">
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<div class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
|
||||||
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="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>
|
||||||
|
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{ __('No machines found') }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 標準化分頁底欄 --}}
|
||||||
|
<div class="mt-8">
|
||||||
|
{{ $machines->appends(request()->except('machine_page'))->links('vendor.pagination.luxury') }}
|
||||||
|
</div>
|
||||||
@@ -8,11 +8,11 @@ window.stockApp = function(initialMachineId) {
|
|||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
selectedMachine: null,
|
selectedMachine: null,
|
||||||
slots: [],
|
slots: [],
|
||||||
viewMode: initialMachineId ? 'detail' : (new URLSearchParams(window.location.search).has('search') || new URLSearchParams(window.location.search).has('page') ? 'history' : 'history'),
|
viewMode: initialMachineId ? 'detail' : 'history',
|
||||||
// 預設為 history,但我們會確保在有搜尋或分頁時維持 history
|
|
||||||
history: @js($history),
|
history: @js($history),
|
||||||
loading: false,
|
loading: false,
|
||||||
updating: false,
|
updating: false,
|
||||||
|
tabLoading: false,
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
@@ -22,13 +22,123 @@ window.stockApp = function(initialMachineId) {
|
|||||||
batch_no: ''
|
batch_no: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 機器搜尋狀態
|
||||||
|
machineSearch: '{{ request('machine_search') }}',
|
||||||
|
historySearch: '{{ request('search') }}',
|
||||||
|
historyStartDate: '{{ request('start_date') }}',
|
||||||
|
historyEndDate: '{{ request('end_date') }}',
|
||||||
|
historyStatus: '{{ request('status') }}',
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (initialMachineId) {
|
if (initialMachineId) {
|
||||||
const machine = this.machines.find(m => m.id == initialMachineId);
|
const machine = this.machines.data.find(m => m.id == initialMachineId);
|
||||||
if (machine) {
|
if (machine) {
|
||||||
await this.selectMachine(machine);
|
await this.selectMachine(machine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 首次載入時綁定分頁連結
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.historyContent) this.bindPaginationLinks(this.$refs.historyContent, 'history');
|
||||||
|
if (this.$refs.machinesContent) this.bindPaginationLinks(this.$refs.machinesContent, 'machines');
|
||||||
|
});
|
||||||
|
// 觸發進度條
|
||||||
|
this.$watch('tabLoading', (val) => {
|
||||||
|
const bar = document.getElementById('top-loading-bar');
|
||||||
|
if (bar) {
|
||||||
|
if (val) bar.classList.add('loading');
|
||||||
|
else bar.classList.remove('loading');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// === AJAX 搜尋/分頁 ===
|
||||||
|
async searchInTab(tab = 'history', extraQuery = '') {
|
||||||
|
this.tabLoading = true;
|
||||||
|
let qs = `_ajax=1&tab=${tab}`;
|
||||||
|
|
||||||
|
if (tab === 'machines') {
|
||||||
|
if (this.machineSearch) qs += `&machine_search=${encodeURIComponent(this.machineSearch)}`;
|
||||||
|
} else {
|
||||||
|
if (this.historySearch) qs += `&search=${encodeURIComponent(this.historySearch)}`;
|
||||||
|
if (this.historyStartDate) qs += `&start_date=${encodeURIComponent(this.historyStartDate)}`;
|
||||||
|
if (this.historyEndDate) qs += `&end_date=${encodeURIComponent(this.historyEndDate)}`;
|
||||||
|
if (this.historyStatus) qs += `&status=${encodeURIComponent(this.historyStatus)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraQuery) qs += extraQuery;
|
||||||
|
|
||||||
|
// 同步 URL(不含 _ajax)
|
||||||
|
const visibleQs = qs.replace(/&?_ajax=1/, '');
|
||||||
|
history.pushState({}, '', `{{ route('admin.remote.stock') }}${visibleQs ? '?' + visibleQs : ''}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`{{ route('admin.remote.stock') }}?${qs}`,
|
||||||
|
{ headers: { 'X-Requested-With': 'XMLHttpRequest' } }
|
||||||
|
);
|
||||||
|
const html = await res.text();
|
||||||
|
const ref = (tab === 'machines') ? this.$refs.machinesContent : this.$refs.historyContent;
|
||||||
|
if (ref) {
|
||||||
|
ref.innerHTML = html;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
Alpine.initTree(ref);
|
||||||
|
this.bindPaginationLinks(ref, tab);
|
||||||
|
if (window.HSStaticMethods) {
|
||||||
|
setTimeout(() => window.HSStaticMethods.autoInit(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Search failed:', e);
|
||||||
|
window.dispatchEvent(new CustomEvent('toast', { detail: { message: '{{ __('Failed to load content') }}', type: 'error' } }));
|
||||||
|
} finally {
|
||||||
|
this.tabLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 攔截分頁連結
|
||||||
|
bindPaginationLinks(container, tab) {
|
||||||
|
if (!container) return;
|
||||||
|
container.querySelectorAll('a[href]').forEach(a => {
|
||||||
|
const href = a.getAttribute('href');
|
||||||
|
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
|
||||||
|
try {
|
||||||
|
const url = new URL(href, window.location.origin);
|
||||||
|
const pageKey = (tab === 'machines') ? 'machine_page' : 'history_page';
|
||||||
|
if (!url.searchParams.has(pageKey) || a.closest('td.px-6')) return;
|
||||||
|
|
||||||
|
a.addEventListener('click', (e) => {
|
||||||
|
if (a.title) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const page = url.searchParams.get(pageKey) || 1;
|
||||||
|
const perPage = url.searchParams.get('per_page') || '';
|
||||||
|
let extra = `&${pageKey}=${page}`;
|
||||||
|
if (perPage) extra += `&per_page=${perPage}`;
|
||||||
|
this.searchInTab(tab, extra);
|
||||||
|
});
|
||||||
|
} catch (err) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll('select[onchange]').forEach(sel => {
|
||||||
|
const origOnchange = sel.getAttribute('onchange');
|
||||||
|
sel.removeAttribute('onchange');
|
||||||
|
sel.addEventListener('change', () => {
|
||||||
|
const val = sel.value;
|
||||||
|
const pageKey = (tab === 'machines') ? 'machine_page' : 'history_page';
|
||||||
|
try {
|
||||||
|
if (val.startsWith('http') || val.startsWith('/')) {
|
||||||
|
const url = new URL(val, window.location.origin);
|
||||||
|
const page = url.searchParams.get(pageKey) || 1;
|
||||||
|
const perPage = url.searchParams.get('per_page') || '';
|
||||||
|
let extra = `&${pageKey}=${page}`;
|
||||||
|
if (perPage) extra += `&per_page=${perPage}`;
|
||||||
|
this.searchInTab(tab, extra);
|
||||||
|
} else if (origOnchange && origOnchange.includes('per_page')) {
|
||||||
|
this.searchInTab(tab, `&per_page=${val}`);
|
||||||
|
}
|
||||||
|
} catch (err) { }
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async selectMachine(machine) {
|
async selectMachine(machine) {
|
||||||
@@ -229,8 +339,7 @@ window.stockApp = function(initialMachineId) {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-2 pb-20"
|
<div class="space-y-2 pb-20" x-data="stockApp('{{ $selectedMachine ? $selectedMachine->id : '' }}')"
|
||||||
x-data="stockApp('{{ $selectedMachine ? $selectedMachine->id : '' }}')"
|
|
||||||
@keydown.escape.window="showEditModal = false">
|
@keydown.escape.window="showEditModal = false">
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
@@ -255,7 +364,8 @@ window.stockApp = function(initialMachineId) {
|
|||||||
|
|
||||||
<!-- Tab Navigation (Only visible when not in specific machine detail) -->
|
<!-- Tab Navigation (Only visible when not in specific machine detail) -->
|
||||||
<template x-if="viewMode !== 'detail'">
|
<template x-if="viewMode !== 'detail'">
|
||||||
<div class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50">
|
<div
|
||||||
|
class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50">
|
||||||
<button @click="viewMode = 'history'"
|
<button @click="viewMode = 'history'"
|
||||||
:class="viewMode === 'history' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
:class="viewMode === 'history' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
||||||
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
||||||
@@ -274,189 +384,39 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<!-- History View: Operation Records -->
|
<!-- History View: Operation Records -->
|
||||||
<div x-show="viewMode === 'history'" x-cloak>
|
<div x-show="viewMode === 'history'" x-cloak>
|
||||||
<div class="space-y-6 animate-luxury-in">
|
<div class="space-y-6 animate-luxury-in">
|
||||||
<div class="luxury-card rounded-3xl p-8 overflow-hidden">
|
<div class="luxury-card rounded-3xl p-8 overflow-hidden relative">
|
||||||
<!-- Filters Area -->
|
<!-- Spinner Overlay -->
|
||||||
<div class="mb-8">
|
<div x-show="tabLoading" x-transition:enter="transition ease-out duration-300"
|
||||||
<form method="GET" action="{{ route('admin.remote.stock') }}" class="flex flex-wrap items-center gap-4">
|
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||||
<!-- Search Box -->
|
x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100"
|
||||||
<div class="relative group flex-[1.5] min-w-[200px]">
|
x-transition:leave-end="opacity-0"
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center"
|
||||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
x-cloak>
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
|
||||||
stroke-linejoin="round">
|
<div
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin">
|
||||||
<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-full">
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin"
|
||||||
<!-- Date Range -->
|
style="animation-duration: 3s; direction: reverse;"></div>
|
||||||
<div class="relative group flex-[2] min-w-[340px]"
|
<div class="relative w-8 h-8 flex items-center justify-center">
|
||||||
x-data="{
|
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor"
|
||||||
fp: null,
|
viewBox="0 0 24 24">
|
||||||
startDate: '{{ request('start_date') }}',
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||||
endDate: '{{ request('end_date') }}'
|
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" />
|
||||||
}"
|
|
||||||
x-init="fp = flatpickr($refs.dateRange, {
|
|
||||||
mode: 'range',
|
|
||||||
dateFormat: 'Y-m-d H:i', enableTime: true, time_24hr: true,
|
|
||||||
locale: 'zh_tw',
|
|
||||||
defaultDate: startDate && endDate ? [startDate, endDate] : (startDate ? [startDate] : []),
|
|
||||||
onChange: function(selectedDates, dateStr, instance) {
|
|
||||||
if (selectedDates.length === 2) {
|
|
||||||
$refs.startDate.value = instance.formatDate(selectedDates[0], 'Y-m-d H:i');
|
|
||||||
$refs.endDate.value = instance.formatDate(selectedDates[1], 'Y-m-d H:i');
|
|
||||||
} else if (selectedDates.length === 0) {
|
|
||||||
$refs.startDate.value = '';
|
|
||||||
$refs.endDate.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})">
|
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10 text-slate-400 group-focus-within:text-cyan-500 transition-colors">
|
|
||||||
<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" 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="hidden" name="start_date" x-ref="startDate" value="{{ request('start_date') }}">
|
|
||||||
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
|
||||||
<input type="text" x-ref="dateRange"
|
|
||||||
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
|
||||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Command Type -->
|
|
||||||
<!-- Status -->
|
|
||||||
<div class="flex-1 min-w-[160px]">
|
|
||||||
<x-searchable-select
|
|
||||||
name="status"
|
|
||||||
:options="[
|
|
||||||
'pending' => __('Pending'),
|
|
||||||
'sent' => __('Sent'),
|
|
||||||
'success' => __('Success'),
|
|
||||||
'failed' => __('Failed'),
|
|
||||||
'superseded' => __('Superseded'),
|
|
||||||
]"
|
|
||||||
:selected="request('status')"
|
|
||||||
:placeholder="__('All Status')"
|
|
||||||
:hasSearch="false"
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-600 text-white hover:bg-cyan-500 shadow-lg shadow-cyan-500/20 transition-all active:scale-95">
|
|
||||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
|
||||||
</button>
|
|
||||||
<a href="{{ route('admin.remote.stock') }}" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
|
||||||
<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="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>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-left border-separate border-spacing-y-0 text-sm">
|
|
||||||
<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">{{ __('Machine Information') }}</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-center whitespace-nowrap">{{ __('Creation 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 text-center whitespace-nowrap">{{ __('Picked up 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">{{ __('Command Type') }}</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">{{ __('Operator') }}</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-center">{{ __('Status') }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
||||||
@foreach ($history as $item)
|
|
||||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(@js($item->machine))">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 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 shadow-sm overflow-hidden">
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">{{ $item->machine->name }}</div>
|
|
||||||
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ $item->machine->serial_no }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p
|
||||||
</td>
|
class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">
|
||||||
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
{{ __('Loading Data') }}...</p>
|
||||||
<div class="flex flex-col">
|
|
||||||
<span>{{ $item->created_at->format('Y/m/d') }}</span>
|
|
||||||
<span class="text-[15px] font-bold text-slate-500 dark:text-slate-400">{{ $item->created_at->format('H:i:s') }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 font-mono text-xs font-black text-slate-400 tracking-widest whitespace-nowrap text-center">
|
|
||||||
@if($item->executed_at)
|
|
||||||
<div class="flex flex-col text-cyan-600/80 dark:text-cyan-400/60">
|
|
||||||
<span>{{ $item->executed_at->format('Y/m/d') }}</span>
|
|
||||||
<span class="text-[15px] font-bold">{{ $item->executed_at->format('H:i:s') }}</span>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<span class="text-slate-300 dark:text-slate-700">-</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6">
|
|
||||||
<div class="flex flex-col min-w-[200px]">
|
|
||||||
<span class="text-sm font-black text-slate-700 dark:text-slate-300 tracking-tight" x-text="getCommandName(@js($item->command_type))"></span>
|
|
||||||
<div class="flex flex-col gap-0.5 mt-1">
|
|
||||||
<span x-show="getPayloadDetails(@js($item))" class="text-[11px] font-bold text-cyan-600 dark:text-cyan-400/80 bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10 w-fit" x-text="getPayloadDetails(@js($item))"></span>
|
|
||||||
@if($item->note)
|
|
||||||
<span class="text-[10px] text-slate-400 italic pl-1" x-text="translateNote(@js($item->note))"></span>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 whitespace-nowrap">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-6 h-6 rounded-full bg-cyan-500/10 flex items-center justify-center text-[10px] font-black text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
|
||||||
{{ mb_substr($item->user ? $item->user->name : __('System'), 0, 1) }}
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $item->user ? $item->user->name : __('System') }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex flex-col items-center gap-1.5">
|
|
||||||
<div class="inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-black uppercase tracking-widest shadow-sm"
|
|
||||||
:class="getCommandBadgeClass(@js($item->status))">
|
|
||||||
<div class="w-1.5 h-1.5 rounded-full mr-2"
|
|
||||||
:class="{
|
|
||||||
'bg-amber-500 animate-pulse': @js($item->status) === 'pending',
|
|
||||||
'bg-cyan-500': @js($item->status) === 'sent',
|
|
||||||
'bg-emerald-500': @js($item->status) === 'success',
|
|
||||||
'bg-rose-500': @js($item->status) === 'failed',
|
|
||||||
'bg-slate-400': @js($item->status) === 'superseded'
|
|
||||||
}"></div>
|
|
||||||
<span x-text="getCommandStatus(@js($item->status))"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
@if($history->isEmpty())
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="px-6 py-20 text-center">
|
|
||||||
<div class="flex flex-col items-center gap-3">
|
|
||||||
<div class="w-16 h-16 rounded-full bg-slate-50 dark:bg-slate-900/50 flex items-center justify-center text-slate-200 dark:text-slate-800">
|
|
||||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-400 font-bold tracking-widest uppercase text-xs">{{ __('No records found') }}</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@endif
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Area -->
|
<!-- AJAX 可替換區域 -->
|
||||||
<div class="mt-8">
|
<div
|
||||||
{{ $history->appends(request()->query())->links('vendor.pagination.luxury') }}
|
:class="tabLoading ? 'opacity-30 pointer-events-none transition-opacity duration-300' : 'transition-opacity duration-300'">
|
||||||
|
<div x-ref="historyContent">
|
||||||
|
@include('admin.remote.partials.tab-history', ['history' => $history])
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -466,124 +426,49 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<!-- Master View: Machine List -->
|
<!-- Master View: Machine List -->
|
||||||
<div x-show="viewMode === 'list'" x-cloak>
|
<div x-show="viewMode === 'list'" x-cloak>
|
||||||
<div class="space-y-6 animate-luxury-in">
|
<div class="space-y-6 animate-luxury-in">
|
||||||
<div class="luxury-card rounded-3xl p-8 overflow-hidden">
|
<div class="luxury-card rounded-3xl p-8 overflow-hidden relative">
|
||||||
|
<!-- AJAX Spinner (Machine List) -->
|
||||||
|
<div x-show="tabLoading"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center" 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-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filters Area -->
|
<!-- Filters Area -->
|
||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div class="relative group">
|
<form @submit.prevent="searchInTab('machines')" class="relative group">
|
||||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linejoin="round">
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" x-model="searchQuery"
|
<input type="text" x-model="machineSearch"
|
||||||
placeholder="{{ __('Search...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-72">
|
placeholder="{{ __('Search machines...') }}"
|
||||||
</div>
|
class="luxury-input py-2.5 pl-12 pr-6 block w-72">
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto pb-4">
|
<div :class="tabLoading ? 'opacity-30 pointer-events-none transition-opacity duration-300' : 'transition-opacity duration-300'">
|
||||||
<table class="w-full text-left border-separate border-spacing-y-0 text-sm whitespace-nowrap">
|
<div x-ref="machinesContent">
|
||||||
<thead>
|
@include('admin.remote.partials.tab-machines', ['machines' => $machines])
|
||||||
<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">{{ __('Machine Information') }}</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-center">{{ __('Status') }}</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-center">{{ __('Alerts') }}</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-center">{{ __('Last Sync') }}</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>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
||||||
<template x-for="machine in machines.filter(m =>
|
|
||||||
m.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
m.serial_no.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
)" :key="machine.id">
|
|
||||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
||||||
<td class="px-6 py-6 cursor-pointer" @click="selectMachine(machine)">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 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">
|
|
||||||
<template x-if="machine.image_urls && machine.image_urls[0]">
|
|
||||||
<img :src="machine.image_urls[0]" class="w-full h-full object-cover">
|
|
||||||
</template>
|
|
||||||
<template x-if="!machine.image_urls || !machine.image_urls[0]">
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-5.25v9" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div class="text-[17px] font-black text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight" x-text="machine.name"></div>
|
|
||||||
<div class="text-[11px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5" x-text="machine.serial_no"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<template x-if="machine.status === 'online' || !machine.status">
|
|
||||||
<div class="flex items-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="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-[0.1em] uppercase">{{ __('Online') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="machine.status === 'offline'">
|
|
||||||
<div class="flex items-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-500 dark:text-slate-400 tracking-[0.1em] uppercase">{{ __('Offline') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="machine.status && machine.status !== 'online' && machine.status !== 'offline'">
|
|
||||||
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
|
|
||||||
<div class="relative flex h-2 w-2">
|
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-400 opacity-75"></span>
|
|
||||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-rose-500"></span>
|
|
||||||
</div>
|
|
||||||
<span class="text-[10px] font-black text-rose-600 dark:text-rose-400 tracking-[0.1em] uppercase">{{ __('Abnormal') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex flex-col items-center gap-1.5">
|
|
||||||
<template x-if="machine.low_stock_count > 0">
|
|
||||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-500/10 text-rose-500 text-[10px] font-black border border-rose-500/20 uppercase tracking-widest leading-none shadow-sm shadow-rose-500/5">
|
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse"></span>
|
|
||||||
<span x-text="machine.low_stock_count"></span> {{ __('Low') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template x-if="machine.expiring_soon_count > 0">
|
|
||||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-amber-500/10 text-amber-500 text-[10px] font-black border border-amber-500/20 uppercase tracking-widest leading-none shadow-sm shadow-amber-500/5">
|
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
|
|
||||||
<span x-text="machine.expiring_soon_count"></span> {{ __('Expiring') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template x-if="!machine.low_stock_count && !machine.expiring_soon_count">
|
|
||||||
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-600 uppercase tracking-[0.1em]">{{ __('All Stable') }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span class="text-sm font-black text-slate-700 dark:text-slate-200" x-text="formatTime(machine.last_heartbeat_at)"></span>
|
|
||||||
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5" x-text="machine.last_heartbeat_at ? machine.last_heartbeat_at.split('T')[0] : '--'"></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-6 text-right">
|
|
||||||
<button @click="selectMachine(machine)"
|
|
||||||
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="{{ __('Manage') }}">
|
|
||||||
<svg class="size-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>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -594,26 +479,36 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<div class="space-y-8 animate-luxury-in">
|
<div class="space-y-8 animate-luxury-in">
|
||||||
|
|
||||||
<!-- Machine Header Info -->
|
<!-- Machine Header Info -->
|
||||||
<div class="luxury-card rounded-[2.5rem] p-8 md:p-10 flex flex-col md:flex-row md:items-center justify-between gap-8 border border-slate-200/60 dark:border-slate-800/60">
|
<div
|
||||||
|
class="luxury-card rounded-[2.5rem] p-8 md:p-10 flex flex-col md:flex-row md:items-center justify-between gap-8 border border-slate-200/60 dark:border-slate-800/60">
|
||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-8">
|
||||||
<div class="w-24 h-24 rounded-3xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 overflow-hidden shadow-inner">
|
<div
|
||||||
|
class="w-24 h-24 rounded-3xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 overflow-hidden shadow-inner">
|
||||||
<template x-if="selectedMachine?.image_urls && selectedMachine?.image_urls[0]">
|
<template x-if="selectedMachine?.image_urls && selectedMachine?.image_urls[0]">
|
||||||
<img :src="selectedMachine.image_urls[0]" class="w-full h-full object-cover">
|
<img :src="selectedMachine.image_urls[0]" class="w-full h-full object-cover">
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!selectedMachine?.image_urls || !selectedMachine?.image_urls[0]">
|
<template x-if="!selectedMachine?.image_urls || !selectedMachine?.image_urls[0]">
|
||||||
<svg class="w-12 h-12 stroke-[1.2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-12 h-12 stroke-[1.2]" fill="none" stroke="currentColor"
|
||||||
<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" />
|
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>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 x-text="selectedMachine?.name" class="text-4xl font-black text-slate-800 dark:text-white tracking-tighter leading-tight"></h1>
|
<h1 x-text="selectedMachine?.name"
|
||||||
|
class="text-4xl font-black text-slate-800 dark:text-white tracking-tighter leading-tight">
|
||||||
|
</h1>
|
||||||
<div class="flex items-center gap-4 mt-3">
|
<div class="flex items-center gap-4 mt-3">
|
||||||
<span x-text="selectedMachine?.serial_no" class="px-3 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 text-xs font-mono font-bold uppercase tracking-widest border border-cyan-500/20"></span>
|
<span x-text="selectedMachine?.serial_no"
|
||||||
<div class="flex items-center gap-2 text-slate-400 uppercase tracking-widest text-[10px] font-black">
|
class="px-3 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 text-xs font-mono font-bold uppercase tracking-widest border border-cyan-500/20"></span>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 text-slate-400 uppercase tracking-widest text-[10px] font-black">
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||||
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span x-text="selectedMachine?.location || '{{ __('No Location') }}'"></span>
|
<span x-text="selectedMachine?.location || '{{ __('No Location') }}'"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -622,13 +517,19 @@ window.stockApp = function(initialMachineId) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-5">
|
<div class="flex items-center gap-5">
|
||||||
<div class="px-7 py-4 rounded-[1.75rem] bg-slate-50 dark:bg-slate-800/50 flex flex-col items-center min-w-[120px] border border-slate-100 dark:border-slate-800/50">
|
<div
|
||||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Total Slots') }}</span>
|
class="px-7 py-4 rounded-[1.75rem] bg-slate-50 dark:bg-slate-800/50 flex flex-col items-center min-w-[120px] border border-slate-100 dark:border-slate-800/50">
|
||||||
<span class="text-3xl font-black text-slate-700 dark:text-slate-200" x-text="slots.length"></span>
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{
|
||||||
|
__('Total Slots') }}</span>
|
||||||
|
<span class="text-3xl font-black text-slate-700 dark:text-slate-200"
|
||||||
|
x-text="slots.length"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-7 py-4 rounded-[1.75rem] bg-rose-500/5 border border-rose-500/10 flex flex-col items-center min-w-[120px]">
|
<div
|
||||||
<span class="text-[10px] font-black text-rose-500 uppercase tracking-widest mb-1">{{ __('Low Stock') }}</span>
|
class="px-7 py-4 rounded-[1.75rem] bg-rose-500/5 border border-rose-500/10 flex flex-col items-center min-w-[120px]">
|
||||||
<span class="text-3xl font-black text-rose-600" x-text="slots.filter(s => s != null && s.stock <= 5).length"></span>
|
<span class="text-[10px] font-black text-rose-500 uppercase tracking-widest mb-1">{{ __('Low
|
||||||
|
Stock') }}</span>
|
||||||
|
<span class="text-3xl font-black text-rose-600"
|
||||||
|
x-text="slots.filter(s => s != null && s.stock <= 5).length"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -640,25 +541,36 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-8">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span class="w-3.5 h-3.5 rounded-full bg-rose-500 shadow-lg shadow-rose-500/30"></span>
|
<span class="w-3.5 h-3.5 rounded-full bg-rose-500 shadow-lg shadow-rose-500/30"></span>
|
||||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{ __('Expired') }}</span>
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{
|
||||||
|
__('Expired') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span class="w-3.5 h-3.5 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
|
<span
|
||||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{ __('Warning') }}</span>
|
class="w-3.5 h-3.5 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
|
||||||
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{
|
||||||
|
__('Warning') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<span class="w-3.5 h-3.5 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
|
<span
|
||||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{ __('Normal') }}</span>
|
class="w-3.5 h-3.5 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
|
||||||
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">{{
|
||||||
|
__('Normal') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[500px]">
|
<div
|
||||||
|
class="luxury-card rounded-[2.5rem] p-8 border border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[500px]">
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div x-show="loading" class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500">
|
<div x-show="loading"
|
||||||
|
class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500">
|
||||||
<div class="flex flex-col items-center gap-6">
|
<div class="flex flex-col items-center gap-6">
|
||||||
<div class="w-16 h-16 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
<div
|
||||||
<span class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.3em] ml-2 animate-pulse">{{ __('Loading Cabinet...') }}</span>
|
class="w-16 h-16 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.3em] ml-2 animate-pulse">{{
|
||||||
|
__('Loading Cabinet...') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -668,19 +580,23 @@ window.stockApp = function(initialMachineId) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Slots Grid -->
|
<!-- Slots Grid -->
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-6 relative z-10" x-show="!loading">
|
<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">
|
<template x-for="slot in slots" :key="slot.id">
|
||||||
<div @click="openEdit(slot)"
|
<div @click="openEdit(slot)" :class="getSlotColorClass(slot)"
|
||||||
:class="getSlotColorClass(slot)"
|
|
||||||
class="min-h-[300px] rounded-[2.5rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-500 cursor-pointer group hover:scale-[1.08] hover:-translate-y-3 hover:shadow-2xl active:scale-[0.98] relative">
|
class="min-h-[300px] rounded-[2.5rem] p-5 flex flex-col items-center justify-center border-2 transition-all duration-500 cursor-pointer group hover:scale-[1.08] hover:-translate-y-3 hover:shadow-2xl active:scale-[0.98] relative">
|
||||||
|
|
||||||
<!-- Slot Header (Pinned to top) -->
|
<!-- Slot Header (Pinned to top) -->
|
||||||
<div class="absolute top-4 left-5 right-5 flex justify-between items-center z-20">
|
<div class="absolute top-4 left-5 right-5 flex justify-between items-center z-20">
|
||||||
<div class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
|
<div
|
||||||
<span class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white" x-text="slot.slot_no"></span>
|
class="px-2.5 py-1 rounded-xl bg-slate-900/10 dark:bg-white/10 backdrop-blur-md border border-slate-900/5 dark:border-white/10 flex-shrink-0">
|
||||||
|
<span
|
||||||
|
class="text-xs font-black uppercase tracking-tighter text-slate-800 dark:text-white"
|
||||||
|
x-text="slot.slot_no"></span>
|
||||||
</div>
|
</div>
|
||||||
<template x-if="slot.stock <= 2">
|
<template x-if="slot.stock <= 2">
|
||||||
<div class="px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
<div
|
||||||
|
class="px-2.5 py-1.5 rounded-xl bg-rose-500 text-white text-[9px] font-black uppercase tracking-widest shadow-lg shadow-rose-500/30 animate-pulse whitespace-nowrap select-none">
|
||||||
{{ __('Low') }}
|
{{ __('Low') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -688,14 +604,19 @@ window.stockApp = function(initialMachineId) {
|
|||||||
|
|
||||||
<!-- Product Image -->
|
<!-- Product Image -->
|
||||||
<div class="relative w-20 h-20 mb-4 mt-1">
|
<div class="relative w-20 h-20 mb-4 mt-1">
|
||||||
<div class="absolute inset-0 rounded-[2rem] bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner group-hover:scale-105 transition-transform duration-500 overflow-hidden">
|
<div
|
||||||
|
class="absolute inset-0 rounded-[2rem] bg-white/20 dark:bg-slate-900/40 backdrop-blur-xl border border-white/30 dark:border-white/5 shadow-inner group-hover:scale-105 transition-transform duration-500 overflow-hidden">
|
||||||
<template x-if="slot.product && slot.product.image_url">
|
<template x-if="slot.product && slot.product.image_url">
|
||||||
<img :src="slot.product.image_url" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
<img :src="slot.product.image_url"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!slot.product">
|
<template x-if="!slot.product">
|
||||||
<div class="w-full h-full flex items-center justify-center">
|
<div class="w-full h-full flex items-center justify-center">
|
||||||
<svg class="w-8 h-8 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-8 h-8 opacity-20" fill="none" stroke="currentColor"
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -705,22 +626,27 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<!-- Slot Info -->
|
<!-- Slot Info -->
|
||||||
<div class="text-center w-full space-y-3">
|
<div class="text-center w-full space-y-3">
|
||||||
<template x-if="slot.product">
|
<template x-if="slot.product">
|
||||||
<div class="text-base font-black truncate w-full opacity-90 tracking-tight" x-text="slot.product.name"></div>
|
<div class="text-base font-black truncate w-full opacity-90 tracking-tight"
|
||||||
|
x-text="slot.product.name"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- Stock Level -->
|
<!-- Stock Level -->
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<div class="flex items-baseline gap-1">
|
<div class="flex items-baseline gap-1">
|
||||||
<span class="text-2xl font-black tracking-tighter leading-none" x-text="slot.stock"></span>
|
<span class="text-2xl font-black tracking-tighter leading-none"
|
||||||
|
x-text="slot.stock"></span>
|
||||||
<span class="text-xs font-black opacity-30">/</span>
|
<span class="text-xs font-black opacity-30">/</span>
|
||||||
<span class="text-sm font-bold opacity-50" x-text="slot.max_stock || 10"></span>
|
<span class="text-sm font-bold opacity-50"
|
||||||
|
x-text="slot.max_stock || 10"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Expiry Date -->
|
<!-- Expiry Date -->
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span class="text-base font-black tracking-tight leading-none opacity-80" x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'"></span>
|
<span
|
||||||
|
class="text-base font-black tracking-tight leading-none opacity-80"
|
||||||
|
x-text="slot.expiry_date ? slot.expiry_date.replace(/-/g, '/') : '----/--/--'"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -733,18 +659,14 @@ window.stockApp = function(initialMachineId) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Integrated Edit Modal -->
|
<!-- Integrated Edit Modal -->
|
||||||
<div x-show="showEditModal"
|
<div x-show="showEditModal" class="fixed inset-0 z-[100] overflow-y-auto" style="display: none;"
|
||||||
class="fixed inset-0 z-[100] overflow-y-auto"
|
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
||||||
style="display: none;"
|
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
||||||
x-transition:enter="ease-out duration-300"
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
|
||||||
x-transition:enter-start="opacity-0"
|
|
||||||
x-transition:enter-end="opacity-100"
|
|
||||||
x-transition:leave="ease-in duration-200"
|
|
||||||
x-transition:leave-start="opacity-100"
|
|
||||||
x-transition:leave-end="opacity-0">
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showEditModal = false"></div>
|
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm"
|
||||||
|
@click="showEditModal = false"></div>
|
||||||
|
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||||
|
|
||||||
@@ -754,17 +676,22 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div class="flex justify-between items-center mb-8">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none">
|
<h3
|
||||||
{{ __('Edit Slot') }} <span x-text="selectedSlot?.slot_no || ''" class="text-cyan-500"></span>
|
class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none">
|
||||||
|
{{ __('Edit Slot') }} <span x-text="selectedSlot?.slot_no || ''"
|
||||||
|
class="text-cyan-500"></span>
|
||||||
</h3>
|
</h3>
|
||||||
<template x-if="selectedSlot && selectedSlot.product">
|
<template x-if="selectedSlot && selectedSlot.product">
|
||||||
<p x-text="selectedSlot?.product?.name" class="text-base font-black text-slate-400 uppercase tracking-widest mt-3 ml-0.5"></p>
|
<p x-text="selectedSlot?.product?.name"
|
||||||
|
class="text-base font-black text-slate-400 uppercase tracking-widest mt-3 ml-0.5">
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<button @click="showEditModal = false"
|
<button @click="showEditModal = false"
|
||||||
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800">
|
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors p-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800">
|
||||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||||
|
d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -773,20 +700,26 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Stock Count Widget -->
|
<!-- Stock Count Widget -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-base font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Stock Quantity') }}</label>
|
<label class="text-base font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||||
<div class="flex items-center gap-4 bg-slate-50 dark:bg-slate-900/50 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/50">
|
__('Stock Quantity') }}</label>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-4 bg-slate-50 dark:bg-slate-900/50 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/50">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input type="number" x-model="formData.stock" min="0" :max="selectedSlot ? selectedSlot.max_stock : 99"
|
<input type="number" x-model="formData.stock" min="0"
|
||||||
|
:max="selectedSlot ? selectedSlot.max_stock : 99"
|
||||||
class="w-full bg-transparent border-none p-0 text-5xl font-black text-slate-800 dark:text-white focus:ring-0 placeholder-slate-200">
|
class="w-full bg-transparent border-none p-0 text-5xl font-black text-slate-800 dark:text-white focus:ring-0 placeholder-slate-200">
|
||||||
<div class="text-sm font-black text-slate-400 mt-2 uppercase tracking-wider pl-0.5">
|
<div class="text-sm font-black text-slate-400 mt-2 uppercase tracking-wider pl-0.5">
|
||||||
{{ __('Max Capacity:') }} <span class="text-slate-600 dark:text-slate-300" x-text="selectedSlot?.max_stock || 0"></span>
|
{{ __('Max Capacity:') }} <span class="text-slate-600 dark:text-slate-300"
|
||||||
|
x-text="selectedSlot?.max_stock || 0"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="formData.stock = 0" class="px-5 py-3 rounded-lg bg-white dark:bg-slate-800 text-slate-400 hover:text-rose-500 border border-slate-200 dark:border-slate-700 transition-all text-sm font-black uppercase tracking-widest active:scale-95 shadow-sm">
|
<button @click="formData.stock = 0"
|
||||||
|
class="px-5 py-3 rounded-lg bg-white dark:bg-slate-800 text-slate-400 hover:text-rose-500 border border-slate-200 dark:border-slate-700 transition-all text-sm font-black uppercase tracking-widest active:scale-95 shadow-sm">
|
||||||
{{ __('Clear') }}
|
{{ __('Clear') }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="formData.stock = selectedSlot?.max_stock || 0" class="px-5 py-3 rounded-lg bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white border border-cyan-500/20 transition-all text-sm font-black uppercase tracking-widest active:scale-95 shadow-sm">
|
<button @click="formData.stock = selectedSlot?.max_stock || 0"
|
||||||
|
class="px-5 py-3 rounded-lg bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500 hover:text-white border border-cyan-500/20 transition-all text-sm font-black uppercase tracking-widest active:scale-95 shadow-sm">
|
||||||
{{ __('Max') }}
|
{{ __('Max') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -796,14 +729,15 @@ window.stockApp = function(initialMachineId) {
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<!-- Expiry Date -->
|
<!-- Expiry Date -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Expiry Date') }}</label>
|
<label class="text-sm font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||||
<input type="date" x-model="formData.expiry_date"
|
__('Expiry Date') }}</label>
|
||||||
class="luxury-input w-full py-4 px-5">
|
<input type="date" x-model="formData.expiry_date" class="luxury-input w-full py-4 px-5">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Batch Number -->
|
<!-- Batch Number -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Batch Number') }}</label>
|
<label class="text-sm font-black text-slate-500 uppercase tracking-widest pl-1">{{
|
||||||
|
__('Batch Number') }}</label>
|
||||||
<input type="text" x-model="formData.batch_no" placeholder="B2026-XXXX"
|
<input type="text" x-model="formData.batch_no" placeholder="B2026-XXXX"
|
||||||
class="luxury-input w-full py-4 px-5">
|
class="luxury-input w-full py-4 px-5">
|
||||||
</div>
|
</div>
|
||||||
@@ -813,9 +747,13 @@ window.stockApp = function(initialMachineId) {
|
|||||||
|
|
||||||
<!-- Footer Actions -->
|
<!-- Footer Actions -->
|
||||||
<div class="flex justify-end gap-x-4 mt-10 pt-8 border-t border-slate-100 dark:border-slate-800/50">
|
<div class="flex justify-end gap-x-4 mt-10 pt-8 border-t border-slate-100 dark:border-slate-800/50">
|
||||||
<button type="button" @click="showEditModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
<button type="button" @click="showEditModal = false" class="btn-luxury-ghost px-8">{{
|
||||||
<button type="button" @click="saveChanges()" :disabled="updating" class="btn-luxury-primary px-12 min-w-[160px]">
|
__('Cancel') }}</button>
|
||||||
<div x-show="updating" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin mr-3"></div>
|
<button type="button" @click="saveChanges()" :disabled="updating"
|
||||||
|
class="btn-luxury-primary px-12 min-w-[160px]">
|
||||||
|
<div x-show="updating"
|
||||||
|
class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin mr-3">
|
||||||
|
</div>
|
||||||
<span x-text="updating ? '{{ __('Saving...') }}' : '{{ __('Confirm Changes') }}'"></span>
|
<span x-text="updating ? '{{ __('Saving...') }}' : '{{ __('Confirm Changes') }}'"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -831,17 +769,36 @@ input::-webkit-inner-spin-button {
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number] {
|
input[type=number] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
.hide-scrollbar {
|
||||||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
-ms-overflow-style: none;
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e133; border-radius: 10px; }
|
scrollbar-width: none;
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #cbd5e166; }
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e133;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #cbd5e166;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
Reference in New Issue
Block a user