Compare commits
14 Commits
2a6170b4ce
...
02918ce0e1
| Author | SHA1 | Date | |
|---|---|---|---|
| 02918ce0e1 | |||
| a38387f2ad | |||
| 80436caee7 | |||
| c1b40185eb | |||
| f15038fcbd | |||
| 1d035d2766 | |||
| 1c9edf8e46 | |||
| fba4f26575 | |||
| 4b64fede2e | |||
| d905501a77 | |||
| 56c9a55944 | |||
| c30c3a399d | |||
| 21e064ff91 | |||
| adea7feb7b |
@@ -1,528 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
STAR CLOUD 後台管理系統 - 智能販賣機管理平台
|
||||
|
||||
一、儀錶板模組 (Dashboard)
|
||||
1.1 主頁面
|
||||
|
||||
功能描述: 系統總覽與關鍵數據展示
|
||||
主要內容:
|
||||
|
||||
即時銷售數據
|
||||
機台運行狀態
|
||||
庫存警示
|
||||
營收統計圖表
|
||||
|
||||
|
||||
|
||||
|
||||
二、應用程式管理 (Application Management)
|
||||
2.1 個人檔案
|
||||
|
||||
功能描述: 使用者個人資訊管理
|
||||
包含功能:
|
||||
|
||||
基本資料編輯
|
||||
密碼修改
|
||||
通知設定
|
||||
|
||||
|
||||
|
||||
|
||||
三、機台管理模組 (Machine Management)
|
||||
3.1 機台日誌
|
||||
|
||||
功能描述: 機台操作歷史紀錄回溯
|
||||
資料內容:
|
||||
|
||||
操作時間戳記
|
||||
事件類型
|
||||
操作人員
|
||||
詳細描述
|
||||
|
||||
|
||||
|
||||
3.2 機台列表
|
||||
|
||||
功能描述: 所有機台資訊總覽
|
||||
顯示資訊:
|
||||
|
||||
溫度監控
|
||||
下位機狀態
|
||||
刷卡機連線
|
||||
掃碼機狀態
|
||||
機台回傳訊息
|
||||
|
||||
|
||||
|
||||
3.3 機台權限
|
||||
|
||||
功能描述: 機台存取權限控管
|
||||
設定項目:
|
||||
|
||||
人員權限分配
|
||||
操作級別設定
|
||||
|
||||
|
||||
|
||||
3.4 機台稼動率
|
||||
|
||||
功能描述: 機台運行效率分析
|
||||
統計數據:
|
||||
|
||||
運行時間
|
||||
停機時間
|
||||
稼動率百分比
|
||||
|
||||
|
||||
|
||||
3.5 效期管理
|
||||
|
||||
功能描述: 商品效期與貨道出貨控制
|
||||
管理項目:
|
||||
|
||||
設定貨道是否可出貨
|
||||
效期到期提醒
|
||||
商品下架設定
|
||||
|
||||
|
||||
|
||||
3.6 維修管理單
|
||||
|
||||
功能描述: 機台維修工單系統
|
||||
包含功能:
|
||||
|
||||
報修單建立
|
||||
維修進度追蹤
|
||||
維修歷史紀錄
|
||||
|
||||
|
||||
|
||||
3.7 機台管理擴充欄位
|
||||
|
||||
新增欄位:
|
||||
|
||||
保固區間
|
||||
交機日期
|
||||
租賃區間
|
||||
保內/保外狀態顯示
|
||||
|
||||
|
||||
|
||||
3.8 機台設定參數
|
||||
|
||||
刷卡機秒數: 刷卡逾時設定
|
||||
卡機結帳時間: 結帳流程時間限制
|
||||
卡機結帳時間2: 備用結帳時間設定
|
||||
金流緩衝時間: 金流處理緩衝時間
|
||||
刷卡機編號: 刷卡機裝置識別
|
||||
發票狀態碼: 發票開立狀態管理
|
||||
|
||||
3.9 APP到期提醒
|
||||
|
||||
功能描述: APP授權到期通知系統
|
||||
|
||||
|
||||
四、APP管理模組 (APP Management)
|
||||
4.1 UI元素設定
|
||||
|
||||
功能描述: APP版面配置設定
|
||||
注意事項: 與新版差異較大,需特別處理
|
||||
|
||||
4.2 小幫手設定
|
||||
|
||||
功能描述: APP內建輔助功能設定
|
||||
|
||||
4.3 問卷設定
|
||||
|
||||
功能描述: 互動問卷建立與管理
|
||||
|
||||
4.4 互動遊戲設定
|
||||
|
||||
功能描述: APP互動遊戲配置
|
||||
|
||||
4.5 計時器
|
||||
|
||||
功能描述: 時間相關功能設定
|
||||
|
||||
|
||||
五、倉庫管理模組 (Warehouse Management)
|
||||
5.1 倉庫列表
|
||||
|
||||
5.1.1 倉庫列表(全部): 顯示所有倉庫
|
||||
5.1.2 倉庫列表(個人): 顯示個人負責倉庫
|
||||
|
||||
5.2 庫存管理單
|
||||
|
||||
功能描述: 倉庫庫存異動管理
|
||||
|
||||
5.3 調撥單
|
||||
|
||||
功能描述: 倉庫間商品調撥作業
|
||||
|
||||
5.4 採購單
|
||||
|
||||
功能描述: 商品採購申請與管理
|
||||
|
||||
5.5 機台補貨管理
|
||||
|
||||
5.5.1 機台補貨單: 補貨工單建立
|
||||
5.5.2 機台補貨紀錄: 個別補貨歷史
|
||||
5.5.3 機台補貨紀錄(總): 所有補貨總覽
|
||||
|
||||
5.6 庫存查詢
|
||||
|
||||
5.6.1 機台庫存: 各機台即時庫存
|
||||
5.6.2 人員庫存: 人員持有庫存
|
||||
|
||||
5.7 回庫單
|
||||
|
||||
功能描述: 商品退回倉庫管理
|
||||
|
||||
|
||||
六、銷售管理模組 (Sales Management)
|
||||
6.1 銷售&金流紀錄
|
||||
|
||||
功能描述: 銷售交易與金流明細
|
||||
包含項目:
|
||||
|
||||
現金出貨 API
|
||||
發票系統整合
|
||||
各種出貨方式整理
|
||||
|
||||
|
||||
|
||||
6.2 取貨碼設定
|
||||
|
||||
功能描述: 取貨驗證碼管理
|
||||
|
||||
6.3 購買單
|
||||
|
||||
功能描述: 購買訂單管理
|
||||
|
||||
6.4 促銷時段設定
|
||||
|
||||
功能描述: 促銷活動時間設定
|
||||
重要功能: (W) 重啟掃描商品 API
|
||||
|
||||
6.5 通行碼設定
|
||||
|
||||
功能描述: 特殊通行碼權限管理
|
||||
|
||||
6.6 來店禮設定
|
||||
|
||||
功能描述: 來店優惠活動設定
|
||||
包含: 來店禮開關控制
|
||||
|
||||
|
||||
七、分析管理模組 (Analysis Management)
|
||||
7.1 零錢庫存分析
|
||||
|
||||
功能描述: 機台零錢數量監測與分析
|
||||
|
||||
7.2 機台報表分析
|
||||
|
||||
功能描述: 機台運營數據分析報表
|
||||
|
||||
7.3 商品報表分析
|
||||
|
||||
功能描述: 商品銷售數據分析
|
||||
|
||||
7.4 互動問卷分析
|
||||
|
||||
功能描述: 問卷結果統計與分析
|
||||
|
||||
|
||||
八、稽核管理模組 (Audit Management)
|
||||
8.1 採購單稽核
|
||||
|
||||
功能描述: 採購單審核流程
|
||||
|
||||
8.2 調撥單稽核
|
||||
|
||||
功能描述: 調撥單審核流程
|
||||
|
||||
8.3 補貨單稽核
|
||||
|
||||
功能描述: 補貨單審核流程
|
||||
|
||||
|
||||
九、資料設定模組 (Data Configuration)
|
||||
9.1 機台管理
|
||||
|
||||
功能描述: 機台基本資料設定
|
||||
|
||||
9.2 商品管理
|
||||
|
||||
功能描述: 商品資料維護
|
||||
|
||||
9.3 廣告管理
|
||||
|
||||
功能描述: 機台廣告影片管理
|
||||
用途: 機台可讀取後台廣告影片
|
||||
|
||||
9.4 管理者可賣商品
|
||||
|
||||
功能描述: 管理者商品銷售權限
|
||||
|
||||
9.5 帳號管理
|
||||
|
||||
功能描述: 主帳號管理
|
||||
|
||||
9.6 子帳號管理
|
||||
|
||||
功能描述: 子帳號建立與管理
|
||||
|
||||
9.7 子帳號角色管理
|
||||
|
||||
功能描述: 子帳號權限角色設定
|
||||
|
||||
9.8 點數設定
|
||||
|
||||
功能描述: 客戶點數系統設定
|
||||
特殊功能: 支援客戶自行新增點數
|
||||
|
||||
9.9 識別證管理
|
||||
|
||||
功能描述: 識別證資料管理
|
||||
用途: 安霸系統使用
|
||||
|
||||
|
||||
十、遠端管理模組 (Remote Management)
|
||||
10.1 機台庫存
|
||||
|
||||
功能描述: 遠端修改機台庫存
|
||||
|
||||
10.2 機台重啟
|
||||
|
||||
功能描述: 遠端重啟機台系統
|
||||
|
||||
10.3 卡機重啟
|
||||
|
||||
功能描述: 遠端重啟刷卡機
|
||||
|
||||
10.4 遠端結帳
|
||||
|
||||
功能描述: 遠端執行結帳流程
|
||||
|
||||
10.5 遠端鎖定頁
|
||||
|
||||
功能描述: 遠端鎖定機台頁面
|
||||
|
||||
10.6 遠端找零
|
||||
|
||||
功能描述: 遠端執行找零功能
|
||||
|
||||
10.7 遠端出貨
|
||||
|
||||
功能描述: 遠端控制商品出貨
|
||||
|
||||
|
||||
十一、Line管理模組 (Line Integration)
|
||||
11.1 Line會員管理
|
||||
|
||||
功能描述: Line會員資料管理
|
||||
|
||||
11.2 Line機台管理
|
||||
|
||||
功能描述: Line綁定機台管理
|
||||
|
||||
11.3 Line商品管理
|
||||
|
||||
功能描述: Line商城商品設定
|
||||
|
||||
11.4 Line生活圈
|
||||
|
||||
功能描述: Line官方帳號整合
|
||||
|
||||
11.5 Line商城訂單
|
||||
|
||||
功能描述: Line商城訂單管理
|
||||
|
||||
11.6 Line優惠券
|
||||
|
||||
功能描述: Line優惠券發放與管理
|
||||
|
||||
|
||||
十二、預約系統模組 (Reservation System)
|
||||
12.1 Line會員管理
|
||||
|
||||
功能描述: 預約系統會員管理
|
||||
|
||||
12.2 Line店家管理
|
||||
|
||||
功能描述: 店家資訊設定
|
||||
|
||||
12.3 Line時段組合
|
||||
|
||||
功能描述: 預約時段設定
|
||||
|
||||
12.4 Line場地管理
|
||||
|
||||
功能描述: 場地資源管理
|
||||
|
||||
12.5 Line優惠券管理
|
||||
|
||||
功能描述: 預約優惠券管理
|
||||
|
||||
12.6 Line預約管理
|
||||
|
||||
功能描述: 預約單管理
|
||||
|
||||
12.7 Line訂單管理
|
||||
|
||||
功能描述: 預約訂單處理
|
||||
|
||||
|
||||
十三、特殊權限管理模組 (Special Permissions)
|
||||
13.1 庫存清空
|
||||
|
||||
功能描述: 特殊權限庫存清空功能
|
||||
|
||||
13.2 APK版本管理
|
||||
|
||||
功能描述: APP版本控制與更新
|
||||
|
||||
13.3 Discord通知設定
|
||||
|
||||
功能描述: Discord通知整合設定
|
||||
|
||||
|
||||
十四、權限設定模組 (Permission Management)
|
||||
14.1 功能權限設定
|
||||
|
||||
14.1.1 APP功能管理: APP功能權限
|
||||
14.1.2 資料設定: 資料設定權限
|
||||
14.1.3 銷售管理: 銷售管理權限
|
||||
14.1.4 機台管理: 機台管理權限
|
||||
14.1.5 倉庫管理: 倉庫管理權限
|
||||
14.1.6 分析管理: 分析管理權限
|
||||
14.1.7 稽核管理: 稽核管理權限
|
||||
14.1.8 遠端管理: 遠端管理權限
|
||||
14.1.9 Line管理: Line管理權限
|
||||
|
||||
14.2 權限角色設定
|
||||
|
||||
功能描述: 角色權限組合設定
|
||||
|
||||
14.3 其他功能管理
|
||||
|
||||
功能描述: 其他特殊功能權限
|
||||
|
||||
14.4 AI智能預測
|
||||
|
||||
功能描述: AI功能權限設定
|
||||
|
||||
|
||||
資料庫設計建議
|
||||
主要資料表規劃
|
||||
machines # 機台資料表
|
||||
├── warehouses # 倉庫資料表
|
||||
├── products # 商品資料表
|
||||
├── machine_stocks # 機台庫存表
|
||||
├── warehouse_stocks # 倉庫庫存表
|
||||
├── sales_records # 銷售紀錄表
|
||||
├── purchase_orders # 採購單表
|
||||
├── transfer_orders # 調撥單表
|
||||
├── replenishment_orders # 補貨單表
|
||||
├── maintenance_orders # 維修單表
|
||||
├── machine_logs # 機台日誌表
|
||||
├── users # 使用者表
|
||||
├── roles # 角色表
|
||||
├── permissions # 權限表
|
||||
├── line_members # Line會員表
|
||||
├── reservations # 預約表
|
||||
└── advertisements # 廣告表
|
||||
|
||||
API 端點規劃
|
||||
機台管理 API
|
||||
|
||||
GET /api/machines - 取得機台列表
|
||||
POST /api/machines - 新增機台
|
||||
PUT /api/machines/{id} - 更新機台
|
||||
DELETE /api/machines/{id} - 刪除機台
|
||||
GET /api/machines/{id}/logs - 取得機台日誌
|
||||
POST /api/machines/{id}/restart - 遠端重啟
|
||||
|
||||
倉庫管理 API
|
||||
|
||||
GET /api/warehouses - 取得倉庫列表
|
||||
GET /api/warehouses/{id}/stocks - 取得倉庫庫存
|
||||
POST /api/transfer-orders - 建立調撥單
|
||||
POST /api/purchase-orders - 建立採購單
|
||||
POST /api/replenishment-orders - 建立補貨單
|
||||
|
||||
銷售管理 API
|
||||
|
||||
GET /api/sales - 取得銷售紀錄
|
||||
POST /api/sales/cash - 現金出貨
|
||||
GET /api/sales/invoice - 發票查詢
|
||||
POST /api/pickup-codes - 建立取貨碼
|
||||
|
||||
遠端控制 API
|
||||
|
||||
POST /api/remote/checkout - 遠端結帳
|
||||
POST /api/remote/dispense - 遠端出貨
|
||||
POST /api/remote/change - 遠端找零
|
||||
POST /api/remote/lock - 遠端鎖定
|
||||
|
||||
|
||||
技術架構建議
|
||||
後端技術
|
||||
|
||||
框架: Laravel 10+
|
||||
資料庫: MySQL 8.0+
|
||||
快取: Redis
|
||||
佇列: Laravel Queue (Redis Driver)
|
||||
API文件: Swagger/OpenAPI
|
||||
|
||||
前端技術
|
||||
|
||||
模板引擎: Blade
|
||||
CSS框架: Tailwind CSS
|
||||
JavaScript: Alpine.js / Vue.js
|
||||
圖表庫: Chart.js / ApexCharts
|
||||
|
||||
第三方整合
|
||||
|
||||
Line API: Line Messaging API
|
||||
Discord: Discord Webhook
|
||||
金流: 藍新、綠界等
|
||||
發票: 電子發票整合
|
||||
|
||||
|
||||
開發優先順序建議
|
||||
Phase 1 - 核心功能 (1-2個月)
|
||||
|
||||
使用者認證與權限系統
|
||||
機台管理基本功能
|
||||
倉庫管理基本功能
|
||||
銷售紀錄查詢
|
||||
|
||||
Phase 2 - 進階功能 (2-3個月)
|
||||
|
||||
遠端控制功能
|
||||
報表分析功能
|
||||
稽核流程
|
||||
APP管理功能
|
||||
|
||||
Phase 3 - 整合功能 (1-2個月)
|
||||
|
||||
Line整合
|
||||
預約系統
|
||||
AI智能預測
|
||||
Discord通知
|
||||
|
||||
|
||||
注意事項
|
||||
|
||||
安全性: 所有遠端控制功能需要雙重驗證
|
||||
權限控管: 嚴格的角色權限分離
|
||||
日誌記錄: 所有重要操作需記錄日誌
|
||||
API限流: 防止API濫用
|
||||
資料備份: 定期自動備份機制
|
||||
錯誤處理: 完善的異常處理機制
|
||||
測試: 重要功能需撰寫測試案例
|
||||
RetryClaude can make mistakes. Please double-check responses.
|
||||
@@ -100,34 +100,21 @@ Request / Response 均採 JSON,個資欄位請遵守最小授權原則。
|
||||
|
||||
### 3) Machine (機台)
|
||||
|
||||
* GET /api/v1/machines
|
||||
|
||||
* Params: page, per_page, status
|
||||
|
||||
* GET /api/v1/machines/{id}
|
||||
|
||||
* POST /api/v1/machines
|
||||
|
||||
* PUT /api/v1/machines/{id}
|
||||
|
||||
* DELETE /api/v1/machines/{id}
|
||||
|
||||
* POST /api/v1/machines/{id}/status
|
||||
|
||||
* 用於下位機或 APP 回傳機台狀態
|
||||
* request example:
|
||||
|
||||
* **GET /api/v1/machines**
|
||||
* Params: page, per_page, status
|
||||
* **GET /api/v1/machines/{id}**
|
||||
* **POST /api/v1/machines/{id}/logs** (IoT)
|
||||
* 用於機台回傳日誌,後端固定走 **Redis Queue 異步寫入**。
|
||||
* 回傳 `202 Accepted` 表示任務已接收,由 `ProcessMachineLog` 背景處理。
|
||||
* Request Example:
|
||||
```json
|
||||
{
|
||||
"temperature": 23.4,
|
||||
"status_code": "OK",
|
||||
"firmware_version": "1.2.3",
|
||||
"timestamp": "2025-11-20T15:00:00Z"
|
||||
"level": "info",
|
||||
"message": "Temperature stabilized at 23C",
|
||||
"context": { "temp": 23.0 }
|
||||
}
|
||||
```
|
||||
|
||||
* GET /api/v1/machines/{id}/logs
|
||||
|
||||
---
|
||||
|
||||
### 4) Orders / ShoppingCart
|
||||
78
.agents/rules/framework.md
Normal file
78
.agents/rules/framework.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 開發框架規範說明書:Cloud 後台管理系統 (star-cloud)
|
||||
|
||||
## 1. 專案概述
|
||||
* **目標**:打造一個強大且穩定的智能販賣機後台管理系統(Cloud 平台),負責管理機台、商品、銷售數據以及提供給端點機台串接的 API。
|
||||
* **核心架構**:採用 **傳統單體式架構 (Monolithic Architecture)** 配 Laravel Blade 模板引擎進行伺服器端渲染 (SSR)。
|
||||
* **工作流程**:後端處理業務邏輯與資料庫存取,並透過 Blade 引擎渲染包含 Tailwind CSS 類別的 HTML。前端互動行為由輕量級 Alpine.js 負責,UI 元件以 Preline UI 為主體。
|
||||
|
||||
## 2. 技術棧 (Tech Stack)
|
||||
* **後端框架**:PHP 8.5 / Laravel 12
|
||||
* **核心組件**:Redis (用於高併發 IoT 隊列與快取,為系統穩定之必要條件)
|
||||
* **前端視圖 (View)**:Laravel Blade
|
||||
* **前端互動 (JS)**:Alpine.js (專注於行為,不負責渲染)
|
||||
* **介面與樣式 (CSS)**:Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)
|
||||
* **前端建置工具**:Vite
|
||||
* **資料庫**:MySQL 8.0
|
||||
* **開發環境**:Laravel Sail (Docker / WSL2)
|
||||
|
||||
## 3. 目錄結構與慣例
|
||||
|
||||
### 3.1 後端 (Laravel)
|
||||
與標準 Laravel 結構保持一致,無過度拆分的模組化(與 ERP 的 Modular Monolith 區別):
|
||||
* **Controllers**:`app/Http/Controllers/`,負責接收請求並回傳 `view()` 或 JSON。
|
||||
* **Models**:`app/Models/{Domain}/`,按領域分群 (例如 `Machine`, `Member`, `System`)。
|
||||
* **Routes**:`routes/web.php` 用於後台管理介面;`routes/api.php` 提供外部或機台調用介面 (需 V1 版本化)。
|
||||
* **Services** (建議):`app/Services/{Domain}/`,將商業邏輯與資料異動封裝於 Service 中。
|
||||
* **Traits**:`app/Traits/ApiResponse.php` 用於統一 API JSON 回傳格式。
|
||||
* **Jobs**:`app/Jobs/{Domain}/`,**高併發 IoT 場景之必要實作**。所有日誌、心跳上報必須進入 Redis Queue 進行背景異步處理,嚴禁在 API 直連 DB 寫入日誌。
|
||||
|
||||
### 3.2 前端 (Blade / Tailwind / Alpine)
|
||||
* **Views (頁面)**:位於 `resources/views/`。通常依功能建立資料夾(如 `resources/views/admin/machines/index.blade.php`)。
|
||||
* **Layouts (版面)**:位於 `resources/views/layouts/`。定義全站的共用版面結構(如 header, sidebar, footer)。
|
||||
* **Components (組件)**:位於 `resources/views/components/`。封裝可重用的 Blade 元件(如 Button, Modal, Table),支援透過 `<x-button>` 語法呼叫。
|
||||
|
||||
## 4. 開發標準 (Coding Standards)
|
||||
* **命名規範**:
|
||||
* Controllers: `PascalCaseController.php` (例如 `MachineController.php`)
|
||||
* Models: `PascalCase.php` (例如 `Machine.php`)
|
||||
* Blade Views: `kebab-case.blade.php` 或按資源名稱 (例如 `index.blade.php`, `create.blade.php`)
|
||||
* Routes uri: `kebab-case` (例如 `/machine-logs`)
|
||||
* **回傳格式**:
|
||||
* Web 路由:回傳 `view()`,表單驗證失敗時直接使用 Laravel 內建的 redirect with errors。
|
||||
* API 路由:回傳標準 JSON 格式的 `JsonResponse`。
|
||||
|
||||
## 5. UI 與前端開發指南
|
||||
* **樣式撰寫**:全面使用 Tailwind CSS utility classes,**避免撰寫自訂 CSS**(除非少數特定動畫或覆寫)。
|
||||
* **UI 元件庫**:遵循 **Preline UI** 的類別與 HTML 結構進行開發。
|
||||
* **前端腳本**:
|
||||
* 優先使用 **Alpine.js** (`x-data`, `x-show`, `@click` 等) 在 HTML 標籤內完成簡單的 DOM 狀態切換與互動邏輯。
|
||||
* 避免在 Blade 內撰寫冗長的 `<script>` Vanilla JS;若邏輯過於複雜,可將 Alpine state 獨立成 js 檔案再於 Vite 引入,但原則上保持輕量。
|
||||
|
||||
## 6. AI 協作規則 (給 Antigravity AI)
|
||||
* **角色設定**:你是一位專業的全端開發工程師助手。
|
||||
* **代碼生成指令**:
|
||||
* 所有的解釋說明請使用 **繁體中文**。
|
||||
* **【警告】** 此專案前端禁用 React / Vue / Inertia.js。所有的前端頁面生成必須使用 **Blade 模板** 結合 **Tailwind CSS** 與 **Alpine.js**。
|
||||
* 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。
|
||||
* 開發新功能時,請建立標準的 Controller 搭配對應的 `resources/views/.../` 目錄。
|
||||
|
||||
## 7. 運行機制 (Docker / Sail)
|
||||
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
|
||||
|
||||
* **啟動環境**:`./vendor/bin/sail up -d`
|
||||
* **執行 PHP 指令**:`./vendor/bin/sail php -v`
|
||||
* **執行 Artisan 指令**:`./vendor/bin/sail artisan route:list`
|
||||
* **執行 Composer**:`./vendor/bin/sail composer install`
|
||||
* **執行 Node/NPM**:`./vendor/bin/sail npm run dev`
|
||||
|
||||
## 8. 部署與查修環境 (CI/CD)
|
||||
* **自動化部署**:專案具備基於 Gitea Actions 的 CI/CD 自動化部署流程 (`.gitea/workflows/`)。
|
||||
* **Demo 環境 (對應 `demo` 分支)**:
|
||||
* 透過 `deploy-demo.yaml`,合併或推送到 `demo` 分支會自動部署至 `demo-cloud.taiwan-star.com.tw`。
|
||||
* 登入伺服器查修:`ssh gitea_work`,路徑為 `/var/www/star-cloud-demo`。
|
||||
* **Production 環境 (對應 `main` 分支)**:
|
||||
* 透過 `deploy-prod.yaml`,推進到 `main` 會自動部署至正式站。
|
||||
31
.agents/rules/skill-trigger.md
Normal file
31
.agents/rules/skill-trigger.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# 技能觸發規範 (Skill Trigger Rules)
|
||||
|
||||
本文件確保 AI 助手在對話中能**主動辨識**需要參照技能 (Skill) 的時機。
|
||||
Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
||||
**若對話內容命中以下任一觸發條件,必須先使用 `view_file` 讀取對應的 `SKILL.md` 後再進行作業。**
|
||||
|
||||
---
|
||||
|
||||
## 觸發對照表
|
||||
|
||||
| 觸發詞 / 情境 | 對應 Skill | 路徑 |
|
||||
|---|---|---|
|
||||
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報 | **IoT 通訊與高併發處理規範** | `.agents/skills/iot-communication/SKILL.md` |
|
||||
|
||||
---
|
||||
|
||||
## 強制觸發場景
|
||||
|
||||
以下場景**無論對話中是否出現觸發詞**,都必須主動載入對應 Skill:
|
||||
|
||||
### 🔴 新增機台通訊 API 端點時
|
||||
必須讀取:
|
||||
1. **iot-communication** — 決定是否使用異步隊列流程
|
||||
|
||||
### 🔴 修改 Job 或 Service 邏輯時
|
||||
必須讀取:
|
||||
1. **iot-communication** — 確保符合高併發處理架構
|
||||
63
.agents/skills/iot-communication/SKILL.md
Normal file
63
.agents/skills/iot-communication/SKILL.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: IoT 通訊與高併發處理規範
|
||||
description: 規範智能販賣機與 Cloud 平台間的高頻通訊處理流程,包含 API 接收、異步隊列、業務邏輯拆分與日誌記錄。
|
||||
---
|
||||
|
||||
# IoT 通訊與高併發處理規範 (IoT Communication Skill)
|
||||
|
||||
本規範確保 Star-Cloud 系統在處理成千上萬台機台的高頻發報時,能維持伺服器響應速度與資料一致性。
|
||||
|
||||
## 1. 處理管線 (Processing Pipeline)
|
||||
|
||||
所有來自機台的非即時性資料(日誌、心跳、狀態上報)必須遵循以下 pipeline:
|
||||
|
||||
1. **API Controller (接收層)**:驗證 Request 合法性,隨即分派 (Dispatch) 任務至 Queue,並回傳 `202 Accepted`。
|
||||
2. **Job (異步層)**:由背景 Worker 讀取隊列任務,呼叫對應 Service 處理。
|
||||
3. **Service (邏輯層)**:封裝商業邏輯,更新資料庫。
|
||||
4. **Model (儲存層)**:執行資料存取。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **嚴禁**在 API Controller 直接進行資料庫寫入操作(針對機台發訊端點)。
|
||||
|
||||
## 2. 異步任務實作範例
|
||||
|
||||
### 2.1 API Endpoint
|
||||
```php
|
||||
public function storeLog(Request $request, int $id): JsonResponse
|
||||
{
|
||||
// 1. 驗證
|
||||
$data = $request->validate([...]);
|
||||
|
||||
// 2. 異步分派
|
||||
ProcessMachineLog::dispatch($id, $data);
|
||||
|
||||
// 3. 快速回應
|
||||
return $this->successResponse([], 'Accepted', 202);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Job 處理邏輯
|
||||
Job 應保持單純,僅作為 Service 的觸發點:
|
||||
```php
|
||||
public function handle(MachineService $service): void
|
||||
{
|
||||
$service->recordLog($this->machineId, $this->logData);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 隊列配置規範
|
||||
|
||||
- **連接驅動 (Driver)**:預設使用 `Redis` 以確保高併發吞吐量。
|
||||
- **重試機制 (Retry)**:IoT 任務應設定合理的重試次數,避免因網路短暫波動遺失日誌。
|
||||
- **失敗處理 (Failed Jobs)**:關鍵業務(如訂單同步)必須監控 `failed_jobs`。
|
||||
|
||||
## 4. 速率限制 (Rate Limiting)
|
||||
|
||||
- 所有的 IoT API 必須在 `routes/api.php` 中使用 `throttle:api` 或自定義 Middleware。
|
||||
- 針對單一機台 ID 應限制其每一分鐘的最高連線數,防止遭受攻擊或機台 Bug 導致的連線暴衝。
|
||||
|
||||
## 5. 檢核項目 (Checklist)
|
||||
- [ ] 是否使用了 `ApiResponse` Trait?
|
||||
- [ ] 業務邏輯是否已封裝至 `App\Services`?
|
||||
- [ ] 是否使用了 Redis Queue 進行非同步處理?
|
||||
- [ ] 是否在 API 層級進行了基礎的參數驗證?
|
||||
@@ -97,3 +97,17 @@ jobs:
|
||||
php artisan view:cache
|
||||
"
|
||||
docker exec star-cloud-demo-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||
|
||||
- name: Step 5 - Auto Sync workflows to main
|
||||
run: |
|
||||
git config --global user.email "bot@taiwan-star.com.tw"
|
||||
git config --global user.name "CICD Bot"
|
||||
git fetch origin main
|
||||
git checkout main
|
||||
git checkout ${{ github.ref_name }} -- .gitea/workflows/
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "[AUTO] Sync workflows from ${{ github.ref_name }} to main"
|
||||
GIT_SSH_COMMAND="ssh -p 3222 -o StrictHostKeyChecking=no" git push origin main
|
||||
fi
|
||||
git checkout ${{ github.ref_name }}
|
||||
|
||||
|
||||
21
.gitea/workflows/deploy-prod.yaml
Normal file
21
.gitea/workflows/deploy-prod.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
name: star-cloud-deploy-production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-production:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
github-server-url: https://gitea.taiwan-star.com.tw
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
- name: Step 1 - Push Code to Production
|
||||
run: |
|
||||
echo "Production deployment is currently in preparation..."
|
||||
# 待正式環境資料確定後,再補上 rsync 與 SSH 邏輯
|
||||
64
app/Console/Commands/Machine/SimulateMachineLogs.php
Normal file
64
app/Console/Commands/Machine/SimulateMachineLogs.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class SimulateMachineLogs extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'simulate:machine-logs {--count=10 : 發送日誌的次數}';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '模擬機台發送 API 日誌請求到後端 (用於壓測與驗證 Queue)';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$count = (int) $this->option('count');
|
||||
$machines = Machine::all();
|
||||
|
||||
if ($machines->isEmpty()) {
|
||||
$this->error('No machines found. Please run MachineSeeder first.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info("Starting simulation of {$count} logs...");
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
// 由於是在同一個開發環境,且在 Sail 容器內部執行,
|
||||
// 外部 8090 埠對應容器內部 8080 埠。
|
||||
$baseUrl = 'http://localhost:8080/api/v1/machines/';
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$machine = $machines->random();
|
||||
$level = collect(['info', 'warning', 'error'])->random();
|
||||
|
||||
try {
|
||||
Http::post($baseUrl . $machine->id . '/logs', [
|
||||
'level' => $level,
|
||||
'message' => "Simulated message #{$i} for machine {$machine->name}",
|
||||
'context' => [
|
||||
'simulated' => true,
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->error("\nFailed to send log: " . $e->getMessage());
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info('Simulation completed.');
|
||||
}
|
||||
}
|
||||
10
app/Http/Controllers/Admin/AdminController.php
Normal file
10
app/Http/Controllers/Admin/AdminController.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
abstract class AdminController extends Controller
|
||||
{
|
||||
// Admin 相關的共用邏輯可寫於此
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AppConfig;
|
||||
use App\Models\System\AppConfig;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AppConfigController extends Controller
|
||||
|
||||
@@ -3,25 +3,31 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine;
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// 模擬數據或從資料庫獲取
|
||||
// 由於目前沒有數據,我們先傳遞一些預設值或空集合
|
||||
$totalMachines = Machine::count();
|
||||
$onlineMachines = Machine::where('status', 'online')->count();
|
||||
$offlineMachines = Machine::where('status', 'offline')->count();
|
||||
$errorMachines = Machine::where('status', 'error')->count();
|
||||
// 從資料庫獲取真實統計數據
|
||||
$totalRevenue = \App\Models\Member\MemberWallet::sum('balance');
|
||||
$activeMachines = Machine::where('status', 'online')->count();
|
||||
$alertsPending = Machine::where('status', 'error')->count();
|
||||
$memberCount = \App\Models\Member\Member::count();
|
||||
|
||||
// 獲取最新動態 (最近 3 筆機台日誌)
|
||||
$latestActivities = \App\Models\Machine\MachineLog::with('machine')
|
||||
->latest()
|
||||
->limit(3)
|
||||
->get();
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalMachines',
|
||||
'onlineMachines',
|
||||
'offlineMachines',
|
||||
'errorMachines'
|
||||
'totalRevenue',
|
||||
'activeMachines',
|
||||
'alertsPending',
|
||||
'memberCount',
|
||||
'latestActivities'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DepositBonusRule;
|
||||
use App\Models\Member\DepositBonusRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DepositBonusRuleController extends Controller
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiftDefinition;
|
||||
use App\Models\MembershipTier;
|
||||
use App\Models\Member\GiftDefinition;
|
||||
use App\Models\Member\MembershipTier;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GiftDefinitionController extends Controller
|
||||
|
||||
@@ -2,142 +2,36 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine;
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MachineController extends Controller
|
||||
class MachineController extends AdminController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* 顯示所有機台列表
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$machines = Machine::latest()->paginate(10);
|
||||
$machines = Machine::query()
|
||||
->when($request->status, function ($query, $status) {
|
||||
return $query->where('status', $status);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('admin.machines.index', compact('machines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
* 顯示特定機台的日誌與詳細資訊
|
||||
*/
|
||||
public function create()
|
||||
public function show(int $id): View
|
||||
{
|
||||
return view('admin.machines.create');
|
||||
}
|
||||
$machine = Machine::with(['logs' => function ($query) {
|
||||
$query->latest()->limit(50);
|
||||
}])->findOrFail($id);
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'status' => 'required|in:online,offline,error',
|
||||
'temperature' => 'nullable|numeric',
|
||||
'firmware_version' => 'nullable|string|max:50',
|
||||
]);
|
||||
|
||||
Machine::create($validated);
|
||||
|
||||
return redirect()->route('admin.machines.index')
|
||||
->with('success', '機台建立成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Machine $machine)
|
||||
{
|
||||
return view('admin.machines.show', compact('machine'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Machine $machine)
|
||||
{
|
||||
return view('admin.machines.edit', compact('machine'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Machine $machine)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'status' => 'required|in:online,offline,error',
|
||||
'temperature' => 'nullable|numeric',
|
||||
'firmware_version' => 'nullable|string|max:50',
|
||||
]);
|
||||
|
||||
$machine->update($validated);
|
||||
|
||||
return redirect()->route('admin.machines.index')
|
||||
->with('success', '機台更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Machine $machine)
|
||||
{
|
||||
$machine->delete();
|
||||
|
||||
return redirect()->route('admin.machines.index')
|
||||
->with('success', '機台已刪除');
|
||||
}
|
||||
|
||||
// 機台日誌
|
||||
public function logs()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台日誌',
|
||||
'description' => '機台操作歷史紀錄回溯',
|
||||
'features' => [
|
||||
'操作時間戳記',
|
||||
'事件類型分類',
|
||||
'操作人員記錄',
|
||||
'詳細描述查詢',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// 機台權限
|
||||
public function permissions()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台權限',
|
||||
'description' => '機台存取權限控管',
|
||||
]);
|
||||
}
|
||||
|
||||
// 機台稼動率
|
||||
public function utilization()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台稼動率',
|
||||
'description' => '機台運行效率分析',
|
||||
]);
|
||||
}
|
||||
|
||||
// 效期管理
|
||||
public function expiry()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '效期管理',
|
||||
'description' => '商品效期與貨道出貨控制',
|
||||
]);
|
||||
}
|
||||
|
||||
// 維修管理單
|
||||
public function maintenance()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '維修管理單',
|
||||
'description' => '機台維修工單系統',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MembershipTier;
|
||||
use App\Models\Member\MembershipTier;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MembershipTierController extends Controller
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PointRule;
|
||||
use App\Models\Member\PointRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PointRuleController extends Controller
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Member;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\Member\Member;
|
||||
use App\Models\Member\SocialAccount;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
11
app/Http/Controllers/Api/V1/ApiController.php
Normal file
11
app/Http/Controllers/Api/V1/ApiController.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\ApiResponse;
|
||||
|
||||
abstract class ApiController extends Controller
|
||||
{
|
||||
use ApiResponse;
|
||||
}
|
||||
39
app/Http/Controllers/Api/V1/MachineController.php
Normal file
39
app/Http/Controllers/Api/V1/MachineController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Jobs\Machine\ProcessMachineLog;
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class MachineController extends ApiController
|
||||
{
|
||||
/**
|
||||
* 接收機台回傳的日誌 (IoT Endpoint)
|
||||
* 採用異步處理 (Queue)
|
||||
*/
|
||||
public function storeLog(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'level' => 'required|string|in:info,warning,error',
|
||||
'message' => 'required|string',
|
||||
'context' => 'nullable|array',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->errorResponse('Validation error', 422, $validator->errors());
|
||||
}
|
||||
|
||||
// 檢查機台是否存在
|
||||
if (!Machine::where('id', $id)->exists()) {
|
||||
return $this->errorResponse('Machine not found', 404);
|
||||
}
|
||||
|
||||
// 丟入隊列進行異步處理,回傳 202 Accepted
|
||||
ProcessMachineLog::dispatch($id, $request->only(['level', 'message', 'context']));
|
||||
|
||||
return $this->successResponse([], 'Log accepted. Processing asynchronously.', 202);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Member\Member;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
@@ -12,7 +12,7 @@ class TrustProxies extends Middleware
|
||||
*
|
||||
* @var array<int, string>|string|null
|
||||
*/
|
||||
protected $proxies;
|
||||
protected $proxies = '*';
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
|
||||
46
app/Jobs/Machine/ProcessMachineLog.php
Normal file
46
app/Jobs/Machine/ProcessMachineLog.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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 ProcessMachineLog implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $machineId;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $logData;
|
||||
|
||||
public function __construct(int $machineId, array $logData)
|
||||
{
|
||||
$this->machineId = $machineId;
|
||||
$this->logData = $logData;
|
||||
}
|
||||
|
||||
public function getMachineId(): int
|
||||
{
|
||||
return $this->machineId;
|
||||
}
|
||||
|
||||
public function handle(MachineService $service): void
|
||||
{
|
||||
try {
|
||||
$service->recordLog($this->machineId, $this->logData);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to process machine log for machine {$this->machineId}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Machine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'location',
|
||||
@@ -1,12 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MachineLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'level',
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\System;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\System;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
if (!$this->app->isLocal()) {
|
||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
app/Services/Machine/MachineService.php
Normal file
45
app/Services/Machine/MachineService.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineLog;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MachineService
|
||||
{
|
||||
/**
|
||||
* 處理機台日誌寫入與狀態更新
|
||||
*/
|
||||
public function recordLog(int $machineId, array $data): MachineLog
|
||||
{
|
||||
$machine = Machine::findOrFail($machineId);
|
||||
|
||||
// 建立日誌紀錄
|
||||
$log = $machine->logs()->create([
|
||||
'level' => $data['level'] ?? 'info',
|
||||
'message' => $data['message'],
|
||||
'context' => $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';
|
||||
}
|
||||
}
|
||||
49
app/Traits/ApiResponse.php
Normal file
49
app/Traits/ApiResponse.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
trait ApiResponse
|
||||
{
|
||||
/**
|
||||
* 回傳成功的回應
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function successResponse($data = [], string $message = 'OK', int $code = 200): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => empty($data) ? new \stdClass() : $data, // 確保前端收到的是 Object 而非 Empty Array
|
||||
], $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回傳錯誤的回應
|
||||
*
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param mixed $errors
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function errorResponse(string $message, int $code = 400, $errors = null): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => false,
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if (!is_null($errors)) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
}
|
||||
49
artisan
49
artisan
@@ -1,53 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register The Auto Loader
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Composer provides a convenient, automatically generated class loader
|
||||
| for our application. We just need to utilize it! We'll require it
|
||||
| into the script here so that we do not have to worry about the
|
||||
| loading of any of our classes manually. It's great to relax.
|
||||
|
|
||||
*/
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Run The Artisan Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When we run the console application, the current CLI command will be
|
||||
| executed in this console and the response sent back to a terminal
|
||||
| or another output device for the developers. Here goes nothing!
|
||||
|
|
||||
*/
|
||||
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
|
||||
$status = $kernel->handle(
|
||||
$input = new Symfony\Component\Console\Input\ArgvInput,
|
||||
new Symfony\Component\Console\Output\ConsoleOutput
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Shutdown The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Once Artisan has finished running, we will fire off the shutdown events
|
||||
| so that any final work may be done by the application before we shut
|
||||
| down the process. This is the last thing to happen to the request.
|
||||
|
|
||||
*/
|
||||
|
||||
$kernel->terminate($input, $status);
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
|
||||
@@ -2,24 +2,27 @@
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.3",
|
||||
"laravel/tinker": "^2.8"
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/breeze": "^1.29",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^7.0",
|
||||
"phpunit/phpunit": "^10.1",
|
||||
"spatie/laravel-ignition": "^2.0"
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/breeze": "^2.0",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -64,4 +67,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
2631
composer.lock
generated
2631
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ return [
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\User::class,
|
||||
'model' => App\Models\System\User::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
23
database/factories/Machine/MachineFactory.php
Normal file
23
database/factories/Machine/MachineFactory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class MachineFactory extends Factory
|
||||
{
|
||||
protected $model = Machine::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Machine-' . fake()->unique()->numberBetween(101, 999),
|
||||
'location' => fake()->address(),
|
||||
'status' => fake()->randomElement(['online', 'offline', 'error']),
|
||||
'temperature' => fake()->randomFloat(2, 2, 10),
|
||||
'firmware_version' => 'v' . fake()->randomElement(['1.0.0', '1.1.2', '2.0.1']),
|
||||
'last_heartbeat_at' => fake()->dateTimeBetween('-1 day', 'now'),
|
||||
];
|
||||
}
|
||||
}
|
||||
51
database/factories/Machine/MachineLogFactory.php
Normal file
51
database/factories/Machine/MachineLogFactory.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories\Machine;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineLog;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class MachineLogFactory extends Factory
|
||||
{
|
||||
protected $model = MachineLog::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$messages = [
|
||||
'info' => [
|
||||
'機台啟動完成',
|
||||
'系統心跳上報',
|
||||
'交易成功 (訂單 #'.fake()->numberBetween(1000, 9999).')',
|
||||
'補貨作業完成',
|
||||
'環境溫度穩定 (24C)',
|
||||
],
|
||||
'warning' => [
|
||||
'貨道 A3 庫存偏低',
|
||||
'通訊品質不穩定',
|
||||
'感測器回報數值異常',
|
||||
'機門開啟次數過多',
|
||||
],
|
||||
'error' => [
|
||||
'馬達轉動失效 (貨道 B2)',
|
||||
'硬幣器卡幣',
|
||||
'散熱風扇停止運作',
|
||||
'電源供應模組故障',
|
||||
'網路連線中斷',
|
||||
]
|
||||
];
|
||||
|
||||
$level = fake()->randomElement(['info', 'warning', 'error']);
|
||||
|
||||
return [
|
||||
'machine_id' => Machine::factory(),
|
||||
'level' => $level,
|
||||
'message' => fake()->randomElement($messages[$level]),
|
||||
'context' => [
|
||||
'ip' => fake()->ipv4(),
|
||||
'uptime' => fake()->numberBetween(1000, 100000),
|
||||
],
|
||||
'created_at' => fake()->dateTimeBetween('-1 day', 'now'),
|
||||
];
|
||||
}
|
||||
}
|
||||
27
database/factories/Member/MemberFactory.php
Normal file
27
database/factories/Member/MemberFactory.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories\Member;
|
||||
|
||||
use App\Models\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MemberFactory extends Factory
|
||||
{
|
||||
protected $model = Member::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'phone' => '09' . fake()->numberBetween(10000000, 99999999),
|
||||
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
'birthday' => fake()->date(),
|
||||
'gender' => fake()->randomElement(['male', 'female', 'other']),
|
||||
'is_active' => true,
|
||||
'email_verified_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\System\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call([
|
||||
AdminUserSeeder::class,
|
||||
MachineSeeder::class,
|
||||
MemberSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
21
database/seeders/MachineSeeder.php
Normal file
21
database/seeders/MachineSeeder.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine\MachineLog;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MachineSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// 建立 50 台機台
|
||||
Machine::factory()->count(50)->create()->each(function ($machine) {
|
||||
// 每台機台隨機建立 5-10 筆初始日誌
|
||||
MachineLog::factory()->count(rand(5, 10))->create([
|
||||
'machine_id' => $machine->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
22
database/seeders/MemberSeeder.php
Normal file
22
database/seeders/MemberSeeder.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Member\Member;
|
||||
use App\Models\Member\MemberWallet;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MemberSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// 建立 20 位會員並分配錢包
|
||||
Member::factory()->count(20)->create()->each(function ($member) {
|
||||
MemberWallet::create([
|
||||
'member_id' => $member->id,
|
||||
'balance' => fake()->randomFloat(2, 100, 5000),
|
||||
'bonus_balance' => fake()->randomFloat(2, 0, 500),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
|
||||
@@ -1,55 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Check If The Application Is Under Maintenance
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If the application is in maintenance / demo mode via the "down" command
|
||||
| we will load this file so that any pre-rendered content can be shown
|
||||
| instead of starting the framework, which could cause an exception.
|
||||
|
|
||||
*/
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register The Auto Loader
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Composer provides a convenient, automatically generated class loader for
|
||||
| this application. We just need to utilize it! We'll simply require it
|
||||
| into the script here so we don't need to manually load our classes.
|
||||
|
|
||||
*/
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Run The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Once we have the application, we can handle the incoming request using
|
||||
| the application's HTTP kernel. Then, we will send the response back
|
||||
| to this client's browser, allowing them to enjoy our application.
|
||||
|
|
||||
*/
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$kernel = $app->make(Kernel::class);
|
||||
|
||||
$response = $kernel->handle(
|
||||
$request = Request::capture()
|
||||
)->send();
|
||||
|
||||
$kernel->terminate($request, $response);
|
||||
$app->handleRequest(Request::capture());
|
||||
|
||||
34
refactor_tests2.php
Normal file
34
refactor_tests2.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
$dirs = ['tests', 'database', 'app'];
|
||||
$files = [];
|
||||
|
||||
foreach ($dirs as $dirName) {
|
||||
// Note: sail runs in /var/www/html
|
||||
if (!is_dir(__DIR__ . '/' . $dirName)) continue;
|
||||
$dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/' . $dirName));
|
||||
foreach ($dir as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$replacements = [
|
||||
'App\Models\User' => 'App\Models\System\User',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
$original = $content;
|
||||
|
||||
foreach ($replacements as $old => $new) {
|
||||
$content = str_replace($old, $new, $content);
|
||||
}
|
||||
|
||||
if ($content !== $original) {
|
||||
file_put_contents($file, $content);
|
||||
echo "Updated: $file\n";
|
||||
}
|
||||
}
|
||||
echo "Done.\n";
|
||||
@@ -2,8 +2,123 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-display: 'Outfit', var(--font-sans);
|
||||
--color-luxury-deep: #0f172a;
|
||||
--color-luxury-card: #1e293b;
|
||||
--color-accent: #06b6d4;
|
||||
/* Cyan 500 */
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.font-display {
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Luxury Cards */
|
||||
.luxury-card {
|
||||
@apply bg-white dark:bg-[#1e293b] border-0;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.luxury-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Luxury Gradient Accents */
|
||||
.bg-luxury-gradient {
|
||||
background: linear-gradient(135deg, var(--color-accent), #6366f1);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-luxury-in {
|
||||
animation: fadeUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar - Minimal & Elegant */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #94a3b8;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #94a3b8 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.luxury-nav-item.active {
|
||||
@apply bg-slate-100 text-slate-900;
|
||||
@apply dark:bg-white/10 dark:text-white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.luxury-nav-item.active::before {
|
||||
content: "";
|
||||
@apply absolute left-0 w-1 h-5 bg-cyan-500 rounded-full shadow-[0_0_8px_rgba(6,182,212,0.5)];
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Submenu styling */
|
||||
.luxury-submenu {
|
||||
@apply mt-2 space-y-1 ps-4 border-l border-slate-200 ms-4;
|
||||
@apply dark:border-white/10;
|
||||
}
|
||||
}
|
||||
@@ -5,151 +5,116 @@
|
||||
<!-- Grid -->
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
總銷售額
|
||||
</p>
|
||||
<div class="hs-tooltip">
|
||||
<div class="hs-tooltip-toggle">
|
||||
<svg class="flex-shrink-0 w-4 h-4 text-gray-500" 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="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded shadow-sm dark:bg-slate-700" role="tooltip">
|
||||
本月累計銷售總額
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
$72,540
|
||||
</h3>
|
||||
<span class="flex items-center gap-x-1 text-green-600">
|
||||
<svg class="inline-block w-4 h-4 self-center" 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"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
|
||||
<span class="inline-block text-sm">
|
||||
1.7%
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
總營收 (餘額)
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-baseline gap-x-2">
|
||||
<h3 class="text-3xl font-bold text-slate-800 dark:text-white">${{ number_format($totalRevenue, 2) }}</h3>
|
||||
<span class="text-xs font-medium text-cyan-500 bg-cyan-500/10 px-1.5 py-0.5 rounded">實時</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
活躍機台
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
124
|
||||
</h3>
|
||||
<span class="flex items-center gap-x-1 text-red-600">
|
||||
<svg class="inline-block w-4 h-4 self-center" 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"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/></svg>
|
||||
<span class="inline-block text-sm">
|
||||
0.3%
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in" style="animation-delay: 100ms">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
運作中機台
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h3 class="text-3xl font-bold text-slate-800 dark:text-white">{{ $activeMachines }} 台</h3>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
庫存警告
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
12
|
||||
</h3>
|
||||
</div>
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
待處理告警
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h3 class="text-3xl font-bold text-rose-500">{{ $alertsPending }} 則訊號</h3>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
本月新增會員
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
28
|
||||
</h3>
|
||||
</div>
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in" style="animation-delay: 300ms">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
會員總數
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-baseline gap-x-2">
|
||||
<h3 class="text-3xl font-bold text-slate-800 dark:text-white">{{ number_format($memberCount) }}</h3>
|
||||
<span class="text-xs font-medium text-emerald-500">人</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
</div>
|
||||
<!-- End Grid -->
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-4 sm:gap-6">
|
||||
<!-- Card -->
|
||||
<div class="p-4 md:p-5 min-h-[410px] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-sm text-gray-500 dark:text-gray-400">
|
||||
營收趨勢
|
||||
</h2>
|
||||
<p class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
$123,450
|
||||
</p>
|
||||
<div class="grid lg:grid-cols-3 gap-6">
|
||||
<!-- Chart Column -->
|
||||
<div class="lg:col-span-2 luxury-card rounded-3xl p-6 animate-luxury-in" style="animation-delay: 400ms">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-white">營收績效分析</h2>
|
||||
<p class="text-sm text-slate-400">各地區每日營收洞察</p>
|
||||
</div>
|
||||
|
||||
<div x-data="{ open: false, selected: '最近 7 天' }" class="relative inline-block text-left">
|
||||
<button @click="open = !open" type="button" class="inline-flex items-center gap-x-2 px-3 py-2 text-sm font-semibold text-slate-700 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-all shadow-sm">
|
||||
<span x-text="selected"></span>
|
||||
<svg class="shrink-0 w-4 h-4 transition-transform" :class="open ? 'rotate-180' : ''" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<div x-show="open"
|
||||
@click.away="open = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-40 z-10 origin-top-right bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl p-1 focus:outline-none"
|
||||
x-cloak>
|
||||
<button @click="selected = '最近 7 天'; open = false" class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 text-slate-700 dark:text-slate-300 transition-colors">最近 7 天</button>
|
||||
<button @click="selected = '最近 30 天'; open = false" class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 text-slate-700 dark:text-slate-300 transition-colors">最近 30 天</button>
|
||||
<button @click="selected = '最近 90 天'; open = false" class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 text-slate-700 dark:text-slate-300 transition-colors">最近 90 天</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="py-[5px] px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-md bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500">
|
||||
<svg class="inline-block w-3.5 h-3.5" 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="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
|
||||
25%
|
||||
</span>
|
||||
<div class="h-[300px] relative group overflow-hidden rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 flex items-center justify-center transition-all">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-transparent pointer-events-none"></div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-white dark:bg-slate-800 shadow-sm border border-slate-200 dark:border-slate-700 mb-4">
|
||||
<svg class="w-6 h-6 text-cyan-500" 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="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-slate-600 dark:text-slate-400">營收圖表數據載入中</p>
|
||||
<p class="text-xs text-slate-400 mt-1">即時數據串流將在此顯示</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Header -->
|
||||
|
||||
<div id="hs-multiple-bar-charts"></div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="p-4 md:p-5 min-h-[410px] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-sm text-gray-500 dark:text-gray-400">
|
||||
訪客分析
|
||||
</h2>
|
||||
<p class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
92,913
|
||||
</p>
|
||||
<!-- 即時動態 -->
|
||||
<div class="luxury-card rounded-3xl p-6 animate-luxury-in" style="animation-delay: 500ms">
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-white mb-6">即時動態</h2>
|
||||
<div class="space-y-6">
|
||||
@forelse($latestActivities as $activity)
|
||||
<div class="relative pl-6 before:absolute before:left-0 before:top-1 before:bottom-0 before:w-0.5 before:bg-slate-100 dark:before:bg-slate-800">
|
||||
<div class="absolute left-[-4px] top-0 w-2.5 h-2.5 rounded-full {{ $activity->level === 'error' ? 'bg-rose-500' : 'bg-cyan-500' }} border-2 border-white dark:border-[#1e293b]"></div>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-200">#{{ $activity->machine->code ?? 'N/A' }} {{ $activity->content }}</p>
|
||||
<p class="text-xs text-slate-400">{{ $activity->created_at->diffForHumans() }} • {{ $activity->machine->location ?? '未知區域' }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-10">
|
||||
<p class="text-sm text-slate-400">目前尚無動態資料</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="py-[5px] px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-md bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500">
|
||||
<svg class="inline-block w-3.5 h-3.5" 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="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
|
||||
11%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Header -->
|
||||
|
||||
<div id="hs-single-area-chart"></div>
|
||||
<button class="w-full mt-8 py-3 text-sm font-bold text-slate-500 hover:text-cyan-500 transition-colors uppercase">查看所有日誌 →</button>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -1,74 +1,80 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
@endphp
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">機台管理</h3>
|
||||
<a href="{{ route('admin.machines.create') }}" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
|
||||
新增機台
|
||||
</a>
|
||||
</div>
|
||||
@section('header')
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('機台管理') }}
|
||||
</h2>
|
||||
@endsection
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="flex flex-col">
|
||||
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200 dark:border-gray-700">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
@section('content')
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg border border-gray-200">
|
||||
<div class="p-6 text-gray-900">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-lg font-medium">機台列表</h3>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a>
|
||||
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a>
|
||||
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">名稱</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">位置</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">狀態</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">溫度</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">最後心跳</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">操作</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名稱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">位置</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">狀態</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">溫度</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最後心跳</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
@foreach($machines as $machine)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm leading-5 font-medium text-gray-900 dark:text-gray-200">{{ $machine->name }}</div>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach ($machines as $machine)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $machine->name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ $machine->firmware_version }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm leading-5 text-gray-600 dark:text-gray-400">{{ $machine->location ?? '-' }}</div>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $machine->location }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
|
||||
@if($machine->status === 'online')
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">連線中</span>
|
||||
@elseif($machine->status === 'offline')
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">離線</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">異常</span>
|
||||
@endif
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@php
|
||||
$statusClasses = [
|
||||
'online' => 'bg-green-100 text-green-800',
|
||||
'offline' => 'bg-gray-100 text-gray-800',
|
||||
'error' => 'bg-red-100 text-red-800',
|
||||
];
|
||||
$class = $statusClasses[$machine->status] ?? 'bg-blue-100 text-blue-800';
|
||||
@endphp
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $class }}">
|
||||
{{ strtoupper($machine->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm leading-5 text-gray-600 dark:text-gray-400">{{ $machine->temperature ? $machine->temperature . '°C' : '-' }}</div>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $machine->temperature ?? '--' }} °C
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="text-sm leading-5 text-gray-600 dark:text-gray-400">{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '-' }}</div>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '從未連線' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700 text-sm leading-5 font-medium">
|
||||
<a href="{{ route('admin.machines.show', $machine) }}" class="text-indigo-400 hover:text-indigo-600 mr-3">查看</a>
|
||||
<a href="{{ route('admin.machines.edit', $machine) }}" class="text-yellow-400 hover:text-yellow-600 mr-3">編輯</a>
|
||||
<form action="{{ route('admin.machines.destroy', $machine) }}" method="POST" class="inline-block" onsubmit="return confirm('確定要刪除嗎?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-400 hover:text-red-600">刪除</button>
|
||||
</form>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ route('admin.machines.show', $machine->id) }}" class="text-indigo-600 hover:text-indigo-900 bg-indigo-50 px-3 py-1 rounded-md transition border border-indigo-100">查看日誌</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
{{ $machines->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{{ $machines->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -1,78 +1,85 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
@section('header')
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">機台詳情:{{ $machine->name }}</h3>
|
||||
<div>
|
||||
<a href="{{ route('admin.machines.edit', $machine) }}" class="bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded mr-2">
|
||||
編輯
|
||||
</a>
|
||||
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded">
|
||||
返回列表
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
機台詳情: {{ $machine->name }}
|
||||
</h2>
|
||||
<a href="{{ route('admin.machines.index') }}" class="text-sm text-gray-600 hover:text-gray-900">← 返回列表</a>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Basic Info -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold text-gray-200 mb-4">基本資訊</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@section('content')
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<!-- 基本資訊卡片 -->
|
||||
<div class="bg-white shadow sm:rounded-lg p-6 border border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4 border-b pb-2">基本資訊</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-400">位置</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->location ?? '-' }}</p>
|
||||
<p class="text-xs text-gray-500 uppercase">當前狀態</p>
|
||||
<p class="text-lg font-bold {{ $machine->status === 'online' ? 'text-green-600' : ($machine->status === 'error' ? 'text-red-600' : 'text-gray-600') }}">
|
||||
{{ strtoupper($machine->status) }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-400">狀態</p>
|
||||
@if($machine->status === 'online')
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">連線中</span>
|
||||
@elseif($machine->status === 'offline')
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">離線</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">異常</span>
|
||||
@endif
|
||||
<p class="text-xs text-gray-500 uppercase">位置</p>
|
||||
<p class="text-sm">{{ $machine->location }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-400">溫度</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->temperature ? $machine->temperature . '°C' : '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-400">韌體版本</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->firmware_version ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-400">最後心跳</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '-' }}</p>
|
||||
<p class="text-xs text-gray-500 uppercase">最後心跳時間</p>
|
||||
<p class="text-sm">{{ $machine->last_heartbeat_at ?? 'N/A' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold text-gray-200 mb-4">最近日誌</h4>
|
||||
<div class="overflow-y-auto max-h-64">
|
||||
<ul class="divide-y divide-gray-700">
|
||||
@forelse($machine->logs()->latest()->take(10)->get() as $log)
|
||||
<li class="py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
@if($log->level === 'error')
|
||||
<span class="h-2 w-2 rounded-full bg-red-500 mr-2"></span>
|
||||
@elseif($log->level === 'warning')
|
||||
<span class="h-2 w-2 rounded-full bg-yellow-500 mr-2"></span>
|
||||
@else
|
||||
<span class="h-2 w-2 rounded-full bg-blue-500 mr-2"></span>
|
||||
<!-- 日誌顯示區 -->
|
||||
<div class="bg-gray-900 shadow sm:rounded-lg overflow-hidden border border-gray-700">
|
||||
<div class="p-4 bg-gray-800 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-md font-medium text-gray-200">即時操作日誌 (最後 50 筆)</h3>
|
||||
<span class="text-xs text-gray-400">所有時間為系統時區</span>
|
||||
</div>
|
||||
<div class="p-0 max-h-[600px] overflow-y-auto">
|
||||
<table class="min-w-full divide-y divide-gray-800 font-mono text-xs">
|
||||
<thead class="bg-gray-800 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-gray-500">時間</th>
|
||||
<th class="px-4 py-2 text-left text-gray-500">層級</th>
|
||||
<th class="px-4 py-2 text-left text-gray-500">訊息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
@forelse ($machine->logs as $log)
|
||||
<tr class="hover:bg-gray-800/50">
|
||||
<td class="px-4 py-2 text-gray-400 whitespace-nowrap">{{ $log->created_at->format('Y-m-d H:i:s') }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@php
|
||||
$levelClasses = [
|
||||
'info' => 'text-blue-400',
|
||||
'warning' => 'text-yellow-400',
|
||||
'error' => 'text-red-400 font-bold',
|
||||
];
|
||||
@endphp
|
||||
<span class="{{ $levelClasses[$log->level] ?? 'text-gray-300' }}">
|
||||
[{{ strtoupper($log->level) }}]
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-200">
|
||||
{{ $log->message }}
|
||||
@if($log->context)
|
||||
<div class="text-[10px] text-gray-500 mt-1">
|
||||
{{ json_encode($log->context) }}
|
||||
</div>
|
||||
@endif
|
||||
<p class="text-sm text-gray-900 dark:text-gray-300">{{ $log->message }}</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{{ $log->created_at->format('m/d H:i') }}</span>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="py-3 text-center text-gray-500">尚無日誌</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-8 text-center text-gray-500 italic">暫無相關日誌</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
<title>{{ config('app.name', 'Star Cloud') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
<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">
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
@@ -23,7 +24,7 @@
|
||||
</script>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-slate-900 antialiased font-sans h-full" x-data="{ sidebarOpen: false, userDropdownOpen: false }">
|
||||
<body class="bg-gray-50 dark:bg-[#0f172a] antialiased font-sans h-full selection:bg-indigo-100 dark:selection:bg-indigo-900/40" x-data="{ sidebarOpen: false, userDropdownOpen: false }">
|
||||
|
||||
<!-- Sidebar Overlay (Mobile) -->
|
||||
<div x-show="sidebarOpen"
|
||||
@@ -38,7 +39,7 @@
|
||||
x-cloak></div>
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white border-b text-sm py-2.5 sm:py-4 lg:pl-64 dark:bg-gray-800 dark:border-gray-700">
|
||||
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white/80 backdrop-blur-md border-b border-slate-200/50 text-sm py-2.5 sm:py-4 lg:pl-64 dark:bg-[#0f172a]/80 dark:border-slate-800/50 shadow-sm">
|
||||
<nav class="flex basis-full items-center w-full mx-auto px-4 sm:px-6 md:px-8" aria-label="Global">
|
||||
<div class="mr-5 lg:mr-0 lg:hidden">
|
||||
<a class="flex-none text-xl font-semibold dark:text-white" href="#" aria-label="Brand">Star Cloud</a>
|
||||
@@ -144,21 +145,23 @@
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div id="application-sidebar"
|
||||
class="fixed top-0 left-0 bottom-0 z-[60] w-64 bg-white border-r border-gray-200 pt-7 pb-10 overflow-y-auto transition-transform duration-300 transform lg:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
class="fixed top-0 left-0 bottom-0 z-[60] w-64 bg-white dark:bg-[#0f172a] pt-5 pb-10 border-e border-slate-200 dark:border-none overflow-y-auto transition-transform duration-300 transform lg:translate-x-0 shadow-xl dark:shadow-2xl"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'">
|
||||
<!-- Close Button (Mobile) -->
|
||||
<button type="button" @click="sidebarOpen = false" class="absolute top-4 right-4 text-gray-500 hover:text-gray-700 lg:hidden dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<button type="button" @click="sidebarOpen = false" class="absolute top-4 right-4 text-slate-500 hover:text-slate-800 lg:hidden dark:text-slate-400 dark:hover:text-slate-200">
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<svg class="w-5 h-5" 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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="px-6">
|
||||
<a class="flex-none text-xl font-semibold dark:text-white" href="#" aria-label="Brand">Star Cloud</a>
|
||||
<div class="px-6 mb-4">
|
||||
<a class="flex-none text-2xl font-bold text-slate-900 dark:text-white font-display tracking-tight" href="#" aria-label="Brand">
|
||||
Star<span class="text-cyan-500">Cloud</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="p-6 w-full flex flex-col flex-wrap">
|
||||
<nav class="px-6 py-2 w-full flex flex-col flex-wrap">
|
||||
<ul class="space-y-1.5">
|
||||
@include('layouts.partials.sidebar-menu')
|
||||
</ul>
|
||||
@@ -168,7 +171,7 @@
|
||||
|
||||
<!-- Content -->
|
||||
<div class="w-full pt-10 px-4 sm:px-6 md:px-8 lg:pl-72">
|
||||
<main>
|
||||
<main class="animate-fade-up">
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{-- 1. 儀表板 --}}
|
||||
<li>
|
||||
<a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.dashboard') ? 'bg-gray-100 dark:bg-gray-900 text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.dashboard') }}">
|
||||
<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"><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>
|
||||
<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>
|
||||
儀表板
|
||||
</a>
|
||||
</li>
|
||||
@@ -9,15 +9,15 @@
|
||||
{{-- 2. 應用程式 (個人) --}}
|
||||
<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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<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"><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>
|
||||
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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<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 text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('profile.edit') ? 'text-blue-600 dark:text-blue-500' : '' }}" 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 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('profile.edit') }}">
|
||||
個人檔案
|
||||
</a>
|
||||
</li>
|
||||
@@ -27,151 +27,151 @@
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.members.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.members.index') }}">會員列表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.membership-tiers.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.membership-tiers.index') }}">會員等級</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.deposit-bonus-rules.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.deposit-bonus-rules.index') }}">儲值回饋</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.point-rules.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.point-rules.index') }}">點數規則</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.gift-definitions.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.gift-definitions.index') }}">禮品設定</a></li>
|
||||
<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') }}">會員列表</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') }}">會員等級</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') }}">儲值回饋</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') }}">點數規則</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') }}">禮品設定</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.logs') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.logs') }}">機台日誌</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.index') }}">機台列表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.permissions') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.permissions') }}">機台權限</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.utilization') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.utilization') }}">機台稼動率</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.expiry') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.expiry') }}">效期管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.maintenance') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.maintenance') }}">維修管理單</a></li>
|
||||
<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') }}">機台日誌</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') }}">機台列表</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') }}">機台權限</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') }}">機台稼動率</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') }}">效期管理</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') }}">維修管理單</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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>
|
||||
APP管理
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.ui-elements') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.ui-elements') }}">UI元素</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.helper') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.helper') }}">小幫手</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.questionnaire') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.questionnaire') }}">問卷</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.games') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.games') }}">互動遊戲</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.timer') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.timer') }}">計時器</a></li>
|
||||
<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元素</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') }}">小幫手</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') }}">問卷</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') }}">互動遊戲</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') }}">計時器</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.index') }}">倉庫列表(全)</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.personal') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.personal') }}">倉庫列表(個)</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.stock-management') }}">庫存管理單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.transfers') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.transfers') }}">調撥單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.purchases') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.purchases') }}">採購單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.replenishments') }}">機台補貨單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.replenishment-records') }}">機台補貨紀錄</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.machine-stock') }}">機台庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.staff-stock') }}">人員庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.returns') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.returns') }}">回庫單</a></li>
|
||||
<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') }}">倉庫列表(全)</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') }}">倉庫列表(個)</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') }}">庫存管理單</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') }}">調撥單</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') }}">採購單</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') }}">機台補貨單</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') }}">機台補貨紀錄</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') }}">機台庫存</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') }}">人員庫存</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') }}">回庫單</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.index') }}">銷售紀錄</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.pickup-codes') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.pickup-codes') }}">取貨碼</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.orders') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.orders') }}">購買單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.promotions') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.promotions') }}">促銷時段</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.pass-codes') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.pass-codes') }}">通行碼</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.store-gifts') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.store-gifts') }}">來店禮</a></li>
|
||||
<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') }}">銷售紀錄</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') }}">取貨碼</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') }}">購買單</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') }}">促銷時段</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') }}">通行碼</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') }}">來店禮</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.change-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.change-stock') }}">零錢庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.machine-reports') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.machine-reports') }}">機台報表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.product-reports') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.product-reports') }}">商品報表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.survey-analysis') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.survey-analysis') }}">問卷分析</a></li>
|
||||
<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') }}">零錢庫存</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') }}">機台報表</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') }}">商品報表</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') }}">問卷分析</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.audit.purchases') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.audit.purchases') }}">採購單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.audit.transfers') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.audit.transfers') }}">調撥單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.audit.replenishments') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.audit.replenishments') }}">補貨單</a></li>
|
||||
<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') }}">採購單</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') }}">調撥單</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') }}">補貨單</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.products') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.products') }}">商品管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.advertisements') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.advertisements') }}">廣告管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.admin-products') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.admin-products') }}">管理者可賣</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.accounts') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.accounts') }}">帳號管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.sub-accounts') }}">子帳號</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.sub-account-roles') }}">子帳號角色</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.points') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.points') }}">點數設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.badges') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.badges') }}">識別證</a></li>
|
||||
<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') }}">商品管理</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') }}">廣告管理</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') }}">管理者可賣</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.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.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-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') }}">子帳號</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') }}">子帳號角色</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') }}">點數設定</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') }}">識別證</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -179,100 +179,100 @@
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.stock') }}">機台庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.restart') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.restart') }}">機台重啟</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.restart-card-reader') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.restart-card-reader') }}">卡機重啟</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.checkout') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.checkout') }}">遠端結帳</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.lock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.lock') }}">遠端鎖定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.change') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.change') }}">遠端找零</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.dispense') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.dispense') }}">遠端出貨</a></li>
|
||||
<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') }}">機台庫存</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') }}">機台重啟</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') }}">卡機重啟</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') }}">遠端結帳</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') }}">遠端鎖定</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') }}">遠端找零</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') }}">遠端出貨</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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>
|
||||
Line管理
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.members') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.members') }}">Line會員</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.machines') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.machines') }}">Line機台</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.products') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.products') }}">Line商品</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.official-account') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.official-account') }}">Line生活圈</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.orders') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.orders') }}">Line訂單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.coupons') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.coupons') }}">Line優惠券</a></li>
|
||||
<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會員</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機台</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商品</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生活圈</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訂單</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優惠券</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.members') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.members') }}">預約會員</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.stores') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.stores') }}">店家管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.time-slots') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.time-slots') }}">時段組合</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.venues') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.venues') }}">場地管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.coupons') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.coupons') }}">優惠券</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.reservations') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.reservations') }}">預約管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.orders') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.orders') }}">訂單管理</a></li>
|
||||
<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') }}">預約會員</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') }}">店家管理</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') }}">時段組合</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') }}">場地管理</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') }}">優惠券</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') }}">預約管理</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') }}">訂單管理</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.special-permission.clear-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.special-permission.clear-stock') }}">庫存清空</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.special-permission.apk-versions') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.special-permission.apk-versions') }}">APK版本</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.special-permission.discord-notifications') }}">Discord通知</a></li>
|
||||
<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') }}">庫存清空</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版本</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通知</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{{-- 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="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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="ms-auto w-4 h-4 transition-transform duration-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>
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
|
||||
<ul class="pt-2 ps-2">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.app-features') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.app-features') }}">APP功能</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.data-config') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.data-config') }}">資料設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.sales') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.sales') }}">銷售管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.machines') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.machines') }}">機台管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.warehouses') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.warehouses') }}">倉庫管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.analysis') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.analysis') }}">分析管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.audit') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.audit') }}">稽核管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.remote') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.remote') }}">遠端管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.line') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.line') }}">Line管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.roles') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.roles') }}">角色設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.others') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.others') }}">其他功能</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.ai-prediction') }}">AI智能預測</a></li>
|
||||
<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.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功能</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') }}">資料設定</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') }}">銷售管理</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') }}">機台管理</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') }}">倉庫管理</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') }}">分析管理</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') }}">稽核管理</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') }}">遠端管理</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管理</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') }}">角色設定</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') }}">其他功能</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智能預測</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -2,39 +2,53 @@
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\MemberController;
|
||||
use App\Http\Controllers\Api\V1\MemberController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register API routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider and all of them will
|
||||
| be assigned to the "api" middleware group. Make something great!
|
||||
| 這裡註冊所有的 API 路由,預設套用 api middleware group。
|
||||
| 加入 v1 前綴與 throttle 進行速率限制防護。
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
|
||||
|
||||
// 基本的使用者資料查詢
|
||||
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 會員 API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 會員 API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 公開路由(無需認證)
|
||||
Route::prefix('members')->group(function () {
|
||||
Route::post('/register', [MemberController::class, 'register']);
|
||||
Route::post('/login', [MemberController::class, 'login']);
|
||||
Route::post('/social-login', [MemberController::class, 'socialLogin']);
|
||||
});
|
||||
|
||||
// 公開路由(無需認證)
|
||||
Route::prefix('members')->group(function () {
|
||||
Route::post('/register', [MemberController::class, 'register']);
|
||||
Route::post('/login', [MemberController::class, 'login']);
|
||||
Route::post('/social-login', [MemberController::class, 'socialLogin']);
|
||||
});
|
||||
// 需認證路由
|
||||
Route::prefix('members')->middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/profile', [MemberController::class, 'profile']);
|
||||
Route::put('/profile', [MemberController::class, 'updateProfile']);
|
||||
Route::post('/logout', [MemberController::class, 'logout']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 機台 API Routes (IoT)
|
||||
|--------------------------------------------------------------------------
|
||||
| 專門用於機台通訊,頻率較高,建議搭配異步處理。
|
||||
*/
|
||||
Route::prefix('machines')->group(function () {
|
||||
Route::post('/{id}/logs', [\App\Http\Controllers\Api\V1\MachineController::class, 'storeLog']);
|
||||
});
|
||||
|
||||
// 需認證路由
|
||||
Route::prefix('members')->middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/profile', [MemberController::class, 'profile']);
|
||||
Route::put('/profile', [MemberController::class, 'updateProfile']);
|
||||
Route::post('/logout', [MemberController::class, 'logout']);
|
||||
});
|
||||
|
||||
68
tests/Feature/Api/V1/MachineLogTest.php
Normal file
68
tests/Feature/Api/V1/MachineLogTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1;
|
||||
|
||||
use App\Jobs\Machine\ProcessMachineLog;
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MachineLogTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* 測試機台日誌 API 是否能正確接收並派發 Job
|
||||
*/
|
||||
public function test_machine_can_send_log_and_dispatch_job(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
$machine = Machine::factory()->create();
|
||||
|
||||
$response = $this->postJson("/api/v1/machines/{$machine->id}/logs", [
|
||||
'level' => 'info',
|
||||
'message' => 'Test log message',
|
||||
'context' => ['foo' => 'bar'],
|
||||
]);
|
||||
|
||||
$response->assertStatus(202)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'message' => 'Log accepted. Processing asynchronously.',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(ProcessMachineLog::class, function ($job) use ($machine) {
|
||||
return $job->getMachineId() === $machine->id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 測試不存在的機台應回傳 404
|
||||
*/
|
||||
public function test_send_log_to_non_existent_machine_returns_404(): void
|
||||
{
|
||||
$response = $this->postJson("/api/v1/machines/999/logs", [
|
||||
'level' => 'info',
|
||||
'message' => 'Should fail',
|
||||
]);
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* 測試屬性驗證失敗
|
||||
*/
|
||||
public function test_send_invalid_log_data_returns_422(): void
|
||||
{
|
||||
$machine = Machine::factory()->create();
|
||||
|
||||
$response = $this->postJson("/api/v1/machines/{$machine->id}/logs", [
|
||||
'level' => 'invalid-level', // 不符合 in:info,warning,error
|
||||
'message' => '', // 必填
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user