Compare commits

...

13 Commits

Author SHA1 Message Date
eb73def5f8 [FEAT] 新增建立客戶時可選擇初始角色之功能與多語系支援
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 43s
2026-03-19 17:21:15 +08:00
f00fc940a9 [DOCS] 更新 RBAC 實作規範與角色初始化流程建議
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 55s
2026-03-19 17:18:21 +08:00
5548bb1cc9 [REFACTOR] 更改租戶管理員角色名稱為中文 (客戶管理員)
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 40s
2026-03-18 17:46:44 +08:00
64ac398270 [FEAT] 更新 Admin 密碼與新增機台照片更新功能
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 40s
2026-03-18 17:43:40 +08:00
2afcdcebc5 [STYLE] 縮減詳情側拉欄與編輯頁面間距,優化視覺緊湊度
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 47s
2026-03-18 16:40:04 +08:00
fe9c9e0c4a [FIX] 修復 WebP 轉換 Palette 錯誤並優化照片獨立槽位上傳 UI
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m1s
2026-03-18 16:25:58 +08:00
c767fe4849 [STYLE] 優化機台介面:加大 Tab 字體、自定義上傳 UI、優化開關深色模式
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 47s
2026-03-18 15:00:41 +08:00
caac6e264d [FIX] 修正基本設定機台編輯頁面:移除斜體並補上位置欄位
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 45s
2026-03-18 14:54:15 +08:00
efafdc747b [STYLE] 優化機台編輯/建立頁面 UI 並確認位置欄位
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 50s
2026-03-18 14:50:29 +08:00
c21cad7f37 [STYLE] 調換機台設定操作按鈕順序:編輯在左、詳細在右
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 53s
2026-03-18 14:04:49 +08:00
3f41896532 [FEAT] 補全機台與金流設定多語系,並調整帳號管理表格佈局
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 44s
2026-03-18 13:52:54 +08:00
cd34724c76 [FIX] 在部署流程中加入 php artisan storage:link 以確保頭像與靜態資源連結正常
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 41s
2026-03-18 09:36:58 +08:00
7b5a988d60 [FIX] 修正 HTTPS 強制跳轉邏輯,依據 APP_URL 配置自動啟用 HTTPS 防止安全啟動警告
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 43s
2026-03-18 09:26:43 +08:00
35 changed files with 1947 additions and 582 deletions

View File

@@ -91,7 +91,7 @@ trigger: always_on
* **本地測試網址**`http://localhost:8090/` (注意:非 8000 或 8080)
* **預設管理員帳號**`admin`
* **預設管理員密碼**`password`
* **預設管理員密碼**`Star82779061`
> [!IMPORTANT]
> 在執行 `open_browser_url` 或進行 E2E 測試時,請務必優先確認 Port 是否為 `8090`,以避免連線至錯誤的服務環境。

View File

@@ -1,3 +1,7 @@
---
trigger: always_on
---
# 多租戶與權限架構實作規範 (RBAC Rules)
本文件定義 Star Cloud 系統的多租戶與權限RBAC實作標準開發者必須嚴格遵守以下準則以確保資料隔離與安全性。
@@ -50,3 +54,17 @@
## 4. API 安全
- 所有的 API Route 應預設包含 `CheckTenantAccess` Middleware。
- 嚴禁透過 URL 修改 ID 存取不屬於該租戶的資料,必須依賴 `company_id` 的 Scope 過濾。
---
## 5. 客戶初次建立與角色初始化 (Role Provisioning)
### 5.1 初始角色建立
當系統管理員為新客戶(該租戶尚未有任何角色)建立第一個帳號時,應遵循以下邏輯:
1. **選取範本**:從系統預設的「全域角色範本」(`company_id = null``is_system = 0`)中選取一個作為基礎。
2. **自動克隆**:系統會將該範本的權限內容複製一份至該租戶下。
3. **統一命名**:克隆後的角色名稱在該租戶公司內應統一命名為**「管理員」**。
4. **帳號綁定**:該新客戶帳號將被指派至此新建立的「管理員」角色。
### 5.2 角色權限維護
- 初始建立後,該租戶的「管理員」角色即成為獨立資源,可由具有權限的帳號進行細部調整。

View File

