[FEAT] 標準化廣告管理 AJAX 與公司系統設定功能修復

1. 修復公司系統設定 Alpine.js 響應式屬性未初始化問題,確保 9 項功能開關可正確儲存。
2. 統一公司列表「啟用功能」標籤顏色為標準青色 (Cyan),提升視覺一致性。
3. 實作廣告管理模組 AJAX 分頁與搜尋功能,支援無刷新頁面更新。
4. 於廣告管理模組導入極簡奢華風 Loading 載入遮罩,與機台管理模組保持一致。
5. 更新多語系語系檔 (zh_TW, en, ja),補齊功能開關與載入提示文字。
6. 優化分頁元件 (luxury.blade.php),修復 ajax:navigate 事件觸發邏輯。
This commit is contained in:
sky121113 2026-04-23 12:35:52 +08:00
parent 315c226560
commit 2f86bb90a2
12 changed files with 410 additions and 217 deletions

View File

@ -330,13 +330,15 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
> [!TIP]
> 當遇到未定義的 UI 區塊時,優先參考 `admin.dashboard.blade.php` 的卡片與即時動態實作方式進行衍生。
## 13. 加載指示器規範 (Loading Indicator Pattern)
## 13. AJAX 交互與加載規範 (AJAX Interactions & Loaders)
當使用 AJAX 進行局部頁面刷新或異步操作時,必須使用統一的「極奢華加載遮罩」。
為了提供無刷新的 SPA 式流暢體驗,所有管理後台模組必須遵循統一的 AJAX 交互模式。
### 13.1 極奢華多環加載器 (Premium Multi-Ring Loader)
用於表格遮罩、局部更新或狀態切換。具備雙環旋轉與脈衝核心圖示。
### 13.1 標準 HTML 結構
```html
<!-- Loading Spinner Overlay -->
<!-- Loading Overlay Pattern -->
<div x-show="isLoadingTable"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
@ -362,91 +364,8 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
</div>
```
### 13.2 設計要點
1. **多重動畫**:結合 `animate-spin` (不同速度/方向) 與 `animate-pulse`,營造出高科技且細膩的層次感。
2. **顏色規範**:主要使用 `cyan-500` 作為亮點,搭配 `slate` 系列的半透明背景。
3. **字體細節**:文字使用極細小尺寸 (`text-[10px]`)、極粗字重 (`font-black`) 並大幅度拉開字距 (`tracking-[0.4em]`)。
4. **背景質感**:使用 `backdrop-blur-[1px]``bg-white/40` (或 `dark:bg-slate-900/40`),確保遮罩具有玻璃質感但不完全阻斷視線。
5. **防止閃爍**: 必須加上 `x-cloak` 配合對應的 CSS 樣式。
## 14. 頁面佈局與頁籤間距規範 (Page Layout & Tab Spacing)
為了確保管理後台全站視覺的「高度整合感」與「緊湊性」,頁面頂部結構與頁籤導覽必須遵循以下間距規範。
### 14.1 核心佈局間距 (Core Spacing Scale)
- **根容器間距 (Root Space)**: 頂部 Header、Tabs 與內容區域之間的垂直間距統一使用 **`space-y-2`**。
- **標題與描述 (Title & Subtitle)**: 描述文字應緊貼標題,使用 `mt-1`
- **頁籤外層間距 (Tabs Outer)**: 頁籤容器與 Header 之間應由根容器的 `space-y-2` 自然推開,不額外加 `mt`
### 14.2 頁籤導覽樣式 (Tab Navigation Styles)
- **容器內距**: 使用 `p-1.5` 的圓角膠囊背景。
- **按鈕內距**: 頁籤按鈕統一使用 **`px-8 py-3`**。
- **內容間隙**: 頁籤下方內容區與頁籤列之間,建議不額外添加 `mt-6` 等大間距。若內容區域內部需要層次感,使用 `space-y-3` 即可。
### 14.3 實作範例結構 (Reference Structure)
```html
<div class="space-y-2">
<!-- Header -->
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black">標題</h1>
<p class="text-sm mt-1">描述文字</p>
</div>
</div>
<!-- Tabs Container -->
<div class="flex items-center p-1.5 bg-slate-100 rounded-2xl w-fit border border-slate-200/50">
<button class="px-8 py-3 rounded-xl text-sm font-black active">Tab A</button>
<button class="px-8 py-3 rounded-xl text-sm font-black">Tab B</button>
</div>
<!-- Content Area (AJAX Container) -->
<div class="space-y-3">
<!-- 內容卡片 -->
</div>
</div>
```
### 14.4 統計卡片間距 (Stats Grid Spacing)
- **網格間距**: 頂部統計卡片網格 (Grid) 建議使用 **`gap-4`** (16px),避免使用過大的 `gap-6``gap-8` 導致版面鬆散。
- **卡片內距**: 統計卡片內部統一使用 `p-6`
### 15. Premium AJAX Interactions & Loaders
To maintain a seamless "SPA-like" experience without full-page reloads, all administrative modules must follow these AJAX patterns.
#### 15.1 The Premium Multi-Ring Loader
Used for table overlays and state transitions. It features a dual-spin ring with a pulsing core icon.
```html
<!-- Loading Overlay Pattern -->
<div x-show="isLoading"
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-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] rounded-3xl" x-cloak>
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
<!-- Outer: Fast spin -->
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
<!-- Inner: Slow reverse spin -->
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
<!-- Core: Pulsing Icon -->
<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">Syncing Data...</p>
</div>
```
#### 15.2 Standard fetchPage Pattern
All lists should implement the following `fetchPage` method in their `x-data` and wrap the dynamic content in `#ajax-content-container`.
### 13.2 標準 fetchPage 實作模式
所有清單頁面應在 `x-data` 中實作 `fetchPage` 方法,並將動態內容包裹於 `#ajax-content-container` 中。
```javascript
async fetchPage(url) {
@ -470,7 +389,49 @@ async fetchPage(url) {
}
```
#### 15.3 Triggering AJAX Navigation
- **Forms**: Use `@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())"`.
- **Selects**: Use `onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: ... }, bubbles: true }))"`.
- **Links/Pagination**: Use a delegated `@click` listener on the container or `@ajax:navigate.prevent.stop`.
### 13.3 觸發 AJAX 導航
- **搜尋表單**: 使用 `@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())"`
- **下拉選單**: 使用 `onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: ... }, bubbles: true }))"`
- **分頁/連結**: 在容器上使用代理監聽,或直接調用 `fetchPage(url)`
## 14. 頁面佈局與頁籤間距規範 (Page Layout & Tab Spacing)
為了確保管理後台全站視覺的「高度整合感」與「緊湊性」,頁面頂部結構與頁籤導覽必須遵循以下間距規範。
### 14.1 核心佈局間距 (Core Spacing Scale)
- **根容器間距 (Root Space)**: 頂部 Header、Tabs 與內容區域之間的垂直間距統一使用 **`space-y-2`**。
- **標題與描述 (Title & Subtitle)**: 描述文字應緊貼標題,使用 `mt-1`
- **頁籤外層間距 (Tabs Outer)**: 頁籤容器與 Header 之間應由根容器的 `space-y-2` 自然推開,不額外加 `mt`
### 14.2 頁籤導覽樣式 (Tab Navigation Styles)
- **容器內距**: 使用 `p-1.5` 的圓角膠囊背景。
- **按鈕內距**: 頁籤按鈕統一使用 **`px-8 py-3`**。
- **內容間隙**: 頁籤下方內容區與頁籤列之間,建議不額外添加 `mt-6` 等大間距。若內容區域內部需要層次感,使用 `space-y-3` 即可。
### 14.3 實作範例結構 (Reference Structure)
```html
<div class="space-y-2">
<!-- Header Area -->
<div class="flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black">標題</h1>
<p class="text-sm mt-1">描述文字</p>
</div>
</div>
<!-- Tabs Container -->
<div class="flex items-center p-1.5 bg-slate-100 dark:bg-slate-900/40 rounded-2xl w-fit border border-slate-200/50 dark:border-white/5">
<button class="px-8 py-3 rounded-xl text-sm font-black active">Tab A</button>
<button class="px-8 py-3 rounded-xl text-sm font-black">Tab B</button>
</div>
<!-- Content Area (AJAX Container) -->
<div class="space-y-3">
<!-- 內容卡片 -->
</div>
</div>
```
### 14.4 統計卡片間距 (Stats Grid Spacing)
- **網格間距**: 頂部統計卡片網格 (Grid) 建議使用 **`gap-4`** (16px),避免使用過大的 `gap-6``gap-8` 導致版面鬆散。
- **卡片內距**: 統計卡片內部統一使用 `p-6`

