Compare commits
4 Commits
09e1d0dc48
...
99243d4206
| Author | SHA1 | Date | |
|---|---|---|---|
| 99243d4206 | |||
| fc79148879 | |||
| 3ce88ed342 | |||
| 1851e91c86 |
@@ -14,7 +14,8 @@ trigger: always_on
|
||||
* **核心組件**:Redis (用於高併發 IoT 隊列與快取,為系統穩定之必要條件)
|
||||
* **前端視圖 (View)**:Laravel Blade
|
||||
* **前端互動 (JS)**:Alpine.js (專注於行為,不負責渲染)
|
||||
* **介面與樣式 (CSS)**:Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)
|
||||
* **介面與樣式 (CSS)**:Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)。
|
||||
* **重要規範**:Preline UI 僅作為「原子組件」與「JS 互動邏輯」的參考庫。整體的「佈局」與「美學」必須嚴格遵守「極簡奢華風 UI 實作規範 (SKILL.md)」。
|
||||
* **前端建置工具**:Vite
|
||||
* **資料庫**:MySQL 8.0
|
||||
* **開發環境**:Laravel Sail (Docker / WSL2)
|
||||
@@ -64,6 +65,7 @@ trigger: always_on
|
||||
* **角色設定**:你是一位專業的全端開發工程師助手。
|
||||
* **代碼生成指令**:
|
||||
* 所有的解釋說明請使用 **繁體中文**。
|
||||
* **【警告:Preline 冗餘】** Preline UI 的官方範例常包含多餘的控制項(如頂部筆數切換)。**嚴禁**照抄其佈局,必須確保頂部工具列(Header/Toolbar)維持極簡,重複功能一律收納至底部。
|
||||
* **【警告】** 此專案前端禁用 React / Vue / Inertia.js。所有的前端頁面生成必須使用 **Blade 模板** 結合 **Tailwind CSS** 與 **Alpine.js**。
|
||||
* **【多語系強制要求】** 任何新增的 Blade UI 區塊,禁止硬編碼 (Hard-coded) 中文或英文。必須使用 `__('...')` 並同步在 `lang/*.json` 補上翻譯。
|
||||
* 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。
|
||||
|
||||
52
.agents/rules/rbac-rules.md
Normal file
52
.agents/rules/rbac-rules.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 多租戶與權限架構實作規範 (RBAC Rules)
|
||||
|
||||
本文件定義 Star Cloud 系統的多租戶與權限(RBAC)實作標準,開發者必須嚴格遵守以下準則,以確保資料隔離與安全性。
|
||||
|
||||
---
|
||||
|
||||
## 1. 資料隔離核心 (Data Isolation)
|
||||
|
||||
### 1.1 租戶欄位 (`company_id`)
|
||||
任何屬於租戶資源的資料表(如 `users`, `machines`, `transactions` 等),**必須**包含 `company_id` 欄位。
|
||||
- `company_id = null`:系統管理員(SaaS 平台營運商)。
|
||||
- `company_id = {ID}`:特定租戶。
|
||||
|
||||
### 1.2 自動過濾 (Global Scopes)
|
||||
- 資源 Model 必須套用 `TenantScoped` Trait。
|
||||
- 當非系統管理員登入時,所有 Eloquent 查詢必須自動加上 `where('company_id', auth()->user()->company_id)`。
|
||||
- **嚴禁**在 Controller 手動撰寫重複的過濾邏輯,除非是複雜的 Raw SQL。
|
||||
|
||||
### 1.3 寫入安全
|
||||
- 建立新資源時,必須在背景強制綁定 `company_id`,禁止由前端傳參決定。
|
||||
- 範例:`$model->company_id = Auth::user()->company_id;`
|
||||
|
||||
---
|
||||
|
||||
## 2. 權限開發規範 (spatie/laravel-permission)
|
||||
|
||||
### 2.1 租戶感知角色 (Tenant-Aware Roles)
|
||||
- `roles` 資料表已擴充 `company_id` 欄位。
|
||||
- 撈取角色清單供指派時,必須過濾 `company_id` 或為 null 的系統預設角色。
|
||||
|
||||
### 2.2 權限命名
|
||||
- 權限名稱應遵循 `[module].[action]` 格式(例如 `machine.view`, `machine.edit`)。
|
||||
- 所有租戶共用相同的權限定義。
|
||||
|
||||
---
|
||||
|
||||
## 3. 介面安全 (UI/Blade)
|
||||
|
||||
### 3.1 身份判定 Helper
|
||||
使用以下方法進行區分:
|
||||
- `$user->isSystemAdmin()`: 判斷是否為平台營運人員。
|
||||
- `$user->isTenant()`: 判斷是否為租戶帳號。
|
||||
|
||||
### 3.2 Blade 指令
|
||||
- 涉及全站管理或跨租戶功能,必須使用 `@if(auth()->user()->isSystemAdmin())` 包裹。
|
||||
- 確保租戶登入時,不會在 Sidebar 或選單看到不屬於其權限範圍的項目。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 安全
|
||||
- 所有的 API Route 應預設包含 `CheckTenantAccess` Middleware。
|
||||
- 嚴禁透過 URL 修改 ID 存取不屬於該租戶的資料,必須依賴 `company_id` 的 Scope 過濾。
|
||||
@@ -17,6 +17,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
||||
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報 | **IoT 通訊與高併發處理規範** | `.agents/skills/iot-communication/SKILL.md` |
|
||||
| 介面, UI, 佈局, CSS, Tailwind, 奢華, 深色模式, Light Mode, Dark Mode, Blade, 樣式, 間距, 陰影, 動畫 | **極簡奢華風 UI 實作規範** | `.agents/skills/ui-minimal-luxury/SKILL.md` |
|
||||
| 查詢、撈資料、Query、Controller、下拉選單、Eloquent、N+1、`->get()`、select、交易、Transaction、Bulk、分頁、索引 | **資料庫與 ORM 最佳實踐規範** | `/home/mama/.gemini/antigravity/global_skills/database-best-practices/SKILL.md` |
|
||||
| RBAC, 權限, 角色, 租戶, Tenant, Company, Access Control, 多租戶, 權限控管 | **多租戶與權限架構實作規範** | `.agents/rules/rbac-rules.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -27,6 +28,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
||||
### 🔴 新增或修改頁面 (Views/Blade) 時
|
||||
必須讀取:
|
||||
1. **ui-minimal-luxury** — 確保符合極簡奢華風視覺與互動規範
|
||||
2. **rbac-rules** — 確認 UI 區塊的權限顯示控制
|
||||
|
||||
### 🔴 新增機台通訊 API 端點時
|
||||
必須讀取:
|
||||
@@ -39,3 +41,4 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
||||
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
|
||||
必須讀取:
|
||||
1. **database-best-practices** — 確認查詢優化、交易安全、批量寫入與索引規範
|
||||
2. **rbac-rules** — 確保 `company_id` 隔離邏輯正確套用
|
||||
|
||||
@@ -58,20 +58,20 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
## 3. 動畫與互動
|
||||
|
||||
### 進場動畫
|
||||
- **`.animate-luxury-in`**: 所有的主內容區域或卡片在頁面載入時,應具備由下而上的淡入效果。
|
||||
- **`.animate-luxury-in`**: <EFBFBD><EFBFBD><EFBFBD>厩<EFBFBD>銝餃<EFBFBD>摰孵<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>∠<EFBFBD><EFBFBD>券<EFBFBD><EFBFBD>Z<EFBFBD><EFBFBD>交<EFBFBD>嚗峕<EFBFBD><EFBFBD>瑕<EFBFBD><EFBFBD>曹<EFBFBD><EFBFBD>諹<EFBFBD><EFBFBD><EFBFBD>楚<EFBFBD>交<EFBFBD><EFBFBD>栶<EFBFBD><EFBFBD>
|
||||
|
||||
### 鈭鍦<E988AD><E98DA6>擧腹 (Transitions)
|
||||
- **璅蹱<E79285><E8B9B1><EFBFBD><EFBFBD>**: <20><><EFBFBD>厩<EFBFBD><E58EA9>詨<EFBFBD><E8A9A8><EFBFBD>𠧧敶抵<E695B6><E68AB5>𤤿<EFBFBD><F0A4A4BF>擧腹<E693A7><E885B9><EFBFBD>嚗𣬚絞銝<E7B59E>撱箄降雿輻鍂 **`duration-300`** (300ms)<29><>
|
||||
- **靘见<E99D98>**: 璆萄<E79286>蝝啣凝<E595A3><E5879D><EFBFBD>𤩺<EFBFBD>摨西<E691A8><E8A5BF>硋虾蝮桃<E89DAE><E6A183><EFBFBD> `150ms`嚗䔶<EFBFBD>瘨匧<EFBFBD><EFBFBD>峕艶<EFBFBD>脰<EFBFBD>雿滨宏<EFBFBD><EFBFBD><EFBFBD><EFBFBD>蓥<EFBFBD>敺衤誑 `300ms` <20>箸<EFBFBD><E7AEB8><EFBFBD>
|
||||
|
||||
### Alpine.js 互動模式 (以時間選擇器為例)
|
||||
- **互動原則**: 點擊觸發下拉選單時,必須使用 `x-transition` 且帶有 `scale` 偏移。
|
||||
- **樣式要求**: 選單背景需使用玻璃擬態 (Glassmorphism) 或帶透明度的深色背景。
|
||||
|
||||
## 4. UI 檢查清單 (AI 助手執行前必讀)
|
||||
- [ ] 是否使用了正確的 `rounded-2xl` (或更圓) 的導角?
|
||||
- [ ] 所有的圖示是否一致使用 `lucide-react` 風格?
|
||||
- [ ] 卡片是否有適當的間距 (通常為 `p-6`)?
|
||||
- [ ] 文字色階是否符合:
|
||||
- **標題**: `text-slate-900` / `dark:text-white`
|
||||
- **副標/標籤**: 最小應為 `text-slate-500` / `dark:text-slate-400`(避免使用 `slate-400` 以下等級,以確保對比度足以閱讀)。
|
||||
- [ ] **字體大小**: 確保所有文字至少為 `text-xs`,重要的標籤建議為 `text-sm`。
|
||||
- [ ] **<EFBFBD>𡑒”雿<EFBFBD><EFBFBD>**: <20>臬炏<E887AC>∠鍂<E288A0>峕㟲<E5B395><E39FB2><EFBFBD><EFBFBD>∠<EFBFBD><E288A0>滨<EFBFBD>瑽衤<E791BD><E8A1A4>扯<EFBFBD>閮剔<E996AE> `p-8`嚗<EFBFBD>
|
||||
- [ ] **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>蜇<EFBFBD><EFBFBD>**: <20>𡑒”摨閖<E691A8><E99696>臬炏甇<E7828F>Ⅱ<EFBFBD>砍<EFBFBD> `vendor.pagination.luxury`嚗<EFBFBD>
|
||||
- [ ] **<EFBFBD><EFBFBD><EFBFBD><EFBFBD>脤<EFBFBD>**: 蝚血<E89D9A>璅䠷<E79285> `slate-900/white` <20><><EFBFBD>蝐<EFBFBD> `slate-500` <20><><EFBFBD>瘥𥪜漲<F0A5AA9C><E6BCB2>
|
||||
- [ ] **<EFBFBD>航<EFBFBD><EFBFBD>扳炎<EFBFBD><EFBFBD>**: 鈭𣬚<E988AD>鞈<EFBFBD><E99E88><EFBFBD>臬炏<E887AC>𥪜<EFBFBD> `text-xs` (12px) 銝娍<E98A9D><E5A88D>滢<EFBFBD>頞<EFBFBD><E9A09E> `font-bold`嚗<EFBFBD>
|
||||
|
||||
## 5. 開發注意事項 (Important Notes)
|
||||
|
||||
@@ -85,6 +85,23 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
|
||||
## 6. 頁面佈局規範 (Page Layout)
|
||||
|
||||
### 雿<><E99BBF>瘙箇<E79899>閬誩<E996AC> (Layout Decision Rules)
|
||||
|
||||
<EFBFBD>寞<EFBFBD>蝭拚<EFBFBD>璇苷辣<EFBFBD><EFBFBD><EFBFBD><EFBFBD>𦦵<EFBFBD>摨佗<EFBFBD><EFBFBD>豢<EFBFBD><EFBFBD>拍訜<EFBFBD><EFBFBD><EFBFBD><EFBFBD>桅<EFBFBD><EFBFBD>V<EFBFBD>撅<EFBFBD>嚗<EFBFBD>
|
||||
|
||||
#### 1. <20>游<EFBFBD>撘譍<E69298>撅<EFBFBD> (Integrated Layout) - <20>鞾<EFBFBD>閮剜綫<E5899C>艾<EFBFBD><E889BE>
|
||||
- **<EFBFBD>拍鍂<EFBFBD>湔艶**: 蝯訫之憭𡁏彍 CRUD <20>𡑒”<F0A19192><E2809D>
|
||||
- **撖虫<E69296><E899AB>孵<EFBFBD>**: 蝭拚<E89DAD><E68B9A>具<EFBFBD><E585B7>極<EFBFBD>瑕<EFBFBD><E79195><EFBFBD><EFBFBD><EFBFBD>躰”<E8BAB0>澆<EFBFBD><E6BE86>典<EFBFBD>鋆嘥銁<E598A5>䔶<EFBFBD><E494B6><EFBFBD> `luxury-card` 銝准<E98A9D><E58786>
|
||||
- **<EFBFBD>扯<EFBFBD>閬讐<EFBFBD>**: 撘瑕<E69298>雿輻鍂 `p-8` 隞亦㬢敺埈<E695BA>雿喟征瘞<E5BE81><E7989E><EFBFBD><EFBFBD>
|
||||
- **<EFBFBD><EFBFBD>辣<EFBFBD>栞<EFBFBD>**: 蝭拚<E89DAD><E68B9A><EFBFBD><EFBFBD><EFBFBD>”<EFBFBD>潔<EFBFBD><E6BD94>枏𤐄摰帋蝙<E5B88B><E89D99> `mb-10`<EFBFBD><EFBFBD>
|
||||
- **蝭<><E89DAD>**: 撣唾<E692A3>蝞∠<E89D9E><E288A0><EFBFBD><EFBFBD><EFBFBD>脰身摰𠾼<E691B0><F0A0BEBC><EFBFBD><EFBFBD>唳𠯫隤䎚<E99AA4><E48E9A>
|
||||
|
||||
#### 2. <20><>𣪧撘譍<E69298>撅<EFBFBD> (Split Layout)
|
||||
- **<EFBFBD>拍鍂<EFBFBD>湔艶**: 銴<><E98AB4><EFBFBD>亥岷 (Filtered Fields >= 3 <20>硋<EFBFBD>銵𣬚祟<F0A3AC9A><E7A59F>)<29><>
|
||||
- **撖虫<E69296><E899AB>孵<EFBFBD>**: 蝭拚<E89DAD><E68B9A><EFBFBD><EFBFBD>函<EFBFBD><E587BD>箔<EFBFBD><E7AE94><EFBFBD> `luxury-card`嚗䔶<EFBFBD><EFBFBD>寥<EFBFBD><EFBFBD><EFBFBD> `mb-6` 敺<><E695BA><EFBFBD>曄蔭鞈<E894AD><E99E88>皜<EFBFBD>鱓<EFBFBD>∠<EFBFBD><E288A0><EFBFBD>
|
||||
- **璅<><E79285>閬讐<E996AC>**: 蝭拚<E89DAD><E68B9A>∠<EFBFBD><E288A0>𡁜虜雿輻鍂 `p-6`嚗<EFBFBD><EFBFBD>皝𠰴<EFBFBD>嚗㚁<EFBFBD>皜<EFBFBD>鱓<EFBFBD>∠<EFBFBD>雿輻鍂 `p-8`嚗<EFBFBD>祝擛<EFBFBD><EFBFBD>嚗剹<EFBFBD><EFBFBD>
|
||||
- **蝭<><E89DAD>**: 鈭斗<E988AD>蝝<EFBFBD><E89D9D><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>唳𠯫隤䎚<E99AA4><E48E9A>
|
||||
|
||||
### 標準寬版佈局 (Wide Layout)
|
||||
|
||||
```html
|
||||
@@ -140,36 +157,54 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
</select>
|
||||
```
|
||||
|
||||
## 8. 蝺刻摩<E588BB><E691A9>底<EFBFBD><E5BA95><EFBFBD>閬讐<E996AC> (Detail & Edit Views)
|
||||
|
||||
<EFBFBD>箔<EFBFBD>霈枏<EFBFBD>撅方<EFBFBD>閮𦠜凒<EFBFBD>瑁<EFBFBD>閬箏<EFBFBD>撠𠬍<EFBFBD><EFBFBD><EFBFBD><EFBFBD>见<EFBFBD>憛<EFBFBD> (Section) <20><><EFBFBD>蝷箸<E89DB7><E7AEB8>∠鍂銝滚<E98A9D><E6BB9A><EFBFBD><EFBFBD><EFBFBD>脫<EFBFBD>鞊~<E99E8A><EFBD9E>
|
||||
|
||||
### <20><>憛𠰴<E6869B>蝷箄𠧧敶拇<E695B6>鞊<EFBFBD> (Section Icon Palette)
|
||||
- **<EFBFBD>箸𧋦鞈<EFBFBD><EFBFBD> (Basic Info)**: **蝧删<E89DA7><E588A0><EFBFBD> (`Emerald`)**<EFBFBD><EFBFBD>誨銵冽瓲敹<EFBFBD><EFBFBD><EFBFBD>帘摰朞<EFBFBD>韏琿<EFBFBD><EFBFBD><EFBFBD>
|
||||
- 璅<><E79285>: `bg-emerald-500/10 text-emerald-500`
|
||||
- **蝖祇<E89D96>/<2F>埝局閮剖<E996AE>**: **<EFBFBD>亦<EFBFBD><EFBFBD><EFBFBD> (`Amber/Orange`)**<2A><>誨銵典<E98AB5>雿栶<E99BBF><E6A0B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>𦻖<EFBFBD><F0A6BB96>′擃磰郎<E7A3B0>𨳍<EFBFBD><F0A8B38D>
|
||||
- 璅<><E79285>: `bg-amber-500/10 text-amber-500`
|
||||
- **蝟餌絞/<2F>脤<EFBFBD>閮剖<E996AE>**: **<EFBFBD>𥡝<EFBFBD><EFBFBD><EFBFBD> (`Indigo`)**<2A><>誨銵券<E98AB5>頛胯<E9A09B><E883AF><EFBFBD><EFBFBD>鞱<EFBFBD>瘛勗惜<E58B97>滨蔭<E6BBA8><E894AD>
|
||||
- 璅<><E79285>: `bg-indigo-500/10 text-indigo-500`
|
||||
- **<EFBFBD>梢麬/蝘駁膄<E9A781>蓥<EFBFBD>**: **<EFBFBD>怎麯蝝<EFBFBD> (`Rose`)**<2A><>誨銵函聦憯墧<E686AF>扳<EFBFBD>雿栶<E99BBF><E6A0B6>
|
||||
- 璅<><E79285>: `bg-rose-500/10 text-rose-500`
|
||||
```
|
||||
|
||||
## 8. 資料表格規範 (Data Tables)
|
||||
|
||||
為了確保管理後台資料的可讀性與精密感,表格內的所有文字級別必須對齊以下規範:
|
||||
|
||||
### 文字大小與權重 (Typography Hierarchy)
|
||||
- **表頭 (Table Header)**:
|
||||
- 類別: `text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest`
|
||||
- 作用: 提供清晰的欄位定義而不奪取資料視覺焦點。
|
||||
- 憿𧼮ê̌: `text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em]`
|
||||
- 雿𦦵鍂: <20>𣂷<EFBFBD>皜<EFBFBD>苊<EFBFBD><E88B8A><EFBFBD>雿滚<E99BBF>蝢抵<E89DA2>䔶<EFBFBD>憟芸<E6869F>鞈<EFBFBD><E99E88>閬𤥁死<F0A4A581>阡<EFBFBD><E998A1><EFBFBD><EFBFBD><EFBFBD>躰雲憭惩<E686AD>瘥𥪜漲<F0A5AA9C><E6BCB2>
|
||||
- **主標題 (Primary Item)**:
|
||||
- 類別: `text-base font-extrabold text-slate-800 dark:text-slate-100`
|
||||
- 範例: 公司名稱、機台標題。
|
||||
- 蝭<EFBFBD><EFBFBD>: <20>砍虬<E7A08D>滨迂<E6BBA8><E8BF82><EFBFBD>擃𥪜<E69383>蝔晞<E89D94><E6999E>
|
||||
- **次要資訊 (Secondary Info)**:
|
||||
- 類別: `text-[11px] font-bold text-slate-400 dark:text-slate-500 tracking-[0.15em]`
|
||||
- 範例: 機台序號 (SN)、公司代碼。
|
||||
- 憿𧼮ê̌: `text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wide`
|
||||
- 蝭<EFBFBD><EFBFBD>: 雿輻鍂<E8BCBB><E98D82>董<EFBFBD>麄<EFBFBD><E9BA84><EFBFBD>閮颯<E996AE><E9A2AF><EFBFBD><EFBFBD>𣂼<EFBFBD>蝔晞<E89D94><E6999E>
|
||||
- **狀態標籤 (Status Badge)**:
|
||||
- 類別: `text-[11px] font-black tracking-widest`
|
||||
- 樣式: 在線 (`emerald`)、離線 (`rose`)。
|
||||
- **時間訊號 (Signals/Time)**:
|
||||
- 類別: `text-[13px] font-bold font-display tracking-widest`
|
||||
- 作用: 解決數字黏滯感,提升判讀舒適度。
|
||||
- 蝭<EFBFBD><EFBFBD>: <20>毺鍂 (`emerald`)<29><><EFBFBD><EFBFBD><EFBFBD> (`rose`) / 閫坿𠧧<E59DBF>滨迂 (`sky`/`indigo`)<29><>
|
||||
- <EFBFBD>寞<EFBFBD><EFBFBD>: `px-2.5 py-1 rounded-lg text-xs font-bold border tracking-wider`
|
||||
|
||||
- **內距 (Padding)**: 單元格統一使用 `px-6 py-6` 以維持呼吸感。
|
||||
- **懸停 (Hover)**: 表格行需具備 `hover:bg-slate-50/80` (深色: `dark:hover:bg-slate-800/40`) 動態反饋。
|
||||
### 蝛粹<E89D9B><E7B2B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (Spacing & Interaction)
|
||||
- **<EFBFBD>桀<EFBFBD><EFBFBD>澆<EFBFBD>頝<EFBFBD>**: 蝯曹<EFBFBD>雿輻鍂 `px-6 py-6`<EFBFBD><EFBFBD>
|
||||
- **<EFBFBD>詨<EFBFBD><EFBFBD>齿<EFBFBD>**: 敹<><E695B9><EFBFBD><EFBFBD> `tr` 憟㛖鍂 `group` 銝𥪜<E98A9D><F0A5AA9C><EFBFBD><EFBFBD>憟㛖鍂 `group-hover:bg-slate-50/80` (瘛梯𠧧: `dark:group-hover:bg-slate-800/40`) 隞交<E99A9E>靘偦<E99D98>蝝𡁶<E89D9D>鈭鍦<E988AD><E98DA6>毺䰻<E6AFBA><E4B0BB>
|
||||
- **<EFBFBD>𣇉內摰孵膥<EFBFBD>詨<EFBFBD> (Icon Hover Palette)**:
|
||||
- <20>𡑒”撌血<E6928C><E8A180><EFBFBD>蜓<EFBFBD>𣇉內摰孵膥<E5ADB5><E886A5> `group-hover` <20><><EFBFBD><EFBFBD>厩眏瘛∟𠧧<E2889F>峕艶頧厩<E9A0A7> **撖阡<E69296>銝駁<E98A9D><E9A781><EFBFBD>**<EFBFBD><EFBFBD>
|
||||
- 憿𧼮ê̌: `group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300`<EFBFBD><EFBFBD>
|
||||
- **<EFBFBD><EFBFBD><EFBFBD><EFBFBD>峕郊霈𡃏𠧧**:
|
||||
- 銝餅<E98A9D>憿峕<E686BF>摮堒銁 `group-hover` <20><><EFBFBD><EFBFBD>峕郊霈𡃏𠧧嚗䔶誑撘瑕<E69298>暺墧<E69ABA>撘訫<E69298><E8A8AB><EFBFBD>
|
||||
- 憿𧼮ê̌: `group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors`<EFBFBD><EFBFBD>
|
||||
|
||||
### 分頁與列表控制項 (Pagination & Controls)
|
||||
為了維持操作一致性,所有列表的分頁與切換組件必須遵循以下「Luxury Jump」模式:
|
||||
- **統一高度**: 所有控制項(按鈕、下拉選單)固定為 `h-9` (36px)。
|
||||
- **筆數切換 (Limit Selector)**:
|
||||
- 樣式: 使用 `bg-slate-50` (深色: `dark:bg-slate-800`) 配合 `text-[11px] font-black`。
|
||||
- 位置: 位於表格右上方。
|
||||
- 閬讐<EFBFBD>: **蝳<>迫**<EFBFBD>刻”<EFBFBD>潔<EFBFBD><EFBFBD>對<EFBFBD>Header/Toolbar嚗厰<E59A97>銴<EFBFBD>𦆮蝵桃<E89DB5><E6A183>詨<EFBFBD><E8A9A8>偦<EFBFBD><E581A6>柴<EFBFBD><E69FB4>絞銝<E7B59E><E98A9D>嗥<EFBFBD><E597A5>澆<EFBFBD><E6BE86>典<EFBFBD><E585B8><EFBFBD><EFBFBD>雿溻<E99BBF><E6BABB>
|
||||
- **分頁導航 (Luxury Jump)**:
|
||||
- 模式: 捨棄傳統頁碼按鈕,全端統一使用「跳轉選單」。
|
||||
- 寬度: 下拉選單內部 Padding 為 `pl-4 pr-10`。
|
||||
@@ -177,8 +212,132 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
- **指示文字**:
|
||||
- 行動端隱藏多餘詞彙,僅保留「1 - 10 / 50」格式。
|
||||
- 數字顏色對齊 `text-slate-600` (深色: `text-slate-300`)。
|
||||
|
||||
### 摨閖<E691A8>皜<EFBFBD>鱓<EFBFBD>批<EFBFBD><E689B9><EFBFBD> (Bottom List Controls)
|
||||
<EFBFBD>箔<EFBFBD>蝣箔<EFBFBD><EFBFBD>瑕<EFBFBD>銵函<EFBFBD><EFBFBD>滢<EFBFBD>靘踹⏚嚗峕<EFBFBD><EFBFBD>桀<EFBFBD><EFBFBD>### 璅蹱<E79285><E8B9B1>滢<EFBFBD><E6BBA2>厰<EFBFBD> (Standard Action Icons)
|
||||
銵冽聢<EFBFBD>抒<EFBFBD><EFBFBD>滢<EFBFBD>甈<EFBFBD><EFBFBD>嚗<EFBFBD><EFBFBD><EFBFBD>𣬚楊頛胯<EFBFBD>溻<EFBFBD><EFBFBD><EFBFBD><EFBFBD>⏛<EFBFBD>扎<EFBFBD>溻<EFBFBD><EFBFBD><EFBFBD>諹底<EFBFBD><EFBFBD><EFBFBD>㵪<EFBFBD>敹<EFBFBD><EFBFBD>雿輻鍂隞乩<EFBFBD>摰𡁶儔銋<EFBFBD> **<EFBFBD>屸<EFBFBD><EFBFBD>烐<EFBFBD>皞<EFBFBD> (Gold Standard)<29><>**嚗<>
|
||||
|
||||
- **<EFBFBD>勗<EFBFBD>璅<EFBFBD><EFBFBD>**:
|
||||
- 摰孵膥: `p-2 rounded-lg bg-slate-50 dark:bg-slate-800`
|
||||
- 銝餉𠧧: `text-slate-400`
|
||||
- <20>𦠜<EFBFBD>: `border border-transparent` (<28>脤<EFBFBD><E884A4>滩<EFBFBD><E6BBA9><EFBFBD>)
|
||||
- <20>擧腹: `transition-all` (雿輻鍂<E8BCBB>鞱身<E99EB1>笔漲隞亦Ⅱ靽苷<E99DBD><E88BB7>賣<EFBFBD>)
|
||||
- <20>𣇉內蝎㛖敦: `stroke-width="2.5"`
|
||||
- 撠箏站: `w-4 h-4`
|
||||
|
||||
- **蝺刻摩<E588BB>厰<EFBFBD> (Edit)**:
|
||||
- <20>詨<EFBFBD><E8A9A8>寞<EFBFBD>: `hover:text-cyan-500 hover:bg-cyan-500/5 hover:border-cyan-500/20`
|
||||
- SVG 頝臬<E9A09D>:
|
||||
```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>
|
||||
```
|
||||
|
||||
- **<EFBFBD>亦<EFBFBD>閰單<EFBFBD> (View/Detail)**:
|
||||
- <20>詨<EFBFBD><E8A9A8>寞<EFBFBD>: `hover:text-indigo-500 hover:bg-indigo-500/5 hover:border-indigo-500/20`
|
||||
- SVG 頝臬<E9A09D>:
|
||||
```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>
|
||||
```
|
||||
|
||||
- **<EFBFBD>芷膄<EFBFBD>厰<EFBFBD> (Delete)**:
|
||||
- <20>詨<EFBFBD><E8A9A8>寞<EFBFBD>: `hover:text-rose-500 hover:bg-rose-500/5 hover:border-rose-500/20`
|
||||
- SVG 頝臬<E9A09D>:
|
||||
```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">
|
||||
<!-- <20><><EFBFBD><EFBFBD>蹱<EFBFBD>撠𧢲<E692A0><F0A7A2B2>硋<EFBFBD>閬<EFBFBD><E996AC><EFBFBD>擧蕪<E693A7>剁<EFBFBD>蝳<EFBFBD>迫<EFBFBD>滩<EFBFBD>蝑<EFBFBD>彍<EFBFBD><E5BD8D><EFBFBD> -->
|
||||
<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>
|
||||
```
|
||||
|
||||
### 皜<>鱓甈<E9B193><E79488>閬讐<E996AC> (Column Visibility & Standards)
|
||||
- **<EFBFBD>箏<EFBFBD>甈<EFBFBD><EFBFBD>**: 蝚砌<E89D9A>甈<EFBFBD><E79488>𡁜虜<F0A1819C>箝<EFBFBD>屸<EFBFBD><E5B1B8>菜<EFBFBD>霅塩<E99C85>㵪<EFBFBD>憒<EFBFBD> ID <20>𡝗<EFBFBD><F0A19D97>橒<EFBFBD>嚗峕<E59A97><E5B395>瑕<EFBFBD><E79195>寞<EFBFBD>摮烾<E691AE>璅<EFBFBD><E79285><EFBFBD><EFBFBD>
|
||||
- **<EFBFBD>滢<EFBFBD>甈<EFBFBD><EFBFBD>**: 蝯曹<E89DAF>雿齿䲰銵冽聢<E586BD><E881A2><EFBFBD>喟垢嚗䔶蒂<E494B6>賢<EFBFBD><E8B3A2><EFBFBD> `Action` (<28><> `<60>滢<EFBFBD>`)嚗峕<E59A97>憿諹<E686BF><E8ABB9>批捆<E689B9><E68D86><EFBFBD> `text-right`<EFBFBD><EFBFBD>
|
||||
## 9. 蝟餌絞<E9A48C>澆捆<E6BE86>扯<EFBFBD>璅蹱<E79285><E8B9B1><EFBFBD> (Compatibility & Standardization)
|
||||
|
||||
<EFBFBD>箔<EFBFBD>蝣箔<EFBFBD><EFBFBD>其<EFBFBD><EFBFBD>𣬚<EFBFBD><EFBFBD>祉<EFBFBD><EFBFBD>讠䔄<EFBFBD>啣<EFBFBD>銝哨<EFBFBD>憒<EFBFBD>𤌍<EFBFBD>滚<EFBFBD>獢<EFBFBD>蝙<EFBFBD>函<EFBFBD> Tailwind CSS v3.1嚗务I <20>質<EFBFBD>甇<EFBFBD>Ⅱ<EFBFBD><E285A1>𣶹嚗䔶蒂蝬剜<E89DAC><E5899C>函<EFBFBD><E587BD>滢<EFBFBD><E6BBA2>煺<EFBFBD><E785BA>湛<EFBFBD>敹<EFBFBD><E695B9><EFBFBD>萄<EFBFBD>隞乩<E99A9E>憿滚<E686BF>閬讐<E996AC><E8AE90><EFBFBD>
|
||||
|
||||
### Tailwind CSS <20><>𧋦<EFBFBD>澆捆<E6BE86><E68D86> (v3.1)
|
||||
- **蝳<>迫雿輻鍂 `size-` 撅祆<E69285><E7A586>**: <20>羓<EFBFBD>銝齿𣈲<E9BDBF><F0A388B2> `size-4` 蝑㕑<E89D91>瘜𤏪<E7989C>隢衤<E99AA2>敺见<E695BA><E8A781><EFBFBD>神雿<E7A59E> `w-4 h-4`<EFBFBD><EFBFBD>
|
||||
- **<EFBFBD>踹<EFBFBD><EFBFBD>墧<EFBFBD>皞㚚<EFBFBD>頝<EFBFBD>**: <20>踹<EFBFBD>雿輻鍂 `4.5` (`18px`) 蝑劐遙<E58A90>誩<EFBFBD>潘<EFBFBD><E6BD98>芸<EFBFBD>雿輻鍂璅蹱<E79285>蝑厩<E89D91>憒<EFBFBD> `4` (`16px`) <20><> `5` (`20px`)<29><>
|
||||
|
||||
### 璅蹱<E79285><E8B9B1>滢<EFBFBD><E6BBA2>厰<EFBFBD> (Standard Action Icons)
|
||||
銵冽聢<EFBFBD>抒<EFBFBD><EFBFBD>滢<EFBFBD>甈<EFBFBD><EFBFBD>嚗<EFBFBD><EFBFBD><EFBFBD>𣬚楊頛胯<EFBFBD>溻<EFBFBD><EFBFBD><EFBFBD><EFBFBD>⏛<EFBFBD>扎<EFBFBD>㵪<EFBFBD>敹<EFBFBD><EFBFBD>雿輻鍂隞乩<EFBFBD>摰𡁶儔銋𧢲<EFBFBD>皞吔<EFBFBD>
|
||||
|
||||
- **<EFBFBD>勗<EFBFBD>璅<EFBFBD><EFBFBD>**:
|
||||
- 摰孵膥: `p-2 rounded-lg bg-slate-50 dark:bg-slate-800`
|
||||
- 銝餉𠧧: `text-slate-400`
|
||||
- <20>𣇉內蝎㛖敦: `stroke-width="2.5"`
|
||||
- 撠箏站: `w-4 h-4`
|
||||
|
||||
- **蝺刻摩<E588BB>厰<EFBFBD> (Edit)**:
|
||||
- <20>詨<EFBFBD><E8A9A8>寞<EFBFBD>: `hover:text-cyan-500 hover:bg-cyan-500/10 hover:border-cyan-500/20`
|
||||
- SVG 頝臬<E9A09D>:
|
||||
```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>
|
||||
```
|
||||
|
||||
- **<EFBFBD>芷膄<EFBFBD>厰<EFBFBD> (Delete)**:
|
||||
- <20>詨<EFBFBD><E8A9A8>寞<EFBFBD>: `hover:text-rose-500 hover:bg-rose-500/10 hover:border-rose-500/20`
|
||||
- SVG 頝臬<E9A09D>:
|
||||
```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>
|
||||
```
|
||||
|
||||
|
||||
## 10. 摮烾<E691AE><E783BE><EFBFBD><EFBFBD>銵栞<E98AB5>閮𡃏<E996AE>蝭<EFBFBD> (Typography & Technical Data)
|
||||
|
||||
<EFBFBD>箔<EFBFBD>蝣箔<EFBFBD><EFBFBD>函<EFBFBD><EFBFBD>峕活閬<EFBFBD><EFBFBD>閮𨳍<EFBFBD>滚<EFBFBD><EFBFBD>蹱扔銝<EFBFBD><EFBFBD>渡<EFBFBD>擃条<EFBFBD><EFBFBD><EFBFBD><EFBFBD>敹<EFBFBD><EFBFBD><EFBFBD>萄<EFBFBD>隞乩<EFBFBD><EFBFBD>峕<EFBFBD><EFBFBD>唳<EFBFBD>蝡踴<EFBFBD>滩<EFBFBD>蝭<EFBFBD><EFBFBD>
|
||||
|
||||
### <20>詨<EFBFBD>璅<EFBFBD><E79285>蝝𡁜ê̌ (Core Typography Scale)
|
||||
| 鞈<><E99E88>憿𧼮<E686BF> | 摰X<E691B0>/<2F>滨蔭<E6BBA8>滨迂 (璅䠷<E79285>) | <20><>銵㮖誨蝣<E8AAA8> (ID, SN, Code) | <20><><EFBFBD>蝚西<E89D9A> (<28><>) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **摮烾<E691AE><E783BE><EFBFBD>** | `font-sans` (Plus Jakarta Sans) | `font-mono` (敺桃葬<E6A183>见鱓<E8A781>蹱聢) | `font-sans` |
|
||||
| **撠箏站** | `text-base` | `text-xs` (銝滚虾雿輻鍂 10px) | `text-xs` |
|
||||
| **摮烾<E691AE>** | `font-extrabold` (800) | `font-bold` (700) | `font-bold` |
|
||||
| **摮𡑒<E691AE>** | `tracking-tight` (-0.02em) | `tracking-widest` (<28><>撖<EFBFBD>) | `tracking-normal` |
|
||||
| **<EFBFBD>澆<EFBFBD>** | 靽脲<E99DBD><E884B2>笔<EFBFBD><E7AC94>滨迂 | `uppercase` (撘瑕<E69298>憭批神) | N/A |
|
||||
| **<EFBFBD>脣蔗** | `slate-900` / `slate-100` | `slate-500` / `slate-400` | `slate-300` / `slate-700` |
|
||||
|
||||
### 撖虫<E69296>蝳<EFBFBD><E89DB3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
- **蝳<>迫<EFBFBD>𣈯<EFBFBD> (No Italics)**: <20>滨迂甈<E8BF82><E79488><EFBFBD>渡<EFBFBD><E6B8A1><EFBFBD>葆 `italic`嚗<EFBFBD>鸌<EFBFBD>交糓璅䠷<EFBFBD><EFBFBD>㚚<EFBFBD>蝵桀<EFBFBD>蝔梧<EFBFBD>嚗䔶<EFBFBD><EFBFBD><EFBFBD>凒<EFBFBD>箏<EFBFBD>璆剜<EFBFBD><EFBFBD><EFBFBD>
|
||||
- **雿𦦵鍂蝭<E98D82><E89DAD> (Mono Scoping)**: `font-mono` <20><><EFBFBD>雿𦦵鍂<F0A6A6B5>潦<EFBFBD>𣬚<EFBFBD><F0A3AC9A>望<EFBFBD>/<2F>詨<EFBFBD><E8A9A8>滨<EFBFBD>隞<EFBFBD>Ⅳ<EFBFBD><E285A3>mail <20>硋<EFBFBD><E7A18B>娪<EFBFBD>敹<EFBFBD><E695B9><EFBFBD>墧飛 `font-sans` 隞亦Ⅱ靽嘥<E99DBD>瞏扎<E79E8F><E6898E>
|
||||
- **甈𢠃<E79488>頛匧<E9A09B> (Font Weights)**: 蝣箔<E89DA3> HTML Header 頛匧<E9A09B>鈭<EFBFBD> `800` <20><> `900` 甈𢠃<E79488>嚗屸<E59A97><E5B1B8>滨<EFBFBD>讛汗<E8AE9B>冽芋<E586BD>砍枂<E7A08D><E69E82><EFBFBD>蝎烾<E89D8E><E783BE><EFBFBD>
|
||||
---
|
||||
> [!IMPORTANT]
|
||||
> **開發新功能前,必須確認 `app.css` 中的 `.btn-luxury-*` 系列組件是否滿足需求。**
|
||||
|
||||
@@ -8,6 +8,7 @@ APP_PORT=8090
|
||||
|
||||
APP_LOCALE=zh_TW
|
||||
APP_TIMEZONE=Asia/Taipei
|
||||
DB_TIMEZONE="+08:00"
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\BasicSettings;
|
||||
|
||||
use App\Http\Controllers\Admin\AdminController;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineModel;
|
||||
use App\Models\System\PaymentConfig;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MachineSettingController extends AdminController
|
||||
{
|
||||
/**
|
||||
* 顯示機台設定列表
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$per_page = $request->input('per_page', 20);
|
||||
$query = Machine::query()->with(['machineModel', 'paymentConfig', 'company']);
|
||||
|
||||
// 搜尋:名稱或序號
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$machines = $query->latest()
|
||||
->paginate($per_page)
|
||||
->withQueryString();
|
||||
|
||||
$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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 儲存新機台 (僅核心欄位)
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'serial_no' => 'required|string|unique:machines,serial_no',
|
||||
'company_id' => 'required|exists:companies,id',
|
||||
'machine_model_id' => 'required|exists:machine_models,id',
|
||||
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
||||
'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
|
||||
]);
|
||||
|
||||
$imagePaths = [];
|
||||
if ($request->hasFile('images')) {
|
||||
foreach (array_slice($request->file('images'), 0, 3) as $image) {
|
||||
$imagePaths[] = $this->processAndStoreImage($image);
|
||||
}
|
||||
}
|
||||
|
||||
$machine = Machine::create(array_merge($validated, [
|
||||
'status' => 'offline',
|
||||
'creator_id' => auth()->id(),
|
||||
'updater_id' => auth()->id(),
|
||||
'card_reader_seconds' => 30, // 預設值
|
||||
'card_reader_checkout_time_1' => '22:30:00',
|
||||
'card_reader_checkout_time_2' => '23:45:00',
|
||||
'payment_buffer_seconds' => 5,
|
||||
'images' => $imagePaths,
|
||||
]));
|
||||
|
||||
return redirect()->route('admin.basic-settings.machines.index')
|
||||
->with('success', __('Machine created successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示詳細編輯頁面
|
||||
*/
|
||||
public function edit(Machine $machine): View
|
||||
{
|
||||
$models = MachineModel::select('id', 'name')->get();
|
||||
$paymentConfigs = PaymentConfig::select('id', 'name')->get();
|
||||
$companies = \App\Models\System\Company::select('id', 'name')->get();
|
||||
|
||||
return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新機台詳細參數
|
||||
*/
|
||||
public function update(Request $request, Machine $machine): RedirectResponse
|
||||
{
|
||||
Log::info('Machine Update Request', ['machine_id' => $machine->id, 'data' => $request->all()]);
|
||||
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'card_reader_seconds' => 'required|integer|min:0',
|
||||
'payment_buffer_seconds' => 'required|integer|min:0',
|
||||
'card_reader_checkout_time_1' => 'nullable|string',
|
||||
'card_reader_checkout_time_2' => 'nullable|string',
|
||||
'heating_start_time' => 'nullable|string',
|
||||
'heating_end_time' => 'nullable|string',
|
||||
'card_reader_no' => 'nullable|string|max:255',
|
||||
'key_no' => 'nullable|string|max:255',
|
||||
'invoice_status' => 'required|integer|in:0,1,2',
|
||||
'welcome_gift_enabled' => 'boolean',
|
||||
'is_spring_slot_1_10' => 'boolean',
|
||||
'is_spring_slot_11_20' => 'boolean',
|
||||
'is_spring_slot_21_30' => 'boolean',
|
||||
'is_spring_slot_31_40' => 'boolean',
|
||||
'is_spring_slot_41_50' => 'boolean',
|
||||
'is_spring_slot_51_60' => 'boolean',
|
||||
'member_system_enabled' => 'boolean',
|
||||
'machine_model_id' => 'required|exists:machine_models,id',
|
||||
'payment_config_id' => 'nullable|exists:payment_configs,id',
|
||||
]);
|
||||
|
||||
Log::info('Machine Update Validated Data', ['data' => $validated]);
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
Log::error('Machine Update Validation Failed', ['errors' => $e->errors()]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$machine->update(array_merge($validated, [
|
||||
'updater_id' => auth()->id(),
|
||||
]));
|
||||
|
||||
// 處理圖片更新 (若有上傳新圖片,則替換或附加,這裡採簡單邏輯:若有傳 images 則全換)
|
||||
if ($request->hasFile('images')) {
|
||||
// 刪除舊圖
|
||||
if (!empty($machine->images)) {
|
||||
foreach ($machine->images as $oldPath) {
|
||||
Storage::disk('public')->delete($oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
$imagePaths = [];
|
||||
foreach (array_slice($request->file('images'), 0, 3) as $image) {
|
||||
$imagePaths[] = $this->processAndStoreImage($image);
|
||||
}
|
||||
$machine->update(['images' => $imagePaths]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.basic-settings.machines.index')
|
||||
->with('success', __('Machine settings updated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 處理圖片並轉換為 WebP
|
||||
*/
|
||||
private function processAndStoreImage($file): string
|
||||
{
|
||||
$filename = Str::random(40) . '.webp';
|
||||
$path = 'machines/' . $filename;
|
||||
|
||||
// 建立圖資源
|
||||
$image = null;
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
switch ($extension) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
$image = imagecreatefromjpeg($file->getRealPath());
|
||||
break;
|
||||
case 'png':
|
||||
$image = imagecreatefrompng($file->getRealPath());
|
||||
break;
|
||||
case 'gif':
|
||||
$image = imagecreatefromgif($file->getRealPath());
|
||||
break;
|
||||
case 'webp':
|
||||
$image = imagecreatefromwebp($file->getRealPath());
|
||||
break;
|
||||
}
|
||||
|
||||
if ($image) {
|
||||
// 確保目錄存在
|
||||
Storage::disk('public')->makeDirectory('machines');
|
||||
$fullPath = Storage::disk('public')->path($path);
|
||||
|
||||
// 轉換並儲存
|
||||
imagewebp($image, $fullPath, 80); // 品質 80
|
||||
imagedestroy($image);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Fallback to standard store if GD fails
|
||||
return $file->store('machines', 'public');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\BasicSettings;
|
||||
|
||||
use App\Http\Controllers\Admin\AdminController;
|
||||
use App\Models\System\PaymentConfig;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class PaymentConfigController extends AdminController
|
||||
{
|
||||
/**
|
||||
* 顯示金流配置列表
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$per_page = $request->input('per_page', 20);
|
||||
$configs = PaymentConfig::query()
|
||||
->with(['company', 'creator'])
|
||||
->latest()
|
||||
->paginate($per_page)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.basic-settings.payment-configs.index', [
|
||||
'paymentConfigs' => $configs
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示新增頁面
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$companies = \App\Models\System\Company::select('id', 'name')->get();
|
||||
return view('admin.basic-settings.payment-configs.create', compact('companies'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 儲存金流配置
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'company_id' => 'required|exists:companies,id',
|
||||
'settings' => 'required|array',
|
||||
]);
|
||||
|
||||
PaymentConfig::create([
|
||||
'name' => $request->name,
|
||||
'company_id' => $request->company_id,
|
||||
'settings' => $request->settings,
|
||||
'creator_id' => auth()->id(),
|
||||
'updater_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.basic-settings.payment-configs.index')
|
||||
->with('success', __('Payment Configuration created successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示編輯頁面
|
||||
*/
|
||||
public function edit(PaymentConfig $paymentConfig): View
|
||||
{
|
||||
$companies = \App\Models\System\Company::select('id', 'name')->get();
|
||||
return view('admin.basic-settings.payment-configs.edit', compact('paymentConfig', 'companies'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新金流配置
|
||||
*/
|
||||
public function update(Request $request, PaymentConfig $paymentConfig): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'settings' => 'required|array',
|
||||
]);
|
||||
|
||||
$paymentConfig->update([
|
||||
'name' => $request->name,
|
||||
'settings' => $request->settings,
|
||||
'updater_id' => auth()->id(),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.basic-settings.payment-configs.index')
|
||||
->with('success', __('Payment Configuration updated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除金流配置
|
||||
*/
|
||||
public function destroy(PaymentConfig $paymentConfig): RedirectResponse
|
||||
{
|
||||
$paymentConfig->delete();
|
||||
return redirect()->route('admin.basic-settings.payment-configs.index')
|
||||
->with('success', __('Payment Configuration deleted successfully.'));
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,8 @@ class CompanyController extends Controller
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$limit = $request->input('limit', 10);
|
||||
$companies = $query->latest()->paginate($limit)->withQueryString();
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$companies = $query->latest()->paginate($per_page)->withQueryString();
|
||||
|
||||
return view('admin.companies.index', compact('companies'));
|
||||
}
|
||||
@@ -96,7 +96,7 @@ class CompanyController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'code' => 'required|string|max:50|unique:companies,code,' . $company->id,
|
||||
'tax_id' => 'nullable|string|max:50',
|
||||
'contact_name' => 'required|string|max:255',
|
||||
'contact_name' => 'nullable|string|max:255',
|
||||
'contact_phone' => 'nullable|string|max:50',
|
||||
'contact_email' => 'nullable|email|max:255',
|
||||
'valid_until' => 'nullable|date',
|
||||
|
||||
@@ -11,8 +11,9 @@ class DashboardController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
// 每頁顯示筆數限制 (預設為 10)
|
||||
$perPage = $request->get('limit', 10);
|
||||
|
||||
$perPage = (int) request()->input('per_page', 10);
|
||||
if ($perPage <= 0) $perPage = 10;
|
||||
|
||||
// 從資料庫獲取真實統計數據
|
||||
$totalRevenue = \App\Models\Member\MemberWallet::sum('balance');
|
||||
$activeMachines = Machine::where('status', 'online')->count();
|
||||
|
||||
@@ -29,7 +29,7 @@ class DataConfigController extends Controller
|
||||
public function adminProducts()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '管理者可賣商品',
|
||||
'title' => '商品狀態',
|
||||
'description' => '管理者商品銷售權限',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,22 @@ class MachineController extends AdminController
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$limit = $request->input('limit', 10);
|
||||
$machines = Machine::query()
|
||||
->when($request->status, function ($query, $status) {
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$query = Machine::query();
|
||||
|
||||
// 搜尋:名稱或序號
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('serial_no', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$machines = $query->when($request->status, function ($query, $status) {
|
||||
return $query->where('status', $status);
|
||||
})
|
||||
->latest()
|
||||
->paginate($limit)
|
||||
->paginate($per_page)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.machines.index', compact('machines'));
|
||||
@@ -42,7 +51,7 @@ class MachineController extends AdminController
|
||||
*/
|
||||
public function logs(Request $request): View
|
||||
{
|
||||
$limit = $request->input('limit', 20);
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$logs = \App\Models\Machine\MachineLog::with('machine')
|
||||
->when($request->level, function ($query, $level) {
|
||||
return $query->where('level', $level);
|
||||
@@ -51,7 +60,7 @@ class MachineController extends AdminController
|
||||
return $query->where('machine_id', $machineId);
|
||||
})
|
||||
->latest()
|
||||
->paginate($limit)->withQueryString();
|
||||
->paginate($per_page)->withQueryString();
|
||||
|
||||
$machines = Machine::select('id', 'name')->get();
|
||||
|
||||
|
||||
@@ -7,93 +7,50 @@ use Illuminate\Http\Request;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
// APP功能管理
|
||||
public function appFeatures()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => 'APP功能管理',
|
||||
'description' => 'APP功能權限設定',
|
||||
]);
|
||||
}
|
||||
|
||||
// 資料設定權限
|
||||
public function dataConfig()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '資料設定權限',
|
||||
'description' => '資料設定功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 銷售管理權限
|
||||
public function sales()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '銷售管理權限',
|
||||
'description' => '銷售管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 機台管理權限
|
||||
public function machines()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台管理權限',
|
||||
'description' => '機台管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 倉庫管理權限
|
||||
public function warehouses()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '倉庫管理權限',
|
||||
'description' => '倉庫管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 分析管理權限
|
||||
public function analysis()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '分析管理權限',
|
||||
'description' => '分析管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 稽核管理權限
|
||||
public function audit()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '稽核管理權限',
|
||||
'description' => '稽核管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 遠端管理權限
|
||||
public function remote()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '遠端管理權限',
|
||||
'description' => '遠端管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// Line管理權限
|
||||
public function line()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => 'Line管理權限',
|
||||
'description' => 'Line管理功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 權限角色設定
|
||||
public function roles()
|
||||
{
|
||||
$limit = request()->input('limit', 10);
|
||||
$roles = \Spatie\Permission\Models\Role::withCount('users')->latest()->paginate($limit)->withQueryString();
|
||||
return view('admin.permission.roles', compact('roles'));
|
||||
$per_page = request()->input('per_page', 10);
|
||||
$user = auth()->user();
|
||||
$query = \App\Models\System\Role::query()->with(['permissions', 'users']);
|
||||
|
||||
// 租戶隔離:租戶只能看到自己公司的角色 + 系統角色 (company_id is null)
|
||||
if (!$user->isSystemAdmin()) {
|
||||
$query->where(function($q) use ($user) {
|
||||
$q->where('company_id', $user->company_id)
|
||||
->orWhereNull('company_id');
|
||||
});
|
||||
}
|
||||
|
||||
// 搜尋:角色名稱
|
||||
if ($search = request()->input('search')) {
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
}
|
||||
|
||||
$roles = $query->latest()->paginate($per_page)->withQueryString();
|
||||
$all_permissions = \Spatie\Permission\Models\Permission::all()
|
||||
->filter(function($perm) {
|
||||
// 排除子項目的權限,只顯示主選單權限
|
||||
$excluded = [
|
||||
'menu.basic.machines',
|
||||
'menu.basic.payment-configs',
|
||||
'menu.companies',
|
||||
'menu.accounts',
|
||||
'menu.roles',
|
||||
];
|
||||
return !in_array($perm->name, $excluded);
|
||||
})
|
||||
->groupBy(function($perm) {
|
||||
if (str_starts_with($perm->name, 'menu.')) {
|
||||
return 'menu';
|
||||
}
|
||||
return 'other';
|
||||
});
|
||||
|
||||
// 根據路由決定標題
|
||||
$title = request()->routeIs('*.sub-account-roles') ? __('Sub Account Roles') : __('Role Settings');
|
||||
|
||||
return view('admin.permission.roles', compact('roles', 'all_permissions', 'title'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,14 +60,28 @@ class PermissionController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:roles,name',
|
||||
'permissions' => 'nullable|array',
|
||||
'permissions.*' => 'string|exists:permissions,name',
|
||||
]);
|
||||
|
||||
\Spatie\Permission\Models\Role::create([
|
||||
$is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system');
|
||||
|
||||
$role = \App\Models\System\Role::create([
|
||||
'name' => $validated['name'],
|
||||
'guard_name' => 'web',
|
||||
'is_system' => false,
|
||||
'company_id' => $is_system ? null : auth()->user()->company_id,
|
||||
'is_system' => $is_system,
|
||||
]);
|
||||
|
||||
if (!empty($validated['permissions'])) {
|
||||
$perms = $validated['permissions'];
|
||||
// 如果不是系統角色,排除主選單的系統權限
|
||||
if (!$is_system) {
|
||||
$perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']);
|
||||
}
|
||||
$role->syncPermissions($perms);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', __('Role created successfully.'));
|
||||
}
|
||||
|
||||
@@ -119,17 +90,36 @@ class PermissionController extends Controller
|
||||
*/
|
||||
public function updateRole(Request $request, $id)
|
||||
{
|
||||
$role = \Spatie\Permission\Models\Role::findOrFail($id);
|
||||
|
||||
if ($role->is_system) {
|
||||
return redirect()->back()->with('error', __('System roles cannot be renamed.'));
|
||||
}
|
||||
$role = \App\Models\System\Role::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255|unique:roles,name,' . $id,
|
||||
'permissions' => 'nullable|array',
|
||||
'permissions.*' => 'string|exists:permissions,name',
|
||||
]);
|
||||
|
||||
$role->update(['name' => $validated['name']]);
|
||||
if ($role->name === 'super-admin') {
|
||||
return redirect()->back()->with('error', __('The Super Admin role is immutable.'));
|
||||
}
|
||||
|
||||
if (!auth()->user()->isSystemAdmin() && $role->is_system) {
|
||||
return redirect()->back()->with('error', __('System roles cannot be modified by tenant administrators.'));
|
||||
}
|
||||
|
||||
$is_system = auth()->user()->isSystemAdmin() ? $request->boolean('is_system') : $role->is_system;
|
||||
|
||||
$role->update([
|
||||
'name' => $validated['name'],
|
||||
'is_system' => $is_system,
|
||||
'company_id' => $is_system ? null : $role->company_id,
|
||||
]);
|
||||
|
||||
$perms = $validated['permissions'] ?? [];
|
||||
// 如果不是系統角色,排除主選單的系統權限
|
||||
if (!$is_system) {
|
||||
$perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']);
|
||||
}
|
||||
$role->syncPermissions($perms);
|
||||
|
||||
return redirect()->back()->with('success', __('Role updated successfully.'));
|
||||
}
|
||||
@@ -139,10 +129,14 @@ class PermissionController extends Controller
|
||||
*/
|
||||
public function destroyRole($id)
|
||||
{
|
||||
$role = \Spatie\Permission\Models\Role::findOrFail($id);
|
||||
$role = \App\Models\System\Role::findOrFail($id);
|
||||
|
||||
if ($role->is_system) {
|
||||
return redirect()->back()->with('error', __('System roles cannot be deleted.'));
|
||||
if ($role->name === 'super-admin') {
|
||||
return redirect()->back()->with('error', __('The Super Admin role cannot be deleted.'));
|
||||
}
|
||||
|
||||
if (!auth()->user()->isSystemAdmin() && $role->is_system) {
|
||||
return redirect()->back()->with('error', __('System roles cannot be deleted by tenant administrators.'));
|
||||
}
|
||||
|
||||
if ($role->users()->count() > 0) {
|
||||
@@ -154,15 +148,6 @@ class PermissionController extends Controller
|
||||
return redirect()->back()->with('success', __('Role deleted successfully.'));
|
||||
}
|
||||
|
||||
// 其他功能管理
|
||||
public function others()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '其他功能管理',
|
||||
'description' => '其他特殊功能權限',
|
||||
]);
|
||||
}
|
||||
|
||||
// 帳號管理
|
||||
public function accounts(Request $request)
|
||||
{
|
||||
@@ -187,11 +172,22 @@ class PermissionController extends Controller
|
||||
$query->where('company_id', $request->company_id);
|
||||
}
|
||||
|
||||
$limit = $request->input('limit', 10);
|
||||
$users = $query->latest()->paginate($limit)->withQueryString();
|
||||
$per_page = $request->input('per_page', 10);
|
||||
$users = $query->latest()->paginate($per_page)->withQueryString();
|
||||
$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 = $roles_query->get();
|
||||
|
||||
return view('admin.data-config.accounts', compact('users', 'companies'));
|
||||
// 根據路由決定標題
|
||||
$title = request()->routeIs('*.sub-accounts') ? __('Sub Account Management') : __('Account Management');
|
||||
|
||||
return view('admin.data-config.accounts', compact('users', 'companies', 'roles', 'title'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,6 +228,10 @@ class PermissionController extends Controller
|
||||
{
|
||||
$user = \App\Models\System\User::findOrFail($id);
|
||||
|
||||
if ($user->hasRole('super-admin')) {
|
||||
return redirect()->back()->with('error', __('System super admin accounts cannot be modified via this interface.'));
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'username' => 'required|string|max:255|unique:users,username,' . $id,
|
||||
@@ -252,7 +252,13 @@ class PermissionController extends Controller
|
||||
];
|
||||
|
||||
if (auth()->user()->isSystemAdmin()) {
|
||||
$updateData['company_id'] = $validated['company_id'];
|
||||
// 防止超級管理員不小心把自己綁定到租客公司或降級
|
||||
if ($user->id === auth()->id()) {
|
||||
$updateData['company_id'] = null;
|
||||
$validated['role'] = 'super-admin';
|
||||
} else {
|
||||
$updateData['company_id'] = $validated['company_id'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($validated['password'])) {
|
||||
@@ -261,7 +267,12 @@ class PermissionController extends Controller
|
||||
|
||||
$user->update($updateData);
|
||||
|
||||
$user->syncRoles([$validated['role']]);
|
||||
// 如果是編輯自己且原本是超級管理員,強制保留 super-admin 角色
|
||||
if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) {
|
||||
$user->syncRoles(['super-admin']);
|
||||
} else {
|
||||
$user->syncRoles([$validated['role']]);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', __('Account updated successfully.'));
|
||||
}
|
||||
@@ -273,6 +284,10 @@ class PermissionController extends Controller
|
||||
{
|
||||
$user = \App\Models\System\User::findOrFail($id);
|
||||
|
||||
if ($user->hasRole('super-admin')) {
|
||||
return redirect()->back()->with('error', __('System super admin accounts cannot be deleted.'));
|
||||
}
|
||||
|
||||
if ($user->id === auth()->id()) {
|
||||
return redirect()->back()->with('error', __('You cannot delete your own account.'));
|
||||
}
|
||||
@@ -281,13 +296,4 @@ class PermissionController extends Controller
|
||||
|
||||
return redirect()->back()->with('success', __('Account deleted successfully.'));
|
||||
}
|
||||
|
||||
// AI智能預測
|
||||
public function aiPrediction()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => 'AI智能預測',
|
||||
'description' => 'AI功能權限設定',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
123
app/Http/Controllers/Api/V1/App/MachineController.php
Normal file
123
app/Http/Controllers/Api/V1/App/MachineController.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Jobs\Machine\ProcessHeartbeat;
|
||||
use App\Jobs\Machine\ProcessTimerStatus;
|
||||
use App\Jobs\Machine\ProcessCoinInventory;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class MachineController extends Controller
|
||||
{
|
||||
/**
|
||||
* B010: Machine Heartbeat & Status Update (Asynchronous)
|
||||
*/
|
||||
public function heartbeat(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$data = $request->all();
|
||||
|
||||
// 異步處理狀態更新
|
||||
ProcessHeartbeat::dispatch($machine->serial_no, $data);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
'status' => '49' // 某些硬體可能需要的成功碼
|
||||
], 202); // 202 Accepted
|
||||
}
|
||||
|
||||
/**
|
||||
* B017: Get Slot Info & Stock (Synchronous)
|
||||
*/
|
||||
public function getSlots(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$slots = $machine->slots()->with('product')->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'data' => $slots->map(function ($slot) {
|
||||
return [
|
||||
'slot_no' => $slot->slot_no,
|
||||
'product_id' => $slot->product_id,
|
||||
'stock' => $slot->stock,
|
||||
'capacity' => $slot->capacity,
|
||||
'price' => $slot->price,
|
||||
'status' => $slot->status,
|
||||
];
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* B710: Sync Timer status (Asynchronous)
|
||||
*/
|
||||
public function syncTimer(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$data = $request->all();
|
||||
|
||||
ProcessTimerStatus::dispatch($machine->serial_no, $data);
|
||||
|
||||
return response()->json(['success' => true], 202);
|
||||
}
|
||||
|
||||
/**
|
||||
* B220: Sync Coin Inventory (Asynchronous)
|
||||
*/
|
||||
public function syncCoinInventory(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$data = $request->all();
|
||||
|
||||
ProcessCoinInventory::dispatch($machine->serial_no, $data);
|
||||
|
||||
return response()->json(['success' => true], 202);
|
||||
}
|
||||
|
||||
/**
|
||||
* B650: Verify Member Code/Barcode (Synchronous)
|
||||
*/
|
||||
public function verifyMember(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'code' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['success' => false, 'message' => 'Invalid code'], 400);
|
||||
}
|
||||
|
||||
$code = $request->input('code');
|
||||
|
||||
// 搜尋會員 (barcode 或特定驗證碼)
|
||||
$member = \App\Models\Member\Member::where('barcode', $code)
|
||||
->orWhere('id', $code) // 暫時支援 ID
|
||||
->first();
|
||||
|
||||
if (!$member) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'code' => 404,
|
||||
'message' => 'Member not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'data' => [
|
||||
'member_id' => $member->id,
|
||||
'name' => $member->name,
|
||||
'points' => $member->points,
|
||||
'wallet_balance' => $member->wallet_balance ?? 0,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
app/Http/Controllers/Api/V1/App/TransactionController.php
Normal file
66
app/Http/Controllers/Api/V1/App/TransactionController.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Jobs\Transaction\ProcessTransaction;
|
||||
use App\Jobs\Transaction\ProcessInvoice;
|
||||
use App\Jobs\Transaction\ProcessDispenseRecord;
|
||||
|
||||
class TransactionController extends Controller
|
||||
{
|
||||
/**
|
||||
* B600: Record Transaction (Asynchronous)
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$data = $request->all();
|
||||
$data['serial_no'] = $machine->serial_no;
|
||||
|
||||
ProcessTransaction::dispatch($data);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Accepted'
|
||||
], 202);
|
||||
}
|
||||
|
||||
/**
|
||||
* B601: Record Invoice (Asynchronous)
|
||||
*/
|
||||
public function recordInvoice(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$data = $request->all();
|
||||
$data['serial_no'] = $machine->serial_no;
|
||||
|
||||
ProcessInvoice::dispatch($data);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Accepted'
|
||||
], 202);
|
||||
}
|
||||
|
||||
/**
|
||||
* B602: Record Dispense Result (Asynchronous)
|
||||
*/
|
||||
public function recordDispense(Request $request)
|
||||
{
|
||||
$machine = $request->get('machine');
|
||||
$data = $request->all();
|
||||
$data['serial_no'] = $machine->serial_no;
|
||||
|
||||
ProcessDispenseRecord::dispatch($data);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Accepted'
|
||||
], 202);
|
||||
}
|
||||
}
|
||||
@@ -69,5 +69,6 @@ class Kernel extends HttpKernel
|
||||
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
|
||||
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
|
||||
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
|
||||
'iot.auth' => \App\Http\Middleware\IotAuth::class,
|
||||
];
|
||||
}
|
||||
|
||||
39
app/Http/Middleware/IotAuth.php
Normal file
39
app/Http/Middleware/IotAuth.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Machine\Machine;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IotAuth
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = $request->bearerToken();
|
||||
|
||||
// Phase 1: 暫時也接受 Request Body 中的 key 欄位 (相容模式)
|
||||
if (!$token) {
|
||||
$token = $request->input('key');
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized: Missing Token'], 401);
|
||||
}
|
||||
|
||||
$machine = Machine::where('api_token', $token)->first();
|
||||
|
||||
if (!$machine) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized: Invalid Token'], 401);
|
||||
}
|
||||
|
||||
// 將機台物件注入 Request 供後端使用
|
||||
$request->merge(['machine' => $machine]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
57
app/Jobs/Machine/ProcessCoinInventory.php
Normal file
57
app/Jobs/Machine/ProcessCoinInventory.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\CoinInventory;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessCoinInventory implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $serialNo;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $serialNo, array $data)
|
||||
{
|
||||
$this->serialNo = $serialNo;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$machine = Machine::where('serial_no', $this->serialNo)->firstOrFail();
|
||||
|
||||
// Sync inventory: typically the IoT device sends the full state
|
||||
// If it sends partial, logic would differ. For now, we assume simple updateOrCreate per denomination.
|
||||
if (isset($this->data['inventories']) && is_array($this->data['inventories'])) {
|
||||
foreach ($this->data['inventories'] as $inv) {
|
||||
CoinInventory::updateOrCreate(
|
||||
[
|
||||
'machine_id' => $machine->id,
|
||||
'denomination' => $inv['denomination'],
|
||||
'type' => $inv['type'] ?? 'coin'
|
||||
],
|
||||
['count' => $inv['count']]
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to process coin inventory for machine {$this->serialNo}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
app/Jobs/Machine/ProcessHeartbeat.php
Normal file
41
app/Jobs/Machine/ProcessHeartbeat.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Machine;
|
||||
|
||||
use App\Services\Machine\MachineService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessHeartbeat implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $serialNo;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $serialNo, array $data)
|
||||
{
|
||||
$this->serialNo = $serialNo;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(MachineService $machineService): void
|
||||
{
|
||||
try {
|
||||
$machineService->updateHeartbeat($this->serialNo, $this->data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to process heartbeat for machine {$this->serialNo}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/Jobs/Machine/ProcessTimerStatus.php
Normal file
51
app/Jobs/Machine/ProcessTimerStatus.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\TimerStatus;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessTimerStatus implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $serialNo;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $serialNo, array $data)
|
||||
{
|
||||
$this->serialNo = $serialNo;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$machine = Machine::where('serial_no', $this->serialNo)->firstOrFail();
|
||||
|
||||
TimerStatus::updateOrCreate(
|
||||
['machine_id' => $machine->id, 'slot_no' => $this->data['slot_no']],
|
||||
[
|
||||
'status' => $this->data['status'],
|
||||
'remaining_seconds' => $this->data['remaining_seconds'],
|
||||
'end_at' => isset($this->data['end_at']) ? \Carbon\Carbon::parse($this->data['end_at']) : null,
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to process timer status for machine {$this->serialNo}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/Jobs/Transaction/ProcessDispenseRecord.php
Normal file
39
app/Jobs/Transaction/ProcessDispenseRecord.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Transaction;
|
||||
|
||||
use App\Services\Transaction\TransactionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessDispenseRecord implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(TransactionService $transactionService): void
|
||||
{
|
||||
try {
|
||||
$transactionService->recordDispense($this->data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to record dispense for machine {$this->data['serial_no']}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
app/Jobs/Transaction/ProcessInvoice.php
Normal file
42
app/Jobs/Transaction/ProcessInvoice.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Transaction;
|
||||
|
||||
use App\Services\Transaction\TransactionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessInvoice implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(TransactionService $transactionService): void
|
||||
{
|
||||
try {
|
||||
$transactionService->recordInvoice($this->data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to process invoice: ' . $e->getMessage(), [
|
||||
'data' => $this->data,
|
||||
'exception' => $e
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/Jobs/Transaction/ProcessTransaction.php
Normal file
39
app/Jobs/Transaction/ProcessTransaction.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Transaction;
|
||||
|
||||
use App\Services\Transaction\TransactionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessTransaction implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(TransactionService $transactionService): void
|
||||
{
|
||||
try {
|
||||
$transactionService->processTransaction($this->data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to process transaction: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Models\UserLoginLog;
|
||||
use App\Models\System\UserLoginLog;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
23
app/Models/Machine/CoinInventory.php
Normal file
23
app/Models/Machine/CoinInventory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CoinInventory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'denomination',
|
||||
'count',
|
||||
'type',
|
||||
];
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
}
|
||||
@@ -10,24 +10,93 @@ use App\Traits\TenantScoped;
|
||||
class Machine extends Model
|
||||
{
|
||||
use HasFactory, TenantScoped;
|
||||
use \Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'name',
|
||||
'serial_no',
|
||||
'model',
|
||||
'location',
|
||||
'status',
|
||||
'current_page',
|
||||
'door_status',
|
||||
'temperature',
|
||||
'firmware_version',
|
||||
'api_token',
|
||||
'last_heartbeat_at',
|
||||
'card_reader_seconds',
|
||||
'card_reader_checkout_time_1',
|
||||
'card_reader_checkout_time_2',
|
||||
'heating_start_time',
|
||||
'heating_end_time',
|
||||
'payment_buffer_seconds',
|
||||
'card_reader_no',
|
||||
'key_no',
|
||||
'invoice_status',
|
||||
'welcome_gift_enabled',
|
||||
'is_spring_slot_1_10',
|
||||
'is_spring_slot_11_20',
|
||||
'is_spring_slot_21_30',
|
||||
'is_spring_slot_31_40',
|
||||
'is_spring_slot_41_50',
|
||||
'is_spring_slot_51_60',
|
||||
'member_system_enabled',
|
||||
'payment_config_id',
|
||||
'machine_model_id',
|
||||
'images',
|
||||
'creator_id',
|
||||
'updater_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_heartbeat_at' => 'datetime',
|
||||
'welcome_gift_enabled' => 'boolean',
|
||||
'is_spring_slot_1_10' => 'boolean',
|
||||
'is_spring_slot_11_20' => 'boolean',
|
||||
'is_spring_slot_21_30' => 'boolean',
|
||||
'is_spring_slot_31_40' => 'boolean',
|
||||
'is_spring_slot_41_50' => 'boolean',
|
||||
'is_spring_slot_51_60' => 'boolean',
|
||||
'member_system_enabled' => 'boolean',
|
||||
'images' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get machine images absolute URLs
|
||||
*/
|
||||
public function getImageUrlsAttribute(): array
|
||||
{
|
||||
if (empty($this->images)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(fn($path) => \Illuminate\Support\Facades\Storage::disk('public')->url($path), $this->images);
|
||||
}
|
||||
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(MachineLog::class);
|
||||
}
|
||||
|
||||
public function machineModel()
|
||||
{
|
||||
return $this->belongsTo(MachineModel::class);
|
||||
}
|
||||
|
||||
public function paymentConfig()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\PaymentConfig::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
35
app/Models/Machine/MachineModel.php
Normal file
35
app/Models/Machine/MachineModel.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MachineModel extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'company_id',
|
||||
'creator_id',
|
||||
'updater_id',
|
||||
];
|
||||
|
||||
public function machines()
|
||||
{
|
||||
return $this->hasMany(Machine::class);
|
||||
}
|
||||
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\Company::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\User::class, 'updater_id');
|
||||
}
|
||||
}
|
||||
39
app/Models/Machine/MachineSlot.php
Normal file
39
app/Models/Machine/MachineSlot.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\Product\Product;
|
||||
|
||||
class MachineSlot extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'product_id',
|
||||
'slot_no',
|
||||
'slot_name',
|
||||
'capacity',
|
||||
'stock',
|
||||
'price',
|
||||
'status',
|
||||
'last_restocked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'last_restocked_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
31
app/Models/Machine/RemoteCommand.php
Normal file
31
app/Models/Machine/RemoteCommand.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RemoteCommand extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'command',
|
||||
'payload',
|
||||
'status',
|
||||
'response_payload',
|
||||
'executed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'payload' => 'array',
|
||||
'response_payload' => 'array',
|
||||
'executed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
}
|
||||
28
app/Models/Machine/TimerStatus.php
Normal file
28
app/Models/Machine/TimerStatus.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TimerStatus extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'slot_no',
|
||||
'status',
|
||||
'remaining_seconds',
|
||||
'end_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'end_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ class Member extends Authenticatable
|
||||
'avatar',
|
||||
'is_active',
|
||||
'email_verified_at',
|
||||
'company_id',
|
||||
'barcode',
|
||||
'points',
|
||||
'wallet_balance',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -49,6 +53,8 @@ class Member extends Authenticatable
|
||||
'birthday' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
'password' => 'hashed',
|
||||
'points' => 'integer',
|
||||
'wallet_balance' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
40
app/Models/Product/Product.php
Normal file
40
app/Models/Product/Product.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Product;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'category_id',
|
||||
'name',
|
||||
'sku',
|
||||
'barcode',
|
||||
'description',
|
||||
'price',
|
||||
'cost',
|
||||
'type',
|
||||
'image_url',
|
||||
'status',
|
||||
'name_dictionary_key',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'cost' => 'decimal:2',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'category_id');
|
||||
}
|
||||
}
|
||||
24
app/Models/Product/ProductCategory.php
Normal file
24
app/Models/Product/ProductCategory.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Product;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class ProductCategory extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'name',
|
||||
'name_dictionary_key',
|
||||
];
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->hasMany(Product::class, 'category_id');
|
||||
}
|
||||
}
|
||||
40
app/Models/System/PaymentConfig.php
Normal file
40
app/Models/System/PaymentConfig.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PaymentConfig extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'name',
|
||||
'settings',
|
||||
'creator_id',
|
||||
'updater_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'settings' => 'array',
|
||||
];
|
||||
|
||||
public function machines()
|
||||
{
|
||||
return $this->hasMany(\App\Models\Machine\Machine::class);
|
||||
}
|
||||
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\System\Company::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updater_id');
|
||||
}
|
||||
}
|
||||
34
app/Models/System/Role.php
Normal file
34
app/Models/System/Role.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
|
||||
class Role extends SpatieRole
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'guard_name',
|
||||
'company_id',
|
||||
'is_system',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the company that owns the role.
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include roles for a specific company or system roles.
|
||||
*/
|
||||
public function scopeForCompany($query, $company_id)
|
||||
{
|
||||
return $query->where(function($q) use ($company_id) {
|
||||
$q->where('company_id', $company_id)
|
||||
->orWhereNull('company_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
18
app/Models/System/Translation.php
Normal file
18
app/Models/System/Translation.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Translation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'group',
|
||||
'key',
|
||||
'locale',
|
||||
'value',
|
||||
];
|
||||
}
|
||||
@@ -8,11 +8,13 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
use App\Traits\TenantScoped;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable, HasRoles;
|
||||
use HasApiTokens, HasFactory, Notifiable, HasRoles, TenantScoped, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -56,7 +58,7 @@ class User extends Authenticatable
|
||||
*/
|
||||
public function loginLogs()
|
||||
{
|
||||
return $this->hasMany(\App\Models\UserLoginLog::class);
|
||||
return $this->hasMany(UserLoginLog::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\System;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory; // Added this line
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class UserLoginLog extends Model
|
||||
{
|
||||
50
app/Models/Transaction/DispenseRecord.php
Normal file
50
app/Models/Transaction/DispenseRecord.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Product\Product;
|
||||
|
||||
class DispenseRecord extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_id',
|
||||
'flow_id',
|
||||
'machine_id',
|
||||
'product_id',
|
||||
'slot_no',
|
||||
'amount',
|
||||
'remaining_stock',
|
||||
'dispense_status',
|
||||
'member_barcode',
|
||||
'machine_time',
|
||||
'points_used',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2',
|
||||
'machine_time' => 'datetime',
|
||||
'dispense_status' => 'integer',
|
||||
];
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
39
app/Models/Transaction/Invoice.php
Normal file
39
app/Models/Transaction/Invoice.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_id',
|
||||
'machine_id',
|
||||
'flow_id',
|
||||
'invoice_no',
|
||||
'amount',
|
||||
'carrier_id',
|
||||
'invoice_date',
|
||||
'random_number',
|
||||
'love_code',
|
||||
'rtn_code',
|
||||
'rtn_msg',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'total_amount' => 'decimal:2',
|
||||
'tax_amount' => 'decimal:2',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
}
|
||||
64
app/Models/Transaction/Order.php
Normal file
64
app/Models/Transaction/Order.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\TenantScoped;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Member\Member;
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'flow_id',
|
||||
'order_no',
|
||||
'machine_id',
|
||||
'member_id',
|
||||
'total_amount',
|
||||
'discount_amount',
|
||||
'pay_amount',
|
||||
'payment_type',
|
||||
'payment_status',
|
||||
'payment_at',
|
||||
'status',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'total_amount' => 'decimal:2',
|
||||
'discount_amount' => 'decimal:2',
|
||||
'pay_amount' => 'decimal:2',
|
||||
'payment_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function machine()
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
public function member()
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(OrderItem::class);
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->hasOne(Invoice::class);
|
||||
}
|
||||
|
||||
public function dispenseRecords()
|
||||
{
|
||||
return $this->hasMany(DispenseRecord::class);
|
||||
}
|
||||
}
|
||||
39
app/Models/Transaction/OrderItem.php
Normal file
39
app/Models/Transaction/OrderItem.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\Product\Product;
|
||||
|
||||
class OrderItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'product_id',
|
||||
'product_name',
|
||||
'sku',
|
||||
'price',
|
||||
'quantity',
|
||||
'subtotal',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'subtotal' => 'decimal:2',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/Transaction/PaymentType.php
Normal file
23
app/Models/Transaction/PaymentType.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PaymentType extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
'config',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'config' => 'array',
|
||||
];
|
||||
}
|
||||
@@ -4,42 +4,69 @@ namespace App\Services\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineLog;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MachineService
|
||||
{
|
||||
/**
|
||||
* 處理機台日誌寫入與狀態更新
|
||||
* Update machine heartbeat and status.
|
||||
*
|
||||
* @param string $serialNo
|
||||
* @param array $data
|
||||
* @return Machine
|
||||
*/
|
||||
public function updateHeartbeat(string $serialNo, array $data): Machine
|
||||
{
|
||||
return DB::transaction(function () use ($serialNo, $data) {
|
||||
$machine = Machine::where('serial_no', $serialNo)->firstOrFail();
|
||||
|
||||
$updateData = [
|
||||
'status' => 'online',
|
||||
'temperature' => $data['temperature'] ?? $machine->temperature,
|
||||
'current_page' => $data['current_page'] ?? $machine->current_page,
|
||||
'door_status' => $data['door_status'] ?? $machine->door_status,
|
||||
'firmware_version' => $data['firmware_version'] ?? $machine->firmware_version,
|
||||
'last_heartbeat_at' => now(),
|
||||
];
|
||||
|
||||
$machine->update($updateData);
|
||||
|
||||
// Record log if provided
|
||||
if (!empty($data['log'])) {
|
||||
$machine->logs()->create([
|
||||
'level' => $data['log_level'] ?? 'info',
|
||||
'message' => $data['log'],
|
||||
'payload' => $data['log_payload'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $machine;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update machine slot stock.
|
||||
*/
|
||||
public function updateSlotStock(Machine $machine, int $slotNo, int $stock): void
|
||||
{
|
||||
$machine->slots()->where('slot_no', $slotNo)->update([
|
||||
'stock' => $stock,
|
||||
'last_restocked_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy support for recordLog (Existing code).
|
||||
*/
|
||||
public function recordLog(int $machineId, array $data): MachineLog
|
||||
{
|
||||
$machine = Machine::findOrFail($machineId);
|
||||
|
||||
// 建立日誌紀錄
|
||||
$log = $machine->logs()->create([
|
||||
return $machine->logs()->create([
|
||||
'level' => $data['level'] ?? 'info',
|
||||
'message' => $data['message'],
|
||||
'context' => $data['context'] ?? null,
|
||||
'payload' => $data['context'] ?? null,
|
||||
]);
|
||||
|
||||
// 同步更新機台最後活耀時間與狀態
|
||||
$machine->update([
|
||||
'last_heartbeat_at' => now(),
|
||||
'status' => $this->resolveStatus($data),
|
||||
]);
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根據日誌內容判斷機台是否應標記成錯誤
|
||||
*/
|
||||
protected function resolveStatus(array $data): string
|
||||
{
|
||||
if (isset($data['level']) && $data['level'] === 'error') {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'online';
|
||||
}
|
||||
}
|
||||
|
||||
120
app/Services/Transaction/TransactionService.php
Normal file
120
app/Services/Transaction/TransactionService.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Transaction;
|
||||
|
||||
use App\Models\Transaction\Order;
|
||||
use App\Models\Transaction\OrderItem;
|
||||
use App\Models\Transaction\Invoice;
|
||||
use App\Models\Transaction\DispenseRecord;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\Machine\Machine;
|
||||
|
||||
class TransactionService
|
||||
{
|
||||
/**
|
||||
* Process a new transaction (B600).
|
||||
*/
|
||||
public function processTransaction(array $data): Order
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$machine = Machine::where('serial_no', $data['serial_no'])->firstOrFail();
|
||||
|
||||
// Create Order
|
||||
$order = Order::create([
|
||||
'company_id' => $machine->company_id,
|
||||
'flow_id' => $data['flow_id'] ?? null,
|
||||
'order_no' => $data['order_no'] ?? $this->generateOrderNo(),
|
||||
'machine_id' => $machine->id,
|
||||
'member_id' => $data['member_id'] ?? null,
|
||||
'total_amount' => $data['total_amount'],
|
||||
'discount_amount' => $data['discount_amount'] ?? 0,
|
||||
'pay_amount' => $data['pay_amount'],
|
||||
'payment_type' => $data['payment_type'] ?? 0,
|
||||
'payment_status' => $data['payment_status'] ?? 1,
|
||||
'payment_at' => now(),
|
||||
'status' => 'completed',
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
]);
|
||||
|
||||
// Create Order Items
|
||||
if (!empty($data['items'])) {
|
||||
foreach ($data['items'] as $item) {
|
||||
$order->items()->create([
|
||||
'product_id' => $item['product_id'],
|
||||
'product_name' => $item['product_name'] ?? 'Unknown',
|
||||
'sku' => $item['sku'] ?? null,
|
||||
'price' => $item['price'],
|
||||
'quantity' => $item['quantity'],
|
||||
'subtotal' => $item['price'] * $item['quantity'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $order;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique order number.
|
||||
*/
|
||||
protected function generateOrderNo(): string
|
||||
{
|
||||
return 'ORD-' . now()->format('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record Invoice (B601).
|
||||
*/
|
||||
public function recordInvoice(array $data): Invoice
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$machine = Machine::where('serial_no', $data['serial_no'])->firstOrFail();
|
||||
|
||||
$order = null;
|
||||
if (!empty($data['flow_id'])) {
|
||||
$order = Order::where('flow_id', $data['flow_id'])->first();
|
||||
}
|
||||
|
||||
return Invoice::create([
|
||||
'company_id' => $machine->company_id,
|
||||
'order_id' => $order?->id ?? ($data['order_id'] ?? null),
|
||||
'machine_id' => $machine->id,
|
||||
'flow_id' => $data['flow_id'] ?? null,
|
||||
'invoice_no' => $data['invoice_no'] ?? null,
|
||||
'amount' => $data['amount'] ?? 0,
|
||||
'carrier_id' => $data['carrier_id'] ?? null,
|
||||
'invoice_date' => $data['invoice_date'] ?? null,
|
||||
'random_number' => $data['random_no'] ?? null,
|
||||
'love_code' => $data['love_code'] ?? null,
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record dispense result (B602).
|
||||
*/
|
||||
public function recordDispense(array $data): DispenseRecord
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$machine = Machine::where('serial_no', $data['serial_no'])->firstOrFail();
|
||||
|
||||
$order = null;
|
||||
if (!empty($data['flow_id'])) {
|
||||
$order = Order::where('flow_id', $data['flow_id'])->first();
|
||||
}
|
||||
|
||||
return DispenseRecord::create([
|
||||
'company_id' => $machine->company_id,
|
||||
'order_id' => $order?->id ?? ($data['order_id'] ?? null),
|
||||
'flow_id' => $data['flow_id'] ?? null,
|
||||
'machine_id' => $machine->id,
|
||||
'slot_no' => $data['slot_no'] ?? 'unknown',
|
||||
'product_id' => $data['product_id'] ?? null,
|
||||
'amount' => $data['amount'] ?? 0,
|
||||
'dispense_status' => $data['dispense_status'] ?? 0,
|
||||
'machine_time' => $data['machine_time'] ?? now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,16 @@ trait TenantScoped
|
||||
public static function bootTenantScoped(): void
|
||||
{
|
||||
static::addGlobalScope('tenant', function (Builder $query) {
|
||||
// 避免在 User Model 本身套用此 Scope,否則在 auth()->user() 讀取 User 時會產生循環引用
|
||||
if (static::class === \App\Models\System\User::class) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if running in console/migration
|
||||
if (app()->runningInConsole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
// 如果使用者已登入且有綁定公司,則自動注入過濾條件
|
||||
|
||||
@@ -19,6 +19,7 @@ services:
|
||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||
TZ: 'Asia/Taipei'
|
||||
volumes:
|
||||
- '.:/var/www/html'
|
||||
networks:
|
||||
@@ -41,6 +42,7 @@ services:
|
||||
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||
MYSQL_EXTRA_OPTIONS: '${MYSQL_EXTRA_OPTIONS:-}'
|
||||
TZ: 'Asia/Taipei'
|
||||
volumes:
|
||||
- 'sail-mysql:/var/lib/mysql'
|
||||
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||
|
||||
@@ -57,6 +57,7 @@ return [
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'timezone' => env('DB_TIMEZONE', '+08:00'),
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
|
||||
@@ -24,7 +24,7 @@ return [
|
||||
* `Spatie\Permission\Contracts\Role` contract.
|
||||
*/
|
||||
|
||||
'role' => Spatie\Permission\Models\Role::class,
|
||||
'role' => \App\Models\System\Role::class,
|
||||
|
||||
],
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ return new class extends Migration
|
||||
$table->foreignId('company_id')->nullable()->after('id')
|
||||
->constrained('companies')->nullOnDelete();
|
||||
$table->tinyInteger('status')->default(1)->after('role'); // 1:啟用, 0:停用
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +26,7 @@ return new class extends Migration
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('company_id');
|
||||
$table->dropColumn('status');
|
||||
$table->dropColumn(['status', 'deleted_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->foreignId('company_id')->nullable()->after('id')
|
||||
->constrained('companies')->onDelete('cascade');
|
||||
|
||||
// 移除舊有的唯一鍵
|
||||
$table->dropUnique('roles_name_guard_name_unique');
|
||||
// 新增複合唯一鍵 (涵蓋租戶)
|
||||
$table->unique(['name', 'guard_name', 'company_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->dropUnique(['name', 'guard_name', 'company_id']);
|
||||
$table->unique(['name', 'guard_name']);
|
||||
$table->dropConstrainedForeignId('company_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->string('serial_no')->unique()->after('name')->comment('機台序號');
|
||||
$table->string('model')->nullable()->after('serial_no')->comment('型號');
|
||||
$table->tinyInteger('current_page')->default(0)->after('status')->comment('當前頁面狀態');
|
||||
$table->string('door_status')->nullable()->after('current_page')->comment('門禁狀態');
|
||||
$table->string('api_token')->nullable()->after('firmware_version')->comment('API Token');
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->dropColumn(['serial_no', 'model', 'current_page', 'door_status', 'api_token', 'deleted_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('members', 'company_id')) {
|
||||
$table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade');
|
||||
}
|
||||
if (!Schema::hasColumn('members', 'barcode')) {
|
||||
$table->string('barcode')->nullable()->index();
|
||||
}
|
||||
if (!Schema::hasColumn('members', 'points')) {
|
||||
$table->integer('points')->default(0);
|
||||
}
|
||||
if (!Schema::hasColumn('members', 'wallet_balance')) {
|
||||
$table->decimal('wallet_balance', 10, 2)->default(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropColumn(['company_id', 'barcode', 'points', 'wallet_balance']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('product_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained('companies')->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->string('name_dictionary_key')->nullable()->comment('多語系 Key');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained('companies')->onDelete('cascade');
|
||||
$table->foreignId('category_id')->nullable()->constrained('product_categories')->onDelete('set null');
|
||||
$table->string('name');
|
||||
$table->string('name_dictionary_key')->nullable()->comment('多語系 Key');
|
||||
$table->string('sku')->nullable()->comment('商品編號');
|
||||
$table->decimal('price', 10, 2)->default(0)->comment('售價');
|
||||
$table->decimal('cost', 10, 2)->nullable()->comment('成本');
|
||||
$table->string('image')->nullable()->comment('圖片 URL');
|
||||
$table->string('barcode')->nullable()->comment('條碼');
|
||||
$table->boolean('is_timer_product')->default(false)->comment('是否為計時型商品');
|
||||
$table->boolean('is_active')->default(true)->comment('是否上架');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['company_id', 'sku']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
Schema::dropIfExists('product_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('machine_slots', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('slot_no')->comment('貨道編號 (B017 tid, B710 cid)');
|
||||
$table->foreignId('product_id')->nullable()->constrained('products')->onDelete('set null');
|
||||
$table->integer('stock')->default(0)->comment('當前庫存');
|
||||
$table->integer('max_stock')->default(0)->comment('最大容量');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['machine_id', 'slot_no']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('machine_slots');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('translations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('group', 50)->comment('分組 (product, category, system)');
|
||||
$table->string('key', 100)->comment('字典鍵值');
|
||||
$table->string('locale', 10)->comment('語系代碼');
|
||||
$table->text('value')->comment('翻譯內容');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group', 'key', 'locale']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('translations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payment_types', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->smallInteger('code')->unique()->comment('金流代碼');
|
||||
$table->string('name')->comment('中文名稱');
|
||||
$table->string('category')->nullable()->comment('大分類');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained('companies')->onDelete('cascade');
|
||||
$table->string('flow_id')->nullable()->unique()->comment('Cloud 金流 ID (B600 response)');
|
||||
$table->string('order_no')->nullable()->index()->comment('APP 訂單號 (B600 req9)');
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->foreignId('member_id')->nullable()->constrained('members')->onDelete('set null');
|
||||
$table->smallInteger('payment_type')->comment('金流類型碼');
|
||||
$table->decimal('total_amount', 10, 2)->comment('消費金額');
|
||||
$table->decimal('discount_amount', 10, 2)->default(0)->comment('折扣金額');
|
||||
$table->decimal('pay_amount', 10, 2)->comment('實付金額');
|
||||
$table->decimal('change_amount', 10, 2)->default(0)->comment('找零');
|
||||
$table->integer('points_used')->default(0)->comment('使用點數');
|
||||
$table->decimal('original_amount', 10, 2)->nullable()->comment('折扣前金額');
|
||||
$table->tinyInteger('payment_status')->comment('0:失敗/1:成功');
|
||||
$table->text('payment_request')->nullable()->comment('金流送出 data');
|
||||
$table->text('payment_response')->nullable()->comment('金流回傳 data');
|
||||
$table->datetime('payment_at')->nullable()->comment('支付時間');
|
||||
$table->string('member_barcode')->nullable()->comment('會員條碼');
|
||||
$table->string('invoice_info')->nullable()->comment('發票歸戶');
|
||||
$table->datetime('machine_time')->nullable()->comment('機台時間');
|
||||
$table->string('status')->default('completed')->comment('訂單狀態');
|
||||
$table->json('metadata')->nullable()->comment('其他資訊');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['company_id', 'machine_id', 'created_at']);
|
||||
});
|
||||
|
||||
Schema::create('order_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('order_id')->constrained('orders')->onDelete('cascade');
|
||||
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
|
||||
$table->string('product_name')->nullable()->comment('商品名稱 (備份)');
|
||||
$table->string('sku')->nullable()->comment('商品編號 (備份)');
|
||||
$table->integer('quantity');
|
||||
$table->decimal('price', 10, 2);
|
||||
$table->decimal('subtotal', 10, 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained('companies')->onDelete('cascade');
|
||||
$table->foreignId('order_id')->nullable()->constrained('orders')->onDelete('set null');
|
||||
$table->string('flow_id')->index()->comment('金流 ID');
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('rtn_code')->nullable();
|
||||
$table->string('rtn_msg')->nullable();
|
||||
$table->string('invoice_no')->nullable();
|
||||
$table->decimal('amount', 10, 2)->default(0);
|
||||
$table->string('carrier_id')->nullable();
|
||||
$table->date('invoice_date')->nullable();
|
||||
$table->string('random_number')->nullable();
|
||||
$table->string('love_code')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('dispense_records', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->nullable()->constrained('companies')->onDelete('cascade');
|
||||
$table->foreignId('order_id')->nullable()->constrained('orders')->onDelete('set null');
|
||||
$table->string('flow_id')->nullable()->index()->comment('金流 ID');
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('product_id')->comment('機台端傳入之商品 ID');
|
||||
$table->string('slot_no')->comment('貨道編號');
|
||||
$table->decimal('amount', 10, 2);
|
||||
$table->integer('remaining_stock')->nullable();
|
||||
$table->tinyInteger('dispense_status')->comment('0:成功/1:失敗');
|
||||
$table->string('member_barcode')->nullable();
|
||||
$table->datetime('machine_time')->nullable();
|
||||
$table->integer('points_used')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['company_id', 'machine_id', 'flow_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('dispense_records');
|
||||
Schema::dropIfExists('invoices');
|
||||
Schema::dropIfExists('order_items');
|
||||
Schema::dropIfExists('orders');
|
||||
Schema::dropIfExists('payment_types');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('remote_commands', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('command_type', 50)->comment('reboot, lock, stock_update, dispense...');
|
||||
$table->enum('status', ['pending', 'sent', 'success', 'failed'])->default('pending');
|
||||
$table->json('payload')->nullable()->comment('指令參數');
|
||||
$table->integer('ttl')->default(60)->comment('失效秒數');
|
||||
$table->timestamp('executed_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['machine_id', 'status']);
|
||||
});
|
||||
|
||||
Schema::create('coin_inventories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->integer('value_1')->default(0);
|
||||
$table->integer('value_5')->default(0);
|
||||
$table->integer('value_10')->default(0);
|
||||
$table->integer('value_50')->default(0);
|
||||
$table->integer('value_100')->default(0);
|
||||
$table->integer('value_500')->default(0);
|
||||
$table->integer('value_1000')->default(0);
|
||||
$table->string('operator')->nullable()->comment('操作人 (0=消費者)');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('timer_statuses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('slot_no')->comment('貨道 ID (B710 cid)');
|
||||
$table->foreignId('product_id')->nullable()->constrained('products')->onDelete('set null');
|
||||
$table->tinyInteger('status')->default(0)->comment('0:未啟用/1:使用中/2:異常');
|
||||
$table->integer('remaining_seconds')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['machine_id', 'slot_no']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('timer_statuses');
|
||||
Schema::dropIfExists('coin_inventories');
|
||||
Schema::dropIfExists('remote_commands');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->string('current_page')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->tinyInteger('current_page')->default(0)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('machine_models', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('型號名稱');
|
||||
$table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade')->comment('關聯公司');
|
||||
$table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者');
|
||||
$table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('machine_models');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payment_configs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained()->onDelete('cascade')->comment('關聯公司');
|
||||
$table->string('name')->comment('組合名稱');
|
||||
$table->json('settings')->nullable()->comment('金流參數 (JSON)');
|
||||
$table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者');
|
||||
$table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payment_configs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->integer('card_reader_seconds')->nullable()->default(30)->comment('刷卡機秒數');
|
||||
$table->time('card_reader_checkout_time_1')->nullable()->default('22:30:00')->comment('卡機結帳時間1');
|
||||
$table->time('card_reader_checkout_time_2')->nullable()->default('23:45:00')->comment('卡機結帳時間2');
|
||||
$table->time('heating_start_time')->default('00:00:00')->comment('開啟-加熱時間');
|
||||
$table->time('heating_end_time')->default('00:00:00')->comment('關閉-加熱時間');
|
||||
$table->integer('payment_buffer_seconds')->nullable()->default(5)->comment('金流緩衝時間(s)');
|
||||
$table->string('card_reader_no')->nullable()->comment('刷卡機編號');
|
||||
$table->string('key_no')->nullable()->comment('鑰匙編號');
|
||||
$table->tinyInteger('invoice_status')->default(0)->comment('發票狀態碼: 0=不開發票, 1=預設捐, 2=預設不捐');
|
||||
$table->boolean('welcome_gift_enabled')->default(0)->comment('來店禮開關');
|
||||
$table->boolean('is_spring_slot_1_10')->default(0)->comment('貨道類型(1~10): 0=履帶, 1=彈簧');
|
||||
$table->boolean('is_spring_slot_11_20')->default(0)->comment('貨道類型(11~20): 0=履帶, 1=彈簧');
|
||||
$table->boolean('is_spring_slot_21_30')->default(0)->comment('貨道類型(21~30): 0=履帶, 1=彈簧');
|
||||
$table->boolean('is_spring_slot_31_40')->default(0)->comment('貨道類型(31~40): 0=履帶, 1=彈簧');
|
||||
$table->boolean('is_spring_slot_41_50')->default(0)->comment('貨道類型(41~50): 0=履帶, 1=彈簧');
|
||||
$table->boolean('is_spring_slot_51_60')->default(0)->comment('貨道類型(51~60): 0=履帶, 1=彈簧');
|
||||
$table->boolean('member_system_enabled')->default(0)->comment('會員系統開關');
|
||||
|
||||
$table->foreignId('payment_config_id')->nullable()->constrained('payment_configs')->nullOnDelete()->comment('關聯金流參數組合');
|
||||
$table->foreignId('machine_model_id')->nullable()->constrained('machine_models')->nullOnDelete()->comment('關類型號組合');
|
||||
$table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者');
|
||||
$table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->dropForeign(['payment_config_id']);
|
||||
$table->dropForeign(['machine_model_id']);
|
||||
$table->dropForeign(['creator_id']);
|
||||
$table->dropForeign(['updater_id']);
|
||||
|
||||
$table->dropColumn([
|
||||
'card_reader_seconds',
|
||||
'card_reader_checkout_time_1',
|
||||
'card_reader_checkout_time_2',
|
||||
'heating_start_time',
|
||||
'heating_end_time',
|
||||
'payment_buffer_seconds',
|
||||
'card_reader_no',
|
||||
'key_no',
|
||||
'invoice_status',
|
||||
'welcome_gift_enabled',
|
||||
'is_spring_slot_1_10',
|
||||
'is_spring_slot_11_20',
|
||||
'is_spring_slot_21_30',
|
||||
'is_spring_slot_31_40',
|
||||
'is_spring_slot_41_50',
|
||||
'is_spring_slot_51_60',
|
||||
'member_system_enabled',
|
||||
'payment_config_id',
|
||||
'machine_model_id',
|
||||
'creator_id',
|
||||
'updater_id',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->json('images')->after('firmware_version')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('machines', function (Blueprint $table) {
|
||||
$table->dropColumn('images');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -17,15 +17,52 @@ class RoleSeeder extends Seeder
|
||||
// 重設快取
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
// 建立權限
|
||||
$permissions = [
|
||||
'menu.members',
|
||||
'menu.machines',
|
||||
'menu.app',
|
||||
'menu.warehouses',
|
||||
'menu.sales',
|
||||
'menu.analysis',
|
||||
'menu.audit',
|
||||
'menu.data-config',
|
||||
'menu.remote',
|
||||
'menu.line',
|
||||
'menu.reservation',
|
||||
'menu.special-permission',
|
||||
'menu.basic-settings',
|
||||
'menu.permissions',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
Permission::updateOrCreate(['name' => $permission, 'guard_name' => 'web']);
|
||||
}
|
||||
|
||||
// 建立角色
|
||||
Role::updateOrCreate(
|
||||
$superAdmin = Role::updateOrCreate(
|
||||
['name' => 'super-admin'],
|
||||
['is_system' => true]
|
||||
);
|
||||
$superAdmin->syncPermissions(Permission::all());
|
||||
|
||||
Role::updateOrCreate(
|
||||
$tenantAdmin = Role::updateOrCreate(
|
||||
['name' => 'tenant-admin'],
|
||||
['is_system' => true]
|
||||
['is_system' => false]
|
||||
);
|
||||
$tenantAdmin->syncPermissions([
|
||||
'menu.members',
|
||||
'menu.machines',
|
||||
'menu.app',
|
||||
'menu.warehouses',
|
||||
'menu.sales',
|
||||
'menu.analysis',
|
||||
'menu.audit',
|
||||
'menu.data-config',
|
||||
'menu.remote',
|
||||
'menu.line',
|
||||
'menu.reservation',
|
||||
'menu.special-permission',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
87
docs/api/iot-spec.md
Normal file
87
docs/api/iot-spec.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# IoT API 測試與對接文件 (IoT API Testing & Documentation)
|
||||
|
||||
本文件紀錄 Star Cloud IoT API 的測試紀錄與對接規格,供後續開發與測試追蹤。
|
||||
|
||||
---
|
||||
|
||||
## 🟢 B010: 心跳上報與狀態同步 (Heartbeat & Status)
|
||||
機台定時(建議每 5-10 秒)上送,用於確認連線狀態、溫度及門禁狀態。
|
||||
|
||||
### 1. API 資訊
|
||||
- **Endpoint**: `POST /api/v1/app/machine/status/B010`
|
||||
- **認證方式**: Bearer Token (或 Request Body 帶 `key`)
|
||||
- **處理方式**: 異步處理 (Redis Queue),立即回傳 202。
|
||||
|
||||
### 2. 請求範例 (JSON)
|
||||
```json
|
||||
{
|
||||
"temperature": 5.2,
|
||||
"door_status": 0,
|
||||
"current_page": "home",
|
||||
"firmware_version": "1.0.5",
|
||||
"log": "Status heartbeat test",
|
||||
"log_level": "info"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 回應規格
|
||||
- **成功**: `202 Accepted`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "OK",
|
||||
"status": "49"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔵 B600: 交易紀錄上報 (Transaction Record)
|
||||
當機台端完成交易(收款或扣點成功)後上傳。
|
||||
|
||||
### 1. API 資訊
|
||||
- **Endpoint**: `POST /api/v1/app/B600`
|
||||
- **認證方式**: Bearer Token
|
||||
- **處理方式**: 異步處理 (Redis Queue)。
|
||||
|
||||
### 2. 請求範例 (JSON)
|
||||
```json
|
||||
{
|
||||
"flow_id": "F123456789",
|
||||
"total_amount": 100,
|
||||
"pay_amount": 100,
|
||||
"payment_type": 1,
|
||||
"items": [
|
||||
{
|
||||
"product_id": 1,
|
||||
"product_name": "Test Product",
|
||||
"price": 50,
|
||||
"quantity": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 測試紀錄 (Test Logs)
|
||||
|
||||
### 2026-03-16: B600 交易功能測試
|
||||
- **測試機台**: `SN001`
|
||||
- **測試方式**: `curl` 命令模擬
|
||||
- **驗證項目**:
|
||||
- [ ] HTTP 回傳 `202`
|
||||
- [ ] 資料庫 `orders` 產生紀錄
|
||||
- [ ] 資料庫 `order_items` 產生紀錄
|
||||
- **結果**: 準備中...
|
||||
|
||||
### 2026-03-16: B010 首次演練測試
|
||||
- **測試機台**: `SN001`
|
||||
- **測試方式**: `curl` 命令模擬
|
||||
- **驗證項目**:
|
||||
- [x] HTTP 回傳 `202`
|
||||
- [x] 資料庫 `machines.last_heartbeat_at` 更新為台北時間
|
||||
- [x] `machine_logs` 產生對應日誌
|
||||
- **測試備註**: 過程中發現 `current_page` 為 `tinyInteger` 導致寫入失敗,已將其修改為 `string` 以支援彈性的頁面名稱。
|
||||
- **結果**: ✅ **成功 (SUCCESS)**。機台狀態已同步為 5.2度,當前頁面「home」。
|
||||
@@ -187,18 +187,59 @@ Spatie 預設的 roles 表必須加上多租戶設計,確保留戶只能管理
|
||||
|
||||
---
|
||||
|
||||
#### 🟡 `machines` 表 — 需擴充欄位
|
||||
### 機台設定與參數模型 (Configuration & Parameters)
|
||||
|
||||
現有 `machines` 表僅有基礎欄位,需為 B010 API 補充:
|
||||
為了提升機台的可維護性與金流設定的靈活性,系統引入了以下結構:
|
||||
|
||||
| 新增欄位 | 類型 | 說明 | 來源 API |
|
||||
|----------|------|------|----------|
|
||||
| `serial_no` | `VARCHAR UNIQUE` | 機台序號(API 用此識別) | B010 `machine` |
|
||||
| `model` | `VARCHAR` | 機台型號 | B010 `M_Stus` |
|
||||
| `current_page` | `TINYINT` | 當前頁面狀態碼 | B010 `M_Stus2` |
|
||||
| `door_status` | `VARCHAR` | 門禁狀態 | B010 `door` |
|
||||
| `is_online` | `BOOLEAN` | 是否在線(心跳超時判斷) | 計算欄位 |
|
||||
| `api_token` | `VARCHAR` | 機台專屬 API Token | 取代硬編碼 key |
|
||||
#### 1. 機台型號設定表 (machine_models)
|
||||
記錄系統支援的機台型號,供機台綁定基礎參數。
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `id` | BIGINT PK | — |
|
||||
| `name` | VARCHAR(255) | 型號名稱 (例如: "B010-STD") |
|
||||
| `company_id` | BIGINT FK | 所屬公司 (支援多租戶隔離) |
|
||||
| `creator_id / updater_id` | BIGINT FK | 審計記錄 (建立者/修改者) |
|
||||
| `timestamps` | — | — |
|
||||
|
||||
---
|
||||
|
||||
#### 2. 金流參數組合表 (payment_configs)
|
||||
**範本化設計**:由公司建立支付參數組合範本,機台端選擇套用。
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `id` | BIGINT PK | — |
|
||||
| `company_id` | BIGINT FK | 所屬公司 |
|
||||
| `name` | VARCHAR(255) | 組合名稱 (例如: "玉山+綠界組合A") |
|
||||
| `settings` | **JSON** | 儲存各支付平台 API Key (Ecpay, Esun, Tappay, LinePay 等) |
|
||||
| `creator_id / updater_id` | BIGINT FK | 審計記錄 |
|
||||
| `timestamps` | — | — |
|
||||
|
||||
---
|
||||
|
||||
#### 3. 機台主表擴充 (machines)
|
||||
針對 `machines` 表擴充以下欄位以支援 IoT 通訊與進階營運設定:
|
||||
|
||||
| 類別 | 欄位 | 類型 | 說明 |
|
||||
|------|------|------|------|
|
||||
| **基礎識別** | `serial_no` | `VARCHAR` | 機台序號 (API 唯一識別碼) |
|
||||
| **狀態監控** | `current_page` | `VARCHAR` | 機台當前畫面停留頁面 |
|
||||
| | `door_status` | `VARCHAR` | 門禁狀態 (open/closed) |
|
||||
| | `temperature` | `DECIMAL` | 機器溫度 (來自硬體回傳) |
|
||||
| **金流設定** | `payment_config_id` | `BIGINT FK` | 關聯金流參數樣本 (`payment_configs`) |
|
||||
| | `card_reader_seconds` | `INT` | 刷卡機秒數 (預設 30) |
|
||||
| | `card_reader_checkout_time_1 / 2` | `TIME` | 卡機結帳清機時間 |
|
||||
| | `payment_buffer_seconds` | `INT` | 金流回傳緩衝時間 (預設 5) |
|
||||
| **硬體與營運**| `machine_model_id` | `BIGINT FK` | 關聯機台型號 (`machine_models`) |
|
||||
| | `heating_start / end_time` | `TIME` | 加熱自動排程開啟與關閉時間 |
|
||||
| | `card_reader_no / key_no` | `VARCHAR` | 刷卡機編號與鑰匙編號 |
|
||||
| | `invoice_status` | `TINYINT` | 發票狀態 (0:不開, 1:預設捐, 2:預設不捐) |
|
||||
| | `welcome_gift_enabled` | `BOOLEAN` | 來店禮開關 |
|
||||
| | `member_system_enabled` | `BOOLEAN` | 會員系統開關 |
|
||||
| | `is_spring_slot_1_10` ~ `60` | `BOOLEAN` | 貨道類型標記 (0=履帶, 1=彈簧) |
|
||||
| **審計權限** | `company_id` | `BIGINT FK` | 關聯所屬公司 (租戶隔離) |
|
||||
| | `creator_id / updater_id` | `BIGINT FK` | 建立者與最後修改者 ID |
|
||||
|
||||
---
|
||||
|
||||
|
||||
68
lang/en.json
68
lang/en.json
@@ -25,7 +25,6 @@
|
||||
"Permanently Delete Account": "Permanently Delete Account",
|
||||
"Password": "Password",
|
||||
"Enter your password to confirm": "Enter your password to confirm",
|
||||
|
||||
"Dashboard": "Dashboard",
|
||||
"Connectivity Status": "Connectivity Status",
|
||||
"Real-time status monitoring": "Real-time status monitoring",
|
||||
@@ -141,29 +140,52 @@
|
||||
"Clear Stock": "Clear Stock",
|
||||
"APK Versions": "APK Versions",
|
||||
"Discord Notifications": "Discord Notifications",
|
||||
"Basic Settings": "Basic Settings",
|
||||
"Machine Settings": "Machine Settings",
|
||||
"Permission Settings": "Permission Settings",
|
||||
"APP Features": "APP Features",
|
||||
"Sales": "Sales",
|
||||
"Others": "Others",
|
||||
"AI Prediction": "AI Prediction",
|
||||
"Roles": "Roles",
|
||||
"Role Management": "Role Management",
|
||||
"Define and manage security roles for the system.": "Define and manage security roles for the system.",
|
||||
"Add Role": "Add Role",
|
||||
"Role Name": "Role Name",
|
||||
"Type": "Type",
|
||||
"Users": "Users",
|
||||
"System Role": "System Role",
|
||||
"System": "System",
|
||||
"Custom": "Custom",
|
||||
"Edit": "Edit",
|
||||
"Are you sure you want to delete this role?": "Are you sure you want to delete this role?",
|
||||
"Delete": "Delete",
|
||||
"Protected": "Protected",
|
||||
"Roles": "Role Permissions",
|
||||
"Role Management": "Role Permission Management",
|
||||
"Define and manage security roles and permissions.": "Define and manage security roles and permissions.",
|
||||
"Search roles...": "Search roles...",
|
||||
"No permissions": "No permissions",
|
||||
"No roles found.": "No roles found.",
|
||||
"Create Role": "Create Role",
|
||||
"Edit Role": "Edit Role",
|
||||
"Update existing role and permissions.": "Update existing role and permissions.",
|
||||
"Create a new role and assign permissions.": "Create a new role and assign permissions.",
|
||||
"Enter role name": "Enter role name",
|
||||
"Add Role": "Add Role",
|
||||
"Role Name": "Role Name",
|
||||
"Type": "Type",
|
||||
"Permissions": "Permissions",
|
||||
"Users": "Users",
|
||||
"System role name cannot be modified.": "System role name cannot be modified.",
|
||||
"The Super Admin role name cannot be modified.": "The Super Admin role name cannot be modified.",
|
||||
"System Level": "System Level",
|
||||
"Company Level": "Company Level",
|
||||
"Global roles accessible by all administrators.": "Global roles accessible by all administrators.",
|
||||
"Roles scoped to specific customer companies.": "Roles scoped to specific customer companies.",
|
||||
"members": "Member Management",
|
||||
"machines": "Machine Management",
|
||||
"app": "APP Management",
|
||||
"warehouses": "Warehouse Management",
|
||||
"sales": "Sales Management",
|
||||
"analysis": "Analysis Management",
|
||||
"audit": "Audit Management",
|
||||
"data-config": "Data Configuration",
|
||||
"remote": "Remote Management",
|
||||
"line": "Line Management",
|
||||
"reservation": "Reservation System",
|
||||
"special-permission": "Special Permission",
|
||||
"companies": "Customer Management",
|
||||
"accounts": "Account Management",
|
||||
"roles": "Role Permissions",
|
||||
"Role Permissions": "Role Permissions",
|
||||
"Role Settings": "Role Permissions",
|
||||
"No login history yet": "No login history yet",
|
||||
"Signed in as": "Signed in as",
|
||||
"Logout": "Logout",
|
||||
@@ -244,5 +266,17 @@
|
||||
"to": "to",
|
||||
"of": "of",
|
||||
"items": "items",
|
||||
"Showing": "Showing"
|
||||
}
|
||||
"Showing": "Showing",
|
||||
"Monitor events and system activity across your vending fleet.": "Monitor events and system activity across your vending fleet.",
|
||||
"All Machines": "All Machines",
|
||||
"All Levels": "All Levels",
|
||||
"Timestamp": "Timestamp",
|
||||
"Message Content": "Message Content",
|
||||
"No matching logs found": "No matching logs found",
|
||||
"Unknown": "Unknown",
|
||||
"Info": "Info",
|
||||
"Warning": "Warning",
|
||||
"basic-settings": "Basic Settings",
|
||||
"permissions": "Permission Settings",
|
||||
"Error": "Error"
|
||||
}
|
||||
94
lang/ja.json
94
lang/ja.json
@@ -25,7 +25,6 @@
|
||||
"Permanently Delete Account": "アカウントを永久に削除",
|
||||
"Password": "パスワード",
|
||||
"Enter your password to confirm": "確認のためパスワードを入力してください",
|
||||
|
||||
"Dashboard": "ダッシュボード",
|
||||
"Connectivity Status": "接続ステータス概況",
|
||||
"Real-time status monitoring": "リアルタイムステータス監視",
|
||||
@@ -141,29 +140,51 @@
|
||||
"Clear Stock": "在庫クリア",
|
||||
"APK Versions": "APKバージョン",
|
||||
"Discord Notifications": "Discord通知",
|
||||
"Basic Settings": "基本設定",
|
||||
"Machine Settings": "機台設定",
|
||||
"Permission Settings": "権限設定",
|
||||
"APP Features": "APP機能",
|
||||
"Sales": "販売",
|
||||
"Others": "その他",
|
||||
"AI Prediction": "AI予測",
|
||||
"Roles": "ロール",
|
||||
"Role Management": "ロール管理",
|
||||
"Define and manage security roles for the system.": "システムのセキュリティロールを定義および管理します。",
|
||||
"Roles": "ロール権限",
|
||||
"Role Management": "ロール権限管理",
|
||||
"Define and manage security roles and permissions.": "システムのセキュリティロールと権限を定義および管理します。",
|
||||
"Search roles...": "ロールを検索...",
|
||||
"No permissions": "権限項目なし",
|
||||
"No roles found.": "ロールが見つかりませんでした。",
|
||||
"Create Role": "ロール作成",
|
||||
"Edit Role": "ロール編集",
|
||||
"Update existing role and permissions.": "既存のロールと権限を更新します。",
|
||||
"Create a new role and assign permissions.": "新しいロールを作成し、権限を割り当てます。",
|
||||
"Enter role name": "ロール名を入力してください",
|
||||
"Add Role": "ロールを追加",
|
||||
"Role Name": "ロール名",
|
||||
"Type": "タイプ",
|
||||
"Permissions": "権限",
|
||||
"Users": "ユーザー数",
|
||||
"System Role": "システムロール",
|
||||
"System": "システム",
|
||||
"Custom": "カスタム",
|
||||
"Edit": "編集",
|
||||
"Are you sure you want to delete this role?": "このロールを削除してもよろしいですか?",
|
||||
"Delete": "削除",
|
||||
"Protected": "保護済み",
|
||||
"No roles found.": "ロールが見見つかりません。",
|
||||
"Create Role": "ロールの作成",
|
||||
"Edit Role": "ロールの編集",
|
||||
"Enter role name": "ロール名を入力してください",
|
||||
"System role name cannot be modified.": "システムロール名は変更できません。",
|
||||
"System Level": "システムレベル",
|
||||
"Company Level": "顧客レベル",
|
||||
"Menu Permissions": "メニュー権限",
|
||||
"Save Changes": "変更を保存",
|
||||
"members": "会員管理",
|
||||
"machines": "機台管理",
|
||||
"app": "APP管理",
|
||||
"warehouses": "倉庫管理",
|
||||
"sales": "販売管理",
|
||||
"analysis": "分析管理",
|
||||
"audit": "監査管理",
|
||||
"data-config": "データ設定",
|
||||
"remote": "リモート管理",
|
||||
"line": "Line管理",
|
||||
"reservation": "予約システム",
|
||||
"special-permission": "特別権限",
|
||||
"companies": "顧客管理",
|
||||
"accounts": "アカウント管理",
|
||||
"roles": "ロール権限",
|
||||
"Role Permissions": "ロール権限",
|
||||
"Role Settings": "ロール權限",
|
||||
"No login history yet": "ログイン履歴はまだありません",
|
||||
"Signed in as": "ログイン中",
|
||||
"Logout": "ログアウト",
|
||||
@@ -172,23 +193,23 @@
|
||||
"Total Logins": "総ログイン数",
|
||||
"Account Status": "アカウント状態",
|
||||
"Active": "アクティブ",
|
||||
"Customer Management": "客戶管理",
|
||||
"Customer Management": "顧客管理",
|
||||
"Manage all tenant accounts and validity": "すべてのテナントアカウントと有効期限を管理します",
|
||||
"Add Customer": "客戶を追加",
|
||||
"Total Customers": "客戶総数",
|
||||
"Add Customer": "顧客を追加",
|
||||
"Total Customers": "顧客総数",
|
||||
"Expired / Disabled": "期限切れ / 停止中",
|
||||
"Search customers...": "客戶を検索...",
|
||||
"Search customers...": "顧客を検索...",
|
||||
"All": "すべて",
|
||||
"Disabled": "停止中",
|
||||
"Customer Info": "客戶情報",
|
||||
"Customer Info": "顧客情報",
|
||||
"Accounts / Machines": "アカウント / 機台",
|
||||
"Valid Until": "有効期限",
|
||||
"Actions": "操作",
|
||||
"Permanent": "永久認可",
|
||||
"Are you sure to delete this customer?": "この客戶を削除してもよろしいですか?",
|
||||
"No customers found": "客戶が見つかりません",
|
||||
"Edit Customer": "客戶を編集",
|
||||
"Update Customer": "客戶を更新",
|
||||
"Are you sure to delete this customer?": "この顧客を削除してもよろしいですか?",
|
||||
"No customers found": "顧客が見つかりません",
|
||||
"Edit Customer": "顧客を編集",
|
||||
"Update Customer": "顧客を更新",
|
||||
"Create": "作成",
|
||||
"Company Name": "会社名",
|
||||
"Company Code": "会社コード",
|
||||
@@ -198,9 +219,9 @@
|
||||
"Contact Phone": "連絡先電話番号",
|
||||
"Contact Email": "連絡先メールアドレス",
|
||||
"Notes": "備考",
|
||||
"Customer created successfully.": "客戶が正常に作成されました。",
|
||||
"Customer updated successfully.": "客戶が正常に更新されました。",
|
||||
"Customer deleted successfully.": "客戶が正常に削除されました。",
|
||||
"Customer created successfully.": "顧客が正常に作成されました。",
|
||||
"Customer updated successfully.": "顧客が正常に更新されました。",
|
||||
"Customer deleted successfully.": "顧客が正常に削除されました。",
|
||||
"Cannot delete company with active accounts.": "アクティブなアカウントを持つ会社は削除できません。",
|
||||
"Contract Until (Optional)": "契約期限 (任意)",
|
||||
"Company Information": "会社情報",
|
||||
@@ -229,8 +250,7 @@
|
||||
"Audit Permissions": "監査管理權限",
|
||||
"Remote Permissions": "リモート管理權限",
|
||||
"Line Permissions": "Line管理權限",
|
||||
"Company": "所属客戶",
|
||||
"Save Changes": "変更を保存",
|
||||
"Company": "所属顧客",
|
||||
"User": "一般ユーザー",
|
||||
"Admin": "管理者",
|
||||
"Super Admin": "スーパー管理者",
|
||||
@@ -244,5 +264,17 @@
|
||||
"to": "から",
|
||||
"of": "件中",
|
||||
"items": "個の項目",
|
||||
"Showing": "表示中"
|
||||
}
|
||||
"Showing": "表示中",
|
||||
"Monitor events and system activity across your vending fleet.": "自販機フリート全体のイベントとシステムアクティビティを監視します。",
|
||||
"All Machines": "すべての機体",
|
||||
"All Levels": "すべてのレベル",
|
||||
"Timestamp": "タイムスタンプ",
|
||||
"Message Content": "ログ内容",
|
||||
"No matching logs found": "一致するログが見つかりません",
|
||||
"Unknown": "不明",
|
||||
"Info": "情報",
|
||||
"Warning": "警告",
|
||||
"basic-settings": "基本設定",
|
||||
"permissions": "權限設定",
|
||||
"Error": "エラー"
|
||||
}
|
||||
144
lang/zh_TW.json
144
lang/zh_TW.json
@@ -25,7 +25,6 @@
|
||||
"Permanently Delete Account": "永久刪除帳號",
|
||||
"Password": "密碼",
|
||||
"Enter your password to confirm": "請輸入您的密碼以確認",
|
||||
|
||||
"Dashboard": "儀表板",
|
||||
"Connectivity Status": "連線狀態概況",
|
||||
"Real-time status monitoring": "即時監控機台連線動態",
|
||||
@@ -141,29 +140,52 @@
|
||||
"Clear Stock": "庫存清空",
|
||||
"APK Versions": "APK版本",
|
||||
"Discord Notifications": "Discord通知",
|
||||
"Basic Settings": "基本設定",
|
||||
"Machine Settings": "機台設定",
|
||||
"Permission Settings": "權限設定",
|
||||
"APP Features": "APP功能",
|
||||
"Sales": "銷售管理",
|
||||
"Others": "其他功能",
|
||||
"AI Prediction": "AI智能預測",
|
||||
"Roles": "角色設定",
|
||||
"Role Management": "角色管理",
|
||||
"Define and manage security roles for the system.": "定義與管理系統的安全角色。",
|
||||
"Add Role": "新增角色",
|
||||
"Role Name": "角色名稱",
|
||||
"Type": "類型",
|
||||
"Users": "使用者人數",
|
||||
"System Role": "系統角色",
|
||||
"System": "系統",
|
||||
"Custom": "自定義",
|
||||
"Edit": "編輯",
|
||||
"Are you sure you want to delete this role?": "您確定要刪除此角色嗎?",
|
||||
"Delete": "刪除",
|
||||
"Protected": "受保護",
|
||||
"Roles": "角色權限",
|
||||
"Role Management": "角色權限管理",
|
||||
"Define and manage security roles and permissions.": "定義並管理系統安全角色與權限。",
|
||||
"Search roles...": "搜尋角色...",
|
||||
"No permissions": "無權限項目",
|
||||
"No roles found.": "找不到角色資料。",
|
||||
"Create Role": "建立角色",
|
||||
"Edit Role": "編輯角色",
|
||||
"Update existing role and permissions.": "更新現有角色與權限設定。",
|
||||
"Create a new role and assign permissions.": "建立新角色並分配對應權限。",
|
||||
"Enter role name": "請輸入角色名稱",
|
||||
"Add Role": "新增角色",
|
||||
"Role Name": "角色名稱",
|
||||
"Type": "類型",
|
||||
"Permissions": "權限",
|
||||
"Users": "帳號數",
|
||||
"System role name cannot be modified.": "內建系統角色的名稱無法修改。",
|
||||
"The Super Admin role name cannot be modified.": "超級管理員角色的名稱無法修改。",
|
||||
"System Level": "系統層級",
|
||||
"Company Level": "客戶層級",
|
||||
"Global roles accessible by all administrators.": "適用於所有管理者的全域角色。",
|
||||
"Roles scoped to specific customer companies.": "適用於各個客戶單位的特定角色。",
|
||||
"members": "會員管理",
|
||||
"machines": "機台管理",
|
||||
"app": "APP 管理",
|
||||
"warehouses": "倉庫管理",
|
||||
"sales": "銷售管理",
|
||||
"analysis": "分析管理",
|
||||
"audit": "稽核管理",
|
||||
"data-config": "資料設定",
|
||||
"remote": "遠端管理",
|
||||
"line": "Line 管理",
|
||||
"reservation": "預約系統",
|
||||
"special-permission": "特殊權限",
|
||||
"companies": "客戶管理",
|
||||
"accounts": "帳號管理",
|
||||
"roles": "角色權限",
|
||||
"Role Permissions": "角色權限",
|
||||
"Role Settings": "角色權限",
|
||||
"No login history yet": "尚無登入紀錄",
|
||||
"Signed in as": "登入身份",
|
||||
"Logout": "登出",
|
||||
@@ -244,5 +266,93 @@
|
||||
"to": "至",
|
||||
"of": "總計",
|
||||
"items": "筆項目",
|
||||
"Showing": "顯示第"
|
||||
}
|
||||
"Showing": "顯示第",
|
||||
"Full Name": "全名",
|
||||
"super-admin": "超級管理員",
|
||||
"admin": "管理員",
|
||||
"user": "一般用戶",
|
||||
"Product Status": "商品狀態",
|
||||
"Monitor events and system activity across your vending fleet.": "跨機台連線動態與系統日誌監控。",
|
||||
"All Machines": "所有機台",
|
||||
"All Levels": "所有層級",
|
||||
"Timestamp": "時間戳記",
|
||||
"Message Content": "日誌內容",
|
||||
"No matching logs found": "找不到符合條件的日誌",
|
||||
"Unknown": "未知",
|
||||
"Info": "一般",
|
||||
"Warning": "警告",
|
||||
"Error": "錯誤",
|
||||
"Management of operational parameters": "機台運作參數管理",
|
||||
"Add Machine": "新增機台",
|
||||
"Search machines...": "搜尋機台...",
|
||||
"Items": "項",
|
||||
"Machine Name": "機台名稱",
|
||||
"Serial No": "機台序號",
|
||||
"Owner": "所屬客戶",
|
||||
"Model": "機台型號",
|
||||
"Action": "操作",
|
||||
"No location set": "尚未設定位置",
|
||||
"Edit Settings": "編輯設定",
|
||||
"Enter machine name": "請輸入機台名稱",
|
||||
"Enter serial number": "請輸入機台序號",
|
||||
"Select Owner": "請選擇所屬客戶",
|
||||
"Select Model": "請選擇機台型號",
|
||||
"Customer Payment Config": "客戶金流設定",
|
||||
"Not Used": "不使用",
|
||||
"Edit Machine Settings": "編輯機台設定",
|
||||
"Operational Parameters": "運作參數",
|
||||
"Card Reader Seconds": "刷卡機秒數",
|
||||
"Payment Buffer Seconds": "金流緩衝時間(s)",
|
||||
"Checkout Time 1": "卡機結帳時間1",
|
||||
"Checkout Time 2": "卡機結帳時間2",
|
||||
"Heating Start Time": "開啟-加熱時間",
|
||||
"Heating End Time": "關閉-加熱時間",
|
||||
"Hardware & Slots": "硬體與貨道設定",
|
||||
"Card Reader No": "刷卡機編號",
|
||||
"Key No": "鑰匙編號",
|
||||
"Slot Mechanism (default: Conveyor, check for Spring)": "貨道類型 (預設履帶,勾選為彈簧)",
|
||||
"Payment & Invoice": "金流與發票",
|
||||
"Invoice Status": "發票狀態碼",
|
||||
"No Invoice": "不開發票",
|
||||
"Default Donate": "開發票預設捐",
|
||||
"Default Not Donate": "開發票預設不捐",
|
||||
"Member & External": "會員與外部系統",
|
||||
"Welcome Gift": "來店禮開關",
|
||||
"Enabled/Disabled": "啟用/禁用",
|
||||
"Member System": "會員系統",
|
||||
"Payment Configuration": "客戶金流設定",
|
||||
"Merchant payment gateway settings management": "特約商店支付網關參數管理",
|
||||
"Create Config": "建立配置",
|
||||
"Config Name": "配置名稱",
|
||||
"Last Updated": "最後更新日期",
|
||||
"Are you sure you want to delete this configuration?": "您確定要刪除此金流配置嗎?",
|
||||
"Create Payment Config": "建立金流配置",
|
||||
"Define new third-party payment parameters": "定義新的第三方支付介接參數",
|
||||
"Save Config": "儲存配置",
|
||||
"Configuration Name": "金流組合名稱",
|
||||
"Belongs To Company": "所屬客戶公司",
|
||||
"ECPay Invoice": "綠界發票",
|
||||
"Store ID": "特約商店代號 (MerchantID)",
|
||||
"HashKey": "HashKey",
|
||||
"HashIV": "HashIV",
|
||||
"E.SUN QR Scan": "玉山掃碼",
|
||||
"StoreID": "商店代號 (StoreID)",
|
||||
"TermID": "終端代號 (TermID)",
|
||||
"Key": "金鑰 (Key)",
|
||||
"LINE Pay Direct": "Line官方支付",
|
||||
"ChannelId": "ChannelId",
|
||||
"ChannelSecret": "ChannelSecret",
|
||||
"TapPay Integration": "TapPay 整合支付",
|
||||
"PARTNER_KEY": "PARTNER_KEY",
|
||||
"APP_ID": "APP_ID",
|
||||
"APP_KEY": "APP_KEY",
|
||||
"Merchant IDs": "特約商店代號 (Merchant IDs)",
|
||||
"LINE_MERCHANT_ID": "LINE Pay 商店代號",
|
||||
"JKO_MERCHANT_ID": "街口支付 商店代號",
|
||||
"PI_MERCHANT_ID": "Pi 拍錢包 商店代號",
|
||||
"PS_MERCHANT_ID": "全盈+Pay 商店代號",
|
||||
"EASY_MERCHANT_ID": "悠遊付 商店代號",
|
||||
"basic-settings": "基本設定",
|
||||
"permissions": "權限設定",
|
||||
"Edit Payment Config": "編輯金流配置"
|
||||
}
|
||||
@@ -99,9 +99,9 @@
|
||||
|
||||
@layer components {
|
||||
.luxury-nav-item {
|
||||
@apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-medium rounded-xl transition-all duration-200;
|
||||
@apply text-slate-500 hover:text-slate-900 hover:bg-slate-100;
|
||||
@apply dark:text-slate-400 dark:hover:text-white dark:hover:bg-white/5;
|
||||
@apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-semibold rounded-xl transition-all duration-200;
|
||||
@apply text-slate-600 hover:text-slate-900 hover:bg-slate-100;
|
||||
@apply dark:text-slate-300 dark:hover:text-white dark:hover:bg-white/5;
|
||||
}
|
||||
|
||||
.luxury-nav-item.active {
|
||||
@@ -184,13 +184,17 @@
|
||||
@apply dark:ring-cyan-500/20 dark:border-cyan-400/50;
|
||||
}
|
||||
|
||||
/* Date Input Calendar Icon Optimization */
|
||||
.luxury-input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
/* Input Icon Optimization (Date/Time) */
|
||||
.luxury-input[type="date"]::-webkit-calendar-picker-indicator,
|
||||
.luxury-input[type="time"]::-webkit-calendar-picker-indicator,
|
||||
.luxury-input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
||||
@apply cursor-pointer transition-opacity hover:opacity-100;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
.dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator,
|
||||
.dark .luxury-input[type="time"]::-webkit-calendar-picker-indicator,
|
||||
.dark .luxury-input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
|
||||
251
resources/views/admin/basic-settings/machines/edit.blade.php
Normal file
251
resources/views/admin/basic-settings/machines/edit.blade.php
Normal file
@@ -0,0 +1,251 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-10 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">
|
||||
<a href="{{ route('admin.basic-settings.machines.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 text-slate-500 hover:text-cyan-500 transition-all shadow-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Edit Machine Settings') }}</h1>
|
||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ $machine->name }} / {{ $machine->serial_no }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" form="edit-form" class="btn-luxury-primary px-8 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="edit-form" action="{{ route('admin.basic-settings.machines.update', $machine) }}" method="POST" enctype="multipart/form-data" class="space-y-8">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Validation Errors -->
|
||||
@if ($errors->any())
|
||||
<div class="luxury-card p-6 bg-rose-50/50 dark:bg-rose-500/5 border-rose-100 dark:border-rose-500/20 mb-8 animate-luxury-in">
|
||||
<div class="flex items-center gap-3 mb-4 text-rose-600 dark:text-rose-400">
|
||||
<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="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/></svg>
|
||||
<h4 class="font-black text-sm tracking-tight italic">{{ __('Some fields need attention') }}</h4>
|
||||
</div>
|
||||
<ul class="space-y-1">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li class="text-[11px] font-bold text-rose-500/80">{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- 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="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>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Machine Name') }}</label>
|
||||
<input type="text" name="name" value="{{ old('name', $machine->name) }}" class="luxury-input w-full" required>
|
||||
</div>
|
||||
<div>
|
||||
<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">{{ __('Machine Model') }}</label>
|
||||
<select name="machine_model_id" class="luxury-select w-full" required>
|
||||
@foreach($models as $model)
|
||||
<option value="{{ $model->id }}" {{ old('machine_model_id', $machine->machine_model_id) == $model->id ? 'selected' : '' }}>{{ $model->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operational Parameters -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 50ms">
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Card Reader Seconds') }}</label>
|
||||
<input type="number" name="card_reader_seconds" value="{{ $machine->card_reader_seconds }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Payment Buffer Seconds') }}</label>
|
||||
<input type="number" name="payment_buffer_seconds" value="{{ $machine->payment_buffer_seconds }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Checkout Time 1') }}</label>
|
||||
<x-luxury-time-input name="card_reader_checkout_time_1" value="{{ $machine->card_reader_checkout_time_1 }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Checkout Time 2') }}</label>
|
||||
<x-luxury-time-input name="card_reader_checkout_time_2" value="{{ $machine->card_reader_checkout_time_2 }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Heating Start Time') }}</label>
|
||||
<x-luxury-time-input name="heating_start_time" value="{{ $machine->heating_start_time }}" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Heating End Time') }}</label>
|
||||
<x-luxury-time-input name="heating_end_time" value="{{ $machine->heating_end_time }}" />
|
||||
</div>
|
||||
</div>
|
||||
</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="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>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Card Reader No') }}</label>
|
||||
<input type="text" name="card_reader_no" value="{{ $machine->card_reader_no }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Key No') }}</label>
|
||||
<input type="text" name="key_no" value="{{ $machine->key_no }}" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-4">{{ __('Slot Mechanism (default: Conveyor, check for Spring)') }}</label>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4">
|
||||
@foreach([['1-10', 'is_spring_slot_1_10'], ['11-20', 'is_spring_slot_11_20'], ['21-30', 'is_spring_slot_21_30'], ['31-40', 'is_spring_slot_31_40'], ['41-50', 'is_spring_slot_41_50'], ['51-60', 'is_spring_slot_51_60']] as $slot)
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 cursor-pointer hover:border-cyan-500/30 transition-all">
|
||||
<input type="hidden" name="{{ $slot[1] }}" value="0">
|
||||
<input type="checkbox" name="{{ $slot[1] }}" value="1" {{ $machine->{$slot[1]} ? 'checked' : '' }} class="w-4 h-4 rounded text-cyan-500 focus:ring-cyan-500/20">
|
||||
<span class="text-xs font-black text-slate-600 dark:text-slate-400">{{ $slot[0] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: System & Payment -->
|
||||
<div class="space-y-8">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Payment Config') }}</label>
|
||||
<select name="payment_config_id" class="luxury-select w-full">
|
||||
<option value="">{{ __('Not Used') }}</option>
|
||||
@foreach($paymentConfigs as $config)
|
||||
<option value="{{ $config->id }}" {{ $machine->payment_config_id == $config->id ? 'selected' : '' }}>{{ $config->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Status') }}</label>
|
||||
<select name="invoice_status" class="luxury-select w-full">
|
||||
<option value="0" {{ $machine->invoice_status == 0 ? 'selected' : '' }}>{{ __('No Invoice') }}</option>
|
||||
<option value="1" {{ $machine->invoice_status == 1 ? 'selected' : '' }}>{{ __('Default Donate') }}</option>
|
||||
<option value="2" {{ $machine->invoice_status == 2 ? 'selected' : '' }}>{{ __('Default Not Donate') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 300ms">
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<label class="flex items-center justify-between p-4 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/30 dark:bg-slate-900/30 cursor-pointer">
|
||||
<div>
|
||||
<span class="block text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Welcome Gift') }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __('Enabled/Disabled') }}</span>
|
||||
</div>
|
||||
<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>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center justify-between p-4 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/30 dark:bg-slate-900/30 cursor-pointer">
|
||||
<div>
|
||||
<span class="block text-sm font-black text-slate-700 dark:text-slate-200">{{ __('Member System') }}</span>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{{ __('Enabled/Disabled') }}</span>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
@endsection
|
||||
317
resources/views/admin/basic-settings/machines/index.blade.php
Normal file
317
resources/views/admin/basic-settings/machines/index.blade.php
Normal file
@@ -0,0 +1,317 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-10 pb-20" x-data="{
|
||||
showCreateModal: false,
|
||||
showDetailDrawer: false,
|
||||
currentMachine: null,
|
||||
openDetail(machine) {
|
||||
this.currentMachine = machine;
|
||||
this.showDetailDrawer = true;
|
||||
}
|
||||
}">
|
||||
<!-- 1. Header Area -->
|
||||
<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">{{ __('Machine Settings') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Management of operational parameters') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="showCreateModal = true" class="btn-luxury-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
<span>{{ __('Add Machine') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Main Integrated Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar & Filters -->
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<form method="GET" action="{{ route('admin.basic-settings.machines.index') }}" class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" placeholder="{{ __('Search machines...') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-64">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- scrollable table -->
|
||||
<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-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine 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">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Card Reader') }}</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">{{ __('Owner') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@foreach($machines as $machine)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 cursor-pointer" @click="openDetail({{ $machine->toJson() }})">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ $machine->serial_no }}</span>
|
||||
<span class="text-xs text-slate-300 dark:text-slate-700">•</span>
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ $machine->machineModel->name ?? '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
@php
|
||||
$isOnline = $machine->last_heartbeat_at && $machine->last_heartbeat_at->diffInMinutes() < 5;
|
||||
@endphp
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="relative flex h-2.5 w-2.5">
|
||||
@if($isOnline)
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
||||
@else
|
||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-slate-300 dark:bg-slate-600"></span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="text-xs font-bold uppercase tracking-wider {{ $isOnline ? 'text-emerald-500' : 'text-slate-500 dark:text-slate-400' }}">
|
||||
{{ $isOnline ? __('Online') : __('Offline') }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">
|
||||
{{ $machine->card_reader_seconds ?? 0 }}s <span class="text-slate-300 dark:text-slate-700 mx-1.5">/</span> <span class="text-xs text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">No.{{ $machine->card_reader_no ?? '--' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
|
||||
{{ $machine->company->name ?? __('None') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right space-x-2">
|
||||
<button @click="openDetail({{ json_encode($machine->only(['name', 'serial_no', 'status', 'location', 'last_heartbeat_at', 'card_reader_no', 'card_reader_seconds', 'firmware_version', 'api_token', 'heating_start_time', 'heating_end_time', 'image_urls'])) }})"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 border border-transparent hover:border-cyan-500/20 transition-all inline-flex" title="{{ __('View Details') }}">
|
||||
<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>
|
||||
</button>
|
||||
<a href="{{ route('admin.basic-settings.machines.edit', $machine) }}"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 border border-transparent hover:border-cyan-500/20 transition-all inline-flex" title="{{ __('Edit Settings') }}">
|
||||
<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>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 3. Standard Pagination Footer -->
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $machines->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<template x-teleport="body">
|
||||
<div x-show="showCreateModal"
|
||||
class="fixed inset-0 z-[100] overflow-y-auto"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity" aria-hidden="true" @click="showCreateModal = false">
|
||||
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"></div>
|
||||
</div>
|
||||
|
||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||
|
||||
<div class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-3xl text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full animate-luxury-in border border-slate-100 dark:border-slate-800">
|
||||
<div class="px-8 pt-8 pb-6 border-b border-slate-50 dark:border-slate-800/50 flex justify-between items-center">
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Add Machine') }}</h3>
|
||||
<button @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.basic-settings.machines.store') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="px-8 py-8 space-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Machine Name') }}</label>
|
||||
<input type="text" name="name" required class="luxury-input w-full" placeholder="{{ __('Enter machine name') }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Serial No') }}</label>
|
||||
<input type="text" name="serial_no" required class="luxury-input w-full" placeholder="{{ __('Enter serial number') }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Owner') }}</label>
|
||||
<select name="company_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Owner') }}</option>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Model') }}</label>
|
||||
<select name="machine_model_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Model') }}</option>
|
||||
@foreach($models as $model)
|
||||
<option value="{{ $model->id }}">{{ $model->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Customer Payment Config') }}</label>
|
||||
<select name="payment_config_id" class="luxury-select w-full">
|
||||
<option value="">{{ __('Not Used') }}</option>
|
||||
@foreach($paymentConfigs as $config)
|
||||
<option value="{{ $config->id }}">{{ $config->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Machine 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">* {{ __('Will be automatically converted to WebP for optimization') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-6 bg-slate-50 dark:bg-slate-900/50 flex justify-end gap-3 rounded-b-3xl border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-8">{{ __('Save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Detail Drawer -->
|
||||
<template x-teleport="body">
|
||||
<div x-show="showDetailDrawer"
|
||||
class="fixed inset-0 z-[150]"
|
||||
x-cloak>
|
||||
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity"
|
||||
x-show="showDetailDrawer"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click="showDetailDrawer = false"></div>
|
||||
|
||||
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
||||
<div class="w-screen max-w-md"
|
||||
x-show="showDetailDrawer"
|
||||
x-transition:enter="transform transition ease-in-out duration-500"
|
||||
x-transition:enter-start="translate-x-full"
|
||||
x-transition:enter-end="translate-x-0"
|
||||
x-transition:leave="transform transition ease-in-out duration-500"
|
||||
x-transition:leave-start="translate-x-0"
|
||||
x-transition:leave-end="translate-x-full">
|
||||
<div class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
||||
<!-- Drawer Header -->
|
||||
<div class="px-6 py-8 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-black text-slate-800 dark:text-white">{{ __('Parameters') }}</h2>
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-1" x-text="currentMachine?.name"></p>
|
||||
</div>
|
||||
<button @click="showDetailDrawer = false" class="p-2 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
|
||||
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Drawer Content -->
|
||||
<div class="flex-1 overflow-y-auto px-6 py-8 space-y-10 custom-scrollbar">
|
||||
<!-- Machine Images Gallery -->
|
||||
<template x-if="currentMachine?.image_urls && currentMachine.image_urls.length > 0">
|
||||
<section class="space-y-4">
|
||||
<h3 class="text-[11px] font-black text-indigo-500 uppercase tracking-[0.3em]">{{ __('Machine Images') }}</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<template x-for="(url, index) in currentMachine.image_urls" :key="index">
|
||||
<div class="relative group aspect-square rounded-2xl overflow-hidden border border-slate-100 dark:border-slate-800 shadow-sm bg-slate-50 dark:bg-slate-800/50">
|
||||
<img :src="url" class="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- Technical Specs -->
|
||||
<section class="space-y-6">
|
||||
<h3 class="text-[11px] font-black text-cyan-500 uppercase tracking-[0.3em]">{{ __('Hardware & Network') }}</h3>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{ __('Serial & Version') }}</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs font-mono font-bold text-slate-700 dark:text-slate-300" x-text="currentMachine?.serial_no"></div>
|
||||
<span class="px-2 py-0.5 rounded-md bg-white dark:bg-slate-900 text-[9px] font-black text-slate-500 border border-slate-100 dark:border-slate-800" x-text="'v' + (currentMachine?.firmware_version || '1.0')"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-800/40 p-5 rounded-2xl border border-slate-100 dark:border-slate-800/80">
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1.5">{{ __('Heartbeat') }}</span>
|
||||
<div class="text-xs font-bold text-slate-700 dark:text-slate-300" x-text="currentMachine?.last_heartbeat_at ? new Date(currentMachine.last_heartbeat_at).toLocaleString() : '--'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Operational Settings -->
|
||||
<section class="space-y-6">
|
||||
<h3 class="text-[11px] font-black text-amber-500 uppercase tracking-[0.3em]">{{ __('Operations') }}</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between p-2 border-b border-slate-50 dark:border-white/5">
|
||||
<span class="text-xs font-bold text-slate-500">{{ __('Heating Range') }}</span>
|
||||
<span class="text-xs font-black text-slate-700 dark:text-slate-300" x-text="(currentMachine?.heating_start_time ? currentMachine.heating_start_time.substring(0, 5) : '00:00') + ' ~ ' + (currentMachine?.heating_end_time ? currentMachine.heating_end_time.substring(0, 5) : '00:00')"></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2 border-b border-slate-50 dark:border-white/5">
|
||||
<span class="text-xs font-bold text-slate-500">{{ __('Card Reader No') }}</span>
|
||||
<span class="text-xs font-black text-slate-700 dark:text-slate-300" x-text="currentMachine?.card_reader_no || '--'"></span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2 border-b border-slate-50 dark:border-white/5">
|
||||
<span class="text-xs font-bold text-slate-500">{{ __('API Token') }}</span>
|
||||
<span class="text-[10px] font-mono text-slate-400 truncate max-w-[150px]" x-text="currentMachine?.api_token || '--'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Location -->
|
||||
<section class="space-y-4">
|
||||
<h3 class="text-[11px] font-black text-emerald-500 uppercase tracking-[0.3em]">{{ __('Location') }}</h3>
|
||||
<div class="p-4 bg-emerald-50/30 dark:bg-emerald-500/5 rounded-2xl border border-emerald-100/50 dark:border-emerald-500/10">
|
||||
<p class="text-xs text-emerald-700 dark:text-emerald-400 leading-relaxed font-bold" x-text="currentMachine?.location || '{{ __('No location set') }}'"></p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Drawer Footer -->
|
||||
<div class="p-6 border-t border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<button @click="showDetailDrawer = false" class="w-full btn-luxury-ghost">{{ __('Close Panel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,187 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-10 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">
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 text-slate-500 hover:text-cyan-500 transition-all shadow-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Create Payment Config') }}</h1>
|
||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define new third-party payment parameters') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" form="create-form" class="btn-luxury-primary px-8 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<span>{{ __('Save Config') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="create-form" action="{{ route('admin.basic-settings.payment-configs.store') }}" method="POST" class="space-y-8">
|
||||
@csrf
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<!-- Left Column: Primary Info -->
|
||||
<div class="lg:col-span-12 space-y-8">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Configuration Name') }} <span class="text-rose-500">*</span></label>
|
||||
<input type="text" name="name" required class="luxury-input w-full" placeholder="{{ __('e.g., Company Standard Pay') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Belongs To Company') }} <span class="text-rose-500">*</span></label>
|
||||
<select name="company_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Company') }}</option>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- provider groups -->
|
||||
<div class="lg:col-span-6 space-y-8">
|
||||
<!-- ECPay -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 50ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-emerald-50 dark:bg-emerald-500/10 flex items-center justify-center text-emerald-500 border border-emerald-100 dark:border-emerald-500/20 shadow-sm shadow-emerald-500/10">
|
||||
<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>
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E.SUN -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 100ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-500 border border-indigo-100 dark:border-indigo-500/20 shadow-sm shadow-indigo-500/10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('StoreID') }}</label>
|
||||
<input type="text" name="settings[esun_scan][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">{{ __('TermID') }}</label>
|
||||
<input type="text" name="settings[esun_scan][term_id]" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Key') }}</label>
|
||||
<input type="text" name="settings[esun_scan][key]" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LINE Pay Direct -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 150ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-emerald-50 dark:bg-emerald-500/10 flex items-center justify-center text-emerald-500 border border-emerald-100 dark:border-emerald-500/20 shadow-sm shadow-emerald-500/10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('ChannelId') }}</label>
|
||||
<input type="text" name="settings[line_pay][channel_id]" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('ChannelSecret') }}</label>
|
||||
<input type="text" name="settings[line_pay][channel_secret]" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-6 space-y-8">
|
||||
<!-- Tappay -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-500 border border-indigo-100 dark:border-indigo-500/20 shadow-sm shadow-indigo-500/10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('PARTNER_KEY') }}</label>
|
||||
<input type="text" name="settings[tappay][partner_key]" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('APP_ID') }}</label>
|
||||
<input type="text" name="settings[tappay][app_id]" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('APP_KEY') }}</label>
|
||||
<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>
|
||||
<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>
|
||||
<input type="text" name="settings[tappay][line_merchant_id]" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('JKO_MERCHANT_ID') }}</label>
|
||||
<input type="text" name="settings[tappay][jko_merchant_id]" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('PI_MERCHANT_ID') }}</label>
|
||||
<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>
|
||||
<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>
|
||||
<input type="text" name="settings[tappay][easy_merchant_id]" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,191 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-10 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">
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.index') }}" class="p-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 text-slate-500 hover:text-cyan-500 transition-all shadow-sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Edit Payment Config') }}</h1>
|
||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ $paymentConfig->name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="submit" form="edit-form" class="btn-luxury-primary px-8 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="edit-form" action="{{ route('admin.basic-settings.payment-configs.update', $paymentConfig) }}" method="POST" class="space-y-8">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<!-- Left Column: Primary Info -->
|
||||
<div class="lg:col-span-12 space-y-8">
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Configuration Name') }} <span class="text-rose-500">*</span></label>
|
||||
<input type="text" name="name" value="{{ $paymentConfig->name }}" required class="luxury-input w-full" placeholder="{{ __('e.g., Company Standard Pay') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Belongs To Company') }} <span class="text-rose-500">*</span></label>
|
||||
<select name="company_id" required class="luxury-select w-full">
|
||||
<option value="">{{ __('Select Company') }}</option>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}" {{ $paymentConfig->company_id == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- provider groups -->
|
||||
@php
|
||||
$settings = $paymentConfig->settings ?? [];
|
||||
@endphp
|
||||
<div class="lg:col-span-6 space-y-8">
|
||||
<!-- ECPay -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 50ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-emerald-50 dark:bg-emerald-500/10 flex items-center justify-center text-emerald-500 border border-emerald-100 dark:border-emerald-500/20 shadow-sm shadow-emerald-500/10">
|
||||
<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>
|
||||
</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">
|
||||
</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">
|
||||
</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">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E.SUN -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 100ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-500 border border-indigo-100 dark:border-indigo-500/20 shadow-sm shadow-indigo-500/10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('StoreID') }}</label>
|
||||
<input type="text" name="settings[esun_scan][store_id]" value="{{ $settings['esun_scan']['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">{{ __('TermID') }}</label>
|
||||
<input type="text" name="settings[esun_scan][term_id]" value="{{ $settings['esun_scan']['term_id'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Key') }}</label>
|
||||
<input type="text" name="settings[esun_scan][key]" value="{{ $settings['esun_scan']['key'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LINE Pay Direct -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 150ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-emerald-50 dark:bg-emerald-500/10 flex items-center justify-center text-emerald-500 border border-emerald-100 dark:border-emerald-500/20 shadow-sm shadow-emerald-500/10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('ChannelId') }}</label>
|
||||
<input type="text" name="settings[line_pay][channel_id]" value="{{ $settings['line_pay']['channel_id'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('ChannelSecret') }}</label>
|
||||
<input type="text" name="settings[line_pay][channel_secret]" value="{{ $settings['line_pay']['channel_secret'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-6 space-y-8">
|
||||
<!-- Tappay -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-500 border border-indigo-100 dark:border-indigo-500/20 shadow-sm shadow-indigo-500/10">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('PARTNER_KEY') }}</label>
|
||||
<input type="text" name="settings[tappay][partner_key]" value="{{ $settings['tappay']['partner_key'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('APP_ID') }}</label>
|
||||
<input type="text" name="settings[tappay][app_id]" value="{{ $settings['tappay']['app_id'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('APP_KEY') }}</label>
|
||||
<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>
|
||||
<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>
|
||||
<input type="text" name="settings[tappay][line_merchant_id]" value="{{ $settings['tappay']['line_merchant_id'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('JKO_MERCHANT_ID') }}</label>
|
||||
<input type="text" name="settings[tappay][jko_merchant_id]" value="{{ $settings['tappay']['jko_merchant_id'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">{{ __('PI_MERCHANT_ID') }}</label>
|
||||
<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>
|
||||
<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>
|
||||
<input type="text" name="settings[tappay][easy_merchant_id]" value="{{ $settings['tappay']['easy_merchant_id'] ?? '' }}" class="luxury-input w-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,82 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-10 pb-20">
|
||||
<!-- Header -->
|
||||
<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">{{ __('Payment Configuration') }}</h1>
|
||||
<p class="text-xs font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Merchant payment gateway settings management') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.create') }}" class="btn-luxury-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
<span>{{ __('Create Config') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<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">{{ __('Config 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">{{ __('Belongs To') }}</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">{{ __('Last Updated') }}</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">
|
||||
@foreach($paymentConfigs as $config)
|
||||
<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">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
||||
<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="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div 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 whitespace-nowrap">{{ $config->name }}</div>
|
||||
<div class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest mt-0.5">ID: {{ str_pad($config->id, 5, '0', STR_PAD_LEFT) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 border-b border-transparent">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold border border-sky-100 dark:border-sky-900/30 bg-sky-50 dark:bg-sky-900/20 text-sky-600 dark:text-sky-400 tracking-widest">
|
||||
{{ $config->company->name ?? __('None') }}
|
||||
</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>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right space-x-2">
|
||||
<a href="{{ route('admin.basic-settings.payment-configs.edit', $config) }}"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 border border-transparent hover:border-cyan-500/20 transition-all inline-flex" title="{{ __('Edit') }}">
|
||||
<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>
|
||||
</a>
|
||||
<form action="{{ route('admin.basic-settings.payment-configs.destroy', $config) }}" method="POST" class="inline-block" onsubmit="return confirm('{{ __('Are you sure you want to delete this configuration?') }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 border border-transparent hover:border-rose-500/20 transition-all"
|
||||
title="{{ __('Delete') }}">
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
|
||||
{{ $paymentConfigs->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -62,30 +62,20 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 300ms">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
|
||||
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none"
|
||||
class="py-2.5 pl-12 pr-6 block w-64 luxury-input"
|
||||
placeholder="{{ __('Search customers...') }}">
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</form>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="flex items-center gap-2">
|
||||
@if(request('search'))<input type="hidden" name="search" value="{{ request('search') }}">@endif
|
||||
@if(request('status'))<input type="hidden" name="status" value="{{ request('status') }}">@endif
|
||||
<select name="limit" onchange="this.form.submit()" class="luxury-select text-[11px] py-2 px-3">
|
||||
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
|
||||
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
|
||||
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
|
||||
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50">
|
||||
@@ -103,7 +93,6 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
@@ -134,7 +123,7 @@
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all">
|
||||
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2">
|
||||
stroke-width="2.5">
|
||||
<path
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
@@ -144,7 +133,7 @@
|
||||
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">{{
|
||||
$company->name }}</span>
|
||||
<span
|
||||
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-[0.15em]">{{
|
||||
class="text-xs font-mono font-bold text-slate-500 dark:text-slate-400 mt-0.5 tracking-widest uppercase">{{
|
||||
$company->code }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,12 +141,12 @@
|
||||
<td class="px-6 py-6 text-center">
|
||||
@if($company->status)
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
|
||||
{{ __('Active') }}
|
||||
</span>
|
||||
@else
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-wider uppercase">
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
|
||||
{{ __('Disabled') }}
|
||||
</span>
|
||||
@endif
|
||||
@@ -265,12 +254,10 @@
|
||||
</div>
|
||||
|
||||
<form
|
||||
:action="editing ? '{{ route('admin.permission.companies.index') }}/' + currentCompany.id : '{{ route('admin.permission.companies.store') }}'"
|
||||
:action="editing ? '{{ url('admin/permission/companies') }}/' + currentCompany.id : '{{ route('admin.permission.companies.store') }}'"
|
||||
method="POST" class="space-y-6">
|
||||
@csrf
|
||||
<template x-if="editing">
|
||||
@method('PUT')
|
||||
</template>
|
||||
<input type="hidden" name="_method" :value="editing ? 'PUT' : 'POST'">
|
||||
|
||||
<!-- Profile Section -->
|
||||
<div class="space-y-6">
|
||||
|
||||
@@ -129,16 +129,6 @@
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.dashboard') }}" method="GET" class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<span class="text-[11px] font-black text-slate-400 uppercase tracking-widest hidden sm:inline">{{ __('Show') }}:</span>
|
||||
<select name="limit" onchange="this.form.submit()" class="luxury-select py-3 min-w-[100px]">
|
||||
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
|
||||
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
|
||||
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
|
||||
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -147,6 +137,7 @@
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" placeholder="{{ __('Quick search...') }}">
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@php
|
||||
$routeName = request()->route()->getName();
|
||||
$baseRoute = str_contains($routeName, 'sub-accounts') ? 'admin.data-config.sub-accounts' : 'admin.permission.accounts';
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6" x-data="{
|
||||
showModal: false,
|
||||
@@ -21,34 +26,43 @@
|
||||
},
|
||||
openEditModal(user) {
|
||||
this.editing = true;
|
||||
this.currentUser = { ...user };
|
||||
this.currentUser = {
|
||||
...user,
|
||||
role: user.roles && user.roles.length > 0 ? user.roles[0].name : 'user'
|
||||
};
|
||||
this.showModal = true;
|
||||
}
|
||||
}">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Account Management') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage administrative and tenant accounts') }}</p>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ $title }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
||||
{{ __('Manage administrative and tenant accounts') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button @click="openCreateModal()" class="btn-luxury-primary">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
<span>{{ __('Add Account') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button @click="openCreateModal()" class="btn-luxury-primary">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
||||
<span>{{ __('Add Account') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<!-- Accounts Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form action="{{ route('admin.permission.accounts') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center gap-4">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Filters & Search -->
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="mb-10">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center gap-4 w-full md:w-auto">
|
||||
<div class="relative group w-full md:w-80">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-3 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search users...') }}">
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search users...') }}">
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
@@ -59,97 +73,109 @@
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
|
||||
<select name="limit" onchange="this.form.submit()" class="luxury-select w-full md:w-auto text-[11px]">
|
||||
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
|
||||
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
|
||||
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
|
||||
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 移除了冗餘的 Filter 按鈕,下拉選單具備自動提交功能 -->
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto mt-8">
|
||||
<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/30">
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('User Info') }}</th>
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Belongs To') }}</th>
|
||||
@endif
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Role') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
<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>
|
||||
@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
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Role') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($users as $user)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 overflow-hidden group-hover:bg-cyan-500/10 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-all duration-500">
|
||||
@if($user->avatar)
|
||||
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<span class="text-sm font-black">{{ substr($user->name, 0, 1) }}</span>
|
||||
<span class="text-xs font-black">{{ substr($user->name, 0, 1) }}</span>
|
||||
@endif
|
||||
</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-[11px] font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-[0.15em]">{{ $user->username }} @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> @if($user->email) • {{ $user->email }} @endif</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<td class="px-6 py-6">
|
||||
@if($user->company)
|
||||
<span class="text-xs font-bold text-slate-600 dark:text-slate-300 tracking-tight">{{ $user->company->name }}</span>
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{ $user->company->name }}</span>
|
||||
@else
|
||||
<span class="text-xs font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{ __('SYSTEM') }}</span>
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{ __('SYSTEM') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
<td class="px-6 py-6 text-center">
|
||||
@foreach($user->roles as $role)
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-widest">
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 uppercase tracking-widest">
|
||||
{{ $role->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
@if($user->status)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-widest uppercase">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-2 animate-pulse"></span>
|
||||
{{ __('Active') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-wider uppercase">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
|
||||
{{ __('Disabled') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<button @click="openEditModal({{ json_encode($user) }})" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
||||
@if(!$user->hasRole('super-admin'))
|
||||
<button @click="openEditModal(@js($user))"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
||||
title="{{ __('Edit') }}">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||
</svg>
|
||||
</button>
|
||||
<form action="{{ route($baseRoute . '.destroy', $user->id) }}"
|
||||
method="POST"
|
||||
onsubmit="return confirm('{{ __('Are you sure you want to delete this account?') }}')"
|
||||
class="inline-block">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20"
|
||||
title="{{ __('Delete') }}">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
<form action="{{ route('admin.permission.accounts.destroy', $user->id) }}" method="POST" onsubmit="return confirm('{{ addslashes(__('Are you sure you want to delete this account?')) }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-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>
|
||||
</button>
|
||||
</form>
|
||||
</form>
|
||||
@else
|
||||
<span class="text-[10px] font-black text-slate-300 dark:text-slate-600 uppercase tracking-[0.15em] px-2">{{ __('Protected') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ auth()->user()->isSystemAdmin() ? 5 : 4 }}" class="px-6 py-24 text-center">
|
||||
<p class="text-slate-400 font-bold">{{ __('No users found') }}</p>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@@ -179,7 +205,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form :action="editing ? '{{ url('admin/permission/accounts') }}/' + currentUser.id : '{{ route('admin.permission.accounts.store') }}'" method="POST" class="space-y-6">
|
||||
<form :action="editing ? '{{ route($baseRoute) }}/' + currentUser.id : '{{ route($baseRoute . '.store') }}'" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
<template x-if="editing">
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
@@ -211,9 +237,9 @@
|
||||
<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">
|
||||
<option value="user">{{ __('User') }}</option>
|
||||
<option value="admin">{{ __('Admin') }}</option>
|
||||
<option value="super-admin">{{ __('Super Admin') }}</option>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}">{{ __($role->name) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
@if(request('status'))
|
||||
<input type="hidden" name="status" value="{{ request('status') }}">
|
||||
@endif
|
||||
<select name="limit" onchange="this.form.submit()" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
|
||||
@foreach([10, 25, 50, 100] as $size)
|
||||
<option value="{{ $size }}" {{ request('limit') == $size ? 'selected' : '' }}>{{ $size }} {{ __('Items') }}</option>
|
||||
@endforeach
|
||||
<select name="per_page" onchange="this.form.submit()" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
|
||||
<option value="20" {{ request('per_page') == 20 ? 'selected' : '' }}>20 {{ __('Items') }}</option>
|
||||
<option value="50" {{ request('per_page') == 50 ? 'selected' : '' }}>50 {{ __('Items') }}</option>
|
||||
<option value="100" {{ request('per_page') == 100 ? 'selected' : '' }}>100 {{ __('Items') }}</option>
|
||||
</select>
|
||||
</form>
|
||||
<div class="flex space-x-2">
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('header')
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('所有機台日誌') }}
|
||||
</h2>
|
||||
</div>
|
||||
@endsection
|
||||
@section('title', __('Machine Logs'))
|
||||
|
||||
@section('content')
|
||||
<div class="py-12">
|
||||
<div class="sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
<!-- 篩選器 -->
|
||||
<div class="luxury-card rounded-2xl p-6 animate-luxury-in">
|
||||
<div class="flex items-center gap-x-2 mb-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
條件篩選
|
||||
</p>
|
||||
</div>
|
||||
<form method="GET" action="{{ route('admin.machines.logs') }}" class="flex flex-wrap gap-4 items-end">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">機台</label>
|
||||
<select name="machine_id" class="block w-48 rounded-md border-slate-300 shadow-sm focus:border-cyan-500 focus:ring focus:ring-cyan-500/20 text-sm dark:bg-slate-800 dark:border-slate-700 dark:text-white dark:focus:border-cyan-500">
|
||||
<option value="">全部機台</option>
|
||||
<div class="space-y-10 pb-20">
|
||||
<!-- Header -->
|
||||
<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">{{ __('Machine Logs') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Monitor events and system activity across your vending fleet.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Machine Logs Content (Integrated Card - Same as Roles) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar (Integrated Filters) -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<form method="GET" action="{{ route('admin.machines.logs') }}" class="flex flex-wrap items-center gap-4 group">
|
||||
<div class="space-y-1">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{{ __('Machine') }}</label>
|
||||
<select name="machine_id" class="luxury-select text-xs h-9 py-0" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Machines') }}</option>
|
||||
@foreach($machines as $machine)
|
||||
<option value="{{ $machine->id }}" {{ request('machine_id') == $machine->id ? 'selected' : '' }}>
|
||||
{{ $machine->name }}
|
||||
@@ -31,97 +28,102 @@
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">層級</label>
|
||||
<select name="level" class="block w-32 rounded-md border-slate-300 shadow-sm focus:border-cyan-500 focus:ring focus:ring-cyan-500/20 text-sm dark:bg-slate-800 dark:border-slate-700 dark:text-white dark:focus:border-cyan-500">
|
||||
<option value="">全部層級</option>
|
||||
<option value="info" {{ request('level') == 'info' ? 'selected' : '' }}>Info</option>
|
||||
<option value="warning" {{ request('level') == 'warning' ? 'selected' : '' }}>Warning</option>
|
||||
<option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>Error</option>
|
||||
|
||||
<div class="space-y-1">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1">{{ __('Level') }}</label>
|
||||
<select name="level" class="luxury-select text-xs h-9 py-0" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Levels') }}</option>
|
||||
<option value="info" {{ request('level') == 'info' ? 'selected' : '' }}>{{ __('Info') }}</option>
|
||||
<option value="warning" {{ request('level') == 'warning' ? 'selected' : '' }}>{{ __('Warning') }}</option>
|
||||
<option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>{{ __('Error') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">筆數</label>
|
||||
<select name="limit" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
|
||||
@foreach([10, 20, 50, 100] as $size)
|
||||
<option value="{{ $size }}" {{ request('limit', 20) == $size ? 'selected' : '' }}>{{ $size }} 筆</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit" class="btn-luxury-primary">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<span>篩選</span>
|
||||
|
||||
<div class="flex items-end gap-2 mt-5">
|
||||
<button type="submit" class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 transition-colors border border-slate-200 dark:border-slate-700">
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</button>
|
||||
<a href="{{ route('admin.machines.logs') }}" class="btn-luxury-ghost">
|
||||
重設
|
||||
<a href="{{ route('admin.machines.logs') }}" class="p-2 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 transition-colors border border-slate-200 dark:border-slate-700">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 日誌清單 -->
|
||||
<div class="luxury-card rounded-2xl p-6 animate-luxury-in overflow-hidden" style="animation-delay: 100ms">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-white">系統日誌清單</h2>
|
||||
<span class="text-xs text-slate-400">所有時間為系統時區</span>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-[#0f172a]">
|
||||
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700/50 font-mono text-xs">
|
||||
<thead class="bg-slate-50 dark:bg-slate-800/80">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">時間</th>
|
||||
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">機台</th>
|
||||
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">層級</th>
|
||||
<th class="px-4 py-3 text-left font-semibold text-slate-600 dark:text-slate-300">訊息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700/50 bg-white dark:bg-transparent">
|
||||
@forelse ($logs as $log)
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
<td class="px-4 py-3 text-slate-500 dark:text-slate-400 whitespace-nowrap">{{ $log->created_at->format('Y-m-d H:i:s') }}</td>
|
||||
<td class="px-4 py-3 text-slate-700 dark:text-slate-300 whitespace-nowrap">
|
||||
<a href="{{ route('admin.machines.show', $log->machine_id) }}" class="hover:text-cyan-600 dark:hover:text-cyan-400 underline decoration-slate-300 dark:decoration-slate-600 underline-offset-2 transition-colors">
|
||||
{{ $log->machine->name ?? '未知機台' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
@php
|
||||
$levelClasses = [
|
||||
'info' => 'text-cyan-600 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-500/20 border-cyan-200 dark:border-cyan-500/30',
|
||||
'warning' => 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/20 border-amber-200 dark:border-amber-500/30 font-semibold',
|
||||
'error' => 'text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/20 border-rose-200 dark:border-rose-500/30 font-bold',
|
||||
];
|
||||
@endphp
|
||||
<span class="px-2 py-0.5 rounded border {{ $levelClasses[$log->level] ?? 'text-slate-500 bg-slate-100 border-slate-200 dark:text-slate-300 dark:bg-slate-800 dark:border-slate-700' }}">
|
||||
{{ strtoupper($log->level) }}
|
||||
<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 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Timestamp') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Level') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em] border-b border-slate-100 dark:border-slate-800">{{ __('Message Content') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse ($logs as $log)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6 transition-colors">
|
||||
<div class="text-[13px] font-bold font-display tracking-widest text-slate-600 dark:text-slate-300">
|
||||
{{ $log->created_at->format('Y-m-d') }}
|
||||
</div>
|
||||
<div class="text-[11px] font-bold text-slate-400 dark:text-slate-500 tracking-wider mt-0.5 uppercase">
|
||||
{{ $log->created_at->format('H:i:s') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 transition-colors">
|
||||
<a href="{{ route('admin.machines.show', $log->machine_id) }}" class="inline-flex items-center gap-2 group/link">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover/link:text-cyan-500 transition-colors">
|
||||
{{ $log->machine->name ?? __('Unknown') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-700 dark:text-slate-200 max-w-xl break-words">
|
||||
<svg class="w-3.5 h-3.5 text-slate-300 dark:text-slate-600 opacity-0 group-hover/link:opacity-100 transition-all -translate-x-2 group-hover/link:translate-x-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"/></svg>
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-6 transition-colors">
|
||||
@php
|
||||
$badgeStyles = [
|
||||
'info' => 'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/20',
|
||||
'warning' => 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20',
|
||||
'error' => 'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20',
|
||||
];
|
||||
$currentStyle = $badgeStyles[$log->level] ?? 'bg-slate-500/10 text-slate-600 border-slate-500/20';
|
||||
@endphp
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-lg border text-[11px] font-black uppercase tracking-wider {{ $currentStyle }}">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-current mr-2 animate-pulse"></span>
|
||||
{{ __(ucfirst($log->level)) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 transition-colors">
|
||||
<p class="text-[14px] font-medium text-slate-700 dark:text-slate-200 leading-relaxed max-w-xl">
|
||||
{{ $log->message }}
|
||||
@if($log->context)
|
||||
<div class="text-[10px] text-slate-500 dark:text-slate-400 mt-2 max-h-24 overflow-y-auto bg-slate-100 dark:bg-[#0f172a] p-2 rounded-lg border border-slate-200 dark:border-slate-800/50 shadow-inner">
|
||||
{{ json_encode($log->context, JSON_UNESCAPED_UNICODE) }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-12 text-center text-slate-500 dark:text-slate-400 italic">暫無相關日誌</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($logs->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $logs->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
@endif
|
||||
</p>
|
||||
@if($log->context)
|
||||
<div class="mt-3 p-4 rounded-xl bg-slate-50/50 dark:bg-[#0f172a]/50 border border-slate-100 dark:border-slate-800/50 group-hover:bg-white dark:group-hover:bg-[#0f172a] transition-colors">
|
||||
<pre class="text-[10px] font-bold text-slate-400 dark:text-slate-500 whitespace-pre-wrap break-all">{{ json_encode($log->context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-24 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="p-4 rounded-full bg-slate-50 dark:bg-slate-800/50 mb-4 border border-slate-100 dark:border-slate-800/50">
|
||||
<svg class="w-8 h-8 text-slate-300 dark:text-slate-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 7v10c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V7c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2z"/><path d="M12 11l4-4"/><path d="M8 15l4-4"/></svg>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('No matching logs found') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
|
||||
{{ $logs->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@endsection
|
||||
@@ -1,16 +1,25 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@php
|
||||
$routeName = request()->route()->getName();
|
||||
$baseRoute = str_contains($routeName, 'sub-account-roles') ? 'admin.data-config.sub-account-roles' : 'admin.permission.roles';
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-6" x-data="{
|
||||
showModal: false,
|
||||
isEdit: false,
|
||||
roleId: '',
|
||||
roleName: '',
|
||||
rolePermissions: [],
|
||||
isSystem: false,
|
||||
modalTitle: '{{ __('Create Role') }}',
|
||||
openModal(edit = false, id = '', name = '') {
|
||||
openModal(edit = false, id = '', name = '', permissions = [], isSys = false) {
|
||||
this.isEdit = edit;
|
||||
this.roleId = id;
|
||||
this.roleName = name;
|
||||
this.rolePermissions = Array.isArray(permissions) ? permissions : (typeof permissions === 'string' ? JSON.parse(permissions) : []);
|
||||
this.isSystem = isSys;
|
||||
this.modalTitle = edit ? '{{ __('Edit Role') }}' : '{{ __('Create Role') }}';
|
||||
this.showModal = true;
|
||||
}
|
||||
@@ -18,101 +27,104 @@
|
||||
<!-- Header -->
|
||||
<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">{{ __('Roles') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define and manage security roles for the system.') }}</p>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ $title }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define and manage security roles and permissions.') }}</p>
|
||||
</div>
|
||||
<button @click="openModal()" class="btn-luxury-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
<button @click="openModal()" class="btn-luxury-primary text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
<span>{{ __('Add Role') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="luxury-card rounded-3xl p-6 mb-6 animate-luxury-in" style="animation-delay: 100ms">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<form action="{{ route('admin.permission.roles') }}" method="GET" class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Roles Content (Integrated Card) -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<form action="{{ route($baseRoute) }}" method="GET" class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
|
||||
</form>
|
||||
|
||||
<form action="{{ route('admin.permission.roles') }}" method="GET" class="flex items-center gap-3">
|
||||
@if(request('search'))<input type="hidden" name="search" value="{{ request('search') }}">@endif
|
||||
<label class="text-[11px] font-black text-slate-400 uppercase tracking-widest">{{ __('Show') }}</label>
|
||||
<select name="limit" onchange="this.form.submit()" class="luxury-select py-1.5 px-3 text-[11px] min-w-[70px]">
|
||||
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
|
||||
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
|
||||
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
|
||||
</select>
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 10) }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles List -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-100 dark:border-slate-700">
|
||||
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Role Name') }}</th>
|
||||
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Type') }}</th>
|
||||
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest text-center">{{ __('Users') }}</th>
|
||||
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest text-right">{{ __('Actions') }}</th>
|
||||
<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">{{ __('Role Name') }}</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">{{ __('Type') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Permissions') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Users') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800">
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
||||
@forelse($roles as $role)
|
||||
<tr class="hover:bg-slate-50/80 dark:hover:bg-slate-800/50 transition-colors group">
|
||||
<td class="px-6 py-5">
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $role->name }}</span>
|
||||
<div class="w-10 h-10 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
||||
</div>
|
||||
<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">{{ $role->name }}</span>
|
||||
@if($role->is_system)
|
||||
<span class="p-1.5 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-600 dark:text-cyan-400 rounded-lg tooltip" title="{{ __('System Role') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<td class="px-6 py-6">
|
||||
@if($role->is_system)
|
||||
<span class="px-2.5 py-1 text-[11px] font-black uppercase tracking-tight bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 rounded-full">
|
||||
{{ __('System') }}
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-800 uppercase tracking-wider">
|
||||
{{ __('System Level') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2.5 py-1 text-[11px] font-black uppercase tracking-tight bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 rounded-full">
|
||||
{{ __('Custom') }}
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-bold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20 tracking-wider uppercase">
|
||||
{{ __('Company Level') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
<span class="text-sm font-black text-slate-600 dark:text-slate-400">{{ $role->users_count }}</span>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex flex-wrap gap-1 max-w-xs">
|
||||
@forelse($role->permissions->take(6) as $permission)
|
||||
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">{{ __(str_replace('menu.', '', $permission->name)) }}</span>
|
||||
@empty
|
||||
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 italic tracking-widest">{{ __('No permissions') }}</span>
|
||||
@endforelse
|
||||
@if($role->permissions->count() > 6)
|
||||
<span class="px-2 py-0.5 text-xs bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 rounded border border-slate-200 dark:border-slate-700 uppercase font-bold tracking-widest">+{{ $role->permissions->count() - 6 }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="text-sm font-extrabold text-slate-700 dark:text-slate-300">{{ $role->users()->count() }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if(!$role->is_system)
|
||||
<button @click="openModal(true, '{{ $role->id }}', '{{ $role->name }}')" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
||||
<button @click="openModal(true, @js($role->id), @js($role->name), @js($role->permissions->pluck('name')), {{ $role->is_system ? 'true' : 'false' }})" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
|
||||
</button>
|
||||
<form action="{{ route('admin.permission.roles.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline">
|
||||
@if($role->name !== 'super-admin' && (auth()->user()->isSystemAdmin() || !$role->is_system))
|
||||
<form action="{{ route($baseRoute . '.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline text-slate-400">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20 tooltip" title="{{ __('Delete') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
|
||||
<svg xmlns="http://www.w3.org/2000/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>
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="px-3 py-1 text-[11px] font-black uppercase tracking-widest text-slate-400 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200/50 dark:border-slate-700/50 italic">{{ __('Protected') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-20 text-center">
|
||||
<td colspan="5" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center justify-center gap-4 text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 opacity-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>
|
||||
<p class="text-lg font-bold">{{ __('No roles found.') }}</p>
|
||||
@@ -134,26 +146,104 @@
|
||||
<div class="fixed inset-0 z-[60] flex items-center justify-center p-4">
|
||||
<div @click="showModal = false" class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"></div>
|
||||
|
||||
<div class="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-2xl shadow-2xl overflow-hidden animate-luxury-in">
|
||||
<div class="flex items-center justify-between p-6 border-b border-slate-100 dark:border-slate-800">
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white" x-text="modalTitle"></h3>
|
||||
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<div class="relative w-full max-w-3xl bg-white dark:bg-slate-900 rounded-3xl shadow-2xl overflow-hidden animate-luxury-in">
|
||||
<div class="flex items-center justify-between p-8 border-b border-slate-100 dark:border-slate-800">
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white" x-text="modalTitle"></h3>
|
||||
<p class="text-sm font-bold text-slate-400 mt-1" x-text="isEdit ? '{{ __('Update existing role and permissions.') }}' : '{{ __('Create a new role and assign permissions.') }}'"></p>
|
||||
</div>
|
||||
<button @click="showModal = false" class="p-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form :action="isEdit ? '{{ route('admin.permission.roles') }}/' + roleId : '{{ route('admin.permission.roles.store') }}'" method="POST" class="p-6 space-y-6">
|
||||
<form :action="isEdit ? '{{ route($baseRoute) }}/' + roleId : '{{ route($baseRoute . '.store') }}'" method="POST">
|
||||
@csrf
|
||||
<template x-if="isEdit"><input type="hidden" name="_method" value="PUT"></template>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-black text-slate-700 dark:text-slate-300">{{ __('Role Name') }}</label>
|
||||
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full" :placeholder="'{{ __('Enter role name') }}'">
|
||||
<div class="p-8 max-h-[65vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- Left: Basic Info -->
|
||||
<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'">
|
||||
<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>
|
||||
{{ __('The Super Admin role name cannot be modified.') }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
<div class="space-y-4">
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Role Type') }}</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-slate-200 dark:border-slate-800 group has-[:checked]:border-cyan-500/50 has-[:checked]:bg-cyan-500/5">
|
||||
<input type="radio" name="is_system" value="1" x-model="isSystem" class="w-4 h-4 text-cyan-500 bg-transparent border-slate-300 focus:ring-cyan-500" :disabled="isEdit && roleName === 'super-admin'">
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ __('System Level') }}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-slate-200 dark:border-slate-800 group has-[:checked]:border-cyan-500/50 has-[:checked]:bg-cyan-500/5">
|
||||
<input type="radio" name="is_system" value="0" x-model="isSystem" class="w-4 h-4 text-cyan-500 bg-transparent border-slate-300 focus:ring-cyan-500" :disabled="isEdit && roleName === 'super-admin'">
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-200 group-hover:text-cyan-600 dark:group-hover:text-cyan-400">{{ __('Company Level') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Right: Permissions -->
|
||||
<div class="space-y-6">
|
||||
<label class="text-xs font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Menu Permissions') }}</label>
|
||||
<div class="luxury-card border-slate-100 dark:border-slate-800 p-4 rounded-2xl grid grid-cols-1 gap-3">
|
||||
@php
|
||||
$icon_map = [
|
||||
'members' => 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z',
|
||||
'machines' => 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01',
|
||||
'app' => 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z',
|
||||
'warehouses' => 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
|
||||
'sales' => 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'analysis' => 'M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z',
|
||||
'audit' => 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
'data-config' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
||||
'remote' => 'M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
'line' => 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
|
||||
'reservation' => 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
'special-permission' => 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
|
||||
'basic-settings' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
||||
'permissions' => 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
];
|
||||
@endphp
|
||||
@foreach($all_permissions->get('menu', []) as $permission)
|
||||
@php
|
||||
$pure_name = str_replace('menu.', '', $permission->name);
|
||||
$is_restricted = in_array($permission->name, ['menu.basic-settings', 'menu.permissions']);
|
||||
@endphp
|
||||
<label class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer transition-all border border-transparent hover:border-slate-200 dark:hover:border-slate-700 group"
|
||||
@if($is_restricted) x-show="isSystem == '1'" x-transition @endif>
|
||||
<div class="relative flex items-center">
|
||||
<input type="checkbox" name="permissions[]" value="{{ $permission->name }}"
|
||||
x-model="rolePermissions"
|
||||
class="peer w-5 h-5 rounded-lg border-2 border-slate-200 dark:border-slate-700 text-cyan-500 focus:ring-cyan-500 focus:ring-offset-0 bg-transparent transition-all checked:border-cyan-500">
|
||||
<svg class="absolute w-3.5 h-3.5 text-white opacity-0 peer-checked:opacity-100 left-0.5 pointer-events-none transition-opacity" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
</div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-7 h-7 rounded-lg flex items-center justify-center bg-slate-50 dark:bg-slate-800 text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="{{ $icon_map[$pure_name] ?? 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' }}" /></svg>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-600 dark:text-slate-300 group-hover:text-slate-900 dark:group-hover:text-white tracking-wide transition-colors">{{ __($pure_name) }}</span>
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 mt-8">
|
||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-6 py-2.5 rounded-xl font-bold">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-8 py-2.5 rounded-xl font-bold">{{ __('Save Changes') }}</button>
|
||||
<div class="p-8 bg-slate-50 dark:bg-slate-800/50 border-t border-slate-100 dark:border-slate-800 flex items-center justify-end gap-4">
|
||||
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8 py-3 rounded-2xl font-black text-xs uppercase tracking-widest">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-10 py-3 rounded-2xl font-black text-xs uppercase tracking-widest shadow-xl shadow-cyan-500/20">{{ __('Save Changes') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
'admin.reservation' => __('Reservation System'),
|
||||
'admin.special-permission' => __('Special Permission'),
|
||||
'admin.permission' => __('Permission Settings'),
|
||||
'admin.basic-settings' => __('Basic Settings'),
|
||||
];
|
||||
|
||||
// 1. 找出所屬大模組
|
||||
@@ -62,7 +63,8 @@
|
||||
$midLabel = match($midSegment) {
|
||||
'companies' => __('Customer Management'),
|
||||
'members' => __('Member List'),
|
||||
'machines' => __('Machine List'),
|
||||
'machines' => __('Machine Settings'),
|
||||
'payment-configs' => __('Customer Payment Config'),
|
||||
'warehouses' => __('Warehouse List'),
|
||||
'sales' => __('Sales Records'),
|
||||
default => null,
|
||||
@@ -80,7 +82,7 @@
|
||||
// 3. 處理最後一個動作/頁面
|
||||
if ($lastSegment !== 'index') {
|
||||
$pageLabel = match($lastSegment) {
|
||||
'edit' => __('Edit'),
|
||||
'edit' => str_starts_with($routeName, 'profile') ? null : __('Edit'),
|
||||
'create' => __('Create'),
|
||||
'show' => __('Detail'),
|
||||
'logs' => __('Machine Logs'),
|
||||
@@ -152,7 +154,6 @@
|
||||
if ($pageLabel) {
|
||||
$links[] = [
|
||||
'label' => $pageLabel,
|
||||
'url' => route($routeName),
|
||||
'active' => true
|
||||
];
|
||||
}
|
||||
@@ -189,7 +190,7 @@
|
||||
@endif
|
||||
|
||||
@if(!$loop->last)
|
||||
<svg class="flex-shrink-0 mx-3 overflow-visible h-2.5 w-2.5 text-slate-400 dark:text-slate-600" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="flex-shrink-0 mx-3 overflow-visible h-2.5 w-2.5 text-slate-500 dark:text-slate-400" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 1L10.6869 7.16086C10.8637 7.35239 10.8637 7.64761 10.6869 7.83914L5 14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@endif
|
||||
|
||||
49
resources/views/components/luxury-time-input.blade.php
Normal file
49
resources/views/components/luxury-time-input.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@props(['name', 'value' => '', 'label' => ''])
|
||||
|
||||
<div x-data="{
|
||||
time: '{{ $value ? \Carbon\Carbon::parse($value)->format('H:i:s') : '' }}',
|
||||
formatInput(e) {
|
||||
let cursor = e.target.selectionStart;
|
||||
let originalLen = e.target.value.length;
|
||||
let val = e.target.value.replace(/\D/g, '');
|
||||
|
||||
if (val.length > 6) val = val.slice(0, 6);
|
||||
|
||||
let formatted = '';
|
||||
if (val.length > 0) {
|
||||
formatted = val.slice(0, 2);
|
||||
if (val.length > 2) {
|
||||
formatted += ':' + val.slice(2, 4);
|
||||
if (val.length > 4) {
|
||||
formatted += ':' + val.slice(4, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.time = formatted;
|
||||
|
||||
// Minor delay to fix cursor position if needed,
|
||||
// though for time strings move-to-end is often acceptable.
|
||||
this.$nextTick(() => {
|
||||
let diff = formatted.length - originalLen;
|
||||
let newPos = cursor + diff;
|
||||
// e.target.setSelectionRange(newPos, newPos); // Optional cursor fix
|
||||
});
|
||||
}
|
||||
}" class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="{{ $name }}"
|
||||
:value="time"
|
||||
@input="formatInput"
|
||||
maxlength="8"
|
||||
placeholder="HH:mm:ss"
|
||||
class="luxury-input w-full pr-12 font-mono tracking-wider"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400 dark:text-slate-500">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
@@ -145,9 +145,9 @@
|
||||
class="absolute right-0 top-full mt-2 min-w-[15rem] bg-white shadow-xl rounded-2xl p-2 dark:bg-gray-800 dark:border dark:border-gray-700 z-50 border border-slate-100"
|
||||
x-cloak>
|
||||
<div class="py-3 px-5 -m-2 bg-slate-50 rounded-t-2xl dark:bg-slate-900/50 border-b border-slate-100 dark:border-slate-700">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">{{ __('Signed in as') }}</p>
|
||||
<p class="text-[11px] font-bold uppercase tracking-widest text-slate-500 dark:text-slate-400">{{ __('Signed in as') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate">{{ Auth::user()->name }}</p>
|
||||
<p class="text-[10px] font-medium text-slate-400 truncate">{{ Auth::user()->email }}</p>
|
||||
<p class="text-xs font-medium text-slate-500 truncate">{{ Auth::user()->email }}</p>
|
||||
</div>
|
||||
<div class="mt-2 py-2">
|
||||
<a class="flex items-center gap-x-3.5 py-2.5 px-3 rounded-xl text-sm font-bold text-slate-700 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-slate-300 dark:hover:bg-gray-700 dark:hover:text-white transition-colors" href="{{ route('profile.edit') }}">
|
||||
@@ -221,7 +221,7 @@
|
||||
<!-- End Sidebar -->
|
||||
|
||||
<!-- Content -->
|
||||
<div class="w-full pt-6 lg:pt-10 px-4 sm:px-6 md:px-8 lg:pl-72">
|
||||
<div class="w-full pt-6 lg:pt-10 pb-12 px-4 sm:px-6 md:px-8 lg:pl-72">
|
||||
<x-breadcrumbs class="mb-4 hidden lg:flex" />
|
||||
<main class="animate-fade-up">
|
||||
@yield('content')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{-- 1. 儀表板 --}}
|
||||
<li>
|
||||
<a class="luxury-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.dashboard') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.dashboard') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>
|
||||
{{ __('Dashboard') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -10,14 +10,14 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_profile') === 'true' || {{ request()->routeIs('profile.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_profile', open)"
|
||||
class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('profile.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('profile.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
{{ __('Profile Settings') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li>
|
||||
<a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('profile.edit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('profile.edit') }}">
|
||||
<a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('profile.edit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('profile.edit') }}">
|
||||
{{ __('Profile') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -25,259 +25,316 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@can('menu.members')
|
||||
{{-- 3. 會員管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_members') === 'true' || {{ request()->routeIs('admin.members.*') || request()->routeIs('admin.membership-tiers.*') || request()->routeIs('admin.deposit-bonus-rules.*') || request()->routeIs('admin.point-rules.*') || request()->routeIs('admin.gift-definitions.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_members', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.members.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.members.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
||||
{{ __('Member Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.members.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.members.index') }}">{{ __('Member List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.membership-tiers.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.membership-tiers.index') }}">{{ __('Membership Tiers') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.deposit-bonus-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.deposit-bonus-rules.index') }}">{{ __('Deposit Bonus') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.point-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.point-rules.index') }}">{{ __('Point Rules') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.gift-definitions.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.gift-definitions.index') }}">{{ __('Gift Definitions') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.members.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.members.index') }}">{{ __('Member List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.membership-tiers.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.membership-tiers.index') }}">{{ __('Membership Tiers') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.deposit-bonus-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.deposit-bonus-rules.index') }}">{{ __('Deposit Bonus') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.point-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.point-rules.index') }}">{{ __('Point Rules') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.gift-definitions.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.gift-definitions.index') }}">{{ __('Gift Definitions') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.machines')
|
||||
{{-- 4. 機台管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_machines') === 'true' || {{ request()->routeIs('admin.machines.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_machines', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.machines.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.machines.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01" /></svg>
|
||||
{{ __('Machine Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.logs') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.logs') }}">{{ __('Machine Logs') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.index') }}">{{ __('Machine List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.permissions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.permissions') }}">{{ __('Machine Permissions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.utilization') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.utilization') }}">{{ __('Utilization Rate') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.expiry') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.expiry') }}">{{ __('Expiry Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.maintenance') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.maintenance') }}">{{ __('Maintenance Records') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.logs') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.logs') }}">{{ __('Machine Logs') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.index') }}">{{ __('Machine List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.permissions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.permissions') }}">{{ __('Machine Permissions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.utilization') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.utilization') }}">{{ __('Utilization Rate') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.expiry') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.expiry') }}">{{ __('Expiry Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.maintenance') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.maintenance') }}">{{ __('Maintenance Records') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.app')
|
||||
{{-- 5. APP管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_app') === 'true' || {{ request()->routeIs('admin.app.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_app', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.app.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.app.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||
{{ __('APP Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.ui-elements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.ui-elements') }}">{{ __('UI Elements') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.helper') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.helper') }}">{{ __('Helper') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.questionnaire') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.questionnaire') }}">{{ __('Questionnaire') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.games') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.games') }}">{{ __('Games') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.timer') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.timer') }}">{{ __('Timer') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.ui-elements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.ui-elements') }}">{{ __('UI Elements') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.helper') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.helper') }}">{{ __('Helper') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.questionnaire') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.questionnaire') }}">{{ __('Questionnaire') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.games') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.games') }}">{{ __('Games') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.timer') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.timer') }}">{{ __('Timer') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.warehouses')
|
||||
{{-- 6. 倉庫管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_warehouses') === 'true' || {{ request()->routeIs('admin.warehouses.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_warehouses', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.warehouses.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.warehouses.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
|
||||
{{ __('Warehouse Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">{{ __('Warehouse List (All)') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">{{ __('Warehouse List (Individual)') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">{{ __('Stock Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">{{ __('Transfers') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">{{ __('Purchases') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">{{ __('Replenishments') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">{{ __('Replenishment Records') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">{{ __('Machine Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.staff-stock') }}">{{ __('Staff Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.returns') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.returns') }}">{{ __('Returns') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">{{ __('Warehouse List (All)') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">{{ __('Warehouse List (Individual)') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">{{ __('Stock Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">{{ __('Transfers') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">{{ __('Purchases') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">{{ __('Replenishments') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">{{ __('Replenishment Records') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">{{ __('Machine Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.staff-stock') }}">{{ __('Staff Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.returns') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.returns') }}">{{ __('Returns') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.sales')
|
||||
{{-- 7. 銷售管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_sales') === 'true' || {{ request()->routeIs('admin.sales.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_sales', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.sales.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.sales.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ __('Sales Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.index') }}">{{ __('Sales Records') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pickup-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pickup-codes') }}">{{ __('Pickup Codes') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.orders') }}">{{ __('Orders') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.promotions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.promotions') }}">{{ __('Promotions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pass-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pass-codes') }}">{{ __('Pass Codes') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.store-gifts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.store-gifts') }}">{{ __('Store Gifts') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.index') }}">{{ __('Sales Records') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pickup-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pickup-codes') }}">{{ __('Pickup Codes') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.orders') }}">{{ __('Orders') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.promotions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.promotions') }}">{{ __('Promotions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pass-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pass-codes') }}">{{ __('Pass Codes') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.store-gifts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.store-gifts') }}">{{ __('Store Gifts') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.analysis')
|
||||
{{-- 8. 分析管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_analysis') === 'true' || {{ request()->routeIs('admin.analysis.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_analysis', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.analysis.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.analysis.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" /></svg>
|
||||
{{ __('Analysis Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.change-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.change-stock') }}">{{ __('Change Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.machine-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.machine-reports') }}">{{ __('Machine Reports') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.product-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.product-reports') }}">{{ __('Product Reports') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.survey-analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.survey-analysis') }}">{{ __('Survey Analysis') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.change-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.change-stock') }}">{{ __('Change Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.machine-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.machine-reports') }}">{{ __('Machine Reports') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.product-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.product-reports') }}">{{ __('Product Reports') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.survey-analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.survey-analysis') }}">{{ __('Survey Analysis') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.audit')
|
||||
{{-- 9. 稽核管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_audit') === 'true' || {{ request()->routeIs('admin.audit.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_audit', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.audit.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.audit.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
{{ __('Audit Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.purchases') }}">{{ __('Purchase Audit') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.transfers') }}">{{ __('Transfer Audit') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.replenishments') }}">{{ __('Replenishment Audit') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.purchases') }}">{{ __('Purchase Audit') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.transfers') }}">{{ __('Transfer Audit') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.replenishments') }}">{{ __('Replenishment Audit') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.data-config')
|
||||
{{-- 10. 資料設定 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_data_config') === 'true' || {{ request()->routeIs('admin.data-config.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_data_config', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.data-config.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.data-config.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
{{ __('Data Configuration') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.products') }}">{{ __('Product Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.advertisements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.advertisements') }}">{{ __('Advertisement Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.admin-products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.admin-products') }}">{{ __('Admin Sellable Products') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-accounts') }}">{{ __('Sub Accounts') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">{{ __('Sub Account Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">{{ __('Point Settings') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.badges') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.badges') }}">{{ __('Badge Settings') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.products') }}">{{ __('Product Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.advertisements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.advertisements') }}">{{ __('Advertisement Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.admin-products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.admin-products') }}">{{ __('Product Status') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-accounts') }}">{{ __('Sub Accounts') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">{{ __('Sub Account Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">{{ __('Point Settings') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.badges') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.badges') }}">{{ __('Badge Settings') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
|
||||
@can('menu.remote')
|
||||
{{-- 11. 遠端管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_remote') === 'true' || {{ request()->routeIs('admin.remote.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_remote', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.remote.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.remote.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ __('Remote Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.stock') }}">{{ __('Machine Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart') }}">{{ __('Machine Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart-card-reader') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart-card-reader') }}">{{ __('Card Reader Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.checkout') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.checkout') }}">{{ __('Remote Checkout') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.lock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.lock') }}">{{ __('Remote Lock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.change') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.change') }}">{{ __('Remote Change') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.dispense') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.dispense') }}">{{ __('Remote Dispense') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.stock') }}">{{ __('Machine Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart') }}">{{ __('Machine Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart-card-reader') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart-card-reader') }}">{{ __('Card Reader Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.checkout') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.checkout') }}">{{ __('Remote Checkout') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.lock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.lock') }}">{{ __('Remote Lock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.change') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.change') }}">{{ __('Remote Change') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.dispense') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.dispense') }}">{{ __('Remote Dispense') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
|
||||
@can('menu.line')
|
||||
{{-- 12. Line管理 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_line') === 'true' || {{ request()->routeIs('admin.line.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_line', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.line.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.line.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg>
|
||||
{{ __('Line Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Members') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.machines') }}">{{ __('Line Machines') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.products') }}">{{ __('Line Products') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.official-account') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.official-account') }}">{{ __('Line Official Account') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.orders') }}">{{ __('Line Orders') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.coupons') }}">{{ __('Line Coupons') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.official-account') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.official-account') }}">{{ __('Official Account') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Members') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.machines') }}">{{ __('Line Machines') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.products') }}">{{ __('Line Products') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.orders') }}">{{ __('Line Orders') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.coupons') }}">{{ __('Line Coupons') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
@can('menu.reservation')
|
||||
{{-- 13. 預約系統 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_reservation') === 'true' || {{ request()->routeIs('admin.reservation.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_reservation', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.reservation.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.reservation.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
{{ __('Reservation System') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.members') }}">{{ __('Reservation Members') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.stores') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.stores') }}">{{ __('Store Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.time-slots') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.time-slots') }}">{{ __('Time Slots') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.venues') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.venues') }}">{{ __('Venue Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.coupons') }}">{{ __('Coupons') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.reservations') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.reservations') }}">{{ __('Reservations') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.orders') }}">{{ __('Order Management') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.reservations') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.reservations') }}">{{ __('Reservation List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.members') }}">{{ __('Reservation Members') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.stores') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.stores') }}">{{ __('Stores Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.time-slots') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.time-slots') }}">{{ __('Time Slots') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.venues') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.venues') }}">{{ __('Venues Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.coupons') }}">{{ __('Reservation Coupons') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.orders') }}">{{ __('Reservation Orders') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
@role('super-admin')
|
||||
@can('menu.special-permission')
|
||||
{{-- 14. 特殊權限 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_special_permission', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.special-permission.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.special-permission.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
{{ __('Special Permission') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.clear-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.clear-stock') }}">{{ __('Clear Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.apk-versions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.apk-versions') }}">{{ __('APK Versions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.discord-notifications') }}">{{ __('Discord Notifications') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.clear-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.clear-stock') }}">{{ __('Clear Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.apk-versions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.apk-versions') }}">{{ __('APK Versions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.discord-notifications') }}">{{ __('Discord Notifications') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endrole
|
||||
@endcan
|
||||
|
||||
@role('super-admin')
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
@can('menu.basic-settings')
|
||||
{{-- 14.5. 基本設定 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_basic_settings') === 'true' || {{ request()->routeIs('admin.basic-settings.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_basic_settings', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.basic-settings.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
{{ __('Basic Settings') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.basic-settings.machines.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.basic-settings.machines.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" /></svg>
|
||||
{{ __('Machine Settings') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.basic-settings.payment-configs.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.basic-settings.payment-configs.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>
|
||||
{{ __('Customer Payment Config') }}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->isSystemAdmin())
|
||||
@can('menu.permissions')
|
||||
{{-- 15. 權限設定 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_permissions', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.permission.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.permission.*') ? 'text-cyan-500' : 'text-slate-600 group-hover:text-cyan-500 dark:text-slate-300 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
|
||||
{{ __('Permission Settings') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-600 dark:text-slate-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.companies.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.companies.index') }}">{{ __('Customer Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.accounts') }}">{{ __('Account Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">{{ __('Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.app-features') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.app-features') }}">{{ __('APP Features') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.data-config') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.data-config') }}">{{ __('Data Configuration') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.sales') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.sales') }}">{{ __('Sales') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.machines') }}">{{ __('Machine Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.warehouses') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.warehouses') }}">{{ __('Warehouse Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.analysis') }}">{{ __('Analysis Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.audit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.audit') }}">{{ __('Audit Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.remote') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.remote') }}">{{ __('Remote Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.line') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.others') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.others') }}">{{ __('Others') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.ai-prediction') }}">{{ __('AI Prediction') }}</a></li>
|
||||
<ul class="luxury-submenu" data-sidebar-sub>
|
||||
<li><a class="flex items-center gap-x-3 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.companies.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.companies.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>
|
||||
{{ __('Customer Management') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.accounts') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ __('Account Management') }}
|
||||
</a></li>
|
||||
<li><a class="flex items-center gap-x-3 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
||||
{{ __('Role Permissions') }}
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endrole
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
@if ($paginator->hasPages())
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between w-full gap-4">
|
||||
{{-- Total Items Info --}}
|
||||
<div class="order-2 sm:order-1">
|
||||
<p class="text-[10px] sm:text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest text-center sm:text-left">
|
||||
@if ($paginator->total() > 0)
|
||||
<div class="flex flex-col lg:flex-row items-center justify-between w-full gap-6">
|
||||
{{-- Left Side: Limit Selector & Info --}}
|
||||
<div class="order-2 lg:order-1 flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
|
||||
{{-- Limit Selector --}}
|
||||
<div class="flex items-center gap-3 bg-slate-50/50 dark:bg-slate-900/50 px-3 py-1.5 rounded-2xl border border-slate-100 dark:border-slate-800 shadow-sm leading-none">
|
||||
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest pl-1 leading-none">{{ __('Show') }}</span>
|
||||
<div class="relative group flex items-center">
|
||||
@php
|
||||
$currentLimit = request('per_page', 10);
|
||||
$limits = [10, 25, 50, 100];
|
||||
@endphp
|
||||
<select onchange="const params = new URLSearchParams(window.location.search); params.set('per_page', this.value); params.delete('page'); window.location.href = window.location.pathname + '?' + params.toString();"
|
||||
class="h-7 pl-2 pr-8 rounded-lg bg-white dark:bg-slate-800 border-none text-[11px] font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 outline-none transition-all cursor-pointer shadow-sm leading-none py-0">
|
||||
@foreach($limits as $l)
|
||||
<option value="{{ $l }}" {{ $currentLimit == $l ? 'selected' : '' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none opacity-40 group-hover:opacity-100 transition-opacity">
|
||||
<svg class="size-3 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Total Items Info --}}
|
||||
<p class="text-[10px] sm:text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest text-center sm:text-left whitespace-nowrap">
|
||||
<span class="hidden xs:inline">{{ __('Showing') }}</span>
|
||||
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->firstItem() }}</span>
|
||||
{{ __('to') }}
|
||||
@@ -14,7 +35,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="order-1 sm:order-2 flex items-center gap-x-1.5 sm:gap-x-2">
|
||||
<div class="order-1 lg:order-2 flex items-center gap-x-1.5 sm:gap-x-2">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-slate-50 dark:bg-slate-800 text-slate-300 dark:text-slate-600 border border-slate-100 dark:border-slate-800 cursor-not-allowed">
|
||||
@@ -30,7 +51,9 @@
|
||||
|
||||
{{-- Unified Quick Jump Selection (Desktop & Mobile) --}}
|
||||
<div class="relative group">
|
||||
<select onchange="window.location.href = this.value" class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600">
|
||||
<select onchange="window.location.href = this.value"
|
||||
class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600 {{ $paginator->lastPage() <= 1 ? 'opacity-50 cursor-not-allowed pointer-events-none' : '' }}"
|
||||
{{ $paginator->lastPage() <= 1 ? 'disabled' : '' }}>
|
||||
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
|
||||
<option value="{{ $paginator->url($i) }}" {{ $i == $paginator->currentPage() ? 'selected' : '' }}>
|
||||
{{ $i }} / {{ $paginator->lastPage() }}
|
||||
|
||||
@@ -47,6 +47,20 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
|
||||
|--------------------------------------------------------------------------
|
||||
| 專門用於機台通訊,頻率較高,建議搭配異步處理。
|
||||
*/
|
||||
Route::prefix('app')->middleware(['iot.auth', 'throttle:100,1'])->group(function () {
|
||||
// 心跳與狀態 (B010, B017, B710, B220)
|
||||
Route::post('machine/status/B010', [App\Http\Controllers\Api\V1\App\MachineController::class, 'heartbeat']);
|
||||
Route::post('machine/reload_msg/B017', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getSlots']);
|
||||
Route::post('machine/timer/B710', [App\Http\Controllers\Api\V1\App\MachineController::class, 'syncTimer']);
|
||||
Route::post('machine/coins/B220', [App\Http\Controllers\Api\V1\App\MachineController::class, 'syncCoinInventory']);
|
||||
Route::post('machine/member/verify/B650', [App\Http\Controllers\Api\V1\App\MachineController::class, 'verifyMember']);
|
||||
|
||||
// 交易、發票與出貨 (B600, B601, B602)
|
||||
Route::post('B600', [App\Http\Controllers\Api\V1\App\TransactionController::class, 'store']);
|
||||
Route::post('B601', [App\Http\Controllers\Api\V1\App\TransactionController::class, 'recordInvoice']);
|
||||
Route::post('B602', [App\Http\Controllers\Api\V1\App\TransactionController::class, 'recordDispense']);
|
||||
});
|
||||
|
||||
Route::prefix('machines')->group(function () {
|
||||
Route::post('/{id}/logs', [\App\Http\Controllers\Api\V1\MachineController::class, 'storeLog']);
|
||||
});
|
||||
|
||||
@@ -108,8 +108,14 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
Route::get('/products', [App\Http\Controllers\Admin\DataConfigController::class , 'products'])->name('products');
|
||||
Route::get('/advertisements', [App\Http\Controllers\Admin\DataConfigController::class , 'advertisements'])->name('advertisements');
|
||||
Route::get('/admin-products', [App\Http\Controllers\Admin\DataConfigController::class , 'adminProducts'])->name('admin-products');
|
||||
Route::get('/sub-accounts', [App\Http\Controllers\Admin\DataConfigController::class , 'subAccounts'])->name('sub-accounts');
|
||||
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\DataConfigController::class , 'subAccountRoles'])->name('sub-account-roles');
|
||||
Route::get('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('sub-accounts');
|
||||
Route::post('/sub-accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('sub-accounts.store');
|
||||
Route::put('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('sub-accounts.update');
|
||||
Route::delete('/sub-accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('sub-accounts.destroy');
|
||||
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class , 'roles'])->name('sub-account-roles');
|
||||
Route::post('/sub-account-roles', [App\Http\Controllers\Admin\PermissionController::class , 'storeRole'])->name('sub-account-roles.store');
|
||||
Route::put('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateRole'])->name('sub-account-roles.update');
|
||||
Route::delete('/sub-account-roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyRole'])->name('sub-account-roles.destroy');
|
||||
Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class , 'points'])->name('points');
|
||||
Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class , 'badges'])->name('badges');
|
||||
}
|
||||
@@ -158,28 +164,31 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
}
|
||||
);
|
||||
|
||||
// 14. 權限設定
|
||||
// 14. 基本設定
|
||||
Route::prefix('basic-settings')->name('basic-settings.')->group(function () {
|
||||
// 機台設定
|
||||
Route::prefix('machines')->name('machines.')->group(function () {
|
||||
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');
|
||||
Route::post('/', [App\Http\Controllers\Admin\BasicSettings\MachineSettingController::class, 'store'])->name('store');
|
||||
});
|
||||
|
||||
// 客戶金流設定
|
||||
Route::resource('payment-configs', App\Http\Controllers\Admin\BasicSettings\PaymentConfigController::class)->except(['show']);
|
||||
});
|
||||
|
||||
// 15. 權限設定
|
||||
Route::prefix('permission')->name('permission.')->group(function () {
|
||||
Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit']);
|
||||
Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('accounts');
|
||||
Route::post('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('accounts.store');
|
||||
Route::put('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('accounts.update');
|
||||
Route::delete('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('accounts.destroy');
|
||||
Route::get('/app-features', [App\Http\Controllers\Admin\PermissionController::class , 'appFeatures'])->name('app-features');
|
||||
Route::get('/data-config', [App\Http\Controllers\Admin\PermissionController::class , 'dataConfig'])->name('data-config');
|
||||
Route::get('/sales', [App\Http\Controllers\Admin\PermissionController::class , 'sales'])->name('sales');
|
||||
Route::get('/machines', [App\Http\Controllers\Admin\PermissionController::class , 'machines'])->name('machines');
|
||||
Route::get('/warehouses', [App\Http\Controllers\Admin\PermissionController::class , 'warehouses'])->name('warehouses');
|
||||
Route::get('/analysis', [App\Http\Controllers\Admin\PermissionController::class , 'analysis'])->name('analysis');
|
||||
Route::get('/audit', [App\Http\Controllers\Admin\PermissionController::class , 'audit'])->name('audit');
|
||||
Route::get('/remote', [App\Http\Controllers\Admin\PermissionController::class , 'remote'])->name('remote');
|
||||
Route::get('/line', [App\Http\Controllers\Admin\PermissionController::class , 'line'])->name('line');
|
||||
Route::get('/roles', [App\Http\Controllers\Admin\PermissionController::class , 'roles'])->name('roles');
|
||||
Route::post('/roles', [App\Http\Controllers\Admin\PermissionController::class , 'storeRole'])->name('roles.store');
|
||||
Route::put('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateRole'])->name('roles.update');
|
||||
Route::delete('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyRole'])->name('roles.destroy');
|
||||
Route::get('/others', [App\Http\Controllers\Admin\PermissionController::class , 'others'])->name('others');
|
||||
Route::get('/ai-prediction', [App\Http\Controllers\Admin\PermissionController::class , 'aiPrediction'])->name('ai-prediction');
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user