@@ -25,8 +25,7 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
### 豪華卡片 (Luxury Card)
```html
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
<!-- 鍏у<E98D8F> -->
</div>
</div>
```
- **特效**: 懸停時帶有 Y 軸平移與深度投影。
@@ -45,20 +44,18 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- **Ghost**: `.btn-luxury-ghost` (無背景,適用於取消、查看更多)
```html
<!-- Primary -->
<button class="btn-luxury-primary">
<i class="lucide-plus size-4"></i>
<i class="lucide-plus w-4 h-4"></i>
<span>建立新機台</span>
</button>
<!-- Ghost -->
<button class="btn-luxury-ghost">取消</button>
```
## 3. 動畫與互動
### 進場動畫
- **`.animate-luxury-in`**: 鎵€鏈夌殑涓诲収瀹瑰崁鍩熸垨鍗墖鍦ㄩ爜闈㈣級鍏ユ檪锛屾噳鍏峰倷鐢变笅鑰岃綁鐨勬贰鍏ユ晥鏋溿€<EFBFBD>
- **`.animate-luxury-in`**: 所有的主內容區域或卡片在頁面載入時,應具備由下而上的淡入效果。
### 互動過渡 (Transitions)
- **標準時間**: 所有的懸停、色彩變換等過渡效果,統一建議使用 **`duration-300`** (300ms)。
@@ -68,6 +65,8 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- **互動原則**: 點擊觸發下拉選單時,必須使用 `x-transition` 且帶有 `scale` 偏移。
- **樣式要求**: 選單背景需使用玻璃擬態 (Glassmorphism) 或帶透明度的深色背景。
## 4. 實作檢查清單 (Checklist)
- [ ] **列表佈局**: 是否採用「整合式卡片」結構且內距設為 `p-8`
- [ ] **分頁與總數**: 列表底部是否正確召喚 `vendor.pagination.luxury`
- [ ] **文字色階**: 符合標題 `slate-900/white` 與標籤 `slate-500` 的對比度。
@@ -76,7 +75,7 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
## 5. 開發注意事項 (Important Notes)
### 技術限制備忘
- **CSS 绶ㄨ<EFBFBD>**: 瑜囬洔鐨<EFBFBD> `box-shadow` 鎴栨几灞ゆ噳鐩存帴瀵<EFBFBD>師鐢<EFBFBD> CSS 灞<>€э紝閬垮厤鍦<E58EA4> `@apply`<>娇鐢ㄥ付绌烘牸鐨勬暩鍊煎皫鑷寸法璀<E6B395>け鏁楋紙瑭宠<E791AD> KI: `tailwind-luxury-ui-patterns`锛夈€<EFBFBD>
- **CSS 編譯**: 複雜的 `box-shadow` 或漸層應直接寫原生 CSS 屬性,避免在 `@apply` 中使用帶空格的數值導致編譯失敗。
- **深色模式**: 互動式按鈕在深色模式下必須強化文字亮度(`dark:text-white`),並輔以青色發光效果。
### 即時動態呈現規範
@@ -107,24 +106,46 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
```html
@section('content')
<div class="space-y-6">
<!-- Header: 妯欓<E5A6AF>鑸囨搷浣滄寜閳<E5AF9C> -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Title') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Subtitle') }}</p>
</div>
<div class="flex items-center gap-3">
<button class="btn-luxury-primary">...</button>
<button class="btn-luxury-primary">
<i class="lucide-plus w-4 h-4"></i>
<span>新增</span>
</button>
</div>
</div>
<!-- Main Container: 鍗墖鑸囪〃鏍<E38083> -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="flex items-center justify-between mb-10">
<form class="relative group">
<input type="text" class="luxury-input pl-12 pr-6 w-64" placeholder="{{ __('Search...') }}">
</form>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<!-- Table Content -->
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Name') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 font-extrabold text-slate-800 dark:text-slate-100 italic">Example Name</td>
<td class="px-6 py-6 text-right"> </td>
</tr>
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $items->links('vendor.pagination.luxury') }}
</div>
</div>
</div>
@endsection
@@ -170,9 +191,8 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- 樣式: `bg-indigo-500/10 text-indigo-500`
- **危險/移除動作**: **玫瑰紅 (`Rose`)**。代表破壞性操作。
- 樣式: `bg-rose-500/10 text-rose-500`
```
## 8. 璩囨枡琛ㄦ牸瑕忕瘎 (Data Tables)
## 9. 資料表格規範 (Data Tables)
為了確保管理後台資料的可讀性與精密感,表格內的所有文字級別必須對齊以下規範:
@@ -214,7 +234,9 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- 數字顏色對齊 `text-slate-600` (深色: `text-slate-300`)。
### 底部清單控制項 (Bottom List Controls)
鐐轰簡纰轰繚闀峰垪琛ㄧ殑鎿嶄綔渚垮埄锛屾竻鍠<EFBFBD>簳閮### 妯欐簴鎿嶄綔鎸夐垥 (Standard Action Icons)
為了確保長列表的操作便利,清單底部應保持乾淨,統一由分頁與總數組件接管操作。
### 標準操作按鈕 (Standard Action Icons)
表格內的操作欄位(如「編輯」、「刪除」、「詳情」)必須使用以下定義之 **「黃金標準 (Gold Standard)」**
- **共同樣式**:
@@ -226,92 +248,19 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- 尺寸: `w-4 h-4`
- **編輯按鈕 (Edit)**:
- 鎳稿仠鐗规晥: `hover:text-cyan-500 hover:bg-cyan-500/5 hover:border-cyan-500/20`
- 懸停特效: `hover:text-cyan-500 hover:bg-cyan-500/10 hover:border-cyan-500/20`
- SVG 路徑:
```html
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
```
- **查看詳情 (View/Detail)**:
- 鎳稿仠鐗规晥: `hover:text-indigo-500 hover:bg-indigo-500/5 hover:border-indigo-500/20`
- 懸停特效: `hover:text-indigo-500 hover:bg-indigo-500/10 hover:border-indigo-500/20`
- SVG 路徑:
```html
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"/></svg>
```
- **鍒<>櫎鎸夐垥 (Delete)**:
- 鎳稿仠鐗规晥: `hover:text-rose-500 hover:bg-rose-500/5 hover:border-rose-500/20`
- SVG 璺<>緫:
```html
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
```
y items-center gap-2">...</button>
</div>
</div>
<!-- 2. Main Integrated Card -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<!-- Toolbar & Filters (mb-10) -->
<div class="flex items-center justify-between mb-10">
<form class="relative group">
<!-- 鍍呬繚鐣欐悳灏嬫<E7818F>鎴栧繀瑕佺殑閬庢烤鍣<E783A4>紝绂佹<E7BB82>閲嶈<E996B2>绛嗘暩鍒囨彌 -->
<input type="text" class="luxury-input pl-12 pr-6 w-64" placeholder="{{ __('Search...') }}">
</form>
</div>
<!-- Scrollable Table Area -->
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Name') }}</th>
<th class="px-6 py-4 text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6 font-extrabold text-slate-800 dark:text-slate-100 italic">Example Name</td>
<td class="px-6 py-6 text-right"> <!-- Action row --> </td>
</tr>
</tbody>
</table>
</div>
<!-- 3. Standard Pagination Footer (mt-8) -->
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $items->links('vendor.pagination.luxury') }}
</div>
</div>
</div>
```
### 娓呭柈娆勪綅瑕忕瘎 (Column Visibility & Standards)
- **鍥哄畾娆勪綅**: 绗<>竴娆勯€氬父鐐恒€岄棞閸垫<E996B8>璀樸€嶏紙濡<E7B499> ID 鎴栨檪闁擄級锛屾噳鍏峰倷鐗规畩瀛楅珨妯紡銆<E7B4A1>
- **鎿嶄綔娆勪綅**: 绲变竴浣嶆柤琛ㄦ牸鏈€鍙崇<E98D99>锛屼甫鍛藉悕鐐<E68295> `Action` (鎴<> `鎿嶄綔`)锛屾<E9949B>椤岃垏鍏у<E98D8F>鐨嗘噳 `text-right`<EFBFBD>
## 9. 绯荤当鍏煎<E98D8F>鎬ц垏妯欐簴鍖<E7B0B4> (Compatibility & Standardization)
鐐轰簡纰轰繚鍦ㄤ笉鍚岀増鏈<EFBFBD>殑闁嬬櫦鐠板<EFBFBD><EFBFBD>紙濡傜洰鍓嶅皥妗堜娇鐢ㄧ殑 Tailwind CSS v3.1锛塙I 閮借兘姝⒑鍛堢従锛屼甫缍<E794AB>寔鍏ㄧ珯鎿嶄綔鎰熶竴鑷达紝蹇呴爤閬靛畧浠ヤ笅椤嶅<E6A4A4>瑕忕瘎銆<E7988E>
### Tailwind CSS 鐗堟湰鍏煎<E98D8F><EFBFBD> (v3.1)
- **绂佹<E7BB82>浣跨敤 `size-` 灞<><EFBFBD>**: 鑸婄増涓嶆敮鎻<E695AE> `size-4` 绛夎獮娉曪紝璜嬩竴寰嬪垎鎷嗗<E98EB7><EFBFBD> `w-4 h-4`<EFBFBD>
- **閬垮厤闈炴<E99788>婧栭枔璺<E69E94>**: 閬垮厤浣跨敤 `4.5` (`18px`) 绛変换鎰忓€硷紝鍎<E7B49D>厛浣跨敤妯欐簴绛夌礆濡<E7A486> `4` (`16px`) 鎴<> `5` (`20px`)銆<>
### 妯欐簴鎿嶄綔鎸夐垥 (Standard Action Icons)
琛ㄦ牸鍏х殑鎿嶄綔娆勪綅锛堝<EFBFBD>銆岀法杓<EFBFBD>€嶃€併€屽埅闄ゃ€嶏級蹇呴爤浣跨敤浠ヤ笅瀹氱京涔嬫<EFBFBD>婧栵細
- **鍏卞悓妯e紡**:
- 瀹瑰櫒: `p-2 rounded-lg bg-slate-50 dark:bg-slate-800`
- 涓昏壊: `text-slate-400`
- 鍦栫ず绮楃窗: `stroke-width="2.5"`
- 灏哄<E7818F>: `w-4 h-4`
- **绶ㄨ集鎸夐垥 (Edit)**:
- 鎳稿仠鐗规晥: `hover:text-cyan-500 hover:bg-cyan-500/10 hover:border-cyan-500/20`
- SVG 璺<>緫:
```html
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
```
- **刪除按鈕 (Delete)**:
- 懸停特效: `hover:text-rose-500 hover:bg-rose-500/10 hover:border-rose-500/20`
- SVG 路徑:
@@ -319,25 +268,34 @@ y items-center gap-2">...</button>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
```
## 10. 系統兼容性與標準化 (Compatibility & Standardization)
## 10. 瀛楅珨鑸囨妧琛撹硣瑷婅<E791B7><EFBFBD> (Typography & Technical Data)
為了確保在不同版本的開發環境中(如目前專案使用的 Tailwind CSS v3.1UI 都能正確呈現,並維持全站操作感一致,必須遵守以下額外規範。
### Tailwind CSS 版本兼容性 (v3.1)
- **禁止使用 `size-` 屬性**: 舊版不支援 `size-4` 等語法,請一律分拆寫作 `w-4 h-4`
- **避免非標準間距**: 避免使用 `4.5` (`18px`) 等任意值,優先使用標準等級如 `4` (`16px`) 或 `5` (`20px`)。
## 11. 字體與技術資訊規範 (Typography & Technical Data)
為了確保全站「次要資訊」具備極一致的高級感,必須遵守以下「機台標竿」規範:
### 核心樣式級別 (Core Typography Scale)
| 璩囪▕椤炲瀷 | 瀹㈡埗/閰嶇疆鍚嶇ū (妯欓<E5A6AF>) | 鎶€琛撲唬纰<E594AC> (ID, SN, Code) | 鍒嗛殧绗﹁櫉 (鈥<>) |
| :--- | :--- | :--- | :--- |
| **瀛楅珨鏃<EFBFBD>** | `font-sans` (Plus Jakarta Sans) | `font-mono` (<EFBFBD>府鍨嬪柈闆欐牸) | `font-sans` |
| **灏哄<EFBFBD>** | `text-base` | `text-xs` (涓嶅彲浣跨敤 10px) | `text-xs` |
| **瀛楅噸** | `font-extrabold` (800) | `font-bold` (700) | `font-bold` |
| **瀛楄窛** | `tracking-tight` (-0.02em) | `tracking-widest` (鏈€瀵<EFBFBD>) | `tracking-normal` |
| **鏍煎紡** | 淇濇寔鍘熷<EFBFBD>鍚嶇ū | `uppercase` (寮峰埗澶у<EFBFBD>) | N/A |
| **鑹插僵** | `slate-900` / `slate-100` | `slate-500` / `slate-400` | `slate-300` / `slate-700` |
| 資訊類型 | 客戶/配置名稱 (標題) | 技術代碼 (ID, SN, Code) | 清單時間 (Timestamps) | 分隔符號 (•) |
| :--- | :--- | :--- | :--- | :--- |
| **字體族** | `font-sans` (Plus Jakarta Sans) | `font-mono` (微縮型單雙格) | `font-mono` (或 `sans` 視場景) | `font-sans` |
| **尺寸** | `text-base` | `text-xs` (不可使用 10px) | `text-xs` | `text-xs` |
| **字重** | `font-extrabold` (800) | `font-bold` (700) | `font-black` (900) | `font-bold` |
| **字距** | `tracking-tight` (-0.02em) | `tracking-widest` (最寬) | `tracking-widest` | `tracking-normal` |
| **格式** | 保持原始名稱 | `uppercase` (強制大寫) | `uppercase` | N/A |
| **色彩** | `slate-900` / `slate-100` | `slate-500` / `slate-400` | `slate-400` / `slate-400/80` | `slate-300` / `slate-700` |
### 實作禁忌與準則
- **禁止斜體 (No Italics)**: 名稱欄位嚴禁附帶 `italic`(特別是標題或配置名稱),保持直挺專業感。
- **作用範圍 (Mono Scoping)**: `font-mono` 僅限作用於「純英文/數字」的代碼。Email 或分隔點必須回歸 `font-sans` 以確保圓潤。
- **權重載入 (Font Weights)**: 確保 HTML Header 載入了 `800``900` 權重,避免瀏覽器模擬出的假粗體。
- **清單資訊密度**: 對於高密度清單中的時間資訊,應優先使用 `font-black``tracking-widest` 來建立明確的「標籤感」,而非僅僅是「微縮文字」。
---
> [!IMPORTANT]
> **開發新功能前,必須確認 `app.css` 中的 `.btn-luxury-*` 系列組件是否滿足需求。**

View File

@@ -92,6 +92,7 @@ jobs:
# 3. Laravel 初始化與優化
php artisan migrate --force &&
php artisan storage:link &&
php artisan optimize:clear &&
php artisan optimize &&
php artisan view:cache &&

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Admin\BasicSettings;
use App\Http\Controllers\Admin\AdminController;
use App\Models\Machine\MachineModel;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
class MachineModelController extends AdminController
{
/**
* 顯示機台型號列表 (重新導向至機台設定的標籤頁)
*/
public function index(Request $request): RedirectResponse
{
return redirect()->route('admin.basic-settings.machines.index', ['tab' => 'models']);
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
]);
MachineModel::create(array_merge($validated, [
'company_id' => auth()->user()->company_id,
'creator_id' => auth()->id(),
'updater_id' => auth()->id(),
]));
return redirect()->route('admin.basic-settings.machines.index', ['tab' => 'models'])
->with('success', __('Machine model created successfully.'));
}
/**
* 顯示編輯頁面 ( index 共用 Modal 則不需此方法,但 resource 路由建議保留或調整)
*/
public function edit(MachineModel $machine_model): View
{
// 若採用 index Modal 編輯,此處可回傳 JSON 或維持 Blade
return view('admin.basic-settings.machine-models.edit', compact('machine_model'));
}
public function update(Request $request, MachineModel $machine_model): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
]);
$machine_model->update(array_merge($validated, [
'updater_id' => auth()->id(),
]));
return redirect()->route('admin.basic-settings.machines.index', ['tab' => 'models'])
->with('success', __('Machine model updated successfully.'));
}
public function destroy(MachineModel $machine_model): RedirectResponse
{
if ($machine_model->machines()->count() > 0) {
return redirect()->back()->with('error', __('Cannot delete model that is currently in use by machines.'));
}
$machine_model->delete();
return redirect()->route('admin.basic-settings.machines.index', ['tab' => 'models'])
->with('success', __('Machine model deleted successfully.'));
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Admin\BasicSettings;
use App\Http\Controllers\Controller;
use App\Models\Machine\Machine;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class MachinePhotoController extends Controller
{
/**
* 更新機台照片
*/
public function update(Request $request, Machine $machine): RedirectResponse
{
Log::info('Machine Photo Update Request', [
'machine_id' => $machine->id,
'files' => $request->allFiles()
]);
try {
$images = $machine->images ?? [];
// 處理 3 個索引位置的圖片
for ($i = 0; $i < 3; $i++) {
// 先處理刪除標記
if ($request->input("delete_photo_{$i}") === '1') {
if (isset($images[$i])) {
unset($images[$i]);
}
}
// 再處理檔案上傳(若有上傳會覆蓋掉刪除邏輯或原有的圖)
$fieldName = "machine_image_{$i}";
if ($request->hasFile($fieldName)) {
$file = $request->file($fieldName);
// 轉為 WebP 格式與保存
$path = $this->storeAsWebp($file, "machines/{$machine->id}");
$images[$i] = $path;
Log::info("Machine image uploaded at slot {$i}", ['path' => $path]);
}
}
// 過濾掉 null 並重新整理索引,但這裡我們希望保持 3 個槽位的概念
// 如果用戶想保持順序,我們就直接儲存
ksort($images);
$machine->update([
'images' => $images,
'updater_id' => auth()->id(),
]);
return back()->with('success', __('Machine images updated successfully.'));
} catch (\Exception $e) {
Log::error('Machine Photo Update Failed', [
'machine_id' => $machine->id,
'error' => $e->getMessage()
]);
return back()->with('error', __('Failed to update machine images: ') . $e->getMessage());
}
}
/**
* 將圖片轉換為 WebP 並儲存
*/
protected function storeAsWebp($file, $directory): string
{
$extension = $file->getClientOriginalExtension();
$filename = uniqid() . '.webp';
$path = "{$directory}/{$filename}";
// 讀取原始圖片
$imageType = exif_imagetype($file->getRealPath());
switch ($imageType) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($file->getRealPath());
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($file->getRealPath());
break;
case IMAGETYPE_WEBP:
$source = imagecreatefromwebp($file->getRealPath());
break;
default:
// 如果格式不支援,直接存
return $file->storeAs($directory, $file->hashName(), 'public');
}
if (!$source) {
return $file->storeAs($directory, $file->hashName(), 'public');
}
// 確保支援真彩色(解決 palette image 問題)
if (!imageistruecolor($source)) {
imagepalettetotruecolor($source);
}
// 捕捉輸出
ob_start();
imagewebp($source, null, 80);
$content = ob_get_clean();
imagedestroy($source);
Storage::disk('public')->put($path, $content);
return $path;
}
}

View File