View File

@ -74,6 +74,13 @@ class CompanyController extends Controller
if (isset($validated['settings'])) {
$validated['settings']['enable_material_code'] = filter_var($validated['settings']['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['tax_invoice'] = filter_var($validated['settings']['tax_invoice'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['card_terminal'] = filter_var($validated['settings']['card_terminal'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['scan_pay_esun'] = filter_var($validated['settings']['scan_pay_esun'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['scan_pay_linepay'] = filter_var($validated['settings']['scan_pay_linepay'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['shopping_cart'] = filter_var($validated['settings']['shopping_cart'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['welcome_gift'] = filter_var($validated['settings']['welcome_gift'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['cash_module'] = filter_var($validated['settings']['cash_module'] ?? false, FILTER_VALIDATE_BOOLEAN);
}
DB::transaction(function () use ($validated) {
@ -217,6 +224,7 @@ class CompanyController extends Controller
*/
public function updateSettings(Request $request, Company $company)
{
\Log::info('updateSettings payload:', $request->all());
$settings = $request->input('settings', []);
$formattedSettings = [

View File

@ -460,7 +460,7 @@ return [
'description' => '當機台發生硬體故障(如卡貨、通訊中斷)時即時上報。',
'payload_parameters' => [
'tid' => ['type' => 'integer', 'description' => '貨道編號或任務 ID'],
'error_code' => ['type' => 'string', 'description' => '硬體錯誤代碼,詳見下方 B013 代碼表'],
'error_code' => ['type' => 'string', 'description' => '硬體錯誤代碼 (如 0403 代表卡貨)'],
],
'payload_example' => [
'tid' => 12,
@ -480,7 +480,7 @@ return [
'amount' => ['type' => 'number', 'description' => '交易金額 (create/invoice 時使用)'],
'slot_no' => ['type' => 'string', 'description' => '出貨貨道號碼'],
'invoice_no' => ['type' => 'string', 'description' => '發票號碼 (action 為 invoice 時必填)'],
'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,見狀態定義表)'],
'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,如 success, failed)'],
],
'payload_example' => [
'action' => 'create',
@ -596,7 +596,7 @@ return [
'description' => '機台執行完雲端指令後的主動回報。此回報極為重要,雲端將依此決定交易是否完成或需進行庫存回滾。',
'payload_parameters' => [
'command_id' => ['type' => 'string', 'description' => '對應下發時的指令 ID'],
'result' => ['type' => 'string', 'description' => '執行結果,見狀態定義表'],
'result' => ['type' => 'string', 'description' => '執行結果,僅接受: success 或 failed'],
'stock' => ['type' => 'integer', 'description' => '執行指令後的最終剩餘庫存 (選填,通常出貨指令需帶上)'],
'message' => ['type' => 'string', 'description' => '額外的錯誤訊息 (選填)'],
],
@ -608,39 +608,5 @@ return [
]
]
]
],
'status_definitions' => [
[
'name' => 'B013: 硬體錯誤代碼 (MQTT Error Topic)',
'description' => '當機台透過 machine/{serial_no}/error 上報時error_code 欄位所使用的代碼。',
'codes' => [
['code' => '0402', 'label' => '出貨成功', 'level' => 'info', 'description' => '商品成功掉落至取貨口。'],
['code' => '0403', 'label' => '貨道卡貨', 'level' => 'error', 'description' => '馬達轉動異常或偵測到卡貨,需人工排除。'],
['code' => '0202', 'label' => '貨道缺貨', 'level' => 'warning', 'description' => '偵測到實體貨道已無商品。'],
['code' => '0412', 'label' => '昇降機上升異常', 'level' => 'error', 'description' => '昇降馬達無法正確到達指定位置。'],
['code' => '0415', 'label' => '取貨門異常', 'level' => 'error', 'description' => '取貨門馬達損壞或感應器失效。'],
['code' => '5402', 'label' => '取貨門未關', 'level' => 'warning', 'description' => '取貨逾時或門片未正確閉合。'],
['code' => '5403', 'label' => '昇降系統故障', 'level' => 'error', 'description' => '昇降機通訊中斷或驅動板異常。'],
]
],
[
'name' => '交易與指令執行狀態 (Transaction & Command)',
'description' => '用於 Transaction (dispense) 與 Command ACK 的執行結果判定。',
'codes' => [
['code' => 'success', 'label' => '成功', 'description' => '動作執行圓滿完成。'],
['code' => 'failed', 'label' => '失敗', 'description' => '動作執行失敗,雲端將視情況發起退款或記錄異常。'],
['code' => '1', 'label' => '成功 (數字型態)', 'description' => '部分舊型指令或 B602 使用的成功代碼。'],
['code' => '0', 'label' => '失敗 (數字型態)', 'description' => '部分舊型指令或 B602 使用的失敗代碼。'],
]
],
[
'name' => '機台連線狀態 (Connection Status)',
'description' => '用於 machine/{serial_no}/status 主題,反映機台與 Broker 的連線情況。',
'codes' => [
['code' => 'online', 'label' => '在線', 'description' => '機台已成功連線至 MQTT Broker。'],
['code' => 'offline', 'label' => '離線', 'description' => '機台已斷開連線(通常由 LWT 遺囑訊息觸發)。'],
]
]
]
];

View File

@ -291,6 +291,7 @@
"e.g., Drinks": "e.g., Drinks",
"e.g., Taipei Station": "e.g., Taipei Station",
"e.g., お飲み物": "e.g., O-Nomimono",
"ESUN": "ESUN",
"E.SUN QR Scan": "E.SUN QR Scan",
"E.SUN QR Scan Settings Description": "E.SUN Bank QR Scan Payment Settings",
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID",
@ -315,6 +316,7 @@
"Edit Slot": "Edit Slot",
"Edit Sub Account Role": "Edit Sub Account Role",
"Email": "Email",
"Enabled Features": "Enabled Features",
"Enable": "Enable",
"Enable Material Code": "Enable Material Code",
"Enable Points": "Enable Points",
@ -709,6 +711,7 @@
"Please select a slot": "Please select a slot",
"PNG, JPG up to 2MB": "PNG, JPG up to 2MB",
"PNG, JPG, WEBP up to 10MB": "PNG, JPG, WEBP up to 10MB",
"Points": "Points",
"Point Rules": "Point Rules",
"Point Settings": "Point Settings",
"Points Rule": "Points Rule",

View File

@ -14,8 +14,16 @@
"Coin/Bill Acceptor": "コイン/紙幣機",
"System settings updated successfully.": "システム設定が正常に更新されました。",
"Update Settings": "設定を更新",
"Enabled Features": "有効な機能",
"Material Code": "素材コード",
"Points": "ポイント",
"Electronic Invoice": "電子翻訳",
"Card System": "カード決済システム",
"ESUN": "玉山銀行 (ESUN)",
"LinePay": "LinePay",
"Shopping Cart": "ショッピングカート",
"Welcome Gift": "登録特典",
"Cash Module": "現金決済モジュール",
"Please enter configuration name": "設定名を入力してください",
"Please select a company": "会社を選択してください",
"Warehouse Management": "倉庫管理",

View File

@ -317,6 +317,8 @@
"Edit Slot": "編輯貨道",
"Edit Sub Account Role": "編輯子帳號角色",
"Email": "電子郵件",
"Emblem / Label": "標章 / 標籤",
"Enabled Features": "啟用功能",
"Enable": "啟用",
"Enable Material Code": "啟用物料編號",
"Enable Points": "啟用點數規則",
@ -718,6 +720,7 @@
"PNG, JPG, WEBP up to 10MB": "支援 PNG, JPG, WEBP 格式,且不超過 10MB",
"Point Rules": "點數規則",
"Point Settings": "點數設定",
"Points": "點數功能",
"Points Rule": "點數規則",
"Points Settings": "點數設定",
"Points toggle": "點數開關",
@ -1260,6 +1263,7 @@
"Card System": "刷卡機系統",
"Credit Card / Contactless": "支援信用卡 / 感應支付",
"Scan Pay": "掃碼支付",
"ESUN": "玉山銀行",
"ESUN Scan Pay": "玉山掃碼支付",
"LinePay": "LinePay",
"Other Features": "其他功能",

View File

@ -6,8 +6,17 @@ $baseRoute = 'admin.data-config.advertisements';
@endphp
@section('content')
<div class="space-y-2 pb-20"
<div class="space-y-2 pb-20 relative"
x-data="adManager"
@ajax:navigate.window.prevent="fetchPage($event.detail.url)"
@submit.prevent="if($event.target.method.toLowerCase() === 'get') {
const url = new URL($event.target.action, window.location.origin);
new FormData($event.target).forEach((value, key) => {
if(value.trim()) url.searchParams.set(key, value);
else url.searchParams.delete(key);
});
fetchPage(url.pathname + url.search);
}"
data-ads="{{ json_encode($advertisements->items()) }}"
data-machines="{{ json_encode($machines) }}"
data-all-ads="{{ json_encode($allAds) }}"
@ -59,7 +68,29 @@ $baseRoute = 'admin.data-config.advertisements';
<!-- Tab Contents -->
<div class="mt-6">
<!-- List Tab -->
<div x-show="activeTab === 'list'" class="luxury-card rounded-3xl p-8 animate-luxury-in" x-cloak>
<div x-show="activeTab === 'list'" class="luxury-card rounded-3xl p-8 animate-luxury-in relative" x-cloak>
<!-- Loading Spinner Overlay -->
<div x-show="isLoading"
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-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] rounded-3xl" x-cloak>
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
<div class="relative w-8 h-8 flex items-center justify-center">
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
</div>
</div>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
@ -376,6 +407,7 @@ $baseRoute = 'admin.data-config.advertisements';
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('adManager', () => ({
isLoading: false,
activeTab: 'list',
selectedMachineId: null,
machines: [],
@ -431,6 +463,49 @@ $baseRoute = 'admin.data-config.advertisements';
return this.sequenceAds[this.currentSequenceIndex] || null;
},
async fetchPage(url) {
if (!url || url === window.location.href) return;
this.isLoading = true;
try {
const response = await fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html'
}
});
if (!response.ok) throw new Error('Network response was not ok');
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Update table contents
const newTableBody = doc.querySelector('tbody');
const currentTableBody = document.querySelector('tbody');
if (newTableBody && currentTableBody) {
currentTableBody.innerHTML = newTableBody.innerHTML;
}
// Update pagination
const newPagination = doc.querySelector('.mt-8');
const currentPagination = document.querySelector('.mt-8');
if (newPagination && currentPagination) {
currentPagination.innerHTML = newPagination.innerHTML;
}
// Update URL without reload
window.history.pushState({}, '', url);
} catch (error) {
console.error('Error fetching page:', error);
window.showToast?.('載入失敗,請重試', 'error');
} finally {
this.isLoading = false;
}
},
startSequencePreview(pos) {
if (!this.machineAds[pos] || this.machineAds[pos].length === 0) return;
this.sequenceAds = this.machineAds[pos];

View File

@ -28,7 +28,14 @@
note: '',
settings: {
enable_material_code: false,
enable_points: false
enable_points: false,
tax_invoice: false,
card_terminal: false,
scan_pay_esun: false,
scan_pay_linepay: false,
shopping_cart: false,
welcome_gift: false,
cash_module: false
}
},
openCreateModal() {
@ -40,7 +47,17 @@
warranty_start_date: '', warranty_end_date: '',
software_start_date: '', software_end_date: '',
status: 1, note: '',
settings: { enable_material_code: false, enable_points: false }
settings: {
enable_material_code: false,
enable_points: false,
tax_invoice: false,
card_terminal: false,
scan_pay_esun: false,
scan_pay_linepay: false,
shopping_cart: false,
welcome_gift: false,
cash_module: false
}
};
this.showModal = true;
},
@ -56,7 +73,14 @@
software_end_date: company.software_end_date ? company.software_end_date.substring(0, 10) : '',
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
tax_invoice: company.settings?.tax_invoice || false,
card_terminal: company.settings?.card_terminal || false,
scan_pay_esun: company.settings?.scan_pay_esun || false,
scan_pay_linepay: company.settings?.scan_pay_linepay || false,
shopping_cart: company.settings?.shopping_cart || false,
welcome_gift: company.settings?.welcome_gift || false,
cash_module: company.settings?.cash_module || false
}
};
this.originalStatus = company.status;
@ -85,7 +109,14 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
tax_invoice: company.settings?.tax_invoice || false,
card_terminal: company.settings?.card_terminal || false,
scan_pay_esun: company.settings?.scan_pay_esun || false,
scan_pay_linepay: company.settings?.scan_pay_linepay || false,
shopping_cart: company.settings?.shopping_cart || false,
welcome_gift: company.settings?.welcome_gift || false,
cash_module: company.settings?.cash_module || false
}
};
this.sidebarView = 'detail';
@ -96,7 +127,14 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
tax_invoice: company.settings?.tax_invoice || false,
card_terminal: company.settings?.card_terminal || false,
scan_pay_esun: company.settings?.scan_pay_esun || false,
scan_pay_linepay: company.settings?.scan_pay_linepay || false,
shopping_cart: company.settings?.shopping_cart || false,
welcome_gift: company.settings?.welcome_gift || false,
cash_module: company.settings?.cash_module || false
}
};
this.sidebarView = 'history';
@ -110,7 +148,14 @@
...company,
settings: {
enable_material_code: company.settings?.enable_material_code || false,
enable_points: company.settings?.enable_points || false
enable_points: company.settings?.enable_points || false,
tax_invoice: company.settings?.tax_invoice || false,
card_terminal: company.settings?.card_terminal || false,
scan_pay_esun: company.settings?.scan_pay_esun || false,
scan_pay_linepay: company.settings?.scan_pay_linepay || false,
shopping_cart: company.settings?.shopping_cart || false,
welcome_gift: company.settings?.welcome_gift || false,
cash_module: company.settings?.cash_module || false
}
};
this.showHistoryModal = true;
@ -230,7 +275,7 @@
</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>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<div id="ajax-content-container"
@ -343,13 +388,13 @@
<td class="px-6 py-6 text-center">
<div class="flex flex-col items-center gap-2">
<div class="flex items-center gap-2 min-w-[100px] justify-center">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest w-8 text-right">{{ __('Original:') }}</span>
<span class="text-[12px] font-bold text-slate-400 uppercase tracking-widest w-8 text-right">{{ __('Original:') }}</span>
<span class="px-2 py-0.5 rounded text-xs font-bold {{ $company->original_type === 'buyout' ? 'bg-amber-500/10 text-amber-600' : 'bg-blue-500/10 text-blue-600' }} uppercase tracking-wider">
{{ __($company->original_type === 'buyout' ? 'Buyout' : 'Lease') }}
</span>
</div>
<div class="flex items-center gap-2 min-w-[100px] justify-center">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest w-8 text-right">{{ __('Current:') }}</span>
<span class="text-[12px] font-bold text-slate-400 uppercase tracking-widest w-8 text-right">{{ __('Current:') }}</span>
<span class="px-2 py-0.5 rounded text-xs font-bold {{ $company->current_type === 'buyout' ? 'bg-amber-500/10 text-amber-600 border border-amber-500/20' : 'bg-blue-500/10 text-blue-600 border border-blue-500/20' }} uppercase tracking-wider">
{{ __($company->current_type === 'buyout' ? 'Buyout' : 'Lease') }}
</span>
@ -358,11 +403,11 @@
</td>
<td class="px-6 py-6 text-center py-6">
@if($company->status)
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
<span class="inline-flex items-center px-3 py-1 rounded-full text-[12px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
{{ __('Active') }}
</span>
@else
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">
<span class="inline-flex items-center px-3 py-1 rounded-full text-[12px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">
{{ __('Disabled') }}
</span>
@endif
@ -371,12 +416,12 @@
<div class="flex items-center justify-center gap-x-3">
<div class="flex flex-col items-center">
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $company->users_count }}</span>
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Users') }}</span>
<span class="text-[12px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Users') }}</span>
</div>
<div class="w-px h-6 bg-slate-100 dark:bg-slate-800"></div>
<div class="flex flex-col items-center">
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $company->machines_count }}</span>
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Machines') }}</span>
<span class="text-[12px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Machines') }}</span>
</div>
</div>
</td>
@ -385,10 +430,10 @@
<!-- Contract Period (Only for Lease) -->
@if($company->current_type === 'lease')
<div class="flex items-center gap-3 group/term">
<span class="px-2 py-0.5 rounded-md bg-blue-500/10 text-blue-600 text-[10px] font-black border border-blue-500/20 group-hover/term:bg-blue-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
<span class="px-2.5 py-1 rounded-md bg-blue-500/10 text-blue-600 text-[12px] font-black border border-blue-500/20 group-hover/term:bg-blue-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
{{ __('Contract') }}
</span>
<div class="flex items-center gap-1.5 font-mono text-[11px] font-bold">
<div class="flex items-center gap-1.5 font-mono text-[12px] font-bold">
<span class="text-slate-400">{{ $company->start_date ? $company->start_date->format('Y-m-d') : '--' }}</span>
<span class="text-slate-300">~</span>
<span class="{{ $company->end_date && $company->end_date->isPast() ? 'text-rose-500 shadow-sm shadow-rose-500/10' : 'text-slate-600 dark:text-slate-300' }}">
@ -401,10 +446,10 @@
@if($company->current_type === 'buyout')
<!-- Warranty Period -->
<div class="flex items-center gap-3 group/term">
<span class="px-2 py-0.5 rounded-md bg-amber-500/10 text-amber-600 text-[10px] font-black border border-amber-500/20 group-hover/term:bg-amber-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
<span class="px-2.5 py-1 rounded-md bg-amber-500/10 text-amber-600 text-[12px] font-black border border-amber-500/20 group-hover/term:bg-amber-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
{{ __('Warranty') }}
</span>
<div class="flex items-center gap-1.5 font-mono text-[11px] font-bold">
<div class="flex items-center gap-1.5 font-mono text-[12px] font-bold">
<span class="text-slate-400">{{ $company->warranty_start_date ? $company->warranty_start_date->format('Y-m-d') : '--' }}</span>
<span class="text-slate-300">~</span>
<span class="{{ $company->warranty_end_date && $company->warranty_end_date->isPast() ? 'text-rose-500 shadow-sm shadow-rose-500/10' : 'text-slate-500 dark:text-slate-400' }}">
@ -415,10 +460,10 @@
<!-- Software Period -->
<div class="flex items-center gap-3 group/term">
<span class="px-2 py-0.5 rounded-md bg-purple-500/10 text-purple-600 text-[10px] font-black border border-purple-500/20 group-hover/term:bg-purple-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
<span class="px-2.5 py-1 rounded-md bg-purple-500/10 text-purple-600 text-[12px] font-black border border-purple-500/20 group-hover/term:bg-purple-500 group-hover/term:text-white transition-all tracking-wider whitespace-nowrap min-w-[42px] text-center">
{{ __('Software') }}
</span>
<div class="flex items-center gap-1.5 font-mono text-[11px] font-bold">
<div class="flex items-center gap-1.5 font-mono text-[12px] font-bold">
<span class="text-slate-400">{{ $company->software_start_date ? $company->software_start_date->format('Y-m-d') : '--' }}</span>
<span class="text-slate-300">~</span>
<span class="{{ $company->software_end_date && $company->software_end_date->isPast() ? 'text-rose-500 shadow-sm shadow-rose-500/10' : 'text-slate-500 dark:text-slate-400' }}">
@ -550,34 +595,34 @@
@php $settings = $company->settings ?? []; @endphp
@if(empty($settings) || !in_array(true, array_map(fn($v) => (bool)$v, is_array($settings) ? $settings : ($settings->toArray() ?? [])), true))
<span class="px-2.5 py-1 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-wider">{{ __('None') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-slate-500/10 text-slate-500 dark:text-slate-400 text-[13px] font-bold uppercase tracking-wider">{{ __('None') }}</span>
@else
@if($settings['enable_material_code'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-600 border border-cyan-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Material Code') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Material Code') }}</span>
@endif
@if($settings['enable_points'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-cyan-500/10 text-cyan-600 border border-cyan-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Points') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Points') }}</span>
@endif
@if($settings['tax_invoice'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-emerald-500/10 text-emerald-600 border border-emerald-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Electronic Invoice') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Electronic Invoice') }}</span>
@endif
@if($settings['card_terminal'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-blue-500/10 text-blue-600 border border-blue-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Card System') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Card System') }}</span>
@endif
@if($settings['scan_pay_esun'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-green-500/10 text-green-600 border border-green-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('ESUN') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('ESUN') }}</span>
@endif
@if($settings['scan_pay_linepay'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-green-500/10 text-green-600 border border-green-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('LinePay') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('LinePay') }}</span>
@endif
@if($settings['shopping_cart'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-purple-500/10 text-purple-600 border border-purple-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Shopping Cart') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Shopping Cart') }}</span>
@endif
@if($settings['welcome_gift'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-rose-500/10 text-rose-600 border border-rose-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Welcome Gift') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Welcome Gift') }}</span>
@endif
@if($settings['cash_module'] ?? false)
<span class="px-2.5 py-1 rounded-md bg-amber-500/10 text-amber-600 border border-amber-500/20 text-[10px] font-black uppercase tracking-wider">{{ __('Cash Module') }}</span>
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Cash Module') }}</span>
@endif
@endif
</div>
@ -586,8 +631,8 @@
<button type="button" @click='openSettingsModal({{ json_encode($company) }})'
class="p-2 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="{{ __('Edit Settings') }}">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="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>
@ -673,7 +718,7 @@
<!-- Business Type Selector -->
<div class="p-4 rounded-2xl bg-slate-50/50 dark:bg-slate-800/50 border border-slate-100 dark:border-slate-700/50 space-y-4">
<label class="text-[11px] font-black text-slate-500 uppercase tracking-widest pl-1">
<label class="text-[12px] font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Business Type') }}
<span class="text-rose-500 ml-0.5">*</span>
</label>
@ -696,11 +741,11 @@
<!-- 編輯模式:顯示原始類型固定值 + 當前類型切換 -->
<div x-show="editing" class="flex flex-col gap-5">
<div class="flex items-center gap-4">
<span class="text-[11px] font-black text-slate-400 uppercase tracking-widest">{{ __('Original Type') }}: <span class="text-rose-500 ml-0.5">*</span></span>
<span class="text-[12px] font-black text-slate-400 uppercase tracking-widest">{{ __('Original Type') }}: <span class="text-rose-500 ml-0.5">*</span></span>
<span class="px-4 py-1.5 bg-slate-100 dark:bg-slate-800 rounded-lg text-xs font-bold uppercase tracking-widest text-slate-600 dark:text-slate-400" x-text="currentCompany.original_type === 'buyout' ? '{{ __('Buyout') }}' : '{{ __('Lease') }}'"></span>
</div>
<div class="flex items-center gap-4">
<span class="text-[11px] font-black text-slate-400 uppercase tracking-widest">{{ __('Current Type') }}: <span class="text-rose-500 ml-0.5">*</span></span>
<span class="text-[12px] font-black text-slate-400 uppercase tracking-widest">{{ __('Current Type') }}: <span class="text-rose-500 ml-0.5">*</span></span>
<div class="flex p-1.5 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 w-fit">
<button type="button" @click="currentCompany.current_type = 'lease'"
:class="currentCompany.current_type === 'lease' ? 'bg-blue-500 text-white shadow-lg shadow-blue-500/20' : 'text-slate-400 hover:text-slate-600'"
@ -761,24 +806,24 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid grid-cols-2 gap-3">
<div class="space-y-2">
<label class="text-[10px] font-black text-amber-600 uppercase tracking-widest pl-1">{{ __('Warranty Start') }}</label>
<label class="text-[12px] font-black text-amber-600 uppercase tracking-widest pl-1">{{ __('Warranty Start') }}</label>
<input type="date" name="warranty_start_date" x-model="currentCompany.warranty_start_date"
class="luxury-input w-full px-2 border-amber-100 dark:border-amber-900/30">
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-amber-600 uppercase tracking-widest pl-1">{{ __('Warranty End') }}</label>
<label class="text-[12px] font-black text-amber-600 uppercase tracking-widest pl-1">{{ __('Warranty End') }}</label>
<input type="date" name="warranty_end_date" x-model="currentCompany.warranty_end_date"
class="luxury-input w-full px-2 border-amber-100 dark:border-amber-900/30">
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-2">
<label class="text-[10px] font-black text-indigo-600 uppercase tracking-widest pl-1">{{ __('Software Start') }}</label>
<label class="text-[12px] font-black text-indigo-600 uppercase tracking-widest pl-1">{{ __('Software Start') }}</label>
<input type="date" name="software_start_date" x-model="currentCompany.software_start_date"
class="luxury-input w-full px-2 border-indigo-100 dark:border-indigo-900/30">
</div>
<div class="space-y-2">
<label class="text-[10px] font-black text-indigo-600 uppercase tracking-widest pl-1">{{ __('Software End') }}</label>
<label class="text-[12px] font-black text-indigo-600 uppercase tracking-widest pl-1">{{ __('Software End') }}</label>
<input type="date" name="software_end_date" x-model="currentCompany.software_end_date"
class="luxury-input w-full px-2 border-indigo-100 dark:border-indigo-900/30">
</div>
@ -795,7 +840,7 @@
<div class="h-6 w-1 bg-emerald-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">
{{ __('Initial Admin Account') }}
<span class="text-[10px] text-slate-400 font-normal normal-case tracking-normal ml-2">({{ __('Optional') }})</span>
<span class="text-[12px] text-slate-400 font-normal normal-case tracking-normal ml-2">({{ __('Optional') }})</span>
</h4>
</div>
@ -1180,10 +1225,10 @@
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80 space-y-4">
<div class="flex justify-between items-start pb-3 border-b border-slate-100/50 dark:border-slate-800/50">
<div>
<h4 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.2em]">{{ __('Contract Model') }}</h4>
<h4 class="text-[12px] font-black text-indigo-500 uppercase tracking-[0.2em]">{{ __('Contract Model') }}</h4>
</div>
<span :class="detailCompany.current_type === 'lease' ? 'text-blue-500 bg-blue-500/10' : 'text-amber-500 bg-amber-500/10'"
class="text-[10px] font-black uppercase tracking-wider px-2.5 py-1 rounded-lg">
class="text-[12px] font-black uppercase tracking-wider px-2.5 py-1 rounded-lg">
<span x-text="detailCompany.current_type === 'lease' ? '{{ __('Lease') }}' : '{{ __('Buyout') }}'"></span>
</span>
</div>
@ -1234,6 +1279,7 @@
<section class="space-y-4">
<h3 class="text-xs font-black text-indigo-500 uppercase tracking-[0.3em]">{{ __('Feature Settings') }}</h3>
<div class="space-y-3">
<!-- Material Code -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-indigo-50 dark:bg-indigo-500/10 text-indigo-500">
@ -1246,6 +1292,7 @@
<span x-text="detailCompany.settings?.enable_material_code ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- Points Rule -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-amber-50 dark:bg-amber-500/10 text-amber-500">
@ -1258,6 +1305,97 @@
<span x-text="detailCompany.settings?.enable_points ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- Electronic Invoice -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-emerald-50 dark:bg-emerald-500/10 text-emerald-500">
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('Electronic Invoice') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.tax_invoice ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.tax_invoice ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- Card System -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-blue-50 dark:bg-blue-500/10 text-blue-500">
<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="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('Card System') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.card_terminal ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.card_terminal ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- ESUN Pay -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-50 dark:bg-green-500/10 text-green-500">
<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="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 17h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('ESUN QR Pay') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.scan_pay_esun ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.scan_pay_esun ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- LinePay -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-green-50 dark:bg-green-500/10 text-green-500">
<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="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('LinePay Payment') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.scan_pay_linepay ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.scan_pay_linepay ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- Shopping Cart -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-purple-50 dark:bg-purple-500/10 text-purple-500">
<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="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('Shopping Cart') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.shopping_cart ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.shopping_cart ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- Welcome Gift -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-rose-50 dark:bg-rose-500/10 text-rose-500">
<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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-14v4m0 0l8 4m-8-4l-8 4m8 4v10l8-4m0-14l-8 4" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('Welcome Gift') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.welcome_gift ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.welcome_gift ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
<!-- Cash Module -->
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-amber-50 dark:bg-amber-500/10 text-amber-500">
<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="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
</div>
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('Cash Module') }}</span>
</div>
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
:class="detailCompany.settings?.cash_module ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
<span x-text="detailCompany.settings?.cash_module ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
</div>
</div>
</div>
</section>
@ -1319,7 +1457,7 @@
<div class="flex items-center gap-2.5">
<div class="w-7 h-7 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center text-[9px] font-black text-slate-500" x-text="'#' + (detailCompany.contracts.length - index)"></div>
<div>
<p class="text-[10px] font-black text-slate-700 dark:text-slate-200" x-text="new Date(contract.created_at).toLocaleString()"></p>
<p class="text-[12px] font-black text-slate-700 dark:text-slate-200" x-text="new Date(contract.created_at).toLocaleString()"></p>
<div class="flex items-center gap-1.5 mt-0.5">
<span :class="contract.type === 'lease' ? 'text-blue-500 bg-blue-500/10' : 'text-amber-500 bg-amber-500/10'"
class="text-[8px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded"
@ -1339,11 +1477,11 @@
<template x-if="contract.type === 'lease'">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1.5">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Contract Start') }}</p>
<p class="text-[12px] font-black text-slate-400 uppercase tracking-widest">{{ __('Contract Start') }}</p>
<p class="text-sm font-black text-slate-700 dark:text-white font-mono" x-text="contract.start_date?.substring(0, 10) || '-'"></p>
</div>
<div class="space-y-1.5">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Contract End') }}</p>
<p class="text-[12px] font-black text-slate-400 uppercase tracking-widest">{{ __('Contract End') }}</p>
<p class="text-sm font-black text-slate-700 dark:text-white font-mono" x-text="contract.end_date?.substring(0, 10) || '{{ __('Unlimited') }}'"></p>
</div>
</div>
@ -1352,7 +1490,7 @@
<template x-if="contract.type === 'buyout'">
<div class="grid grid-cols-1 gap-4">
<div class="p-4 bg-amber-50/40 dark:bg-amber-500/5 rounded-2xl border border-amber-100/50 dark:border-amber-500/10">
<p class="text-[10px] font-black text-amber-600 uppercase tracking-widest mb-2">{{ __('Warranty Service') }}</p>
<p class="text-[12px] font-black text-amber-600 uppercase tracking-widest mb-2">{{ __('Warranty Service') }}</p>
<div class="flex items-center gap-2">
<span class="text-sm font-black text-slate-700 dark:text-slate-200 font-mono" x-text="contract.warranty_start_date?.substring(0, 10) || '-'"></span>
<span class="text-slate-400 font-bold">~</span>
@ -1360,7 +1498,7 @@
</div>
</div>
<div class="p-4 bg-indigo-50/40 dark:bg-indigo-500/5 rounded-2xl border border-indigo-100/50 dark:border-indigo-500/10">
<p class="text-[10px] font-black text-indigo-600 uppercase tracking-widest mb-2">{{ __('Software Service') }}</p>
<p class="text-[12px] font-black text-indigo-600 uppercase tracking-widest mb-2">{{ __('Software Service') }}</p>
<div class="flex items-center gap-2">
<span class="text-sm font-black text-slate-700 dark:text-slate-200 font-mono" x-text="contract.software_start_date?.substring(0, 10) || '-'"></span>
<span class="text-slate-400 font-bold">~</span>