@@ -17,31 +17,44 @@ use Illuminate\Support\Facades\Log;
class MachineSettingController extends AdminController
{
/**
* 顯示機台設定列表
* 顯示機台與型號設定列表 (採用標籤頁整合)
*/
public function index(Request $request): View
{
$tab = $request->input('tab', 'machines');
$per_page = $request->input('per_page', 20);
$query = Machine::query()->with(['machineModel', 'paymentConfig', 'company']);
$search = $request->input('search');
// 搜尋:名稱或序號
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
// 1. 處理機台清單 (Machines Tab)
$machineQuery = Machine::query()->with(['machineModel', 'paymentConfig', 'company']);
if ($tab === 'machines' && $search) {
$machineQuery->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('serial_no', 'like', "%{$search}%");
});
}
$machines = $machineQuery->latest()->paginate($per_page, ['*'], 'machines_page')->withQueryString();
$machines = $query->latest()
->paginate($per_page)
->withQueryString();
// 2. 處理型號清單 (Models Tab)
$modelQuery = MachineModel::query()->withCount('machines');
if ($tab === 'models' && $search) {
$modelQuery->where('name', 'like', "%{$search}%");
}
$models_list = $modelQuery->latest()->paginate($per_page, ['*'], 'models_page')->withQueryString();
// 3. 基礎下拉資料 (用於新增/編輯機台的彈窗)
$models = MachineModel::select('id', 'name')->get();
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
// 這裡應根據租戶 (Company) 決定可用的選項,暫採簡單模擬或從 Auth 取得
$companies = \App\Models\System\Company::select('id', 'name')->get();
return view('admin.basic-settings.machines.index', compact('machines', 'models', 'paymentConfigs', 'companies'));
return view('admin.basic-settings.machines.index', compact(
'machines',
'models_list',
'models',
'paymentConfigs',
'companies',
'tab'
));
}
/**
@@ -55,6 +68,7 @@ class MachineSettingController extends AdminController
'company_id' => 'required|exists:companies,id',
'machine_model_id' => 'required|exists:machine_models,id',
'payment_config_id' => 'nullable|exists:payment_configs,id',
'location' => 'nullable|string|max:255',
'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
]);
@@ -121,6 +135,7 @@ class MachineSettingController extends AdminController
'member_system_enabled' => 'boolean',
'machine_model_id' => 'required|exists:machine_models,id',
'payment_config_id' => 'nullable|exists:payment_configs,id',
'location' => 'nullable|string|max:255',
]);
Log::info('Machine Update Validated Data', ['data' => $validated]);
@@ -133,20 +148,30 @@ class MachineSettingController extends AdminController
'updater_id' => auth()->id(),
]));
// 處理圖片更新 (若有上傳新圖片,則替換或附加,這裡採簡單邏輯:若有傳 images 則全換)
// 處理圖片更新 (支援 3 個獨立槽位)
if ($request->hasFile('images')) {
// 刪除舊圖
if (!empty($machine->images)) {
foreach ($machine->images as $oldPath) {
Storage::disk('public')->delete($oldPath);
$currentImages = $machine->images ?? [];
$newImages = $request->file('images');
$updated = false;
foreach ($newImages as $index => $file) {
// 限制 3 個槽位 (0, 1, 2)
if ($index < 0 || $index > 2) continue;
// 刪除該槽位的舊圖
if (isset($currentImages[$index]) && !empty($currentImages[$index])) {
\Illuminate\Support\Facades\Storage::disk('public')->delete($currentImages[$index]);
}
// 處理並儲存新圖
$currentImages[$index] = $this->processAndStoreImage($file);
$updated = true;
}
$imagePaths = [];
foreach (array_slice($request->file('images'), 0, 3) as $image) {
$imagePaths[] = $this->processAndStoreImage($image);
if ($updated) {
ksort($currentImages);
$machine->update(['images' => array_values($currentImages)]);
}
$machine->update(['images' => $imagePaths]);
}
return redirect()->route('admin.basic-settings.machines.index')
@@ -154,46 +179,47 @@ class MachineSettingController extends AdminController
}
/**
* 處理圖片並轉換為 WebP
* 處理並儲存圖片 (轉換為 WebP 並調整大小)
*/
private function processAndStoreImage($file): string
protected function processAndStoreImage($file)
{
$filename = Str::random(40) . '.webp';
$path = 'machines/' . $filename;
$path = 'machines/' . \Illuminate\Support\Str::random(40) . '.webp';
// 建立圖資源
$image = null;
$extension = strtolower($file->getClientOriginalExtension());
// 載入原圖
$imageInfo = getimagesize($file->getRealPath());
$mime = $imageInfo['mime'];
switch ($extension) {
case 'jpeg':
case 'jpg':
switch ($mime) {
case 'image/jpeg':
$image = imagecreatefromjpeg($file->getRealPath());
break;
case 'png':
case 'image/png':
$image = imagecreatefrompng($file->getRealPath());
break;
case 'gif':
case 'image/gif':
$image = imagecreatefromgif($file->getRealPath());
break;
case 'webp':
$image = imagecreatefromwebp($file->getRealPath());
break;
default:
return $file->store('machines', 'public');
}
if ($image) {
// 確保目錄存在
Storage::disk('public')->makeDirectory('machines');
$fullPath = Storage::disk('public')->path($path);
// [修正] imagewebp(): Palette image not supported by webp
// 若為 Palette 圖片 (例如 GIF),轉換為 Truecolor
if (!imageistruecolor($image)) {
imagepalettetotruecolor($image);
}
// 轉換並儲存
imagewebp($image, $fullPath, 80); // 品質 80
\Illuminate\Support\Facades\Storage::disk('public')->makeDirectory('machines');
$fullPath = \Illuminate\Support\Facades\Storage::disk('public')->path($path);
// 轉換並儲存 (品質 80)
imagewebp($image, $fullPath, 80);
imagedestroy($image);
return $path;
}
// Fallback to standard store if GD fails
return $file->store('machines', 'public');
}
}

View File

@@ -32,7 +32,12 @@ class CompanyController extends Controller
$per_page = $request->input('per_page', 10);
$companies = $query->latest()->paginate($per_page)->withQueryString();
return view('admin.companies.index', compact('companies'));
// 取得可供選擇的客戶角色範本 (is_system = 0, company_id = null)
$template_roles = \App\Models\System\Role::where('is_system', 0)
->whereNull('company_id')
->get();
return view('admin.companies.index', compact('companies', 'template_roles'));
}
/**
@@ -54,6 +59,7 @@ class CompanyController extends Controller
'admin_username' => 'nullable|string|max:255|unique:users,username',
'admin_password' => 'nullable|string|min:8',
'admin_name' => 'nullable|string|max:255',
'admin_role' => 'nullable|string|exists:roles,name',
]);
DB::transaction(function () use ($validated) {
@@ -79,8 +85,30 @@ class CompanyController extends Controller
'status' => 1,
]);
// 綁定客戶管理員角色
$user->assignRole('tenant-admin');
// 角色初始化與克隆邏輯 (優先使用選擇的角色,否則使用預設)
$selected_role_name = $validated['admin_role'] ?? '通用客戶角色範本';
$role_to_assign = '管理員';
$template_role = \App\Models\System\Role::where('name', $selected_role_name)
->whereNull('company_id')
->where('is_system', 0)
->first();
if ($template_role) {
// 克隆範本為該公司的「管理員」
$clonedRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $company->id,
'is_system' => false,
]);
$clonedRole->syncPermissions($template_role->permissions);
} else {
// 如果找不到選定的角色範本,退而求其次嘗試指派現有角色 (通常不應發生)
$role_to_assign = $selected_role_name;
}
$user->assignRole($role_to_assign);
}
});

View File

@@ -16,10 +16,7 @@ class PermissionController extends Controller
// 租戶隔離:租戶只能看到自己公司的角色 + 系統角色 (company_id is null)
if (!$user->isSystemAdmin()) {
$query->where(function($q) use ($user) {
$q->where('company_id', $user->company_id)
->orWhereNull('company_id');
});
$query->where('company_id', $user->company_id);
}
// 搜尋:角色名稱
@@ -58,15 +55,21 @@ class PermissionController extends Controller
*/
public function storeRole(Request $request)
{
$is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
$company_id = $is_system ? null : auth()->user()->company_id;
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name',
'name' => [
'required', 'string', 'max:255',
\Illuminate\Validation\Rule::unique('roles', 'name')->where(function ($query) use ($company_id) {
return $query->where('company_id', $company_id);
})
],
'permissions' => 'nullable|array',
'permissions.*' => 'string|exists:permissions,name',
]);
$is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
$role = \App\Models\System\Role::create([
$role = \App\Models\System\Role::query()->create([
'name' => $validated['name'],
'guard_name' => 'web',
'company_id' => $is_system ? null : auth()->user()->company_id,
@@ -92,8 +95,18 @@ class PermissionController extends Controller
{
$role = \App\Models\System\Role::findOrFail($id);
$is_system = $role->is_system;
$company_id = $role->company_id;
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name,' . $id,
'name' => [
'required', 'string', 'max:255',
\Illuminate\Validation\Rule::unique('roles', 'name')
->ignore($id)
->where(function ($query) use ($company_id) {
return $query->where('company_id', $company_id);
})
],
'permissions' => 'nullable|array',
'permissions.*' => 'string|exists:permissions,name',
]);
@@ -177,10 +190,7 @@ class PermissionController extends Controller
$companies = auth()->user()->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
$roles_query = \App\Models\System\Role::where('name', '!=', 'super-admin');
if (!auth()->user()->isSystemAdmin()) {
$roles_query->where(function($q) {
$q->where('company_id', auth()->user()->company_id)
->orWhereNull('company_id');
});
$roles_query->where('company_id', auth()->user()->company_id);
}
$roles = $roles_query->get();
@@ -198,7 +208,7 @@ class PermissionController extends Controller
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'email' => 'nullable|email|max:255|unique:users,email',
'email' => 'required|email|max:255|unique:users,email',
'password' => 'required|string|min:8',
'role' => 'required|string',
'status' => 'required|boolean',
@@ -206,17 +216,73 @@ class PermissionController extends Controller
'phone' => 'nullable|string|max:20',
]);
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
// 查找角色:優先尋找該公司的角色,若無則尋找全域範本
$role = \App\Models\System\Role::where('name', $validated['role'])
->where(function($q) use ($company_id) {
$q->where('company_id', $company_id)->orWhereNull('company_id');
})
->first();
if (!$role) {
return redirect()->back()->with('error', __('Role not found.'));
}
// 驗證角色與公司的匹配性 (RBAC Safeguard)
if ($company_id !== null) {
// 如果是租戶帳號,不能選各項系統角色 (is_system = 1)
if ($role->is_system) {
return redirect()->back()->with('error', __('System roles cannot be assigned to tenant accounts.'));
}
// 如果角色有特定的 company_id必須匹配
if ($role->company_id !== null && $role->company_id != $company_id) {
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
}
} else {
// 如果是系統層級帳號,只能選系統角色 (is_system = 1)
if (!$role->is_system) {
return redirect()->back()->with('error', __('Only system roles can be assigned to platform administrative accounts.'));
}
}
// 角色初始化與克隆邏輯 (只有 super-admin 在幫空白公司開帳號時觸發)
$role_to_assign = $validated['role'];
$company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
if ($company_id && $role && !$role->is_system && $role->company_id === null) {
// 檢查該公司是否已有名為「管理員」的角色
$existingRole = \App\Models\System\Role::where('company_id', $company_id)
->where('name', '管理員')
->first();
if (!$existingRole) {
// 克隆範本為該公司的「管理員」
$clonedRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $company_id,
'is_system' => false,
]);
$clonedRole->syncPermissions($role->permissions);
$role_to_assign = '管理員';
} else {
// 如果已存在名為「管理員」的角色,則直接使用它
$role_to_assign = '管理員';
}
}
$user = \App\Models\System\User::create([
'name' => $validated['name'],
'username' => $validated['username'],
'email' => $validated['email'],
'password' => \Illuminate\Support\Facades\Hash::make($validated['password']),
'status' => $validated['status'],
'company_id' => auth()->user()->isSystemAdmin() ? $validated['company_id'] : auth()->user()->company_id,
'company_id' => $company_id,
'phone' => $validated['phone'],
]);
$user->assignRole($validated['role']);
$user->assignRole($role_to_assign);
return redirect()->back()->with('success', __('Account created successfully.'));
}
@@ -235,7 +301,7 @@ class PermissionController extends Controller
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username,' . $id,
'email' => 'nullable|email|max:255|unique:users,email,' . $id,
'email' => 'required|email|max:255|unique:users,email,' . $id,
'password' => 'nullable|string|min:8',
'role' => 'required|string',
'status' => 'required|boolean',
@@ -243,6 +309,35 @@ class PermissionController extends Controller
'phone' => 'nullable|string|max:20',
]);
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
// 查找角色:優先尋找該公司的角色,若無則尋找全域範本
$roleObj = \App\Models\System\Role::where('name', $validated['role'])
->where(function($q) use ($target_company_id) {
$q->where('company_id', $target_company_id)->orWhereNull('company_id');
})
->first();
if (!$roleObj) {
return redirect()->back()->with('error', __('Role not found.'));
}
// 驗證角色與公司的匹配性 (RBAC Safeguard)
if ($user->id !== auth()->id()) { // 排除編輯自己 (super-admin 有特殊邏輯)
if ($target_company_id !== null) {
if ($roleObj->is_system) {
return redirect()->back()->with('error', __('System roles cannot be assigned to tenant accounts.'));
}
if ($roleObj->company_id !== null && $roleObj->company_id != $target_company_id) {
return redirect()->back()->with('error', __('This role belongs to another company and cannot be assigned.'));
}
} else {
if (!$roleObj->is_system) {
return redirect()->back()->with('error', __('Only system roles can be assigned to platform administrative accounts.'));
}
}
}
$updateData = [
'name' => $validated['name'],
'username' => $validated['username'],
@@ -265,13 +360,37 @@ class PermissionController extends Controller
$updateData['password'] = \Illuminate\Support\Facades\Hash::make($validated['password']);
}
// 角色初始化與克隆邏輯
$role_to_assign = $validated['role'];
$target_company_id = auth()->user()->isSystemAdmin() ? ($validated['company_id'] ?? null) : auth()->user()->company_id;
if ($target_company_id && $roleObj && !$roleObj->is_system && $roleObj->company_id === null) {
// 檢查該公司是否已有名為「管理員」的角色
$existingRole = \App\Models\System\Role::where('company_id', $target_company_id)
->where('name', '管理員')
->first();
if (!$existingRole) {
$clonedRole = \App\Models\System\Role::query()->create([
'name' => '管理員',
'guard_name' => 'web',
'company_id' => $target_company_id,
'is_system' => false,
]);
$clonedRole->syncPermissions($roleObj->permissions);
$role_to_assign = '管理員';
} else {
$role_to_assign = '管理員';
}
}
$user->update($updateData);
// 如果是編輯自己且原本是超級管理員,強制保留 super-admin 角色
if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) {
$user->syncRoles(['super-admin']);
} else {
$user->syncRoles([$validated['role']]);
$user->syncRoles([$role_to_assign]);
}
return redirect()->back()->with('success', __('Account updated successfully.'));
@@ -292,6 +411,12 @@ class PermissionController extends Controller
return redirect()->back()->with('error', __('You cannot delete your own account.'));
}
// 為了解決軟刪除導致的唯一索引佔用問題,刪除前先重命名唯一欄位
$timestamp = now()->getTimestamp();
$user->username = $user->username . '.deleted.' . $timestamp;
$user->email = $user->email . '.deleted.' . $timestamp;
$user->save();
$user->delete();
return redirect()->back()->with('success', __('Account deleted successfully.'));

View File

@@ -49,6 +49,8 @@ class Machine extends Model
'updater_id',
];
protected $appends = ['image_urls'];
protected $casts = [
'last_heartbeat_at' => 'datetime',
'welcome_gift_enabled' => 'boolean',

View File

@@ -3,9 +3,11 @@
namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Model;
use App\Traits\TenantScoped;
class MachineModel extends Model
{
use TenantScoped;
protected $fillable = [
'name',
'company_id',

View File

@@ -22,7 +22,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
if (!$this->app->isLocal()) {
if (str_starts_with(config('app.url'), 'https://')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
}

View File

@@ -26,7 +26,7 @@ class AdminUserSeeder extends Seeder
$admin->update([
'name' => 'Admin',
'email' => 'admin@star-cloud.com',
'password' => Hash::make('password'),
'password' => Hash::make('Star82779061'),
]);
$admin->assignRole('super-admin');
return;

View File

@@ -41,14 +41,14 @@ class RoleSeeder extends Seeder
// 建立角色
$superAdmin = Role::updateOrCreate(
['name' => 'super-admin'],
['is_system' => true]
['name' => 'super-admin'],
['is_system' => true]
);
$superAdmin->syncPermissions(Permission::all());
$tenantAdmin = Role::updateOrCreate(
['name' => 'tenant-admin'],
['is_system' => false]
['name' => '客戶管理員'],
['is_system' => false]
);
$tenantAdmin->syncPermissions([
'menu.members',

View File

@@ -49,9 +49,6 @@
"Current Stock": "Current Stock",
"Last Signal": "Last Signal",
"Alert Summary": "Alert Summary",
"Online": "Online",
"Offline": "Offline",
"Low Stock": "Low Stock",
"No alert summary": "No alert summary",
"No data available": "No data available",
"Showing :from to :to of :total items": "Showing :from to :to of :total items",
@@ -232,6 +229,7 @@
"Enter login ID": "Enter login ID",
"Min 8 characters": "Min 8 characters",
"Admin display name": "Admin display name",
"Initial Role": "Initial Role",
"Contact & Details": "Contact & Details",
"e.g. Taiwan Star": "e.g. Taiwan Star",
"e.g. TWSTAR": "e.g. TWSTAR",
@@ -278,5 +276,106 @@
"Warning": "Warning",
"basic-settings": "Basic Settings",
"permissions": "Permission Settings",
"Error": "Error"
"Error": "Error",
"Machine Model Settings": "Machine Model Settings",
"Machine Model": "Machine Model",
"Model Name": "Model Name",
"Machine Count": "Machine Count",
"Add Machine Model": "Add Machine Model",
"Edit Machine Model": "Edit Machine Model",
"Machine model created successfully.": "Machine model created successfully.",
"Machine model updated successfully.": "Machine model updated successfully.",
"Machine model deleted successfully.": "Machine model deleted successfully.",
"Cannot delete model that is currently in use by machines.": "Cannot delete model that is currently in use by machines.",
"Machine Details": "Machine Details",
"Create Machine": "Create Machine",
"Edit Machine": "Edit Machine",
"Basic Information": "Basic Information",
"Location": "Location",
"Temperature": "Temperature",
"Firmware Version": "Firmware Version",
"Last Heartbeat": "Last Heartbeat",
"Never Connected": "Never Connected",
"View Logs": "View Logs",
"Real-time Operation Logs (Last 50)": "Real-time Operation Logs (Last 50)",
"All Times System Timezone": "All times are in system timezone",
"Level": "Level",
"Logs": "Logs",
"Time": "Time",
"Message": "Message",
"Online": "Online",
"Offline": "Offline",
"Connecting...": "Connecting...",
"No logs found": "No logs found",
"Management of operational parameters and models": "Management of operational parameters and models",
"ECPay Invoice Settings Description": "ECPay Electronic Invoice Settings",
"E.SUN QR Scan Settings Description": "E.SUN Bank QR Scan Payment Settings",
"LINE Pay Direct Settings Description": "LINE Pay Official Direct Connection Settings",
"TapPay Integration Settings Description": "TapPay Payment Integration Settings",
"Merchant IDs": "Merchant IDs",
"Parameters": "Parameters",
"Hardware & Network": "Hardware & Network",
"Serial & Version": "Serial & Version",
"Heartbeat": "Heartbeat",
"Heating Range": "Heating Range",
"API Token": "API Token",
"No location set": "No location set",
"Close Panel": "Close Panel",
"Operations": "Operations",
"Operational Parameters": "Operational Parameters",
"Hardware & Slots": "Hardware & Slots",
"Slot Mechanism (default: Conveyor, check for Spring)": "Slot Mechanism (default: Conveyor, check for Spring)",
"Payment & Invoice": "Payment & Invoice",
"Payment Config": "Payment Config",
"Invoice Status": "Invoice Status",
"No Invoice": "No Invoice",
"Default Donate": "Default Donate",
"Default Not Donate": "Default Not Donate",
"Member & External": "Member & External",
"Welcome Gift": "Welcome Gift",
"Enabled/Disabled": "Enabled/Disabled",
"Member System": "Member System",
"Machine Images": "Machine Images",
"No images uploaded": "No images uploaded",
"Upload New Images": "Upload New Images",
"Max 3": "Max 3",
"Uploading new images will replace all existing images.": "Uploading new images will replace all existing images.",
"Search machines...": "Search machines...",
"Search models...": "Search models...",
"Card Reader": "Card Reader",
"Owner": "Owner",
"Action": "Action",
"Items": "Items",
"View Details": "View Details",
"Edit Settings": "Edit Settings",
"Are you sure?": "Are you sure?",
"Serial No": "Serial No",
"Select Owner": "Select Owner",
"Select Model": "Select Model",
"Machines": "Machines",
"Models": "Models",
"Edit": "Edit",
"Delete": "Delete",
"None": "None",
"Select Company": "Select Company",
"e.g., Company Standard Pay": "e.g., Company Standard Pay",
"APP_ID": "APP_ID",
"APP_KEY": "APP_KEY",
"ChannelId": "ChannelId",
"ChannelSecret": "ChannelSecret",
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID",
"HashIV": "HashIV",
"HashKey": "HashKey",
"JKO_MERCHANT_ID": "JKO_MERCHANT_ID",
"Key": "Key",
"LINE_MERCHANT_ID": "LINE_MERCHANT_ID",
"PARTNER_KEY": "PARTNER_KEY",
"PI_MERCHANT_ID": "PI_MERCHANT_ID",
"PS_MERCHANT_ID": "PS_MERCHANT_ID",
"Save Config": "Save Config",
"StoreID": "StoreID",
"TermID": "TermID",
"Photo Slot": "Photo Slot",
"Optimized for display. Supported formats: JPG, PNG, WebP.": "Optimized for display. Supported formats: JPG, PNG, WebP.",
"Change": "Change"
}

View File

@@ -49,9 +49,6 @@
"Current Stock": "現在の在庫",
"Last Signal": "最終信号時間",
"Alert Summary": "アラート概要",
"Online": "オンライン",
"Offline": "オフライン",
"Low Stock": "在庫少",
"No alert summary": "アラートなし",
"No data available": "データなし",
"Showing :from to :to of :total items": ":total 件中 :from から :to 件を表示",
@@ -144,11 +141,10 @@
"Machine Settings": "機台設定",
"Permission Settings": "権限設定",
"APP Features": "APP機能",
"Sales": "販売",
"Others": "その他",
"AI Prediction": "AI予測",
"Roles": "ロール限",
"Role Management": "ロール限管理",
"Roles": "ロール限",
"Role Management": "ロール限管理",
"Define and manage security roles and permissions.": "システムのセキュリティロールと権限を定義および管理します。",
"Search roles...": "ロールを検索...",
"No permissions": "権限項目なし",
@@ -182,21 +178,21 @@
"special-permission": "特別権限",
"companies": "顧客管理",
"accounts": "アカウント管理",
"roles": "ロール限",
"Role Permissions": "ロール限",
"roles": "ロール限",
"Role Permissions": "ロール限",
"Role Settings": "ロール權限",
"No login history yet": "ログイン履歴はまだありません",
"Signed in as": "ログイン中",
"Logout": "ログアウト",
"Joined": "入会日",
"Recent Login": "最近のログイン",
"Total Logins": "ログイン",
"Account Status": "アカウント態",
"Total Logins": "ログイン",
"Account Status": "アカウント態",
"Active": "アクティブ",
"Customer Management": "顧客管理",
"Manage all tenant accounts and validity": "すべてのテナントアカウントと有効期限を管理します",
"Add Customer": "顧客を追加",
"Total Customers": "顧客総数",
"Total Customers": "顧客總數",
"Expired / Disabled": "期限切れ / 停止中",
"Search customers...": "顧客を検索...",
"All": "すべて",
@@ -231,6 +227,7 @@
"Enter login ID": "ログインIDを入力してください",
"Min 8 characters": "最低8文字",
"Admin display name": "管理者表示名",
"Initial Role": "初期ロール",
"Contact & Details": "連絡先と詳細",
"e.g. Taiwan Star": "例:台湾スター",
"e.g. TWSTAR": "例TWSTAR",
@@ -276,5 +273,116 @@
"Warning": "警告",
"basic-settings": "基本設定",
"permissions": "權限設定",
"Error": "エラー"
"Error": "エラー",
"Machine Model Settings": "機台型號設定",
"Machine Model": "機台型號",
"Model Name": "型號名称",
"Machine Count": "機台数量",
"Add Machine Model": "機台型號を追加",
"Edit Machine Model": "機台型號を編輯",
"Machine model created successfully.": "機台型號が正常に作成されました。",
"Machine model updated successfully.": "機台型號が正常に更新されました。",
"Machine model deleted successfully.": "機台型號が正常に削除されました。",
"Cannot delete model that is currently in use by machines.": "機台で使用中の型號は削除できません。",
"Machine Details": "機台詳情",
"Create Machine": "機台新規作成",
"Edit Machine": "機台編集",
"Basic Information": "基本情報",
"Location": "場所",
"Temperature": "温度",
"Firmware Version": "ファームウェアバージョン",
"Last Heartbeat": "最終ハートビート時間",
"Never Connected": "未接続",
"View Logs": "ログを表示",
"Real-time Operation Logs (Last 50)": "リアルタイム操作ログ (直近 50 件)",
"All Times System Timezone": "時間はすべてシステムタイムゾーンです",
"Level": "レベル",
"Logs": "ログ",
"Time": "時間",
"Message": "メッセージ",
"Online": "オンライン",
"Offline": "オフライン",
"Connecting...": "接続中...",
"No logs found": "ログがありません",
"Management of operational parameters and models": "運用パラメータと型番の管理",
"ECPay Invoice Settings Description": "ECPay 電子発票設定",
"E.SUN QR Scan Settings Description": "玉山銀行 QR スキャン決済設定",
"LINE Pay Direct Settings Description": "LINE Pay 公式直結設定",
"TapPay Integration Settings Description": "TapPay 決済連携設定",
"Merchant IDs": "マーチャント ID",
"Parameters": "パラメータ設定",
"Hardware & Network": "ハードウェアとネットワーク",
"Serial & Version": "シリアルとバージョン",
"Heartbeat": "ハートビート状態",
"Heating Range": "加熱時間帯",
"API Token": "API キー",
"No location set": "場所が未設定です",
"Close Panel": "パネルを閉じる",
"Operations": "運用設定",
"Operational Parameters": "運用パラメータ",
"Hardware & Slots": "ハードウェアと貨道",
"Slot Mechanism (default: Conveyor, check for Spring)": "貨道メカニズム (デフォルト:コンベア、チェックでスプリング)",
"Payment & Invoice": "決済と発票",
"Payment Config": "決済設定",
"Invoice Status": "発票発行状態",
"No Invoice": "発票を発行しない",
"Default Donate": "デフォルト寄付",
"Default Not Donate": "デフォルト寄付しない",
"Member & External": "会員と外部システム",
"Welcome Gift": "会員登録特典",
"Enabled/Disabled": "有効/無効",
"Member System": "会員システム",
"Machine Images": "機台写真",
"No images uploaded": "写真がアップロードされていません",
"Upload New Images": "新しい写真をアップロード",
"Max 3": "最大3枚",
"Uploading new images will replace all existing images.": "新しい写真をアップロードすると、既存のすべての写真が置き換えられます。",
"Search machines...": "機台を検索...",
"Search models...": "型番を検索...",
"Card Reader": "カードリーダー",
"Owner": "所属会社",
"Action": "操作",
"Items": "個の項目",
"View Details": "詳細表示",
"Edit Settings": "設定編集",
"Are you sure?": "よろしいですか?",
"Serial No": "機台シリアル番号",
"Select Owner": "所属会社を選択",
"Select Model": "型番を選択",
"Machines": "機台リスト",
"Models": "型番リスト",
"Edit": "編集",
"Delete": "削除",
"None": "なし",
"Create Payment Config": "決済設定を新規作成",
"Edit Payment Config": "決済設定を編集",
"Define new third-party payment parameters": "新しいサードパーティ決済パラメータを定義",
"Configuration Name": "設定名称",
"Belongs To Company": "所属会社",
"Select Company": "会社を選択",
"ECPay Invoice": "ECPay 電子発票",
"Store ID": "加盟店ID (MerchantID)",
"HashKey": "HashKey",
"HashIV": "HashIV",
"E.SUN QR Scan": "玉山銀行 QR スキャン",
"StoreID": "加盟店ID (StoreID)",
"TermID": "端末ID (TermID)",
"Key": "キー (Key)",
"LINE Pay Direct": "LINE Pay 直結決済",
"ChannelId": "チャンネルID",
"ChannelSecret": "チャンネルシークレット",
"TapPay Integration": "TapPay 統合決済",
"PARTNER_KEY": "パートナーキー",
"APP_ID": "APP_ID",
"APP_KEY": "APP_KEY",
"LINE_MERCHANT_ID": "LINE Pay 加盟店ID",
"JKO_MERCHANT_ID": "街口支付 加盟店ID",
"PI_MERCHANT_ID": "Pi 拍錢包 加盟店ID",
"PS_MERCHANT_ID": "全盈+Pay 加盟店ID",
"EASY_MERCHANT_ID": "悠遊付 加盟店ID",
"Save Config": "設定を保存",
"e.g., Company Standard Pay": "例:標準決済組合せ",
"Photo Slot": "写真スロット",
"Optimized for display. Supported formats: JPG, PNG, WebP.": "表示用に最適化されています。対応形式JPG, PNG, WebP。",
"Change": "変更"
}

View File

@@ -49,9 +49,6 @@
"Current Stock": "當前庫存",
"Last Signal": "最後訊號時間",
"Alert Summary": "告警摘要",
"Online": "在線",
"Offline": "離線",
"Low Stock": "庫存低",
"No alert summary": "暫無告警記錄",
"No data available": "暫無資料",
"Showing :from to :to of :total items": "顯示第 :from 到 :to 項,共 :total 項",
@@ -232,6 +229,7 @@
"Enter login ID": "請輸入登入帳號",
"Min 8 characters": "至少 8 個字元",
"Admin display name": "管理員顯示名稱",
"Initial Role": "初始角色",
"Contact & Details": "聯絡資訊與詳情",
"e.g. Taiwan Star": "例如:台灣之星",
"e.g. TWSTAR": "例如TWSTAR",
@@ -281,22 +279,22 @@
"Unknown": "未知",
"Info": "一般",
"Warning": "警告",
"Error": "錯誤",
"Error": "異常",
"Management of operational parameters": "機台運作參數管理",
"Add Machine": "新增機台",
"Search machines...": "搜尋機台...",
"Items": "",
"Items": "個項目",
"Machine Name": "機台名稱",
"Serial No": "機台序號",
"Owner": "所屬客戶",
"Owner": "所屬公司",
"Model": "機台型號",
"Action": "操作",
"No location set": "尚未設定位置",
"Edit Settings": "編輯設定",
"Enter machine name": "請輸入機台名稱",
"Enter serial number": "請輸入機台序號",
"Select Owner": "選擇所屬客戶",
"Select Model": "選擇機台型號",
"Select Owner": "選擇所屬公司",
"Select Model": "選擇型號",
"Customer Payment Config": "客戶金流設定",
"Not Used": "不使用",
"Edit Machine Settings": "編輯機台設定",
@@ -307,18 +305,18 @@
"Checkout Time 2": "卡機結帳時間2",
"Heating Start Time": "開啟-加熱時間",
"Heating End Time": "關閉-加熱時間",
"Hardware & Slots": "硬體與貨道設定",
"Hardware & Slots": "硬體與貨道",
"Card Reader No": "刷卡機編號",
"Key No": "鑰匙編號",
"Slot Mechanism (default: Conveyor, check for Spring)": "貨道類型 (預設履帶,勾選為彈簧)",
"Slot Mechanism (default: Conveyor, check for Spring)": "貨道機制 (預設履帶,勾選為彈簧)",
"Payment & Invoice": "金流與發票",
"Invoice Status": "發票狀態",
"No Invoice": "不開發票",
"Default Donate": "開發票預設捐",
"Default Not Donate": "開發票預設不捐",
"Invoice Status": "發票開立狀態",
"No Invoice": "不開發票",
"Default Donate": "預設捐",
"Default Not Donate": "預設不捐",
"Member & External": "會員與外部系統",
"Welcome Gift": "來店禮開關",
"Enabled/Disabled": "啟用/用",
"Welcome Gift": "註冊成效禮",
"Enabled/Disabled": "啟用/用",
"Member System": "會員系統",
"Payment Configuration": "客戶金流設定",
"Merchant payment gateway settings management": "特約商店支付網關參數管理",
@@ -346,7 +344,7 @@
"PARTNER_KEY": "PARTNER_KEY",
"APP_ID": "APP_ID",
"APP_KEY": "APP_KEY",
"Merchant IDs": "特約商店代號 (Merchant IDs)",
"Merchant IDs": "商店代號",
"LINE_MERCHANT_ID": "LINE Pay 商店代號",
"JKO_MERCHANT_ID": "街口支付 商店代號",
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
@@ -354,5 +352,68 @@
"EASY_MERCHANT_ID": "悠遊付 商店代號",
"basic-settings": "基本設定",
"permissions": "權限設定",
"Edit Payment Config": "編輯金流配置"
"Edit Payment Config": "編輯金流配置",
"Machine Model Settings": "機台型號設定",
"Machine Model": "機台型號",
"Model Name": "型號名稱",
"Machine Count": "機台數量",
"Add Machine Model": "新增機台型號",
"Edit Machine Model": "編輯機台型號",
"Machine model created successfully.": "機台型號已成功建立。",
"Machine model updated successfully.": "機台型號已成功更新。",
"Machine model deleted successfully.": "機台型號已成功刪除。",
"Cannot delete model that is currently in use by machines.": "無法刪除目前正在被機台使用的型號。",
"Machine Details": "機台詳情",
"Create Machine": "新增機台",
"Edit Machine": "編輯機台",
"Basic Information": "基本資訊",
"Location": "位置",
"Temperature": "溫度",
"Firmware Version": "韌體版本",
"Last Heartbeat": "最後心跳時間",
"Never Connected": "從未連線",
"View Logs": "查看日誌",
"Real-time Operation Logs (Last 50)": "即時操作日誌 (最後 50 筆)",
"All Times System Timezone": "所有時間為系統時區",
"Level": "層級",
"Logs": "日誌",
"Time": "時間",
"Message": "訊息",
"Online": "線上",
"Offline": "離線",
"Connecting...": "連線中",
"No logs found": "暫無相關日誌",
"Management of operational parameters and models": "管理運作參數與型號",
"ECPay Invoice Settings Description": "綠界科技電子發票設定",
"E.SUN QR Scan Settings Description": "玉山銀行掃碼支付設定",
"LINE Pay Direct Settings Description": "LINE Pay 官方直連設定",
"TapPay Integration Settings Description": "喬睿科技支付串接設定",
"Parameters": "參數設定",
"Hardware & Network": "硬體與網路",
"Serial & Version": "序號與版本",
"Heartbeat": "心跳狀態",
"Heating Range": "加熱時段",
"API Token": "API 金鑰",
"Close Panel": "關閉面板",
"Operations": "運作設定",
"Payment Config": "金流配置",
"Machine Images": "機台照片",
"No images uploaded": "尚未上傳照片",
"Upload New Images": "上傳新照片",
"Max 3": "最多 3 張",
"Uploading new images will replace all existing images.": "上傳新照片將會取代所有現有照片。",
"Search models...": "搜尋型號...",
"Card Reader": "刷卡機",
"View Details": "查看詳情",
"Are you sure?": "確定要執行此操作嗎?",
"Machines": "機台列表",
"Models": "型號列表",
"Edit": "編輯",
"Delete": "刪除",
"None": "無",
"Select Company": "選擇所屬公司",
"e.g., Company Standard Pay": "例如:公司標準支付",
"Photo Slot": "照片欄位",
"Optimized for display. Supported formats: JPG, PNG, WebP.": "已針對顯示進行優化。支援格式JPG, PNG, WebP。",
"Change": "更換"
}

View File

@@ -61,6 +61,66 @@
animation: fadeUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
/* Additional Loading Animations */
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-fade-in-right {
animation: fadeInRight 0.8s ease-out forwards;
}
@keyframes trickle {
0% { width: 0%; opacity: 1; }
20% { width: 30%; }
50% { width: 70%; }
80% { width: 90%; }
95% { width: 95%; }
100% { width: 95%; }
}
.animate-trickle {
animation: trickle 10s cubic-bezier(0.1, 0.5, 0.5, 1) forwards;
}
/* Top Progress Bar (Trickle Style) */
.top-loading-bar {
@apply fixed top-0 left-0 right-0 h-0.5 z-[99999] bg-gradient-to-r from-cyan-500 to-blue-500 opacity-0 transition-opacity duration-300;
width: 0%;
}
.top-loading-bar.loading {
@apply opacity-100;
animation: trickle 12s cubic-bezier(0.1, 0.5, 0.5, 1) forwards;
}
@keyframes loadingPulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
/* Skeleton Loading Utilities (Option C) */
.skeleton {
@apply bg-slate-100 dark:bg-slate-800 animate-pulse-subtle rounded-lg overflow-hidden relative border-none !text-transparent selection:bg-transparent pointer-events-none;
}
@keyframes pulse-subtle {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse-subtle {
animation: pulse-subtle 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Custom Scrollbar - Minimal & Elegant */
::-webkit-scrollbar {
width: 6px;

View File

@@ -1,7 +1,7 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-10 pb-20">
<div class="space-y-8 pb-20">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="flex items-center gap-4">
@@ -44,12 +44,12 @@
<!-- Left: Basic info & Hardware -->
<div class="lg:col-span-2 space-y-8">
<!-- Basic Information -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="flex items-center gap-3 mb-8">
<div class="luxury-card rounded-3xl p-7 animate-luxury-in">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 18.75 4.5H5.25a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z"/></svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('Basic Information') }}</h3>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Basic Information') }}</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
@@ -61,6 +61,10 @@
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Serial Number') }}</label>
<input type="text" value="{{ $machine->serial_no }}" class="luxury-input w-full bg-slate-50/50 dark:bg-slate-900/50 text-slate-400 cursor-not-allowed" readonly>
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Location') }}</label>
<input type="text" name="location" value="{{ old('location', $machine->location) }}" class="luxury-input w-full" placeholder="{{ __('e.g., Taipei Station') }}">
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Machine Model') }}</label>
<select name="machine_model_id" class="luxury-select w-full" required>
@@ -78,7 +82,7 @@
<div class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 12h7.5"/></svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('Operational Parameters') }}</h3>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Operational Parameters') }}</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
@@ -110,12 +114,12 @@
</div>
<!-- hardware & slot types -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 150ms">
<div class="flex items-center gap-3 mb-8">
<div class="luxury-card rounded-3xl p-7 animate-luxury-in" style="animation-delay: 150ms">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 7.5V18a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 18V7.5m0-4.5h18M3 7.5h18M3 12h18M3 16.5h18"/></svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('Hardware & Slots') }}</h3>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Hardware & Slots') }}</h3>
</div>
<div class="space-y-8">
@@ -153,7 +157,7 @@
<div class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75m0-1.5a.75.75 0 0 1 .75.75v.75m-.75 0H3m.75 0h.75m-1.5 0a.75.75 0 0 1-.75-.75V3M3 10.5v1.5m10.5-3v-4.5m0 4.5h5.25m-5.25 0V10.5m0-1.5a.75.75 0 0 1 .75-.75h.75m-1.5 0H12m.75 0h.75m-1.5 0a.75.75 0 0 1-.75-.75V3.75M12 4.5H3.75a2.25 2.25 0 0 0-2.25 2.25v10.5a2.25 2.25 0 0 0 2.25 2.25h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 18.75 4.5Z"/></svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('Payment & Invoice') }}</h3>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Payment & Invoice') }}</h3>
</div>
<div class="space-y-6">
@@ -183,7 +187,7 @@
<div class="w-10 h-10 rounded-xl bg-sky-500/10 flex items-center justify-center text-sky-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('Member & External') }}</h3>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Member & External') }}</h3>
</div>
<div class="space-y-6">
@@ -195,7 +199,7 @@
<div class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="welcome_gift_enabled" value="0">
<input type="checkbox" name="welcome_gift_enabled" value="1" {{ $machine->welcome_gift_enabled ? 'checked' : '' }} class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-cyan-300 dark:peer-focus:ring-cyan-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 rounded-full peer peer-focus:ring-4 peer-focus:ring-cyan-300 dark:peer-focus:ring-cyan-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500 transition-colors"></div>
</div>
</label>
@@ -207,43 +211,12 @@
<div class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="member_system_enabled" value="0">
<input type="checkbox" name="member_system_enabled" value="1" {{ $machine->member_system_enabled ? 'checked' : '' }} class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-cyan-300 dark:peer-focus:ring-cyan-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500"></div>
<div class="w-11 h-6 bg-slate-200 dark:bg-slate-700 rounded-full peer peer-focus:ring-4 peer-focus:ring-cyan-300 dark:peer-focus:ring-cyan-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-cyan-500 transition-colors"></div>
</div>
</label>
</div>
</div>
<!-- Machine Images Management -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 400ms">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/></svg>
</div>
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('Machine Images') }}</h3>
</div>
<div class="space-y-6">
@if(!empty($machine->image_urls))
<div class="grid grid-cols-3 gap-3">
@foreach($machine->image_urls as $url)
<div class="relative aspect-square rounded-2xl overflow-hidden border border-slate-100 dark:border-slate-800 shadow-sm group">
<img src="{{ $url }}" class="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
</div>
@endforeach
</div>
@else
<div class="p-6 rounded-2xl border border-dashed border-slate-200 dark:border-slate-800 text-center">
<p class="text-xs font-bold text-slate-400 capitalize">{{ __('No images uploaded') }}</p>
</div>
@endif
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Upload New Images') }} ({{ __('Max 3') }})</label>
<input type="file" name="images[]" multiple accept="image/*" class="luxury-input w-full text-xs py-2">
<p class="text-[10px] text-slate-400 mt-2 italic">* {{ __('Uploading new images will replace all existing images.') }}</p>
</div>
</div>
</div>
</div>
</div>
</form>

File diff suppressed because it is too large Load Diff

View File

@@ -55,22 +55,22 @@
<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="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('ECPay Invoice') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">綠界科技電子發票設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('ECPay Invoice') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('ECPay Invoice Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Store ID') }}</label>
<input type="text" name="settings[ecpay_invoice][store_id]" class="luxury-input w-full" placeholder="2000132">
<input type="text" name="settings[ecpay_invoice][store_id]" class="luxury-input w-full">
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('HashKey') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_key]" class="luxury-input w-full" placeholder="ej67pDIFpSST6p4q">
<input type="text" name="settings[ecpay_invoice][hash_key]" class="luxury-input w-full">
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('HashIV') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_iv]" class="luxury-input w-full" placeholder="q9m3S9p3S9999999">
<input type="text" name="settings[ecpay_invoice][hash_iv]" class="luxury-input w-full">
</div>
</div>
</div>
@@ -82,8 +82,8 @@
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.875 12h1.125a3.375 3.375 0 0 1 3.375 3.375v1.125m-11.25 4.5h1.125a3.375 3.375 0 0 0 3.375-3.375V16.5a3.375 3.375 0 0 1 3.375-3.375h1.125m-11.25 4.5h1.125a3.375 3.375 0 0 1 3.375 3.375v1.125m-3.375-1.125h.008v.008h-.008v-.008Zm3.375 3.375h.008v.008h-.008v-.008Zm0-3.375h.008v.008h-.008v-.008Zm-3.375-3.375h.008v.008h-.008v-.008Zm0 3.375h.008v.008h-.008v-.008Z"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('E.SUN QR Scan') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">玉山銀行掃碼支付設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('E.SUN QR Scan') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('E.SUN QR Scan Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
@@ -109,8 +109,8 @@
<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="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m24.408 0a8.959 8.959 0 0 1 .284-2.253m0 2.253C20.46 17.1 16.485 19.5 12 19.5S3.538 17.1 2.284 14.253"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('LINE Pay Direct') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">LINE Pay 官方直連設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('LINE Pay Direct') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('LINE Pay Direct Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
@@ -134,8 +134,8 @@
<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 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('TapPay Integration') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">喬睿科技支付串接設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('TapPay Integration') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('TapPay Integration Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
@@ -154,7 +154,7 @@
<input type="text" name="settings[tappay][app_key]" class="luxury-input w-full">
</div>
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/80">
<p class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-6 font-display">{{ __('Merchant IDs (商店代號群)') }}</p>
<p class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-6 font-display">{{ __('Merchant IDs') }}</p>
<div class="space-y-6">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('LINE_MERCHANT_ID') }}</label>
@@ -169,11 +169,11 @@
<input type="text" name="settings[tappay][pi_merchant_id]" class="luxury-input w-full">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('PS_MERCHANT_ID') }} (全盈+Pay)</label>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('PS_MERCHANT_ID') }}</label>
<input type="text" name="settings[tappay][ps_merchant_id]" class="luxury-input w-full">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('EASY_MERCHANT_ID') }} (悠遊付)</label>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('EASY_MERCHANT_ID') }}</label>
<input type="text" name="settings[tappay][easy_merchant_id]" class="luxury-input w-full">
</div>
</div>