View File

@ -25,6 +25,11 @@
if (newContent) {
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
window.history.pushState({}, '', url);
// Re-initialize Preline UI components
if (window.HSStaticMethods && typeof window.HSStaticMethods.autoInit === 'function') {
window.HSStaticMethods.autoInit();
}
}
} catch (e) {
console.error('AJAX Load Failed:', e);
@ -152,7 +157,16 @@
</script>
<div class="space-y-2 pb-20" x-data="machineApp()"
@keydown.escape.window="showLogPanel = false; showInventoryPanel = false">
@keydown.escape.window="showLogPanel = false; showInventoryPanel = false"
@ajax:navigate.window.prevent="fetchPage($event.detail.url)"
@submit.prevent="if($event.target.method.toLowerCase() === 'get') {
const url = new URL($event.target.action, window.location.origin);
new FormData($event.target).forEach((value, key) => {
if(value.trim()) url.searchParams.set(key, value);
else url.searchParams.delete(key);
});
fetchPage(url.pathname + url.search);
}">
<!-- Top Header & Actions -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center gap-4">
@ -189,18 +203,15 @@
</svg>
</div>
</div>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">Syncing Data...</p>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<div id="ajax-content-container"
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }"
@ajax:navigate.prevent.stop="fetchPage($event.detail.url)"
@click="if ($event.target.closest('a') && $event.target.closest('a').href && ($event.target.closest('a').href.includes('page=') || $event.target.closest('a').href.includes('search='))) { $event.preventDefault(); fetchPage($event.target.closest('a').href); }">
:class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': isLoadingTable }">
<!-- Filters Area -->
<div class="flex items-center justify-between mb-8">
<form method="GET" action="{{ route('admin.machines.index') }}" class="relative group"
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
<form method="GET" action="{{ route('admin.machines.index') }}" class="relative group">
<input type="hidden" name="tab" value="list">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
@ -213,6 +224,7 @@
</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">
<button type="submit" class="hidden"></button>
</form>
</div>
@ -321,17 +333,17 @@
<div class="flex items-center justify-center gap-4">
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{{
<span class="text-[12px] font-black text-slate-400 uppercase tracking-tighter">{{
__('Normal') }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{{
<span class="text-[12px] font-black text-slate-400 uppercase tracking-tighter">{{
__('Normal') }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500/50"></span>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{{
<span class="text-[12px] font-black text-slate-400 uppercase tracking-tighter">{{
__('Normal') }}</span>
</div>
</div>
@ -433,7 +445,7 @@
<span class="truncate">{{ __('Machine Logs') }}</span>
</h2>
<div
class="mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-[10px] sm:text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
class="mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-[12px] sm:text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
<span x-text="currentMachineSn"
class="font-mono text-cyan-600 dark:text-cyan-400 truncate"></span>
<span class="hidden sm:inline opacity-50"></span>
@ -458,7 +470,7 @@
<div class="grid grid-cols-2 sm:flex sm:items-center gap-3 sm:gap-4 flex-1">
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
<label
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
__('From') }}</label>
<input type="text" x-ref="startDatePicker" x-model="startDate"
x-init="flatpickr($refs.startDatePicker, {
@ -469,11 +481,11 @@
defaultDate: startDate,
onClose: (selectedDates, dateStr) => { startDate = dateStr; fetchLogs(1); }
})"
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
class="luxury-input text-[12px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-1.5 sm:gap-2">
<label
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.1em] whitespace-nowrap">{{
__('To') }}</label>
<input type="text" x-ref="endDatePicker" x-model="endDate"
x-init="flatpickr($refs.endDatePicker, {
@ -484,12 +496,12 @@
defaultDate: endDate,
onClose: (selectedDates, dateStr) => { endDate = dateStr; fetchLogs(1); }
})"
class="luxury-input text-[11px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
class="luxury-input text-[12px] h-9 sm:h-8 py-0 w-full sm:w-44 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
</div>
</div>
<div class="flex justify-start sm:justify-end">
<button @click="startDate = ''; endDate = ''; fetchLogs(1)"
class="text-[10px] font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:text-cyan-500 transition-colors flex items-center gap-1.5">
class="text-[12px] font-bold text-cyan-600 dark:text-cyan-400 uppercase tracking-widest hover:text-cyan-500 transition-colors flex items-center gap-1.5">
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5">
<path d="M18 13.5V19a2 2 0 0 1-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h5.5l7.5 7.5z" />
@ -556,7 +568,7 @@
</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>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<!-- Logs Container -->
@ -569,13 +581,13 @@
<thead class="bg-slate-50 dark:bg-slate-800/50">
<tr>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700 w-40">
class="px-6 py-4 text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700 w-40">
{{ __('Timestamp') }}</th>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700 w-24 text-center">
class="px-6 py-4 text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700 w-24 text-center">
{{ __('Level') }}</th>
<th
class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700">
class="px-6 py-4 text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-200 dark:border-slate-700">
{{ __('Message') }}</th>
</tr>
</thead>
@ -594,7 +606,7 @@
'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20': log.level === 'warning',
'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20': log.level === 'error'
}"
class="inline-flex items-center px-2 py-0.5 rounded-md border text-[10px] font-black uppercase tracking-wider">
class="inline-flex items-center px-2 py-0.5 rounded-md border text-[12px] font-black uppercase tracking-wider">
<span x-text="log.level"></span>
</span>
</td>
@ -615,7 +627,7 @@
class="p-4 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">
<div class="flex items-center justify-between mb-2">
<span
class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest"
class="text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest"
x-text="formatDateTime(log.created_at)"></span>
<span :class="{
'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20': log.level === 'info',
@ -648,7 +660,7 @@
</svg>
</div>
<span
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
class="text-[12px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
__('No matching logs found') }}</span>
</div>
</div>
@ -666,9 +678,9 @@
</button>
<div class="flex items-center gap-1.5">
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest" x-text="currentPage"></span>
<span class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase">/</span>
<span class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest" x-text="lastPage"></span>
<span class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest" x-text="currentPage"></span>
<span class="text-[12px] font-black text-slate-300 dark:text-slate-600 uppercase">/</span>
<span class="text-[12px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest" x-text="lastPage"></span>
</div>
<button @click="fetchLogs(currentPage + 1)"
@ -790,7 +802,7 @@
<span class="truncate">{{ __('Stock & Expiry Overview') }}</span>
</h2>
<div
class="mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-[10px] sm:text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
class="mt-2 flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-[12px] sm:text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest overflow-hidden">
<span x-text="currentMachineSn"
class="font-mono text-cyan-600 dark:text-cyan-400 truncate"></span>
<span class="hidden sm:inline opacity-50"></span>
@ -861,26 +873,26 @@
</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>
<p class="text-[12px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<!-- Status Legend -->
<div class="flex items-center gap-6 mb-6" x-show="!inventoryLoading">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-rose-500 shadow-lg shadow-rose-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
<span class="text-[12px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
__('Expired') }}</span>
</div>
<div class="flex items-center gap-2">
<span
class="w-3 h-3 rounded-full bg-amber-500 shadow-lg shadow-amber-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
<span class="text-[12px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
__('Warning') }}</span>
</div>
<div class="flex items-center gap-2">
<span
class="w-3 h-3 rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/30"></span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
<span class="text-[12px] font-black text-slate-500 uppercase tracking-[0.15em]">{{
__('Normal') }}</span>
</div>
</div>
@ -975,7 +987,7 @@
</svg>
</div>
<span
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
class="text-[12px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{
__('No slot data available') }}</span>
</div>
</div>