View File

@@ -59,22 +59,22 @@
<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="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('ECPay Invoice') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">綠界科技電子發票設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('ECPay Invoice') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('ECPay Invoice Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Store ID') }}</label>
<input type="text" name="settings[ecpay_invoice][store_id]" value="{{ $settings['ecpay_invoice']['store_id'] ?? '' }}" class="luxury-input w-full" placeholder="2000132">
<input type="text" name="settings[ecpay_invoice][store_id]" value="{{ $settings['ecpay_invoice']['store_id'] ?? '' }}" class="luxury-input w-full">
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('HashKey') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_key]" value="{{ $settings['ecpay_invoice']['hash_key'] ?? '' }}" class="luxury-input w-full" placeholder="ej67pDIFpSST6p4q">
<input type="text" name="settings[ecpay_invoice][hash_key]" value="{{ $settings['ecpay_invoice']['hash_key'] ?? '' }}" class="luxury-input w-full">
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('HashIV') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_iv]" value="{{ $settings['ecpay_invoice']['hash_iv'] ?? '' }}" class="luxury-input w-full" placeholder="q9m3S9p3S9999999">
<input type="text" name="settings[ecpay_invoice][hash_iv]" value="{{ $settings['ecpay_invoice']['hash_iv'] ?? '' }}" class="luxury-input w-full">
</div>
</div>
</div>
@@ -86,8 +86,8 @@
<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="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.875 12h1.125a3.375 3.375 0 0 1 3.375 3.375v1.125m-11.25 4.5h1.125a3.375 3.375 0 0 0 3.375-3.375V16.5a3.375 3.375 0 0 1 3.375-3.375h1.125m-11.25 4.5h1.125a3.375 3.375 0 0 1 3.375 3.375v1.125m-3.375-1.125h.008v.008h-.008v-.008Zm3.375 3.375h.008v.008h-.008v-.008Zm0-3.375h.008v.008h-.008v-.008Zm-3.375-3.375h.008v.008h-.008v-.008Zm0 3.375h.008v.008h-.008v-.008Z"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('E.SUN QR Scan') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">玉山銀行掃碼支付設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('E.SUN QR Scan') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('E.SUN QR Scan Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
@@ -113,8 +113,8 @@
<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="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m24.408 0a8.959 8.959 0 0 1 .284-2.253m0 2.253C20.46 17.1 16.485 19.5 12 19.5S3.538 17.1 2.284 14.253"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('LINE Pay Direct') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">LINE Pay 官方直連設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('LINE Pay Direct') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('LINE Pay Direct Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
@@ -138,8 +138,8 @@
<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 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"/></svg>
</div>
<div>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight italic">{{ __('TapPay Integration') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">喬睿科技支付串接設定</p>
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('TapPay Integration') }}</h3>
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('TapPay Integration Settings Description') }}</p>
</div>
</div>
<div class="space-y-6">
@@ -158,7 +158,7 @@
<input type="text" name="settings[tappay][app_key]" value="{{ $settings['tappay']['app_key'] ?? '' }}" class="luxury-input w-full">
</div>
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/80">
<p class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-6 font-display">{{ __('Merchant IDs (商店代號群)') }}</p>
<p class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-6 font-display">{{ __('Merchant IDs') }}</p>
<div class="space-y-6">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('LINE_MERCHANT_ID') }}</label>
@@ -173,11 +173,11 @@
<input type="text" name="settings[tappay][pi_merchant_id]" value="{{ $settings['tappay']['pi_merchant_id'] ?? '' }}" class="luxury-input w-full">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('PS_MERCHANT_ID') }} (全盈+Pay)</label>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('PS_MERCHANT_ID') }}</label>
<input type="text" name="settings[tappay][ps_merchant_id]" value="{{ $settings['tappay']['ps_merchant_id'] ?? '' }}" class="luxury-input w-full">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('EASY_MERCHANT_ID') }} (悠遊付)</label>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('EASY_MERCHANT_ID') }}</label>
<input type="text" name="settings[tappay][easy_merchant_id]" value="{{ $settings['tappay']['easy_merchant_id'] ?? '' }}" class="luxury-input w-full">
</div>
</div>

View File

@@ -48,9 +48,8 @@
</span>
</td>
<td class="px-6 py-6">
<div class="flex flex-col">
<span class="text-xs font-bold text-slate-600 dark:text-slate-300">{{ $config->updated_at->format('Y/m/d H:i') }}</span>
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest mt-0.5">{{ $config->updated_at->diffForHumans() }}</span>
<div class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
{{ $config->updated_at->format('Y/m/d H:i') }}
</div>
</td>
<td class="px-6 py-6 text-right space-x-2">

View File

@@ -322,9 +322,21 @@
placeholder="{{ __('Min 8 characters') }}">
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Admin Name') }}</label>
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Admin Name') }}</label>
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Initial Role') }}</label>
<select name="admin_role" class="luxury-select w-full">
@foreach($template_roles as $role)
<option value="{{ $role->name }}" {{ $role->name == '通用客戶角色範本' ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
</div>
</div>
</div>

View File

@@ -191,9 +191,9 @@
</div>
</td>
<td class="px-6 py-6 text-center">
<span class="text-[13px] text-slate-600 dark:text-slate-200 font-bold font-display tracking-widest bg-slate-50 dark:bg-slate-800/50 px-4 py-1.5 rounded-lg border border-slate-100 dark:border-slate-700/50">
<div class="text-xs font-black text-slate-400 dark:text-slate-400/80 uppercase tracking-widest leading-none">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') : '---' }}
</span>
</div>
</td>
<td class="px-6 py-6 text-right">
<span class="text-[11px] font-bold text-slate-400/30 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-400 transition-colors">{{ __('No alert summary') }}</span>

View File

@@ -7,28 +7,53 @@
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
editing: false,
showModal: {{ $errors->any() ? 'true' : 'false' }},
editing: {{ old('_method') === 'PUT' || (isset($user) && $errors->any()) ? 'true' : 'false' }},
allRoles: @js($roles),
currentUser: {
id: '',
name: '',
username: '',
email: '',
phone: '',
company_id: '',
role: 'user',
status: 1
id: '{{ old('id') }}',
name: '{{ old('name') }}',
username: '{{ old('username') }}',
email: '{{ old('email') }}',
phone: '{{ old('phone') }}',
company_id: '{{ old('company_id', auth()->user()->isSystemAdmin() ? '' : auth()->user()->company_id) }}',
role: '{{ old('role', '') }}',
status: {{ old('status', 1) }}
},
get filteredRoles() {
if (this.currentUser.company_id === '' || this.currentUser.company_id === null) {
// 系統層級:顯示 is_system = 1 的角色
return this.allRoles.filter(r => r.is_system);
} else {
// 客戶層級:只顯示該公司的角色
let roles = this.allRoles.filter(r => r.company_id == this.currentUser.company_id);
// 如果是系統管理員,額外允許選擇「客戶層級範本」
@if(auth()->user()->isSystemAdmin())
let templates = this.allRoles.filter(r => !r.is_system && (r.company_id === null || r.company_id === ''));
roles = [...roles, ...templates];
@endif
return roles;
}
},
openCreateModal() {
this.editing = false;
this.currentUser = { id: '', name: '', username: '', email: '', phone: '', company_id: '', role: 'user', status: 1 };
this.currentUser = { id: '', name: '', username: '', email: '', phone: '', company_id: '{{ auth()->user()->isSystemAdmin() ? "" : auth()->user()->company_id }}', role: '', status: 1 };
this.showModal = true;
// 預設選取第一個可用的角色
this.$nextTick(() => {
if (this.filteredRoles.length > 0) {
this.currentUser.role = this.filteredRoles[0].name;
}
});
},
openEditModal(user) {
this.editing = true;
this.currentUser = {
...user,
role: user.roles && user.roles.length > 0 ? user.roles[0].name : 'user'
company_id: user.company_id || '',
role: user.roles && user.roles.length > 0 ? user.roles[0].name : ''
};
this.showModal = true;
}
@@ -81,6 +106,7 @@
<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">{{ __('User Info') }}</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">{{ __('Email') }}</th>
@if(auth()->user()->isSystemAdmin())
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Belongs To') }}</th>
@endif
@@ -103,10 +129,13 @@
</div>
<div class="flex flex-col">
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $user->name }}</span>
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase"><span class="font-mono">{{ $user->username }}</span> @if($user->email) {{ $user->email }} @endif</span>
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase"><span class="font-mono">{{ $user->username }}</span></span>
</div>
</div>
</td>
<td class="px-6 py-6">
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest">{{ $user->email ?? '-' }}</span>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
@if($user->company)
@@ -171,7 +200,7 @@
</tr>
@empty
<tr>
<td colspan="{{ auth()->user()->isSystemAdmin() ? 5 : 4 }}" class="px-6 py-24 text-center">
<td colspan="{{ auth()->user()->isSystemAdmin() ? 6 : 5 }}" class="px-6 py-24 text-center">
<div class="flex flex-col items-center gap-3 opacity-20">
<svg class="size-16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2m16-10a4 4 0 11-8 0 4 4 0 018 0zM23 21v-2a4 4 0 00-3-3.87m-4-12a4 4 0 010 7.75"/></svg>
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No accounts found') }}</p>
@@ -213,61 +242,97 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Full Name') }}</label>
<input type="text" name="name" x-model="currentUser.name" required class="luxury-input" placeholder="{{ __('e.g. John Doe') }}">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Full Name') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="name" x-model="currentUser.name" required class="luxury-input @error('name') border-rose-500 @enderror" placeholder="{{ __('e.g. John Doe') }}">
@error('name')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Username') }}</label>
<input type="text" name="username" x-model="currentUser.username" required class="luxury-input" placeholder="{{ __('e.g. johndoe') }}">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Username') }} <span class="text-rose-500">*</span>
</label>
<input type="text" name="username" x-model="currentUser.username" required class="luxury-input @error('username') border-rose-500 @enderror" placeholder="{{ __('e.g. johndoe') }}">
@error('username')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Email') }}</label>
<input type="email" name="email" x-model="currentUser.email" class="luxury-input" placeholder="{{ __('john@example.com') }}">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Email') }} <span class="text-rose-500">*</span>
</label>
<input type="email" name="email" x-model="currentUser.email" required class="luxury-input @error('email') border-rose-500 @enderror" placeholder="{{ __('john@example.com') }}">
@error('email')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Phone') }}</label>
<input type="text" name="phone" x-model="currentUser.phone" class="luxury-input">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Role') }}</label>
<select name="role" x-model="currentUser.role" class="luxury-select">
@foreach($roles as $role)
<option value="{{ $role->name }}">{{ __($role->name) }}</option>
@endforeach
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
<select name="status" x-model="currentUser.status" class="luxury-select">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
<input type="text" name="phone" x-model="currentUser.phone" class="luxury-input @error('phone') border-rose-500 @enderror">
@error('phone')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
</div>
@if(auth()->user()->isSystemAdmin())
<div class="space-y-2">
<div class="space-y-2 mb-6">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Company') }}</label>
<select name="company_id" x-model="currentUser.company_id" class="luxury-select">
<select name="company_id" x-model="currentUser.company_id" class="luxury-select @error('company_id') border-rose-500 @enderror"
@change="$nextTick(() => { if (filteredRoles.length > 0 && !filteredRoles.find(r => r.name === currentUser.role)) { currentUser.role = filteredRoles[0].name; } })">
<option value="">{{ __('SYSTEM') }}</option>
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</select>
@error('company_id')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
{{ __('Role') }} <span class="text-rose-500">*</span>
</label>
<select name="role" x-model="currentUser.role" class="luxury-select @error('role') border-rose-500 @enderror">
<template x-for="role in filteredRoles" :key="role.id">
<option :value="role.name" x-text="role.name"></option>
</template>
</select>
@error('role')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
<select name="status" x-model="currentUser.status" class="luxury-select @error('status') border-rose-500 @enderror">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
@error('status')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
<span x-text="editing ? '{{ __('New Password (leave blank to keep current)') }}' : '{{ __('Password') }}'"></span>
<template x-if="!editing">
<span class="text-rose-500">*</span>
</template>
</label>
<input type="password" name="password" :required="!editing" class="luxury-input" placeholder="••••••••">
<input type="password" name="password" :required="!editing" class="luxury-input @error('password') border-rose-500 @enderror" placeholder="••••••••">
@error('password')
<p class="text-[10px] font-bold text-rose-500 mt-1 pl-1 uppercase tracking-tight">{{ $message }}</p>
@enderror
</div>
<div class="flex justify-end gap-x-4 pt-8">