View File

@ -18,6 +18,11 @@
if (newContent) {
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
window.history.pushState({}, '', url);
// Re-initialize Preline UI components
if (window.HSStaticMethods && typeof window.HSStaticMethods.autoInit === 'function') {
window.HSStaticMethods.autoInit();
}
}
} catch (e) {
console.error('AJAX Load Failed:', e);
@ -98,6 +103,14 @@
window.dispatchEvent(new CustomEvent('toast', { detail: { message: e.message, type: 'error' } }));
});
}
}" @ajax:navigate.window.prevent="fetchPage($event.detail.url)"
@submit.prevent="if($event.target.method.toLowerCase() === 'get') {
const url = new URL($event.target.action, window.location.origin);
new FormData($event.target).forEach((value, key) => {
if(value.trim()) url.searchParams.set(key, value);
else url.searchParams.delete(key);
});
fetchPage(url.pathname + url.search);
}">
<!-- 1. Header Area -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
@ -129,7 +142,7 @@
</svg>
</div>
</div>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">Syncing Data...</p>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<div id="ajax-content-container"
@ -138,8 +151,7 @@
<!-- Toolbar & Filters -->
<div class="flex flex-col md:flex-row items-center justify-between mb-8 gap-4">
<form method="GET" action="{{ route('admin.machines.permissions') }}"
class="flex flex-wrap items-center gap-4 w-full md:w-auto"
@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())">
class="flex flex-wrap items-center gap-4 w-full md:w-auto">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
@ -161,6 +173,8 @@
:placeholder="__('All Companies')" onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: this.form.action + '?' + new URLSearchParams(new FormData(this.form)).toString() }, bubbles: true }))" />
</div>
@endif
<button type="submit" class="hidden"></button>
</form>
</div>

View File

@ -279,7 +279,7 @@ window.utilizationDashboard = function(initialMachines, initialStats) {
</svg>
</div>
</div>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">Syncing Data...</p>
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
</div>
<div class="lg:col-span-4 space-y-4" :class="{ 'opacity-40 pointer-events-none transition-opacity duration-500': loading }">

View File

@ -43,7 +43,9 @@
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
<a href="{{ $paginator->previousPageUrl() }}"
onclick="{ event.preventDefault(); const url = this.href; const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
</a>
@ -67,7 +69,9 @@
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
<a href="{{ $paginator->nextPageUrl() }}"
onclick="{ event.preventDefault(); const url = this.href; const evt = new CustomEvent('ajax:navigate', { detail: { url: url }, bubbles: true, cancelable: true }); if (this.dispatchEvent(evt)) { window.location.href = url; } }"
class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all shadow-sm group">
<span class="hidden xxs:inline">{{ __('Next') }}</span>
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
</a>