View File

@@ -2,49 +2,63 @@
@section('content')
<div class="container mx-auto px-6 py-8">
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">新增機台</h3>
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">{{ __('Create Machine') }}</h3>
<div class="mt-8">
<form action="{{ route('admin.machines.store') }}" method="POST" class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
<form action="{{ route('admin.machines.store') }}" method="POST"
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
@csrf
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">機台名稱</label>
<input type="text" name="name" id="name" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{ __('Machine
Name') }}</label>
<input type="text" name="name" id="name"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
required>
@error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">位置</label>
<input type="text" name="location" id="location" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Location') }}</label>
<input type="text" name="location" id="location"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('location') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">狀態</label>
<select name="status" id="status" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline">離線</option>
<option value="online">連線中</option>
<option value="error">異常</option>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{ __('Status')
}}</label>
<select name="status" id="status"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline">{{ __('Offline') }}</option>
<option value="online">{{ __('Connecting...') }}</option>
<option value="error">{{ __('Error') }}</option>
</select>
@error('status') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">溫度 (°C)</label>
<input type="number" step="0.1" name="temperature" id="temperature" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Temperature') }} (°C)</label>
<input type="number" step="0.1" name="temperature" id="temperature"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('temperature') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">韌體版本</label>
<input type="text" name="firmware_version" id="firmware_version" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Firmware Version') }}</label>
<input type="text" name="firmware_version" id="firmware_version"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('firmware_version') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="flex justify-end">
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">取消</a>
<button type="submit" class="btn-luxury-primary">建立</button>
<a href="{{ route('admin.machines.index') }}"
class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">{{
__('Cancel') }}</a>
<button type="submit" class="btn-luxury-primary">{{ __('Create') }}</button>
</div>
</form>
</div>

View File

@@ -2,50 +2,68 @@
@section('content')
<div class="container mx-auto px-6 py-8">
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">編輯機台</h3>
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">{{ __('Edit Machine') }}</h3>
<div class="mt-8">
<form action="{{ route('admin.machines.update', $machine) }}" method="POST" class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
<form action="{{ route('admin.machines.update', $machine) }}" method="POST"
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
@csrf
@method('PUT')
<div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">機台名稱</label>
<input type="text" name="name" id="name" value="{{ old('name', $machine->name) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{ __('Machine
Name') }}</label>
<input type="text" name="name" id="name" value="{{ old('name', $machine->name) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
required>
@error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">位置</label>
<input type="text" name="location" id="location" value="{{ old('location', $machine->location) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Location') }}</label>
<input type="text" name="location" id="location" value="{{ old('location', $machine->location) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('location') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">狀態</label>
<select name="status" id="status" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline" {{ $machine->status == 'offline' ? 'selected' : '' }}>離線</option>
<option value="online" {{ $machine->status == 'online' ? 'selected' : '' }}>連線中</option>
<option value="error" {{ $machine->status == 'error' ? 'selected' : '' }}>異常</option>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{ __('Status')
}}</label>
<select name="status" id="status"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline" {{ $machine->status == 'offline' ? 'selected' : '' }}>{{ __('Offline') }}
</option>
<option value="online" {{ $machine->status == 'online' ? 'selected' : '' }}>{{ __('Connecting...')
}}</option>
<option value="error" {{ $machine->status == 'error' ? 'selected' : '' }}>{{ __('Error') }}</option>
</select>
@error('status') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">溫度 (°C)</label>
<input type="number" step="0.1" name="temperature" id="temperature" value="{{ old('temperature', $machine->temperature) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Temperature') }} (°C)</label>
<input type="number" step="0.1" name="temperature" id="temperature"
value="{{ old('temperature', $machine->temperature) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('temperature') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div>
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">韌體版本</label>
<input type="text" name="firmware_version" id="firmware_version" value="{{ old('firmware_version', $machine->firmware_version) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">{{
__('Firmware Version') }}</label>
<input type="text" name="firmware_version" id="firmware_version"
value="{{ old('firmware_version', $machine->firmware_version) }}"
class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
@error('firmware_version') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="flex justify-end">
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">取消</a>
<button type="submit" class="btn-luxury-primary">更新</button>
<a href="{{ route('admin.machines.index') }}"
class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">{{
__('Cancel') }}</a>
<button type="submit" class="btn-luxury-primary">{{ __('Update') }}</button>
</div>
</form>
</div>

View File

@@ -2,7 +2,7 @@
@section('header')
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('機台管理') }}
{{ __('Machine Management') }}
</h2>
@endsection
@@ -12,7 +12,7 @@
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg border border-gray-200">
<div class="p-6 text-gray-900">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-medium">機台列表</h3>
<h3 class="text-lg font-medium">{{ __('Machine List') }}</h3>
<div class="flex items-center gap-x-4">
<form method="GET" action="{{ route('admin.machines.index') }}" class="flex items-center gap-x-2">
@if(request('status'))
@@ -25,9 +25,9 @@
</select>
</form>
<div class="flex space-x-2">
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a>
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a>
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a>
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">{{ __('All') }}</a>
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">{{ __('Online') }}</a>
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">{{ __('Error') }}</a>
</div>
</div>
</div>
@@ -36,12 +36,12 @@
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名稱</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">位置</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">狀態</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">溫度</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最後心跳</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('Name') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('Location') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('Status') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('Temperature') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('Last Heartbeat') }}</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@@ -64,17 +64,17 @@
$class = $statusClasses[$machine->status] ?? 'bg-blue-100 text-blue-800';
@endphp
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $class }}">
{{ strtoupper($machine->status) }}
{{ __($machine->status === 'online' ? 'Online' : ($machine->status === 'error' ? 'Error' : 'Offline')) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $machine->temperature ?? '--' }} °C
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '從未連線' }}
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : __('Never Connected') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('admin.machines.show', $machine->id) }}" class="text-indigo-600 hover:text-indigo-900 bg-indigo-50 px-3 py-1 rounded-md transition border border-indigo-100">查看日誌</a>
<a href="{{ route('admin.machines.show', $machine->id) }}" class="text-indigo-600 hover:text-indigo-900 bg-indigo-50 px-3 py-1 rounded-md transition border border-indigo-100">{{ __('View Logs') }}</a>
</td>
</tr>
@endforeach

View File

@@ -3,9 +3,9 @@
@section('header')
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
機台詳情: {{ $machine->name }}
{{ __('Machine Details') }}: {{ $machine->name }}
</h2>
<a href="{{ route('admin.machines.index') }}" class="text-sm text-gray-600 hover:text-gray-900"> 返回列表</a>
<a href="{{ route('admin.machines.index') }}" class="text-sm text-gray-600 hover:text-gray-900"> {{ __('Back to List') }}</a>
</div>
@endsection
@@ -14,21 +14,21 @@
<div class="sm:px-6 lg:px-8 space-y-6">
<!-- 基本資訊卡片 -->
<div class="bg-white shadow sm:rounded-lg p-6 border border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-4 border-b pb-2">基本資訊</h3>
<h3 class="text-lg font-medium text-gray-900 mb-4 border-b pb-2">{{ __('Basic Information') }}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p class="text-xs text-gray-500 uppercase">當前狀態</p>
<p class="text-xs text-gray-500 uppercase">{{ __('Status') }}</p>
<p class="text-lg font-bold {{ $machine->status === 'online' ? 'text-green-600' : ($machine->status === 'error' ? 'text-red-600' : 'text-gray-600') }}">
{{ strtoupper($machine->status) }}
{{ __($machine->status === 'online' ? 'Online' : ($machine->status === 'error' ? 'Error' : 'Offline')) }}
</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase">位置</p>
<p class="text-xs text-gray-500 uppercase">{{ __('Location') }}</p>
<p class="text-sm">{{ $machine->location }}</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase">最後心跳時間</p>
<p class="text-sm">{{ $machine->last_heartbeat_at ?? 'N/A' }}</p>
<p class="text-xs text-gray-500 uppercase">{{ __('Last Heartbeat') }}</p>
<p class="text-sm">{{ $machine->last_heartbeat_at ?? __('N/A') }}</p>
</div>
</div>
</div>
@@ -36,16 +36,16 @@
<!-- 日誌顯示區 -->
<div class="bg-gray-900 shadow sm:rounded-lg overflow-hidden border border-gray-700">
<div class="p-4 bg-gray-800 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-md font-medium text-gray-200">即時操作日誌 (最後 50 )</h3>
<span class="text-xs text-gray-400">所有時間為系統時區</span>
<h3 class="text-md font-medium text-gray-200">{{ __('Real-time Operation Logs (Last 50)') }}</h3>
<span class="text-xs text-gray-400">{{ __('All Times System Timezone') }}</span>
</div>
<div class="p-0 max-h-[600px] overflow-y-auto">
<table class="min-w-full divide-y divide-gray-800 font-mono text-xs">
<thead class="bg-gray-800 sticky top-0">
<tr>
<th class="px-4 py-2 text-left text-gray-500">時間</th>
<th class="px-4 py-2 text-left text-gray-500">層級</th>
<th class="px-4 py-2 text-left text-gray-500">訊息</th>
<th class="px-4 py-2 text-left text-gray-500">{{ __('Time') }}</th>
<th class="px-4 py-2 text-left text-gray-500">{{ __('Level') }}</th>
<th class="px-4 py-2 text-left text-gray-500">{{ __('Message') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
@@ -75,7 +75,7 @@
</tr>
@empty
<tr>
<td colspan="3" class="px-4 py-8 text-center text-gray-500 italic">暫無相關日誌</td>
<td colspan="3" class="px-4 py-8 text-center text-gray-500 italic">{{ __('No logs found') }}</td>
</tr>
@endforelse
</tbody>

View File

@@ -7,13 +7,13 @@
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
isEdit: false,
roleId: '',
roleName: '',
rolePermissions: [],
isSystem: false,
modalTitle: '{{ __('Create Role') }}',
showModal: {{ $errors->any() ? 'true' : 'false' }},
isEdit: {{ (old('_method') == 'PUT' || request()->has('edit')) ? 'true' : 'false' }},
roleId: '{{ old('roleId', '') }}',
roleName: '{{ old('name', '') }}',
rolePermissions: @js(old('permissions', [])),
isSystem: {{ old('is_system', '0') }},
modalTitle: '{{ $errors->any() && old('_method') == 'PUT' ? __('Edit Role') : ($errors->any() ? __('Create Role') : __('Create Role')) }}',
openModal(edit = false, id = '', name = '', permissions = [], isSys = false) {
this.isEdit = edit;
this.roleId = id;
@@ -160,6 +160,7 @@
<form :action="isEdit ? '{{ route($baseRoute) }}/' + roleId : '{{ route($baseRoute . '.store') }}'" method="POST">
@csrf
<template x-if="isEdit"><input type="hidden" name="_method" value="PUT"></template>
<input type="hidden" name="roleId" x-model="roleId">
<div class="p-8 max-h-[65vh] overflow-y-auto custom-scrollbar">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
@@ -167,7 +168,13 @@
<div class="space-y-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Name') }}</label>
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full" placeholder="{{ __('Enter role name') }}" :disabled="isEdit && roleName === 'super-admin'">
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full @error('name') border-rose-500 @enderror" placeholder="{{ __('Enter role name') }}" :disabled="isEdit && roleName === 'super-admin'">
@error('name')
<p class="text-[11px] text-rose-500 font-bold mt-1.5 px-1 flex items-center gap-1.5 animate-luxury-in">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
{{ $message }}
</p>
@enderror
<template x-if="isEdit && roleName === 'super-admin'">
<p class="text-[10px] text-amber-500 font-bold mt-1 px-1 flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>

View File

@@ -0,0 +1,51 @@
<div x-data="{
loading: true
}"
x-init="
const urlParams = new URLSearchParams(window.location.search);
const simulateDelay = urlParams.get('simulate_loading');
const delay = simulateDelay ? parseInt(simulateDelay) : 300;
const hideLoading = () => setTimeout(() => loading = false, delay);
if (document.readyState === 'complete') {
hideLoading();
} else {
window.addEventListener('load', hideLoading);
// 安全保險:模擬模式下為 30 秒,正常模式下為 5 秒
const safetyTimeout = simulateDelay ? 30000 : 5000;
setTimeout(hideLoading, safetyTimeout);
}
"
x-show="loading"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-slate-900/60 backdrop-blur-md"
style="display: none;"
x-cloak>
<div class="relative flex flex-col items-center animate-luxury-in">
<!-- Logo with Spinner Animation -->
<div class="relative w-28 h-28 mb-10 flex items-center justify-center">
<!-- Luxury Rotating Ring -->
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin" style="animation-duration: 1.5s;"></div>
<div class="absolute inset-2 rounded-full border border-white/5 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
<!-- Glow Effect -->
<div class="absolute inset-0 rounded-full bg-cyan-500/10 blur-xl animate-pulse"></div>
<!-- Central Logo -->
<div class="relative w-20 h-20 rounded-3xl bg-slate-900/80 backdrop-blur-xl border border-white/20 flex items-center justify-center shadow-2xl">
<span class="text-white text-5xl font-black font-display tracking-tighter">S</span>
</div>
</div>
<!-- Text Animation -->
<div class="flex items-center gap-x-3 text-3xl font-bold text-white font-display tracking-tightest overflow-hidden mb-4">
<span class="animate-fade-in-right opacity-0" style="animation-delay: 100ms; animation-fill-mode: forwards;">Star</span>
<span class="text-cyan-400 animate-fade-in-right opacity-0" style="animation-delay: 300ms; animation-fill-mode: forwards;">Cloud</span>
</div>
<p class="text-[10px] font-black text-white/30 uppercase tracking-[0.6em] animate-pulse">{{ __('Systems Initializing') }}</p>
</div>
</div>

View File

@@ -151,6 +151,26 @@
</div>
<!-- End Card -->
</div>
<!-- Skeleton Example (Option C) -->
<div class="luxury-card p-8 animate-fade-up">
<div class="flex items-center justify-between mb-10">
<div>
<div class="h-4 w-32 skeleton mb-2"></div>
<div class="h-7 w-48 skeleton"></div>
</div>
<div class="h-10 w-24 skeleton"></div>
</div>
<div class="space-y-4">
<div class="h-4 w-full skeleton"></div>
<div class="h-4 w-11/12 skeleton"></div>
<div class="h-4 w-4/5 skeleton"></div>
</div>
<div class="mt-8 pt-6 border-t border-slate-100 dark:border-slate-800 flex gap-x-3">
<div class="h-10 w-28 skeleton"></div>
<div class="h-10 w-28 skeleton"></div>
</div>
</div>
</div>
@endsection

View File

@@ -25,6 +25,22 @@
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50 dark:bg-[#0f172a] antialiased font-sans h-full selection:bg-indigo-100 dark:selection:bg-indigo-900/40" x-data="{ sidebarOpen: false, userDropdownOpen: false }">
<!-- Option A: Loading Screen -->
<x-loading-screen />
<!-- Option B: Top Progress Bar -->
<div id="top-loading-bar" class="top-loading-bar"></div>
<script>
// 僅保留最基本的導航列觸發,不使用全螢幕遮罩防止卡死
window.addEventListener('beforeunload', () => {
document.getElementById('top-loading-bar').classList.add('loading');
});
window.addEventListener('pageshow', () => {
document.getElementById('top-loading-bar').classList.remove('loading');
});
</script>
<!-- Sidebar Overlay (Mobile) -->
<div x-show="sidebarOpen"

View File

@@ -168,6 +168,9 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
Route::prefix('basic-settings')->name('basic-settings.')->group(function () {
// 機台設定
Route::prefix('machines')->name('machines.')->group(function () {
// 機台照片獨立更新
Route::patch('{machine}/photos', [App\Http\Controllers\Admin\BasicSettings\MachinePhotoController::class, 'update'])->name('photos.update');
Route::get('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'index'])->name('index');
Route::get('/{machine}/edit', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'edit'])->name('edit');
Route::put('/{machine}', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'update'])->name('update');
@@ -176,6 +179,9 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
// 客戶金流設定
Route::resource('payment-configs', App\Http\Controllers\Admin\BasicSettings\PaymentConfigController::class)->except(['show']);
// 機台型號設定
Route::resource('machine-models', App\Http\Controllers\Admin\BasicSettings\MachineModelController::class)->except(['show']);
});
// 15. 權限設定