Compare commits
6 Commits
02918ce0e1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86b0cdb1e1 | ||
|
|
0a4872e6c9 | ||
|
|
c4978a389f | ||
|
|
c4807e23f2 | ||
|
|
fb7d0078c6 | ||
| 4bf4898e6a |
@@ -100,21 +100,34 @@ Request / Response 均採 JSON,個資欄位請遵守最小授權原則。
|
||||
|
||||
### 3) Machine (機台)
|
||||
|
||||
* **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:
|
||||
* 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Temperature stabilized at 23C",
|
||||
"context": { "temp": 23.0 }
|
||||
"temperature": 23.4,
|
||||
"status_code": "OK",
|
||||
"firmware_version": "1.2.3",
|
||||
"timestamp": "2025-11-20T15:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
* GET /api/v1/machines/{id}/logs
|
||||
|
||||
---
|
||||
|
||||
### 4) Orders / ShoppingCart
|
||||
528
.agent/rules/backend-rules.md
Normal file
528
.agent/rules/backend-rules.md
Normal file
@@ -0,0 +1,528 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
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` 會自動部署至正式站。
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
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** — 確保符合高併發處理架構
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
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 層級進行了基礎的參數驗證?
|
||||
@@ -1,5 +1,5 @@
|
||||
APP_NAME=starCloud
|
||||
COMPOSE_PROJECT_NAME=star-cloud
|
||||
APP_NAME=startCloud
|
||||
COMPOSE_PROJECT_NAME=start-cloud
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
@@ -25,7 +25,7 @@ LOG_LEVEL=debug
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=star-cloud
|
||||
DB_DATABASE=start-cloud
|
||||
DB_USERNAME=sail
|
||||
DB_PASSWORD=password
|
||||
# FORWARD_DB_PORT=3308
|
||||
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
echo "${{ secrets.DEMO_SSH_KEY }}" > ~/.ssh/id_rsa_demo
|
||||
chmod 600 ~/.ssh/id_rsa_demo
|
||||
rsync -avz --delete \
|
||||
--exclude='.git' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='vendor' \
|
||||
--exclude='storage' \
|
||||
--exclude='.env' \
|
||||
--exclude='public/build' \
|
||||
--exclude='/.git' \
|
||||
--exclude='/node_modules' \
|
||||
--exclude='/vendor' \
|
||||
--exclude='/storage' \
|
||||
--exclude='/.env' \
|
||||
--exclude='/public/build' \
|
||||
-e "ssh -p 2227 -i ~/.ssh/id_rsa_demo -o StrictHostKeyChecking=no" \
|
||||
./ root@220.132.7.82:/var/www/star-cloud-demo/
|
||||
rm ~/.ssh/id_rsa_demo
|
||||
@@ -92,9 +92,13 @@ jobs:
|
||||
|
||||
# 3. Laravel 初始化與優化
|
||||
php artisan migrate --force &&
|
||||
php artisan storage:link &&
|
||||
php artisan optimize:clear &&
|
||||
php artisan optimize &&
|
||||
php artisan view:cache
|
||||
php artisan view:cache &&
|
||||
php artisan queue:restart &&
|
||||
php artisan db:seed --class=RoleSeeder --force &&
|
||||
php artisan db:seed --class=AdminUserSeeder --force
|
||||
"
|
||||
docker exec star-cloud-demo-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||
|
||||
|
||||
404
README.md
404
README.md
@@ -1,392 +1,112 @@
|
||||
# Star Cloud 智能販賣機管理平台
|
||||
|
||||
> 基於 Docker 的全方位智能販賣機後台管理系統
|
||||
## 專案簡介 (Project Description)
|
||||
Star Cloud 是一個專為智能販賣機設計的後台管理系統,旨在提供全方位的機台監控、庫存管理、銷售分析與會員管理功能。透過此平台,管理者可以即時掌握機台運營狀態,優化補貨流程,並透過數據分析提升營運效益。
|
||||
|
||||
Star Cloud 是一個專為智能販賣機設計的後台管理系統,提供機台監控、庫存管理、銷售分析與會員管理等完整功能。本專案採用 Docker Compose 容器化架構,實現快速部署與環境一致性。
|
||||
|
||||
---
|
||||
|
||||
## 技術架構
|
||||
|
||||
### 容器化架構
|
||||
本專案完全運行在 Docker 容器中,包含以下服務:
|
||||
|
||||
| 服務 | 容器名稱 | 技術 | 用途 | 連接埠 |
|
||||
|------|---------|------|------|--------|
|
||||
| **應用程式** | star-cloud-laravel | Laravel 10 + PHP 8.5 | Web 應用與 API | 8090:80, 5175:5175 |
|
||||
| **資料庫** | star-cloud-mysql | MySQL 8.0 | 關聯式資料庫 | 3306:3306 |
|
||||
| **快取** | star-cloud-redis | Redis Alpine | 快取與 Session | 6380:6379 |
|
||||
|
||||
### 後端技術棧
|
||||
## 技術棧 (Technology Stack)
|
||||
|
||||
### 後端 (Backend)
|
||||
- **Framework**: Laravel 10.x
|
||||
- **Language**: PHP 8.5+
|
||||
- **Database**: MySQL 8.0
|
||||
- **Cache/Session**: Redis
|
||||
- **Authentication**: Laravel Sanctum (API Token)
|
||||
- **Package Manager**: Composer 2.x
|
||||
- **Language**: PHP 8.1+
|
||||
- **Database**: MySQL 8.0+
|
||||
- **Authentication**: Laravel Sanctum (API Token Authentication)
|
||||
- **Tools**: Composer
|
||||
|
||||
### 前端技術棧
|
||||
|
||||
- **Template Engine**: Blade Templates
|
||||
- **UI Library**: Preline UI 3.x (Tailwind CSS 組件庫)
|
||||
### 前端 (Frontend)
|
||||
- **Framework**: Blade Templates (Laravel 預設樣板引擎)
|
||||
- **CSS Framework**: Tailwind CSS 3.x
|
||||
- **JavaScript**: Alpine.js 3.x (輕量級互動框架)
|
||||
- **JavaScript**: Alpine.js 3.x
|
||||
- **Build Tool**: Vite 5.x
|
||||
- **HTTP Client**: Axios
|
||||
|
||||
---
|
||||
## 安裝與使用說明 (Installation & Usage)
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 前置需求
|
||||
請依照以下步驟將專案 Clone 至本地端並開始運行:
|
||||
|
||||
### 0. 前置需求 (Prerequisites)
|
||||
確保您的系統已安裝以下軟體:
|
||||
- PHP 8.1+
|
||||
- Composer
|
||||
- Node.js & npm
|
||||
- MySQL 8.0+
|
||||
|
||||
- **Docker** 20.10+
|
||||
- **Docker Compose** 2.0+
|
||||
- **Git**
|
||||
|
||||
> **提示**:Windows 使用者建議安裝 [Docker Desktop](https://www.docker.com/products/docker-desktop/),Linux 使用者可參考 [官方安裝文件](https://docs.docker.com/engine/install/)
|
||||
|
||||
### 安裝步驟
|
||||
|
||||
#### 1. Clone 專案
|
||||
若您尚未安裝 MySQL,Windows 使用者可至 [MySQL 官網](https://dev.mysql.com/downloads/installer/) 下載 Installer,或使用 XAMPP / Laragon 等整合環境。
|
||||
|
||||
### 1. 下載專案 (Clone Repository)
|
||||
```bash
|
||||
git clone <repository_url>
|
||||
cd star-cloud
|
||||
```
|
||||
|
||||
#### 2. 環境設定
|
||||
### 2. 安裝依賴套件 (Install Dependencies)
|
||||
|
||||
複製環境變數範例檔案:
|
||||
安裝後端 PHP 套件:
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
安裝前端 Node.js 套件:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. 環境變數設定 (Environment Setup)
|
||||
複製範例環境設定檔:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
**重要設定**(`.env` 檔案):
|
||||
|
||||
```env
|
||||
# 應用程式設定
|
||||
APP_NAME=Star Cloud
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8090
|
||||
|
||||
# 資料庫設定(對應 Docker Compose 服務)
|
||||
請開啟 `.env` 檔案,並依照您的本地環境設定資料庫連線資訊:
|
||||
```dotenv
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=star_cloud
|
||||
DB_USERNAME=sail
|
||||
DB_PASSWORD=password
|
||||
|
||||
# Redis 設定(對應 Docker Compose 服務)
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Vite 開發伺服器
|
||||
VITE_PORT=5175
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=your_password
|
||||
```
|
||||
|
||||
#### 3. 啟動 Docker 容器
|
||||
|
||||
啟動所有服務(應用程式、資料庫、Redis):
|
||||
|
||||
產生應用程式金鑰 (Application Key):
|
||||
```bash
|
||||
docker compose up -d
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
> **說明**:`-d` 參數表示背景執行
|
||||
|
||||
檢查容器狀態:
|
||||
|
||||
### 4. 資料庫遷移 (Database Migration)
|
||||
執行 Migration 以建立資料庫結構:
|
||||
```bash
|
||||
docker compose ps
|
||||
php artisan migrate
|
||||
```
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
預期輸出:
|
||||
```
|
||||
NAME STATUS PORTS
|
||||
star-cloud-laravel Up X minutes 0.0.0.0:8090->80/tcp, 0.0.0.0:5175->5175/tcp
|
||||
star-cloud-mysql Up X minutes 0.0.0.0:3306->3306/tcp
|
||||
star-cloud-redis Up X minutes 0.0.0.0:6380->6379/tcp
|
||||
```
|
||||
|
||||
#### 4. 初始化應用程式
|
||||
|
||||
**4.1 安裝後端依賴**
|
||||
### 4.1 預設管理員帳號 (Default Admin Account)
|
||||
執行上述指令後,系統會建立一組預設管理員帳號:
|
||||
- **Email**: `admin@star-cloud.com`
|
||||
- **Password**: `password`
|
||||
|
||||
### 5. 編譯前端資源 (Build Frontend Assets)
|
||||
啟動開發模式 (Hot Module Replacement):
|
||||
```bash
|
||||
docker compose exec laravel.test composer install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**4.2 產生應用程式金鑰**
|
||||
|
||||
或編譯生產環境檔案:
|
||||
```bash
|
||||
docker compose exec laravel.test php artisan key:generate
|
||||
npm run build
|
||||
```
|
||||
|
||||
**4.3 執行資料庫遷移與種子**
|
||||
|
||||
### 6. 啟動伺服器 (Start Server)
|
||||
啟動 Laravel 開發伺服器:
|
||||
```bash
|
||||
docker compose exec laravel.test php artisan migrate --seed
|
||||
php artisan serve --port=8001
|
||||
```
|
||||
|
||||
> **預設管理員帳號**:
|
||||
> - Email: `admin`
|
||||
> - Password: `password`
|
||||
|
||||
**4.4 安裝前端依賴**
|
||||
|
||||
```bash
|
||||
docker compose exec laravel.test npm install
|
||||
```
|
||||
|
||||
**4.5 編譯前端資源**
|
||||
|
||||
```bash
|
||||
# 開發模式(支援 Hot Module Replacement)
|
||||
docker compose exec laravel.test npm run dev
|
||||
|
||||
# 或生產模式
|
||||
docker compose exec laravel.test npm run build
|
||||
```
|
||||
|
||||
#### 5. 訪問應用程式
|
||||
|
||||
- **應用程式**: http://localhost:8090
|
||||
- **Vite Dev Server**: http://localhost:5175
|
||||
|
||||
---
|
||||
|
||||
## Docker 常用指令
|
||||
|
||||
### 容器管理
|
||||
|
||||
```bash
|
||||
# 啟動所有服務
|
||||
docker compose up -d
|
||||
|
||||
# 停止所有服務
|
||||
docker compose down
|
||||
|
||||
# 重啟服務
|
||||
docker compose restart
|
||||
|
||||
# 查看容器日誌
|
||||
docker compose logs -f laravel.test
|
||||
|
||||
# 進入應用程式容器
|
||||
docker compose exec laravel.test bash
|
||||
```
|
||||
|
||||
### Laravel 指令
|
||||
|
||||
所有 Laravel Artisan 指令需在容器內執行:
|
||||
|
||||
```bash
|
||||
# 執行 Artisan 指令
|
||||
docker compose exec laravel.test php artisan <command>
|
||||
|
||||
# 範例:清除快取
|
||||
docker compose exec laravel.test php artisan cache:clear
|
||||
|
||||
# 範例:執行 Migration
|
||||
docker compose exec laravel.test php artisan migrate
|
||||
|
||||
# 範例:建立新 Controller
|
||||
docker compose exec laravel.test php artisan make:controller ExampleController
|
||||
```
|
||||
|
||||
### 前端開發
|
||||
|
||||
```bash
|
||||
# 安裝 npm 套件
|
||||
docker compose exec laravel.test npm install
|
||||
|
||||
# 開發模式(即時編譯)
|
||||
docker compose exec laravel.test npm run dev
|
||||
|
||||
# 生產編譯
|
||||
docker compose exec laravel.test npm run build
|
||||
```
|
||||
|
||||
### 資料庫操作
|
||||
|
||||
```bash
|
||||
# 進入 MySQL 容器
|
||||
docker compose exec mysql bash
|
||||
|
||||
# 直接執行 SQL
|
||||
docker compose exec mysql mysql -u sail -ppassword star_cloud
|
||||
|
||||
# 備份資料庫
|
||||
docker compose exec mysql mysqldump -u sail -ppassword star_cloud > backup.sql
|
||||
|
||||
# 還原資料庫
|
||||
docker compose exec -T mysql mysql -u sail -ppassword star_cloud < backup.sql
|
||||
```
|
||||
|
||||
---
|
||||
預設網址為:http://localhost:8001
|
||||
|
||||
## 主要功能模組
|
||||
|
||||
### 核心功能
|
||||
|
||||
| 模組 | 功能描述 |
|
||||
|------|---------|
|
||||
| **儀錶板** | 銷售數據總覽、機台狀態即時監控、營收統計圖表 |
|
||||
| **機台管理** | 機台列表、遠端控制、日誌查詢、維修管理、效期控制 |
|
||||
| **倉庫管理** | 倉庫列表、庫存管理、調撥單、採購單、補貨單 |
|
||||
| **商品管理** | 商品資料、分類管理、商品報表分析 |
|
||||
| **銷售管理** | 交易紀錄、金流管理、促銷設定、營收報表 |
|
||||
| **會員系統** | 會員管理、點數系統、來店禮、Line 整合 |
|
||||
| **權限控制** | 角色管理、權限分配、功能權限設定 |
|
||||
| **遠端管理** | 機台重啟、遠端出貨、遠端結帳、庫存調整 |
|
||||
- **儀錶板 (Dashboard)**: 銷售數據概覽、機台狀態監控
|
||||
- **機台管理 (Machine Management)**: 機台列表、遠端控制、日誌查詢
|
||||
- **商品與庫存 (Inventory)**: 商品管理、進銷存、補貨單
|
||||
- **銷售管理 (Sales)**: 交易紀錄、營收報表
|
||||
- **權限設定 (Permissions)**: 角色與權限分配
|
||||
|
||||
---
|
||||
|
||||
## Preline UI 組件庫
|
||||
|
||||
本專案已整合 **Preline UI 3.x**,這是一個基於 Tailwind CSS 的開源 UI 組件庫,提供 50+ 預構建組件。
|
||||
|
||||
### 可用組件類別
|
||||
|
||||
- **Navigation**: 導航列、側邊欄、分頁、麵包屑、頁籤
|
||||
- **Forms**: 輸入框、選擇器、開關、檔案上傳、日期選擇器
|
||||
- **Overlays**: 模態框、抽屜、下拉選單、提示框、彈出框
|
||||
- **Data Display**: 表格、卡片、時間軸、折疊面板、徽章
|
||||
- **Feedback**: 通知、警告、載入狀態、進度條
|
||||
|
||||
### 使用範例
|
||||
|
||||
```html
|
||||
<!-- 下拉選單 -->
|
||||
<div class="hs-dropdown relative inline-flex">
|
||||
<button type="button" class="hs-dropdown-toggle px-4 py-2 bg-blue-600 text-white rounded-lg">
|
||||
選單 <svg class="w-4 h-4 inline ml-2">...</svg>
|
||||
</button>
|
||||
<div class="hs-dropdown-menu hidden bg-white shadow-lg rounded-lg p-2 mt-2">
|
||||
<a class="block px-3 py-2 rounded hover:bg-gray-100" href="#">選項 1</a>
|
||||
<a class="block px-3 py-2 rounded hover:bg-gray-100" href="#">選項 2</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模態框 -->
|
||||
<button type="button" data-hs-overlay="#my-modal" class="px-4 py-2 bg-blue-600 text-white rounded-lg">
|
||||
開啟模態框
|
||||
</button>
|
||||
<div id="my-modal" class="hs-overlay hidden">
|
||||
<!-- 模態框內容 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**更多資源**:
|
||||
- 官方文件: https://preline.co/docs/
|
||||
- 組件範例: https://preline.co/examples.html
|
||||
- GitHub: https://github.com/htmlstreamofficial/preline
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 容器無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查容器日誌
|
||||
docker compose logs
|
||||
|
||||
# 重建容器
|
||||
docker compose down
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### 連接資料庫失敗
|
||||
|
||||
確認 `.env` 中 `DB_HOST` 設定為 `mysql`(容器服務名稱),而非 `127.0.0.1`。
|
||||
|
||||
### 前端資源編譯失敗
|
||||
|
||||
```bash
|
||||
# 清除 node_modules 重新安裝
|
||||
docker compose exec laravel.test rm -rf node_modules
|
||||
docker compose exec laravel.test npm install
|
||||
docker compose exec laravel.test npm run build
|
||||
```
|
||||
|
||||
### 權限問題
|
||||
|
||||
```bash
|
||||
# 修正儲存目錄權限
|
||||
docker compose exec laravel.test chmod -R 775 storage bootstrap/cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署至生產環境
|
||||
|
||||
### 1. 環境變數設定
|
||||
|
||||
將 `.env` 中的設定調整為生產環境:
|
||||
|
||||
```env
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://your-domain.com
|
||||
```
|
||||
|
||||
### 2. 編譯前端資源
|
||||
|
||||
```bash
|
||||
docker compose exec laravel.test npm run build
|
||||
```
|
||||
|
||||
### 3. 優化 Laravel
|
||||
|
||||
```bash
|
||||
docker compose exec laravel.test php artisan config:cache
|
||||
docker compose exec laravel.test php artisan route:cache
|
||||
docker compose exec laravel.test php artisan view:cache
|
||||
```
|
||||
|
||||
### 4. 設定 HTTPS
|
||||
|
||||
建議使用 Nginx Reverse Proxy + Let's Encrypt SSL 憑證。
|
||||
|
||||
---
|
||||
|
||||
## 開發團隊協作
|
||||
|
||||
### Git Workflow
|
||||
|
||||
```bash
|
||||
# 拉取最新程式碼
|
||||
git pull origin main
|
||||
|
||||
# 重建容器(若 Docker 設定有變更)
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
|
||||
# 更新依賴
|
||||
docker compose exec laravel.test composer install
|
||||
docker compose exec laravel.test npm install
|
||||
|
||||
# 執行 Migration
|
||||
docker compose exec laravel.test php artisan migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 授權與版權
|
||||
|
||||
© Star Cloud. All Rights Reserved.
|
||||
|
||||
---
|
||||
|
||||
## 技術支援
|
||||
|
||||
如有問題或建議,請聯繫開發團隊或提交 Issue。
|
||||
@@ -1,64 +0,0 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?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\System\AppConfig;
|
||||
use App\Models\AppConfig;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AppConfigController extends Controller
|
||||
|
||||
@@ -3,31 +3,25 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\Machine;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// 從資料庫獲取真實統計數據
|
||||
$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();
|
||||
// 模擬數據或從資料庫獲取
|
||||
// 由於目前沒有數據,我們先傳遞一些預設值或空集合
|
||||
$totalMachines = Machine::count();
|
||||
$onlineMachines = Machine::where('status', 'online')->count();
|
||||
$offlineMachines = Machine::where('status', 'offline')->count();
|
||||
$errorMachines = Machine::where('status', 'error')->count();
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalRevenue',
|
||||
'activeMachines',
|
||||
'alertsPending',
|
||||
'memberCount',
|
||||
'latestActivities'
|
||||
'totalMachines',
|
||||
'onlineMachines',
|
||||
'offlineMachines',
|
||||
'errorMachines'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Member\DepositBonusRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DepositBonusRuleController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$rules = DepositBonusRule::orderBy('min_amount')->get();
|
||||
return view('admin.deposit-bonus-rules.index', compact('rules'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'min_amount' => 'required|numeric|min:0',
|
||||
'bonus_type' => 'required|in:fixed,percentage',
|
||||
'bonus_value' => 'required|numeric|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'start_at' => 'nullable|date',
|
||||
'end_at' => 'nullable|date|after:start_at',
|
||||
]);
|
||||
|
||||
DepositBonusRule::create($validated);
|
||||
|
||||
return redirect()->route('admin.deposit-bonus-rules.index')->with('success', '儲值回饋規則已建立');
|
||||
}
|
||||
|
||||
public function update(Request $request, DepositBonusRule $depositBonusRule)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'min_amount' => 'required|numeric|min:0',
|
||||
'bonus_type' => 'required|in:fixed,percentage',
|
||||
'bonus_value' => 'required|numeric|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'start_at' => 'nullable|date',
|
||||
'end_at' => 'nullable|date|after:start_at',
|
||||
]);
|
||||
|
||||
$depositBonusRule->update($validated);
|
||||
|
||||
return redirect()->route('admin.deposit-bonus-rules.index')->with('success', '儲值回饋規則已更新');
|
||||
}
|
||||
|
||||
public function destroy(DepositBonusRule $depositBonusRule)
|
||||
{
|
||||
$depositBonusRule->delete();
|
||||
return redirect()->route('admin.deposit-bonus-rules.index')->with('success', '儲值回饋規則已刪除');
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Member\GiftDefinition;
|
||||
use App\Models\Member\MembershipTier;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GiftDefinitionController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$gifts = GiftDefinition::with('tier')->get();
|
||||
$tiers = MembershipTier::orderBy('sort_order')->get();
|
||||
return view('admin.gift-definitions.index', compact('gifts', 'tiers'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:points,coupon,product,discount,cash',
|
||||
'value' => 'required|numeric|min:0',
|
||||
'tier_id' => 'nullable|exists:membership_tiers,id',
|
||||
'trigger' => 'required|in:register,birthday,annual,upgrade,manual',
|
||||
'validity_days' => 'required|integer|min:1',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
GiftDefinition::create($validated);
|
||||
|
||||
return redirect()->route('admin.gift-definitions.index')->with('success', '禮品已建立');
|
||||
}
|
||||
|
||||
public function update(Request $request, GiftDefinition $giftDefinition)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:points,coupon,product,discount,cash',
|
||||
'value' => 'required|numeric|min:0',
|
||||
'tier_id' => 'nullable|exists:membership_tiers,id',
|
||||
'trigger' => 'required|in:register,birthday,annual,upgrade,manual',
|
||||
'validity_days' => 'required|integer|min:1',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$giftDefinition->update($validated);
|
||||
|
||||
return redirect()->route('admin.gift-definitions.index')->with('success', '禮品已更新');
|
||||
}
|
||||
|
||||
public function destroy(GiftDefinition $giftDefinition)
|
||||
{
|
||||
$giftDefinition->delete();
|
||||
return redirect()->route('admin.gift-definitions.index')->with('success', '禮品已刪除');
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,142 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MachineController extends AdminController
|
||||
class MachineController extends Controller
|
||||
{
|
||||
/**
|
||||
* 顯示所有機台列表
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index()
|
||||
{
|
||||
$machines = Machine::query()
|
||||
->when($request->status, function ($query, $status) {
|
||||
return $query->where('status', $status);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
$machines = Machine::latest()->paginate(10);
|
||||
return view('admin.machines.index', compact('machines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示特定機台的日誌與詳細資訊
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function show(int $id): View
|
||||
public function create()
|
||||
{
|
||||
$machine = Machine::with(['logs' => function ($query) {
|
||||
$query->latest()->limit(50);
|
||||
}])->findOrFail($id);
|
||||
return view('admin.machines.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' => '機台維修工單系統',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Member\MembershipTier;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MembershipTierController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$tiers = MembershipTier::orderBy('sort_order')->get();
|
||||
return view('admin.membership-tiers.index', compact('tiers'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'annual_fee' => 'required|numeric|min:0',
|
||||
'discount_rate' => 'required|numeric|min:0|max:1',
|
||||
'point_multiplier' => 'required|numeric|min:0',
|
||||
'description' => 'nullable|string',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($request->is_default) {
|
||||
MembershipTier::where('is_default', true)->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
MembershipTier::create($validated);
|
||||
|
||||
return redirect()->route('admin.membership-tiers.index')->with('success', '會員等級已建立');
|
||||
}
|
||||
|
||||
public function update(Request $request, MembershipTier $membershipTier)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'annual_fee' => 'required|numeric|min:0',
|
||||
'discount_rate' => 'required|numeric|min:0|max:1',
|
||||
'point_multiplier' => 'required|numeric|min:0',
|
||||
'description' => 'nullable|string',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($request->is_default && !$membershipTier->is_default) {
|
||||
MembershipTier::where('is_default', true)->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
$membershipTier->update($validated);
|
||||
|
||||
return redirect()->route('admin.membership-tiers.index')->with('success', '會員等級已更新');
|
||||
}
|
||||
|
||||
public function destroy(MembershipTier $membershipTier)
|
||||
{
|
||||
$membershipTier->delete();
|
||||
return redirect()->route('admin.membership-tiers.index')->with('success', '會員等級已刪除');
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Member\PointRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PointRuleController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$rules = PointRule::all();
|
||||
return view('admin.point-rules.index', compact('rules'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'trigger' => 'required|in:purchase,deposit,register,birthday,referral',
|
||||
'points_per_unit' => 'required|integer|min:1',
|
||||
'unit_amount' => 'required|numeric|min:0',
|
||||
'validity_days' => 'required|integer|min:1',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
PointRule::create($validated);
|
||||
|
||||
return redirect()->route('admin.point-rules.index')->with('success', '點數規則已建立');
|
||||
}
|
||||
|
||||
public function update(Request $request, PointRule $pointRule)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'trigger' => 'required|in:purchase,deposit,register,birthday,referral',
|
||||
'points_per_unit' => 'required|integer|min:1',
|
||||
'unit_amount' => 'required|numeric|min:0',
|
||||
'validity_days' => 'required|integer|min:1',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$pointRule->update($validated);
|
||||
|
||||
return redirect()->route('admin.point-rules.index')->with('success', '點數規則已更新');
|
||||
}
|
||||
|
||||
public function destroy(PointRule $pointRule)
|
||||
{
|
||||
$pointRule->delete();
|
||||
return redirect()->route('admin.point-rules.index')->with('success', '點數規則已刪除');
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Member\Member;
|
||||
use App\Models\Member\SocialAccount;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class MemberController extends Controller
|
||||
{
|
||||
/**
|
||||
* 會員註冊
|
||||
*/
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['nullable', 'email', 'unique:members,email'],
|
||||
'phone' => ['nullable', 'string', 'unique:members,phone'],
|
||||
'password' => ['required', Password::min(6)],
|
||||
'birthday' => ['nullable', 'date'],
|
||||
'gender' => ['nullable', 'in:male,female,other'],
|
||||
], [
|
||||
'name.required' => '請輸入姓名',
|
||||
'email.unique' => '此 Email 已被註冊',
|
||||
'phone.unique' => '此手機號碼已被註冊',
|
||||
'password.required' => '請輸入密碼',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '驗證失敗',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 必須提供 email 或 phone 其中之一
|
||||
if (empty($request->email) && empty($request->phone)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '請提供 Email 或手機號碼',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$member = Member::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
'password' => $request->password,
|
||||
'birthday' => $request->birthday,
|
||||
'gender' => $request->gender,
|
||||
]);
|
||||
|
||||
$token = $member->createToken('member-token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '註冊成功',
|
||||
'data' => [
|
||||
'member' => $member,
|
||||
'token' => $token,
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 會員登入(Email/Phone + Password)
|
||||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'account' => ['required', 'string'],
|
||||
'password' => ['required', 'string'],
|
||||
], [
|
||||
'account.required' => '請輸入帳號',
|
||||
'password.required' => '請輸入密碼',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '驗證失敗',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 嘗試以 email 或 phone 查詢
|
||||
$member = Member::where('email', $request->account)
|
||||
->orWhere('phone', $request->account)
|
||||
->first();
|
||||
|
||||
if (!$member || !Hash::check($request->password, $member->password)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '帳號或密碼錯誤',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if (!$member->is_active) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '帳號已被停用',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$token = $member->createToken('member-token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '登入成功',
|
||||
'data' => [
|
||||
'member' => $member,
|
||||
'token' => $token,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 社群登入
|
||||
*/
|
||||
public function socialLogin(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'provider' => ['required', 'in:line,google,facebook'],
|
||||
'provider_id' => ['required', 'string'],
|
||||
'access_token' => ['nullable', 'string'],
|
||||
'name' => ['nullable', 'string'],
|
||||
'email' => ['nullable', 'email'],
|
||||
'avatar' => ['nullable', 'string'],
|
||||
], [
|
||||
'provider.required' => '請指定登入平台',
|
||||
'provider.in' => '不支援的登入平台',
|
||||
'provider_id.required' => '缺少社群用戶 ID',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '驗證失敗',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 查詢是否已綁定
|
||||
$socialAccount = SocialAccount::where('provider', $request->provider)
|
||||
->where('provider_id', $request->provider_id)
|
||||
->first();
|
||||
|
||||
if ($socialAccount) {
|
||||
// 已綁定,直接登入
|
||||
$member = $socialAccount->member;
|
||||
|
||||
// 更新 token
|
||||
$socialAccount->update([
|
||||
'access_token' => $request->access_token,
|
||||
]);
|
||||
} else {
|
||||
// 未綁定,建立新會員
|
||||
$member = Member::create([
|
||||
'name' => $request->name ?? '會員',
|
||||
'email' => $request->email,
|
||||
'avatar' => $request->avatar,
|
||||
'email_verified_at' => $request->email ? now() : null, // 社群登入自動驗證
|
||||
]);
|
||||
|
||||
// 綁定社群帳號
|
||||
$member->socialAccounts()->create([
|
||||
'provider' => $request->provider,
|
||||
'provider_id' => $request->provider_id,
|
||||
'access_token' => $request->access_token,
|
||||
'profile_data' => $request->only(['name', 'email', 'avatar']),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$member->is_active) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '帳號已被停用',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$token = $member->createToken('member-token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '登入成功',
|
||||
'data' => [
|
||||
'member' => $member,
|
||||
'token' => $token,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得個人資料
|
||||
*/
|
||||
public function profile(Request $request): JsonResponse
|
||||
{
|
||||
$member = $request->user();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'member' => $member->load('socialAccounts'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新個人資料
|
||||
*/
|
||||
public function updateProfile(Request $request): JsonResponse
|
||||
{
|
||||
$member = $request->user();
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
'birthday' => ['nullable', 'date'],
|
||||
'gender' => ['nullable', 'in:male,female,other'],
|
||||
'avatar' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '驗證失敗',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$member->update($request->only(['name', 'birthday', 'gender', 'avatar']));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '更新成功',
|
||||
'data' => [
|
||||
'member' => $member,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '登出成功',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\ApiResponse;
|
||||
|
||||
abstract class ApiController extends Controller
|
||||
{
|
||||
use ApiResponse;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?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\System\User;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Member\Member;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class MemberController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the members.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$members = Member::query()
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('admin.members.index', [
|
||||
'members' => $members,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class SocialLoginTestController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('test.social-login');
|
||||
}
|
||||
|
||||
public function lineCallback(Request $request)
|
||||
{
|
||||
// 這裡可以實作後端換發 Token 的邏輯
|
||||
// 為了測試方便,我們先直接顯示回傳的 code 與 state
|
||||
// 或者嘗試交換 Token 並取得 User Profile
|
||||
|
||||
$code = $request->input('code');
|
||||
$state = $request->input('state');
|
||||
$error = $request->input('error');
|
||||
|
||||
return view('test.social-login', [
|
||||
'line_data' => [
|
||||
'code' => $code,
|
||||
'state' => $state,
|
||||
'error' => $error
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -27,22 +27,11 @@ class LoginRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => ['required', 'string'],
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得驗證規則的自訂錯誤訊息
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.required' => '請輸入帳號',
|
||||
'password.required' => '請輸入密碼',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
@@ -52,11 +41,11 @@ class LoginRequest extends FormRequest
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('username', 'password'), $this->boolean('remember'))) {
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'username' => trans('auth.failed'),
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -79,7 +68,7 @@ class LoginRequest extends FormRequest
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'username' => trans('auth.throttle', [
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
@@ -91,6 +80,6 @@ class LoginRequest extends FormRequest
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('username')).'|'.$this->ip());
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\System\User;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?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,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,14 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Machine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'location',
|
||||
@@ -1,16 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Machine;
|
||||
namespace App\Models;
|
||||
|
||||
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,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DepositBonusRule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'min_amount',
|
||||
'bonus_type',
|
||||
'bonus_value',
|
||||
'is_active',
|
||||
'start_at',
|
||||
'end_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'min_amount' => 'decimal:2',
|
||||
'bonus_value' => 'decimal:2',
|
||||
'is_active' => 'boolean',
|
||||
'start_at' => 'datetime',
|
||||
'end_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 取得目前有效的規則
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('start_at')->orWhere('start_at', '<=', now());
|
||||
})
|
||||
->where(function ($q) {
|
||||
$q->whereNull('end_at')->orWhere('end_at', '>=', now());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算回饋金額
|
||||
*/
|
||||
public function calculateBonus(float $depositAmount): float
|
||||
{
|
||||
if ($depositAmount < $this->min_amount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->bonus_type === 'fixed') {
|
||||
return $this->bonus_value;
|
||||
}
|
||||
|
||||
// percentage
|
||||
return $depositAmount * ($this->bonus_value / 100);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class GiftDefinition extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'type',
|
||||
'value',
|
||||
'tier_id',
|
||||
'trigger',
|
||||
'validity_days',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'value' => 'decimal:2',
|
||||
'validity_days' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 適用等級
|
||||
*/
|
||||
public function tier(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipTier::class, 'tier_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 發放紀錄
|
||||
*/
|
||||
public function memberGifts(): HasMany
|
||||
{
|
||||
return $this->hasMany(MemberGift::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效禮品
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Member extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* 資料表名稱
|
||||
*/
|
||||
protected $table = 'members';
|
||||
|
||||
/**
|
||||
* 可批量賦值的屬性
|
||||
*/
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'birthday',
|
||||
'gender',
|
||||
'avatar',
|
||||
'is_active',
|
||||
'email_verified_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 隱藏的屬性
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* 屬性轉換
|
||||
*/
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'birthday' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
|
||||
/**
|
||||
* 建立時自動產生 UUID
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->uuid)) {
|
||||
$model->uuid = (string) Str::uuid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯:社群帳號
|
||||
*/
|
||||
public function socialAccounts()
|
||||
{
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯:錢包
|
||||
*/
|
||||
public function wallet()
|
||||
{
|
||||
return $this->hasOne(MemberWallet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯:點數帳戶
|
||||
*/
|
||||
public function points()
|
||||
{
|
||||
return $this->hasOne(MemberPoint::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯:會員資格紀錄
|
||||
*/
|
||||
public function memberships()
|
||||
{
|
||||
return $this->hasMany(MemberMembership::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯:禮品紀錄
|
||||
*/
|
||||
public function gifts()
|
||||
{
|
||||
return $this->hasMany(MemberGift::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得目前有效的會員資格
|
||||
*/
|
||||
public function activeMembership()
|
||||
{
|
||||
return $this->hasOne(MemberMembership::class)->active()->latest('starts_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查是否已綁定指定社群
|
||||
*/
|
||||
public function hasSocialAccount(string $provider): bool
|
||||
{
|
||||
return $this->socialAccounts()->where('provider', $provider)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得或建立錢包
|
||||
*/
|
||||
public function getOrCreateWallet(): MemberWallet
|
||||
{
|
||||
return $this->wallet ?? $this->wallet()->create([
|
||||
'balance' => 0,
|
||||
'bonus_balance' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得或建立點數帳戶
|
||||
*/
|
||||
public function getOrCreatePoints(): MemberPoint
|
||||
{
|
||||
return $this->points ?? $this->points()->create([
|
||||
'available_points' => 0,
|
||||
'pending_points' => 0,
|
||||
'expired_points' => 0,
|
||||
'used_points' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MemberGift extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'gift_definition_id',
|
||||
'status',
|
||||
'claimed_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'claimed_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬會員
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禮品定義
|
||||
*/
|
||||
public function giftDefinition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftDefinition::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 待領取的禮品
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', 'pending')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MemberMembership extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'tier_id',
|
||||
'starts_at',
|
||||
'expires_at',
|
||||
'payment_id',
|
||||
'auto_renew',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'starts_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'auto_renew' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬會員
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 會員等級
|
||||
*/
|
||||
public function tier(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipTier::class, 'tier_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有效
|
||||
*/
|
||||
public function getIsActiveAttribute(): bool
|
||||
{
|
||||
return $this->status === 'active'
|
||||
&& (!$this->expires_at || $this->expires_at->isFuture());
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效會員資格
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class MemberPoint extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'available_points',
|
||||
'pending_points',
|
||||
'expired_points',
|
||||
'used_points',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'available_points' => 'integer',
|
||||
'pending_points' => 'integer',
|
||||
'expired_points' => 'integer',
|
||||
'used_points' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬會員
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 點數異動紀錄
|
||||
*/
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PointTransaction::class, 'member_id', 'member_id');
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class MemberWallet extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'balance',
|
||||
'bonus_balance',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'balance' => 'decimal:2',
|
||||
'bonus_balance' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬會員
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易紀錄
|
||||
*/
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletTransaction::class, 'member_id', 'member_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 總餘額 (儲值 + 回饋)
|
||||
*/
|
||||
public function getTotalBalanceAttribute(): float
|
||||
{
|
||||
return $this->balance + $this->bonus_balance;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class MembershipTier extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'annual_fee',
|
||||
'discount_rate',
|
||||
'point_multiplier',
|
||||
'description',
|
||||
'is_default',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'annual_fee' => 'decimal:2',
|
||||
'discount_rate' => 'decimal:2',
|
||||
'point_multiplier' => 'decimal:2',
|
||||
'is_default' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 此等級的會員紀錄
|
||||
*/
|
||||
public function memberships(): HasMany
|
||||
{
|
||||
return $this->hasMany(MemberMembership::class, 'tier_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 此等級的禮品定義
|
||||
*/
|
||||
public function giftDefinitions(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftDefinition::class, 'tier_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得預設等級
|
||||
*/
|
||||
public static function getDefault(): ?self
|
||||
{
|
||||
return static::where('is_default', true)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否為免費等級
|
||||
*/
|
||||
public function getIsFreeAttribute(): bool
|
||||
{
|
||||
return $this->annual_fee <= 0;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PointRule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'trigger',
|
||||
'points_per_unit',
|
||||
'unit_amount',
|
||||
'validity_days',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'points_per_unit' => 'integer',
|
||||
'unit_amount' => 'decimal:2',
|
||||
'validity_days' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 取得有效規則
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根據金額計算可獲得點數
|
||||
*/
|
||||
public function calculatePoints(float $amount): int
|
||||
{
|
||||
if ($this->unit_amount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) floor($amount / $this->unit_amount) * $this->points_per_unit;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PointTransaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'type',
|
||||
'points',
|
||||
'balance_after',
|
||||
'description',
|
||||
'expires_at',
|
||||
'reference_type',
|
||||
'reference_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'points' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
'expires_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬會員
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已過期
|
||||
*/
|
||||
public function getIsExpiredAttribute(): bool
|
||||
{
|
||||
return $this->expires_at && $this->expires_at->isPast();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* 資料表名稱
|
||||
*/
|
||||
protected $table = 'social_accounts';
|
||||
|
||||
/**
|
||||
* 可批量賦值的屬性
|
||||
*/
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'provider',
|
||||
'provider_id',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'profile_data',
|
||||
'token_expires_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 屬性轉換
|
||||
*/
|
||||
protected $casts = [
|
||||
'profile_data' => 'array',
|
||||
'token_expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 隱藏的屬性
|
||||
*/
|
||||
protected $hidden = [
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* 關聯:會員
|
||||
*/
|
||||
public function member()
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WalletTransaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'type',
|
||||
'amount',
|
||||
'balance_after',
|
||||
'description',
|
||||
'reference_type',
|
||||
'reference_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2',
|
||||
'balance_after' => 'decimal:2',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬會員
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -18,7 +18,6 @@ class User extends Authenticatable
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'username',
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
@@ -19,8 +19,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if (!$this->app->isLocal()) {
|
||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
||||
}
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?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,18 +1,53 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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.
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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);
|
||||
|
||||
exit($status);
|
||||
|
||||
14
compose.yaml
14
compose.yaml
@@ -6,12 +6,12 @@ services:
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
image: 'sail-8.5/app'
|
||||
container_name: star-cloud-laravel
|
||||
hostname: star-cloud-laravel
|
||||
container_name: start-cloud-laravel
|
||||
hostname: start-cloud-laravel
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
ports:
|
||||
- '${APP_PORT:-80}:8080'
|
||||
- '${APP_PORT:-80}:80'
|
||||
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||
environment:
|
||||
WWWUSER: '${WWWUSER}'
|
||||
@@ -29,8 +29,8 @@ services:
|
||||
|
||||
mysql:
|
||||
image: 'mysql/mysql-server:8.0'
|
||||
container_name: star-cloud-mysql
|
||||
hostname: star-cloud-mysql
|
||||
container_name: start-cloud-mysql
|
||||
hostname: start-cloud-mysql
|
||||
ports:
|
||||
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||
environment:
|
||||
@@ -56,8 +56,8 @@ services:
|
||||
timeout: 5s
|
||||
redis:
|
||||
image: 'redis:alpine'
|
||||
container_name: star-cloud-redis
|
||||
hostname: star-cloud-redis
|
||||
container_name: start-cloud-redis
|
||||
hostname: start-cloud-redis
|
||||
ports:
|
||||
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||
volumes:
|
||||
|
||||
@@ -2,27 +2,24 @@
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
"php": "^8.1",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.3",
|
||||
"laravel/tinker": "^2.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -67,4 +64,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
||||
2623
composer.lock
generated
2623
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\System\User::class,
|
||||
'model' => App\Models\User::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?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\System\User>
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('users', 'username')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('username')->unique()->nullable()->after('id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('users', 'username')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('username');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->string('name');
|
||||
$table->string('email')->nullable()->unique();
|
||||
$table->string('phone')->nullable()->unique();
|
||||
$table->string('password')->nullable();
|
||||
$table->date('birthday')->nullable();
|
||||
$table->enum('gender', ['male', 'female', 'other'])->nullable();
|
||||
$table->string('avatar')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('members');
|
||||
}
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('social_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->enum('provider', ['line', 'google', 'facebook']);
|
||||
$table->string('provider_id');
|
||||
$table->text('access_token')->nullable();
|
||||
$table->text('refresh_token')->nullable();
|
||||
$table->json('profile_data')->nullable();
|
||||
$table->timestamp('token_expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// 同一平台同一用戶只能綁定一次
|
||||
$table->unique(['provider', 'provider_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('social_accounts');
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 會員錢包
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('member_wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->decimal('balance', 12, 2)->default(0)->comment('錢包餘額');
|
||||
$table->decimal('bonus_balance', 12, 2)->default(0)->comment('回饋金餘額');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('member_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('member_wallets');
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 錢包交易紀錄
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallet_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->enum('type', ['deposit', 'consume', 'refund', 'bonus', 'adjust'])->comment('交易類型');
|
||||
$table->decimal('amount', 12, 2)->comment('異動金額');
|
||||
$table->decimal('balance_after', 12, 2)->comment('異動後餘額');
|
||||
$table->string('description')->nullable()->comment('說明');
|
||||
$table->string('reference_type')->nullable()->comment('關聯類型');
|
||||
$table->unsignedBigInteger('reference_id')->nullable()->comment('關聯ID');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['member_id', 'created_at']);
|
||||
$table->index(['reference_type', 'reference_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallet_transactions');
|
||||
}
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 儲值回饋規則
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('deposit_bonus_rules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('規則名稱');
|
||||
$table->decimal('min_amount', 12, 2)->comment('最低儲值金額');
|
||||
$table->enum('bonus_type', ['fixed', 'percentage'])->comment('回饋類型');
|
||||
$table->decimal('bonus_value', 12, 2)->comment('回饋值');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->datetime('start_at')->nullable()->comment('開始時間');
|
||||
$table->datetime('end_at')->nullable()->comment('結束時間');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['is_active', 'start_at', 'end_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('deposit_bonus_rules');
|
||||
}
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 會員點數帳戶
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('member_points', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->integer('available_points')->default(0)->comment('可用點數');
|
||||
$table->integer('pending_points')->default(0)->comment('待生效點數');
|
||||
$table->integer('expired_points')->default(0)->comment('已過期點數(統計)');
|
||||
$table->integer('used_points')->default(0)->comment('已使用點數(統計)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('member_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('member_points');
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 點數異動紀錄
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('point_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->enum('type', ['earn', 'use', 'expire', 'gift', 'adjust'])->comment('異動類型');
|
||||
$table->integer('points')->comment('異動點數');
|
||||
$table->integer('balance_after')->comment('異動後餘額');
|
||||
$table->string('description')->nullable()->comment('說明');
|
||||
$table->datetime('expires_at')->nullable()->comment('此筆點數到期日');
|
||||
$table->string('reference_type')->nullable()->comment('關聯類型');
|
||||
$table->unsignedBigInteger('reference_id')->nullable()->comment('關聯ID');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['member_id', 'created_at']);
|
||||
$table->index('expires_at');
|
||||
$table->index(['reference_type', 'reference_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('point_transactions');
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 點數規則
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('point_rules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('規則名稱');
|
||||
$table->enum('trigger', ['purchase', 'deposit', 'register', 'birthday', 'referral'])->comment('觸發條件');
|
||||
$table->integer('points_per_unit')->default(1)->comment('每單位獲得點數');
|
||||
$table->decimal('unit_amount', 12, 2)->default(100)->comment('單位金額');
|
||||
$table->integer('validity_days')->default(365)->comment('點數有效天數');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('point_rules');
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 會員等級定義
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_tiers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('等級名稱');
|
||||
$table->decimal('annual_fee', 12, 2)->default(0)->comment('年費金額');
|
||||
$table->decimal('discount_rate', 4, 2)->default(1.00)->comment('折扣比例(0.95=95折)');
|
||||
$table->decimal('point_multiplier', 4, 2)->default(1.00)->comment('點數倍率');
|
||||
$table->text('description')->nullable()->comment('說明');
|
||||
$table->boolean('is_default')->default(false)->comment('是否為預設等級');
|
||||
$table->integer('sort_order')->default(0)->comment('排序');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('is_default');
|
||||
$table->index('sort_order');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_tiers');
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 會員等級紀錄
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('member_memberships', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->foreignId('tier_id')->constrained('membership_tiers')->onDelete('cascade');
|
||||
$table->datetime('starts_at')->comment('生效日');
|
||||
$table->datetime('expires_at')->nullable()->comment('到期日');
|
||||
$table->unsignedBigInteger('payment_id')->nullable()->comment('付款紀錄ID');
|
||||
$table->boolean('auto_renew')->default(false)->comment('是否自動續約');
|
||||
$table->enum('status', ['active', 'expired', 'cancelled'])->default('active');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['member_id', 'status']);
|
||||
$table->index('expires_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('member_memberships');
|
||||
}
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 禮品/福利定義
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('gift_definitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('禮品名稱');
|
||||
$table->enum('type', ['points', 'coupon', 'product', 'discount', 'cash'])->comment('禮品類型');
|
||||
$table->decimal('value', 12, 2)->default(0)->comment('數值');
|
||||
$table->foreignId('tier_id')->nullable()->constrained('membership_tiers')->nullOnDelete()->comment('適用等級');
|
||||
$table->enum('trigger', ['register', 'birthday', 'annual', 'upgrade', 'manual'])->comment('觸發條件');
|
||||
$table->integer('validity_days')->default(30)->comment('有效天數');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['is_active', 'trigger']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('gift_definitions');
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 會員禮品發放紀錄
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('member_gifts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->foreignId('gift_definition_id')->constrained('gift_definitions')->onDelete('cascade');
|
||||
$table->enum('status', ['pending', 'claimed', 'expired'])->default('pending');
|
||||
$table->datetime('claimed_at')->nullable()->comment('領取時間');
|
||||
$table->datetime('expires_at')->nullable()->comment('有效期限');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['member_id', 'status']);
|
||||
$table->index('expires_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('member_gifts');
|
||||
}
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
/**
|
||||
* 管理員帳號 Seeder
|
||||
*
|
||||
* 執行方式:php artisan db:seed --class=AdminUserSeeder
|
||||
*/
|
||||
class AdminUserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// 檢查是否已存在 admin 帳號,避免重複建立
|
||||
$admin = User::where('username', 'admin')->first();
|
||||
|
||||
if ($admin) {
|
||||
$this->command->info('Admin 帳號已存在,執行更新密碼與資料。');
|
||||
$admin->update([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@star-cloud.com',
|
||||
'password' => Hash::make('password'),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
User::create([
|
||||
'username' => 'admin',
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@star-cloud.com',
|
||||
'password' => Hash::make('password'),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$this->command->info('Admin 帳號建立成功!');
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,15 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*
|
||||
* 執行全部 Seeder:php artisan db:seed
|
||||
* 執行單一 Seeder:php artisan db:seed --class=AdminUserSeeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
AdminUserSeeder::class,
|
||||
MachineSeeder::class,
|
||||
MemberSeeder::class,
|
||||
// \App\Models\User::factory(10)->create();
|
||||
|
||||
\App\Models\User::factory()->create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@star-cloud.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?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),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
LABEL maintainer="Taylor Otwell"
|
||||
|
||||
ARG WWWGROUP
|
||||
ARG NODE_VERSION=22
|
||||
ARG MYSQL_CLIENT="mysql-client"
|
||||
ARG POSTGRES_VERSION=18
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=UTC
|
||||
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
||||
ENV SUPERVISOR_PHP_USER="sail"
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
||||
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
||||
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
||||
|
||||
RUN apt-get update && apt-get upgrade -y \
|
||||
&& mkdir -p /etc/apt/keyrings \
|
||||
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
||||
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y php8.4-cli php8.4-dev \
|
||||
php8.4-pgsql php8.4-sqlite3 php8.4-gd \
|
||||
php8.4-curl php8.4-mongodb \
|
||||
php8.4-imap php8.4-mysql php8.4-mbstring \
|
||||
php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
|
||||
php8.4-intl php8.4-readline \
|
||||
php8.4-ldap \
|
||||
php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \
|
||||
php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
|
||||
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y nodejs \
|
||||
&& npm install -g npm \
|
||||
&& npm install -g pnpm \
|
||||
&& npm install -g bun \
|
||||
&& npx playwright install-deps \
|
||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y yarn \
|
||||
&& apt-get install -y $MYSQL_CLIENT \
|
||||
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
||||
&& apt-get -y autoremove \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
|
||||
|
||||
RUN userdel -r ubuntu
|
||||
RUN groupadd --force -g $WWWGROUP sail
|
||||
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
||||
RUN git config --global --add safe.directory /var/www/html
|
||||
|
||||
COPY start-container /usr/local/bin/start-container
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
|
||||
RUN chmod +x /usr/local/bin/start-container
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
||||
ENTRYPOINT ["start-container"]
|
||||
@@ -1,5 +0,0 @@
|
||||
[PHP]
|
||||
post_max_size = 100M
|
||||
upload_max_filesize = 100M
|
||||
variables_order = EGPCS
|
||||
pcov.directory = .
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
||||
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -z "$WWWUSER" ]; then
|
||||
usermod -u $WWWUSER sail
|
||||
fi
|
||||
|
||||
if [ ! -d /.composer ]; then
|
||||
mkdir /.composer
|
||||
fi
|
||||
|
||||
chmod -R ugo+rw /.composer
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
||||
exec "$@"
|
||||
else
|
||||
exec gosu $WWWUSER "$@"
|
||||
fi
|
||||
else
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:php]
|
||||
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
||||
user=%(ENV_SUPERVISOR_PHP_USER)s
|
||||
environment=LARAVEL_SAIL="1"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
198
docs/members.md
198
docs/members.md
@@ -1,198 +0,0 @@
|
||||
# 會員系統(Members)功能說明
|
||||
|
||||
> 此文件記錄會員系統的設計決策與功能說明,供開發與維護時參閱。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
會員系統用於智能販賣機商城,支援消費者透過多種社群管道(Line、Google、Facebook)加入會員。
|
||||
|
||||
**重要區分**:
|
||||
- `users` 表:後台管理員登入帳號
|
||||
- `members` 表:前台消費者會員帳號
|
||||
|
||||
兩者**完全獨立**,無關聯。
|
||||
|
||||
---
|
||||
|
||||
## 資料表
|
||||
|
||||
### 1. `members` - 會員資料
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `id` | bigint | 主鍵 |
|
||||
| `uuid` | string | 唯一識別碼(對外使用) |
|
||||
| `name` | string | 姓名 |
|
||||
| `email` | string | 電子郵件(可空) |
|
||||
| `phone` | string | 手機號碼(可空) |
|
||||
| `password` | string | 密碼(社群登入可空) |
|
||||
| `birthday` | date | 生日 |
|
||||
| `gender` | enum | 性別 |
|
||||
| `avatar` | string | 頭像 URL |
|
||||
| `is_active` | boolean | 是否啟用 |
|
||||
| `email_verified_at` | timestamp | Email 驗證時間 |
|
||||
|
||||
### 2. `social_accounts` - 社群帳號
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `id` | bigint | 主鍵 |
|
||||
| `member_id` | bigint | 關聯會員 |
|
||||
| `provider` | enum | line / google / facebook |
|
||||
| `provider_id` | string | 社群平台用戶 ID |
|
||||
| `access_token` | text | 存取令牌 |
|
||||
| `refresh_token` | text | 刷新令牌 |
|
||||
| `profile_data` | json | 社群個人資料 |
|
||||
| `token_expires_at` | timestamp | 令牌到期時間 |
|
||||
|
||||
### 3. `member_wallets` - 會員錢包
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `member_id` | bigint | FK,唯一 |
|
||||
| `balance` | decimal | 儲值餘額 |
|
||||
| `bonus_balance` | decimal | 回饋金餘額 |
|
||||
|
||||
### 4. `wallet_transactions` - 錢包交易
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `type` | enum | deposit/consume/refund/bonus/adjust |
|
||||
| `amount` | decimal | 異動金額 |
|
||||
| `balance_after` | decimal | 異動後餘額 |
|
||||
| `reference_type/id` | | 關聯訂單或活動 |
|
||||
|
||||
### 5. `deposit_bonus_rules` - 儲值回饋規則
|
||||
|
||||
設定儲值達指定金額可獲得的回饋(固定金額或百分比)。
|
||||
|
||||
### 6. `member_points` - 點數帳戶
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `available_points` | int | 可用點數 |
|
||||
| `pending_points` | int | 待生效點數 |
|
||||
| `expired_points` | int | 已過期(統計) |
|
||||
| `used_points` | int | 已使用(統計) |
|
||||
|
||||
### 7. `point_transactions` - 點數異動
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `type` | enum | earn/use/expire/gift/adjust |
|
||||
| `points` | int | 異動點數 |
|
||||
| `expires_at` | datetime | **此筆點數到期日** |
|
||||
|
||||
> 每筆獲得點數都記錄 `expires_at`,排程任務定期處理過期。
|
||||
|
||||
### 8. `point_rules` - 點數規則
|
||||
|
||||
設定消費/儲值/註冊等行為可獲得的點數及有效天數。
|
||||
|
||||
### 9. `membership_tiers` - 會員等級
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `name` | string | 等級名稱 |
|
||||
| `annual_fee` | decimal | 年費(0=免費) |
|
||||
| `discount_rate` | decimal | 折扣比例 |
|
||||
| `point_multiplier` | decimal | 點數倍率 |
|
||||
|
||||
### 10. `member_memberships` - 會員等級紀錄
|
||||
|
||||
記錄會員的等級歸屬及有效期間。
|
||||
|
||||
### 11. `gift_definitions` - 禮品定義
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `type` | enum | points/coupon/product/discount/cash |
|
||||
| `trigger` | enum | register/birthday/annual/upgrade/manual |
|
||||
|
||||
### 12. `member_gifts` - 禮品發放紀錄
|
||||
|
||||
記錄發放給會員的禮品及領取狀態。
|
||||
|
||||
---
|
||||
|
||||
## ER 關係圖
|
||||
|
||||
```
|
||||
members
|
||||
├── social_accounts (1:N)
|
||||
├── member_wallets (1:1)
|
||||
│ └── wallet_transactions (1:N)
|
||||
├── member_points (1:1)
|
||||
│ └── point_transactions (1:N)
|
||||
├── member_memberships (1:N)
|
||||
│ └── membership_tiers
|
||||
└── member_gifts (1:N)
|
||||
└── gift_definitions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 登入流程
|
||||
|
||||
```
|
||||
使用者選擇社群登入
|
||||
↓
|
||||
取得 provider + provider_id
|
||||
↓
|
||||
查詢 social_accounts
|
||||
↓
|
||||
┌────┴────┐
|
||||
已綁定 未綁定
|
||||
↓ ↓
|
||||
取得 member 建立新 member + social_account
|
||||
↓ ↓
|
||||
完成登入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email 驗證(可選功能)
|
||||
|
||||
若需要 Email 驗證,需設定 `.env` 的 SMTP 並讓 `Member` Model 實作 `MustVerifyEmail`。
|
||||
|
||||
社群登入時自動標記 `email_verified_at`,僅對手機/密碼註冊要求驗證。
|
||||
|
||||
---
|
||||
|
||||
## API 端點
|
||||
|
||||
| Method | Endpoint | 說明 | 認證 |
|
||||
|--------|----------|------|------|
|
||||
| POST | `/api/members/register` | 註冊會員 | 否 |
|
||||
| POST | `/api/members/login` | 登入 | 否 |
|
||||
| POST | `/api/members/social-login` | 社群登入 | 否 |
|
||||
| GET | `/api/members/profile` | 取得個人資料 | 是 |
|
||||
| PUT | `/api/members/profile` | 更新個人資料 | 是 |
|
||||
| POST | `/api/members/logout` | 登出 | 是 |
|
||||
|
||||
---
|
||||
|
||||
## Postman 測試
|
||||
|
||||
匯入:`docs/postman/Star_Cloud_Members_API.postman_collection.json`
|
||||
|
||||
---
|
||||
|
||||
## 社群登入實測
|
||||
|
||||
訪問 `/test/social-login` 測試 Google/Line 登入。
|
||||
|
||||
---
|
||||
|
||||
## 開發進度
|
||||
|
||||
| 日期 | 項目 | 狀態 |
|
||||
|------|------|------|
|
||||
| 2026-01-12 | 會員核心 (members, social_accounts) | ✅ 完成 |
|
||||
| 2026-01-12 | 錢包系統 (3 表 + 3 Model) | ✅ 完成 |
|
||||
| 2026-01-12 | 點數系統 (3 表 + 3 Model) | ✅ 完成 |
|
||||
| 2026-01-12 | 年度會員 (2 表 + 2 Model) | ✅ 完成 |
|
||||
| 2026-01-12 | 贈送機制 (2 表 + 2 Model) | ✅ 完成 |
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Star Cloud - 會員 API",
|
||||
"description": "智能販賣機商城會員系統 API 測試集合",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "http://localhost/api",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "會員註冊",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"測試會員\",\n \"email\": \"test@example.com\",\n \"phone\": \"0912345678\",\n \"password\": \"password123\",\n \"birthday\": \"1990-01-01\",\n \"gender\": \"male\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/members/register",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"members",
|
||||
"register"
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 201) {",
|
||||
" var jsonData = pm.response.json();",
|
||||
" pm.collectionVariables.set('token', jsonData.data.token);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "會員登入",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"account\": \"test@example.com\",\n \"password\": \"password123\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/members/login",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"members",
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" var jsonData = pm.response.json();",
|
||||
" pm.collectionVariables.set('token', jsonData.data.token);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "社群登入",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"provider\": \"line\",\n \"provider_id\": \"U1234567890abcdef\",\n \"access_token\": \"test_access_token\",\n \"name\": \"Line 用戶\",\n \"email\": \"line@example.com\",\n \"avatar\": \"https://example.com/avatar.jpg\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/members/social-login",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"members",
|
||||
"social-login"
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" var jsonData = pm.response.json();",
|
||||
" pm.collectionVariables.set('token', jsonData.data.token);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "取得個人資料",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/members/profile",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"members",
|
||||
"profile"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "更新個人資料",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"更新後的名字\",\n \"birthday\": \"1995-06-15\",\n \"gender\": \"female\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/members/profile",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"members",
|
||||
"profile"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "登出",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/members/logout",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"members",
|
||||
"logout"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
214
package-lock.json
generated
214
package-lock.json
generated
@@ -1,18 +1,16 @@
|
||||
{
|
||||
"name": "html",
|
||||
"name": "star-cloud",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@alpinejs/collapse": "^3.15.3",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"alpinejs": "^3.4.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"axios": "^1.6.4",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"preline": "^3.2.3",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
@@ -30,13 +28,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@alpinejs/collapse": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.15.3.tgz",
|
||||
"integrity": "sha512-nheS20BsFY1Eh1nyW0YNs7RMOiO/LipCTltEplbWunTcgdCeZtD7YPUim5xtbhc+0nJP4SkR7G0axRXaRf4m1g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -428,34 +419,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -841,74 +804,6 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@svgdotjs/svg.draggable.js": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
|
||||
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgdotjs/svg.filter.js": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
|
||||
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgdotjs/svg.js": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz",
|
||||
"integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Fuzzyma"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgdotjs/svg.resize.js": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
|
||||
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@svgdotjs/svg.select.js": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgdotjs/svg.select.js": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
|
||||
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
|
||||
"integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
||||
@@ -946,13 +841,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@yr/monotone-cubic-spline": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
|
||||
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "3.15.2",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.2.tgz",
|
||||
@@ -984,21 +872,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/apexcharts": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz",
|
||||
"integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.draggable.js": "^3.0.4",
|
||||
"@svgdotjs/svg.filter.js": "^3.0.8",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@svgdotjs/svg.resize.js": "^2.0.2",
|
||||
"@svgdotjs/svg.select.js": "^4.0.1",
|
||||
"@yr/monotone-cubic-spline": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||
@@ -1119,6 +992,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
@@ -1252,27 +1126,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/datatables.net": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.6.tgz",
|
||||
"integrity": "sha512-xQ/dCxrjfxM0XY70wSIzakkTZ6ghERwlLmAPyCnu8Sk5cyt9YvOVyOsFNOa/BZ/lM63Q3i2YSSvp/o7GXZGsbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jquery": ">=1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/datatables.net-dt": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.3.6.tgz",
|
||||
"integrity": "sha512-8OEUNCEfkeW+TuVUDlT1q6/XXOitgVzCdNqBivw8bK9DnaNk5F6JjT8lE2pQ4uAfoL/dTy2J+HKxTHeTh8HJlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"datatables.net": "2.3.6",
|
||||
"jquery": ">=1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1297,17 +1150,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dropzone": {
|
||||
"version": "6.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.2.13",
|
||||
"just-extend": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -1733,24 +1575,11 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/just-extend": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
|
||||
"integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
|
||||
@@ -1916,13 +1745,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nouislider": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.1.tgz",
|
||||
"integrity": "sha512-93TweAi8kqntHJSPiSWQ1o/uZ29VWOmal9YKb6KKGGlCkugaNfAupT7o1qTHqdJvNQ7S0su5rO6qRFCjP8fxtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -2010,6 +1832,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -2153,21 +1976,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preline": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/preline/-/preline-3.2.3.tgz",
|
||||
"integrity": "sha512-S13MFdC/1FWFz3S+oW1PlyZ6Alo0SZxJ9HwaZRg5IQZjcbKqCFIOXAbAhQeX0izauqWJXIQdKofhfCWBizwleQ==",
|
||||
"dev": true,
|
||||
"license": "Licensed under MIT and Preline UI Fair Use License",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"apexcharts": "^4.5.0",
|
||||
"datatables.net-dt": "^2.2.2",
|
||||
"dropzone": "^6.0.0-beta.2",
|
||||
"nouislider": "^15.8.1",
|
||||
"vanilla-calendar-pro": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -2369,6 +2177,7 @@
|
||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -2465,6 +2274,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2530,23 +2340,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vanilla-calendar-pro": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.1.0.tgz",
|
||||
"integrity": "sha512-yXDtCaedcKz6i5OOdWGwui0C8MAmjXjj7JzKZyjDlkczSRqnhI8BDGFygqT2K+qL1uY7R2fLYlTlxA6oyFs2yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://buymeacoffee.com/uvarov"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
@@ -6,14 +6,12 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alpinejs/collapse": "^3.15.3",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"alpinejs": "^3.4.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"axios": "^1.6.4",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"preline": "^3.2.3",
|
||||
"tailwindcss": "^3.1.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -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,20 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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.
|
||||
|
|
||||
*/
|
||||
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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.
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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.
|
||||
|
|
||||
*/
|
||||
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
$kernel = $app->make(Kernel::class);
|
||||
|
||||
$response = $kernel->handle(
|
||||
$request = Request::capture()
|
||||
)->send();
|
||||
|
||||
$kernel->terminate($request, $response);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?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,123 +2,8 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
import collapse from '@alpinejs/collapse';
|
||||
|
||||
Alpine.plugin(collapse);
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
|
||||
// 初始化 Preline UI
|
||||
import 'preline';
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$theme = request()->cookie('theme', 'dark-blue');
|
||||
$isLight = in_array($theme, ['light-blue', 'light-green']);
|
||||
$cardBg = $isLight ? 'bg-white' : 'bg-gray-800';
|
||||
$textPrimary = $isLight ? 'text-gray-900' : 'text-gray-200';
|
||||
$textSecondary = $isLight ? 'text-gray-600' : 'text-gray-400';
|
||||
$borderColor = $isLight ? 'border-gray-200' : 'border-gray-700';
|
||||
$inputBg = $isLight ? 'bg-gray-50 border-gray-300' : 'bg-gray-700 border-gray-600';
|
||||
$inputText = $isLight ? 'text-gray-900' : 'text-gray-300';
|
||||
@endphp
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">APP 管理設定</h3>
|
||||
<h3 class="{{ $textPrimary }} text-3xl font-medium">APP 管理設定</h3>
|
||||
|
||||
<div class="mt-8">
|
||||
<form action="{{ route('admin.app-configs.update') }}" method="POST">
|
||||
@@ -14,44 +21,44 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- UI Settings -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold text-gray-900 dark:text-gray-200 mb-4">UI 元素設定</h4>
|
||||
<div class="{{ $cardBg }} rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold {{ $textPrimary }} mb-4">UI 元素設定</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="ui_primary_color" class="block text-sm font-medium text-gray-600 dark:text-gray-400">主色調 (Hex)</label>
|
||||
<input type="text" name="ui_primary_color" id="ui_primary_color" value="{{ $configs['ui']->where('key', 'ui_primary_color')->first()->value ?? '' }}" class="mt-1 block w-full bg-white dark:bg-gray-700 rounded-md shadow-sm py-2 px-3 {{ $inputText }} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="ui_primary_color" class="block text-sm font-medium {{ $textSecondary }}">主色調 (Hex)</label>
|
||||
<input type="text" name="ui_primary_color" id="ui_primary_color" value="{{ $configs['ui']->where('key', 'ui_primary_color')->first()->value ?? '' }}" class="mt-1 block w-full {{ $inputBg }} rounded-md shadow-sm py-2 px-3 {{ $inputText }} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="ui_logo_url" class="block text-sm font-medium text-gray-600 dark:text-gray-400">Logo URL</label>
|
||||
<input type="text" name="ui_logo_url" id="ui_logo_url" value="{{ $configs['ui']->where('key', 'ui_logo_url')->first()->value ?? '' }}" class="mt-1 block w-full bg-white dark:bg-gray-700 rounded-md shadow-sm py-2 px-3 {{ $inputText }} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="ui_logo_url" class="block text-sm font-medium {{ $textSecondary }}">Logo URL</label>
|
||||
<input type="text" name="ui_logo_url" id="ui_logo_url" value="{{ $configs['ui']->where('key', 'ui_logo_url')->first()->value ?? '' }}" class="mt-1 block w-full {{ $inputBg }} rounded-md shadow-sm py-2 px-3 {{ $inputText }} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Helper Settings -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold text-gray-900 dark:text-gray-200 mb-4">小幫手設定</h4>
|
||||
<div class="{{ $cardBg }} rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold {{ $textPrimary }} mb-4">小幫手設定</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<input type="hidden" name="helper_enabled" value="0">
|
||||
<input type="checkbox" name="helper_enabled" id="helper_enabled" value="1" {{ ($configs['helper']->where('key', 'helper_enabled')->first()->value ?? '0') == '1' ? 'checked' : '' }} class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
<label for="helper_enabled" class="ml-2 block text-sm text-gray-900 dark:text-gray-200">啟用小幫手</label>
|
||||
<label for="helper_enabled" class="ml-2 block text-sm {{ $textPrimary }}">啟用小幫手</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Settings -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold text-gray-900 dark:text-gray-200 mb-4">問卷與互動遊戲</h4>
|
||||
<div class="{{ $cardBg }} rounded-lg shadow-xl overflow-hidden p-6">
|
||||
<h4 class="text-xl font-semibold {{ $textPrimary }} mb-4">問卷與互動遊戲</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<input type="hidden" name="game_enabled" value="0">
|
||||
<input type="checkbox" name="game_enabled" id="game_enabled" value="1" {{ ($configs['game']->where('key', 'game_enabled')->first()->value ?? '0') == '1' ? 'checked' : '' }} class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
<label for="game_enabled" class="ml-2 block text-sm text-gray-900 dark:text-gray-200">啟用互動遊戲</label>
|
||||
<label for="game_enabled" class="ml-2 block text-sm {{ $textPrimary }}">啟用互動遊戲</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="questionnaire_url" class="block text-sm font-medium text-gray-600 dark:text-gray-400">問卷 URL</label>
|
||||
<input type="text" name="questionnaire_url" id="questionnaire_url" value="{{ $configs['game']->where('key', 'questionnaire_url')->first()->value ?? '' }}" class="mt-1 block w-full bg-white dark:bg-gray-700 rounded-md shadow-sm py-2 px-3 {{ $inputText }} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="questionnaire_url" class="block text-sm font-medium {{ $textSecondary }}">問卷 URL</label>
|
||||
<input type="text" name="questionnaire_url" id="questionnaire_url" value="{{ $configs['game']->where('key', 'questionnaire_url')->first()->value ?? '' }}" class="mt-1 block w-full {{ $inputBg }} rounded-md shadow-sm py-2 px-3 {{ $inputText }} focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,130 +1,111 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
<!-- Grid -->
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
<!-- Card -->
|
||||
<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>
|
||||
<!-- Card -->
|
||||
<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>
|
||||
<!-- Card -->
|
||||
<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>
|
||||
<!-- Card -->
|
||||
<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>
|
||||
</div>
|
||||
@php
|
||||
$theme = request()->cookie('theme', 'dark-blue');
|
||||
$isLight = in_array($theme, ['light-blue', 'light-green']);
|
||||
$cardBg = $isLight ? 'bg-white' : 'bg-gray-800';
|
||||
$textPrimary = $isLight ? 'text-gray-900' : 'text-gray-200';
|
||||
$textSecondary = $isLight ? 'text-gray-600' : 'text-gray-400';
|
||||
$borderColor = $isLight ? 'border-gray-200' : 'border-gray-700';
|
||||
@endphp
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h3 class="{{ $textPrimary }} text-3xl font-medium">儀表板</h3>
|
||||
|
||||
<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 class="mt-4">
|
||||
<div class="flex flex-wrap -mx-6">
|
||||
<!-- Total Machines -->
|
||||
<div class="w-full px-6 sm:w-1/2 xl:w-1/4">
|
||||
<div class="flex items-center px-5 py-6 shadow-sm rounded-md {{ $cardBg }}">
|
||||
<div class="p-3 rounded-full bg-indigo-600 bg-opacity-75">
|
||||
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mx-5">
|
||||
<h4 class="text-2xl font-semibold {{ $textPrimary }}">{{ $totalMachines }}</h4>
|
||||
<div class="{{ $textSecondary }}">總機台數</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Online Machines -->
|
||||
<div class="w-full px-6 sm:w-1/2 xl:w-1/4 mt-6 sm:mt-0">
|
||||
<div class="flex items-center px-5 py-6 shadow-sm rounded-md {{ $cardBg }}">
|
||||
<div class="p-3 rounded-full bg-green-600 bg-opacity-75">
|
||||
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mx-5">
|
||||
<h4 class="text-2xl font-semibold {{ $textPrimary }}">{{ $onlineMachines }}</h4>
|
||||
<div class="{{ $textSecondary }}">連線中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offline Machines -->
|
||||
<div class="w-full px-6 sm:w-1/2 xl:w-1/4 mt-6 xl:mt-0">
|
||||
<div class="flex items-center px-5 py-6 shadow-sm rounded-md {{ $cardBg }}">
|
||||
<div class="p-3 rounded-full bg-gray-600 bg-opacity-75">
|
||||
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mx-5">
|
||||
<h4 class="text-2xl font-semibold {{ $textPrimary }}">{{ $offlineMachines }}</h4>
|
||||
<div class="{{ $textSecondary }}">離線</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Machines -->
|
||||
<div class="w-full px-6 sm:w-1/2 xl:w-1/4 mt-6 xl:mt-0">
|
||||
<div class="flex items-center px-5 py-6 shadow-sm rounded-md {{ $cardBg }}">
|
||||
<div class="p-3 rounded-full bg-red-600 bg-opacity-75">
|
||||
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mx-5">
|
||||
<h4 class="text-2xl font-semibold {{ $textPrimary }}">{{ $errorMachines }}</h4>
|
||||
<div class="{{ $textSecondary }}">異常</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 即時動態 -->
|
||||
<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 class="mt-8">
|
||||
<div class="flex flex-col mt-8">
|
||||
<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 {{ $borderColor }}">
|
||||
<!-- 這裡可以放最近的銷售紀錄或日誌 -->
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">標題</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">狀態</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">時間</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="{{ $cardBg }}">
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
<div class="text-sm leading-5 {{ $textPrimary }}">系統初始化</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">正常</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }} text-sm leading-5 {{ $textSecondary }}">
|
||||
剛剛
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-10">
|
||||
<p class="text-sm text-slate-400">目前尚無動態資料</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
// Here you would initialize charts using ApexCharts or similar,
|
||||
// as Preline examples often use ApexCharts.
|
||||
// For now, placeholders are sufficient.
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
@endphp
|
||||
|
||||
{{-- Toast 通知 --}}
|
||||
@if(session('success'))
|
||||
<div x-data="{ show: false }"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
x-init="setTimeout(() => { show = true; setTimeout(() => show = false, 3000) }, 50)"
|
||||
x-transition:enter="transition cubic-bezier(0.34, 1.56, 0.64, 1) duration-300"
|
||||
x-transition:enter-start="opacity-0 -translate-y-40"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-400"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-40"
|
||||
class="fixed top-4 left-0 right-0 mx-auto w-max z-[100] bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">儲值回饋設定</h3>
|
||||
<button onclick="document.getElementById('createModal').classList.remove('hidden')" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
|
||||
新增規則
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white dark:bg-gray-800 rounded-lg overflow-hidden">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">名稱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">最低儲值</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">回饋類型</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">回饋值</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">狀態</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y border-gray-200 dark:border-gray-700">
|
||||
@forelse($rules as $rule)
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $rule->name }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">${{ number_format($rule->min_amount) }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $rule->bonus_type == 'fixed' ? '固定金額' : '百分比' }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $rule->bonus_type == 'fixed' ? '$'.number_format($rule->bonus_value) : $rule->bonus_value.'%' }}</td>
|
||||
<td class="px-6 py-4">
|
||||
@if($rule->is_active)
|
||||
<span class="px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs">啟用</span>
|
||||
@else
|
||||
<span class="px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs">停用</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<form action="{{ route('admin.deposit-bonus-rules.destroy', $rule) }}" method="POST" class="inline" onsubmit="return confirm('確定要刪除嗎?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">刪除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-4 text-center text-gray-600 dark:text-gray-400">尚無資料</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Create Modal --}}
|
||||
<div id="createModal" class="hidden fixed inset-0 z-50 overflow-y-auto" @keydown.escape.window="document.getElementById('createModal').classList.add('hidden')">
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" onclick="document.getElementById('createModal').classList.add('hidden')"></div>
|
||||
|
||||
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-lg font-semibold">新增儲值回饋規則</h3>
|
||||
</div>
|
||||
<form action="{{ route('admin.deposit-bonus-rules.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">名稱</label>
|
||||
<input type="text" name="name" required class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">最低儲值金額</label>
|
||||
<input type="number" name="min_amount" value="0" step="0.01" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">回饋類型</label>
|
||||
<select name="bonus_type" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
<option value="fixed">固定金額</option>
|
||||
<option value="percentage">百分比</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">回饋值</label>
|
||||
<input type="number" name="bonus_value" value="0" step="0.01" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1" checked id="is_active" class="mr-2 rounded text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_active" class="text-gray-600 dark:text-gray-400 text-sm">啟用</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">取消</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">建立</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,158 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$typeLabels = [
|
||||
'points' => '點數',
|
||||
'coupon' => '優惠券',
|
||||
'product' => '商品',
|
||||
'discount' => '折扣',
|
||||
'cash' => '現金',
|
||||
];
|
||||
|
||||
$triggerLabels = [
|
||||
'register' => '註冊',
|
||||
'birthday' => '生日',
|
||||
'annual' => '年度',
|
||||
'upgrade' => '升級',
|
||||
'manual' => '手動',
|
||||
];
|
||||
@endphp
|
||||
|
||||
{{-- Toast 通知 --}}
|
||||
@if(session('success'))
|
||||
<div x-data="{ show: false }"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
x-init="setTimeout(() => { show = true; setTimeout(() => show = false, 3000) }, 50)"
|
||||
x-transition:enter="transition cubic-bezier(0.34, 1.56, 0.64, 1) duration-300"
|
||||
x-transition:enter-start="opacity-0 -translate-y-40"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-400"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-40"
|
||||
class="fixed top-4 left-0 right-0 mx-auto w-max z-[100] bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">禮品設定</h3>
|
||||
<button onclick="document.getElementById('createModal').classList.remove('hidden')" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
|
||||
新增禮品
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white dark:bg-gray-800 rounded-lg overflow-hidden">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">名稱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">類型</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">數值</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">適用等級</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">觸發條件</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">狀態</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y border-gray-200 dark:border-gray-700">
|
||||
@forelse($gifts as $gift)
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $gift->name }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $typeLabels[$gift->type] ?? $gift->type }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $gift->value }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $gift->tier?->name ?? '全部' }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $triggerLabels[$gift->trigger] ?? $gift->trigger }}</td>
|
||||
<td class="px-6 py-4">
|
||||
@if($gift->is_active)
|
||||
<span class="px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs">啟用</span>
|
||||
@else
|
||||
<span class="px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs">停用</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<form action="{{ route('admin.gift-definitions.destroy', $gift) }}" method="POST" class="inline" onsubmit="return confirm('確定要刪除嗎?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">刪除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-4 text-center text-gray-600 dark:text-gray-400">尚無資料</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Create Modal --}}
|
||||
<div id="createModal" class="hidden fixed inset-0 z-50 overflow-y-auto" @keydown.escape.window="document.getElementById('createModal').classList.add('hidden')">
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" onclick="document.getElementById('createModal').classList.add('hidden')"></div>
|
||||
|
||||
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-lg font-semibold">新增禮品</h3>
|
||||
</div>
|
||||
<form action="{{ route('admin.gift-definitions.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">名稱</label>
|
||||
<input type="text" name="name" required class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">類型</label>
|
||||
<select name="type" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
@foreach($typeLabels as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">數值</label>
|
||||
<input type="number" name="value" value="0" step="0.01" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">適用等級</label>
|
||||
<select name="tier_id" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
<option value="">全部</option>
|
||||
@foreach($tiers as $tier)
|
||||
<option value="{{ $tier->id }}">{{ $tier->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">觸發條件</label>
|
||||
<select name="trigger" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
@foreach($triggerLabels as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">有效天數</label>
|
||||
<input type="number" name="validity_days" value="30" min="1" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1" checked id="is_active" class="mr-2 rounded text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_active" class="text-gray-600 dark:text-gray-400 text-sm">啟用</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">取消</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">建立</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -2,27 +2,27 @@
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">新增機台</h3>
|
||||
<h3 class="text-gray-300 text-3xl font-medium">新增機台</h3>
|
||||
|
||||
<div class="mt-8">
|
||||
<form action="{{ route('admin.machines.store') }}" method="POST" class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
|
||||
<form action="{{ route('admin.machines.store') }}" method="POST" class="bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">機台名稱</label>
|
||||
<input type="text" name="name" id="name" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
<label for="name" class="block text-sm font-medium text-gray-400">機台名稱</label>
|
||||
<input type="text" name="name" id="name" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
@error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">位置</label>
|
||||
<input type="text" name="location" id="location" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="location" class="block text-sm font-medium text-gray-400">位置</label>
|
||||
<input type="text" name="location" id="location" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
@error('location') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">狀態</label>
|
||||
<select name="status" id="status" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="status" class="block text-sm font-medium text-gray-400">狀態</label>
|
||||
<select name="status" id="status" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="offline">離線</option>
|
||||
<option value="online">連線中</option>
|
||||
<option value="error">異常</option>
|
||||
@@ -31,19 +31,19 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">溫度 (°C)</label>
|
||||
<input type="number" step="0.1" name="temperature" id="temperature" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="temperature" class="block text-sm font-medium text-gray-400">溫度 (°C)</label>
|
||||
<input type="number" step="0.1" name="temperature" id="temperature" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
@error('temperature') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">韌體版本</label>
|
||||
<input type="text" name="firmware_version" id="firmware_version" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="firmware_version" class="block text-sm font-medium text-gray-400">韌體版本</label>
|
||||
<input type="text" name="firmware_version" id="firmware_version" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
@error('firmware_version') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">取消</a>
|
||||
<a href="{{ route('admin.machines.index') }}" class="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded mr-2">取消</a>
|
||||
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">建立</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">編輯機台</h3>
|
||||
<h3 class="text-gray-300 text-3xl font-medium">編輯機台</h3>
|
||||
|
||||
<div class="mt-8">
|
||||
<form action="{{ route('admin.machines.update', $machine) }}" method="POST" class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
|
||||
<form action="{{ route('admin.machines.update', $machine) }}" method="POST" class="bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-400">機台名稱</label>
|
||||
<input type="text" name="name" id="name" value="{{ old('name', $machine->name) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
<label for="name" class="block text-sm font-medium text-gray-400">機台名稱</label>
|
||||
<input type="text" name="name" id="name" value="{{ old('name', $machine->name) }}" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" required>
|
||||
@error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-gray-400">位置</label>
|
||||
<input type="text" name="location" id="location" value="{{ old('location', $machine->location) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="location" class="block text-sm font-medium text-gray-400">位置</label>
|
||||
<input type="text" name="location" id="location" value="{{ old('location', $machine->location) }}" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
@error('location') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">狀態</label>
|
||||
<select name="status" id="status" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="status" class="block text-sm font-medium text-gray-400">狀態</label>
|
||||
<select name="status" id="status" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<option value="offline" {{ $machine->status == 'offline' ? 'selected' : '' }}>離線</option>
|
||||
<option value="online" {{ $machine->status == 'online' ? 'selected' : '' }}>連線中</option>
|
||||
<option value="error" {{ $machine->status == 'error' ? 'selected' : '' }}>異常</option>
|
||||
@@ -32,19 +32,19 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="temperature" class="block text-sm font-medium text-gray-700 dark:text-gray-400">溫度 (°C)</label>
|
||||
<input type="number" step="0.1" name="temperature" id="temperature" value="{{ old('temperature', $machine->temperature) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="temperature" class="block text-sm font-medium text-gray-400">溫度 (°C)</label>
|
||||
<input type="number" step="0.1" name="temperature" id="temperature" value="{{ old('temperature', $machine->temperature) }}" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
@error('temperature') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="firmware_version" class="block text-sm font-medium text-gray-700 dark:text-gray-400">韌體版本</label>
|
||||
<input type="text" name="firmware_version" id="firmware_version" value="{{ old('firmware_version', $machine->firmware_version) }}" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<label for="firmware_version" class="block text-sm font-medium text-gray-400">韌體版本</label>
|
||||
<input type="text" name="firmware_version" id="firmware_version" value="{{ old('firmware_version', $machine->firmware_version) }}" class="mt-1 block w-full bg-gray-700 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
@error('firmware_version') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded mr-2">取消</a>
|
||||
<a href="{{ route('admin.machines.index') }}" class="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded mr-2">取消</a>
|
||||
<button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">更新</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('header')
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('機台管理') }}
|
||||
</h2>
|
||||
@endsection
|
||||
|
||||
@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>
|
||||
@php
|
||||
$theme = request()->cookie('theme', 'dark-blue');
|
||||
$isLight = in_array($theme, ['light-blue', 'light-green']);
|
||||
$cardBg = $isLight ? 'bg-white' : 'bg-gray-800';
|
||||
$textPrimary = $isLight ? 'text-gray-900' : 'text-gray-200';
|
||||
$textSecondary = $isLight ? 'text-gray-600' : 'text-gray-400';
|
||||
$borderColor = $isLight ? 'border-gray-200' : 'border-gray-700';
|
||||
@endphp
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="{{ $textPrimary }} 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>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<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 {{ $borderColor }}">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名稱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">位置</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">狀態</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">溫度</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">最後心跳</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">名稱</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">位置</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">狀態</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">溫度</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">最後心跳</th>
|
||||
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $cardBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<tbody class="{{ $cardBg }}">
|
||||
@foreach($machines as $machine)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
<div class="text-sm leading-5 font-medium {{ $textPrimary }}">{{ $machine->name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $machine->location }}
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
<div class="text-sm leading-5 {{ $textSecondary }}">{{ $machine->location ?? '-' }}</div>
|
||||
</td>
|
||||
<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 class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
@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>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $machine->temperature ?? '--' }} °C
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
<div class="text-sm leading-5 {{ $textSecondary }}">{{ $machine->temperature ? $machine->temperature . '°C' : '-' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '從未連線' }}
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||
<div class="text-sm leading-5 {{ $textSecondary }}">{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '-' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ route('admin.machines.show', $machine->id) }}" class="text-indigo-600 hover:text-indigo-900 bg-indigo-50 px-3 py-1 rounded-md transition border border-indigo-100">查看日誌</a>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }} 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>
|
||||
</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,85 +1,78 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('header')
|
||||
<div class="flex justify-between items-center">
|
||||
<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
|
||||
|
||||
@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 class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="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-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded">
|
||||
返回列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Basic Info -->
|
||||
<div class="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">
|
||||
<div>
|
||||
<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>
|
||||
<p class="text-sm text-gray-400">位置</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->location ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase">位置</p>
|
||||
<p class="text-sm">{{ $machine->location }}</p>
|
||||
<p class="text-sm 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
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase">最後心跳時間</p>
|
||||
<p class="text-sm">{{ $machine->last_heartbeat_at ?? 'N/A' }}</p>
|
||||
<p class="text-sm 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-400">韌體版本</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->firmware_version ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">最後心跳</p>
|
||||
<p class="text-lg text-gray-200">{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日誌顯示區 -->
|
||||
<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>
|
||||
<!-- Logs -->
|
||||
<div class="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>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-8 text-center text-gray-500 italic">暫無相關日誌</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-sm 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
@endphp
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">會員列表</h3>
|
||||
|
||||
<div class="mt-8">
|
||||
{{-- 搜尋與篩選 (預留空間) --}}
|
||||
|
||||
<div class="flex flex-col mt-4">
|
||||
<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>
|
||||
<tr>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-gray-700 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">
|
||||
UUID
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-gray-700 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-gray-100 dark:bg-gray-700 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">
|
||||
Email
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-gray-700 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-gray-100 dark:bg-gray-700 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-gray-100 dark:bg-gray-700 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">
|
||||
註冊時間
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
@forelse ($members as $member)
|
||||
<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-600 dark:text-gray-400">{{ $member->uuid }}</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 font-bold text-gray-900 dark:text-gray-200">{{ $member->name }}</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-900 dark:text-gray-200">{{ $member->email ?? '-' }}</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-900 dark:text-gray-200">{{ $member->phone ?? '-' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
|
||||
@if($member->is_active)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-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>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700 text-sm leading-5 text-gray-600 dark:text-gray-400">
|
||||
{{ $member->created_at->format('Y-m-d H:i') }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700 text-center text-gray-600 dark:text-gray-400">
|
||||
尚無會員資料
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $members->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,120 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
@endphp
|
||||
|
||||
{{-- Toast 通知 --}}
|
||||
@if(session('success'))
|
||||
<div x-data="{ show: false }"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
x-init="setTimeout(() => { show = true; setTimeout(() => show = false, 3000) }, 50)"
|
||||
x-transition:enter="transition cubic-bezier(0.34, 1.56, 0.64, 1) duration-300"
|
||||
x-transition:enter-start="opacity-0 -translate-y-40"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-400"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-40"
|
||||
class="fixed top-4 left-0 right-0 mx-auto w-max z-[100] bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">會員等級設定</h3>
|
||||
<button onclick="document.getElementById('createModal').classList.remove('hidden')" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
|
||||
新增等級
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white dark:bg-gray-800 rounded-lg overflow-hidden">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">名稱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">年費</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">折扣</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">點數倍率</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">預設</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y border-gray-200 dark:border-gray-700">
|
||||
@forelse($tiers as $tier)
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $tier->name }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $tier->annual_fee == 0 ? '免費' : '$'.number_format($tier->annual_fee) }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $tier->discount_rate * 100 }}%</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $tier->point_multiplier }}x</td>
|
||||
<td class="px-6 py-4">
|
||||
@if($tier->is_default)
|
||||
<span class="px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs">預設</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<form action="{{ route('admin.membership-tiers.destroy', $tier) }}" method="POST" class="inline" onsubmit="return confirm('確定要刪除嗎?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">刪除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-4 text-center text-gray-600 dark:text-gray-400">尚無資料</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Create Modal --}}
|
||||
<div id="createModal" class="hidden fixed inset-0 z-50 overflow-y-auto" x-data @keydown.escape.window="document.getElementById('createModal').classList.add('hidden')">
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
||||
{{-- 背景遮罩 --}}
|
||||
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" onclick="document.getElementById('createModal').classList.add('hidden')"></div>
|
||||
|
||||
{{-- Modal 內容 --}}
|
||||
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-lg font-semibold">新增會員等級</h3>
|
||||
</div>
|
||||
<form action="{{ route('admin.membership-tiers.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">名稱</label>
|
||||
<input type="text" name="name" required class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500 focus:border-transparent">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">年費</label>
|
||||
<input type="number" name="annual_fee" value="0" step="0.01" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">折扣比例 (0.95 = 95折)</label>
|
||||
<input type="number" name="discount_rate" value="1.00" step="0.01" min="0" max="1" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">點數倍率</label>
|
||||
<input type="number" name="point_multiplier" value="1.00" step="0.01" min="0" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="is_default" value="1" id="is_default" class="mr-2 rounded text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_default" class="text-gray-600 dark:text-gray-400 text-sm">設為預設等級</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">取消</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">建立</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,26 +1,39 @@
|
||||
@php
|
||||
$theme = request()->cookie('theme', 'dark-blue');
|
||||
$themes = [
|
||||
'dark-blue' => ['card' => 'bg-gray-800', 'accent' => 'indigo'],
|
||||
'dark-purple' => ['card' => 'bg-slate-800', 'accent' => 'purple'],
|
||||
'dark-green' => ['card' => 'bg-zinc-800', 'accent' => 'emerald'],
|
||||
'light-blue' => ['card' => 'bg-white', 'accent' => 'blue'],
|
||||
'light-green' => ['card' => 'bg-white', 'accent' => 'green'],
|
||||
];
|
||||
$currentTheme = $themes[$theme] ?? $themes['dark-blue'];
|
||||
$isLight = in_array($theme, ['light-blue', 'light-green']);
|
||||
@endphp
|
||||
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 text-center border border-gray-200 dark:border-gray-700">
|
||||
<div class="{{ $currentTheme['card'] }} rounded-lg shadow-lg p-8 text-center border {{ $isLight ? 'border-gray-200' : 'border-gray-700' }}">
|
||||
<div class="mb-6">
|
||||
<svg class="mx-auto h-24 w-24 text-gray-400 dark:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="mx-auto h-24 w-24 text-{{ $currentTheme['accent'] }}-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">{{ $title ?? '功能頁面' }}</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6 text-lg">{{ $description ?? '此功能正在開發中' }}</p>
|
||||
<div class="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold">
|
||||
<h1 class="text-3xl font-bold {{ $isLight ? 'text-gray-900' : 'text-white' }} mb-4">{{ $title ?? '功能頁面' }}</h1>
|
||||
<p class="{{ $isLight ? 'text-gray-600' : 'text-gray-400' }} mb-6 text-lg">{{ $description ?? '此功能正在開發中' }}</p>
|
||||
<div class="inline-block px-6 py-3 bg-{{ $currentTheme['accent'] }}-600 text-white rounded-lg font-semibold">
|
||||
🚧 功能開發中
|
||||
</div>
|
||||
|
||||
@if(isset($features) && count($features) > 0)
|
||||
<div class="mt-8 text-left max-w-2xl mx-auto">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">規劃功能:</h3>
|
||||
<ul class="space-y-2 text-gray-700 dark:text-gray-300">
|
||||
<h3 class="text-xl font-semibold {{ $isLight ? 'text-gray-900' : 'text-white' }} mb-4">規劃功能:</h3>
|
||||
<ul class="space-y-2 {{ $isLight ? 'text-gray-700' : 'text-gray-300' }}">
|
||||
@foreach($features as $feature)
|
||||
<li class="flex items-start">
|
||||
<svg class="h-6 w-6 text-blue-500 mr-2 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg class="h-6 w-6 text-{{ $currentTheme['accent'] }}-500 mr-2 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{{ $feature }}</span>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
|
||||
$triggerLabels = [
|
||||
'purchase' => '消費',
|
||||
'deposit' => '儲值',
|
||||
'register' => '註冊',
|
||||
'birthday' => '生日',
|
||||
'referral' => '推薦',
|
||||
];
|
||||
@endphp
|
||||
|
||||
{{-- Toast 通知 --}}
|
||||
@if(session('success'))
|
||||
<div x-data="{ show: false }"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
x-init="setTimeout(() => { show = true; setTimeout(() => show = false, 3000) }, 50)"
|
||||
x-transition:enter="transition cubic-bezier(0.34, 1.56, 0.64, 1) duration-300"
|
||||
x-transition:enter-start="opacity-0 -translate-y-40"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-400"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-40"
|
||||
class="fixed top-4 left-0 right-0 mx-auto w-max z-[100] bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="container mx-auto px-6 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">點數規則設定</h3>
|
||||
<button onclick="document.getElementById('createModal').classList.remove('hidden')" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
|
||||
新增規則
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white dark:bg-gray-800 rounded-lg overflow-hidden">
|
||||
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">名稱</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">觸發條件</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">每單位點數</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">單位金額</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">有效天數</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">狀態</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-400 uppercase">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y border-gray-200 dark:border-gray-700">
|
||||
@forelse($rules as $rule)
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $rule->name }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $triggerLabels[$rule->trigger] ?? $rule->trigger }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $rule->points_per_unit }} 點</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">${{ number_format($rule->unit_amount) }}</td>
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-gray-200">{{ $rule->validity_days }} 天</td>
|
||||
<td class="px-6 py-4">
|
||||
@if($rule->is_active)
|
||||
<span class="px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs">啟用</span>
|
||||
@else
|
||||
<span class="px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs">停用</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<form action="{{ route('admin.point-rules.destroy', $rule) }}" method="POST" class="inline" onsubmit="return confirm('確定要刪除嗎?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">刪除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-6 py-4 text-center text-gray-600 dark:text-gray-400">尚無資料</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Create Modal --}}
|
||||
<div id="createModal" class="hidden fixed inset-0 z-50 overflow-y-auto" @keydown.escape.window="document.getElementById('createModal').classList.add('hidden')">
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" onclick="document.getElementById('createModal').classList.add('hidden')"></div>
|
||||
|
||||
<div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-gray-900 dark:text-gray-200 text-lg font-semibold">新增點數規則</h3>
|
||||
</div>
|
||||
<form action="{{ route('admin.point-rules.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">名稱</label>
|
||||
<input type="text" name="name" required class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">觸發條件</label>
|
||||
<select name="trigger" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
@foreach($triggerLabels as $key => $label)
|
||||
<option value="{{ $key }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">每單位獲得點數</label>
|
||||
<input type="number" name="points_per_unit" value="1" min="1" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">單位金額</label>
|
||||
<input type="number" name="unit_amount" value="100" step="0.01" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-600 dark:text-gray-400 text-sm block mb-1">有效天數</label>
|
||||
<input type="number" name="validity_days" value="365" min="1" class="w-full px-3 py-2 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 border rounded-md text-gray-900 dark:text-gray-200 focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" name="is_active" value="1" checked id="is_active" class="mr-2 rounded text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_active" class="text-gray-600 dark:text-gray-400 text-sm">啟用</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 text-gray-600 dark:text-gray-400 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">取消</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">建立</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,123 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ config('app.name', 'Laravel') }}</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" />
|
||||
<script>
|
||||
// Dark mode
|
||||
const html = document.querySelector('html');
|
||||
const isLightOrAuto = localStorage.getItem('hs_theme') === 'light' || (localStorage.getItem('hs_theme') !== 'dark' && !window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
const isDarkOrAuto = localStorage.getItem('hs_theme') === 'dark' || (localStorage.getItem('hs_theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
<x-guest-layout>
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
if (isLightOrAuto && html.classList.contains('dark')) html.classList.remove('dark');
|
||||
else if (isDarkOrAuto && html.classList.contains('light')) html.classList.remove('light');
|
||||
else if (isDarkOrAuto && !html.classList.contains('dark')) html.classList.add('dark');
|
||||
else if (isLightOrAuto && !html.classList.contains('light')) html.classList.add('light');
|
||||
</script>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased bg-white dark:bg-slate-900">
|
||||
<div class="flex h-screen">
|
||||
<!-- Sidebar - Image Section -->
|
||||
<div class="hidden md:flex md:w-1/2 lg:w-3/5 bg-gray-50 border-r border-gray-200 dark:bg-slate-800 dark:border-slate-700 justify-center items-center relative overflow-hidden">
|
||||
<!-- Background Pattern or Gradient -->
|
||||
<div class="absolute inset-0 bg-gradient-to-tr from-blue-600 to-purple-500 opacity-90 dark:from-blue-900 dark:to-purple-900"></div>
|
||||
|
||||
<div class="relative z-10 text-center px-6">
|
||||
<h2 class="text-3xl font-bold text-white sm:text-4xl">
|
||||
{{ config('app.name', 'Star Cloud') }}
|
||||
</h2>
|
||||
<p class="mt-3 text-lg text-blue-100">
|
||||
智能販賣機管理平台
|
||||
</p>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Main Content - Login Form -->
|
||||
<div class="w-full md:w-1/2 lg:w-2/5 flex flex-col justify-center px-4 sm:px-6 md:px-8 lg:px-10 py-8">
|
||||
<div class="max-w-md w-full mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">登入</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
請輸入您的帳號密碼以繼續
|
||||
</p>
|
||||
</div>
|
||||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<!-- Session Status -->
|
||||
@if (session('status'))
|
||||
<div class="mb-4 text-sm font-medium text-green-600">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="current-password" />
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-y-4">
|
||||
<!-- Form Group -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm mb-2 dark:text-white">帳號</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="username" name="username" value="{{ old('username') }}" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-slate-700 dark:text-gray-400 dark:focus:ring-gray-600" required autofocus autocomplete="username">
|
||||
<div class="hidden absolute inset-y-0 end-0 flex items-center pointer-events-none pe-3">
|
||||
<svg class="h-5 w-5 text-red-500" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@if ($errors->get('username'))
|
||||
<p class="text-xs text-red-600 mt-2" id="username-error">{{ $errors->first('username') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<!-- End Form Group -->
|
||||
|
||||
<!-- Form Group -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<label for="password" class="block text-sm mb-2 dark:text-white">密碼</label>
|
||||
@if (Route::has('password.request'))
|
||||
<a class="text-sm text-blue-600 decoration-2 hover:underline font-medium dark:text-blue-500" href="{{ route('password.request') }}">
|
||||
忘記密碼?
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input type="password" id="password" name="password" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-slate-700 dark:text-gray-400 dark:focus:ring-gray-600" required autocomplete="current-password">
|
||||
<div class="hidden absolute inset-y-0 end-0 flex items-center pointer-events-none pe-3">
|
||||
<svg class="h-5 w-5 text-red-500" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@if ($errors->get('password'))
|
||||
<p class="text-xs text-red-600 mt-2" id="password-error">{{ $errors->first('password') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<!-- End Form Group -->
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex">
|
||||
<input id="remember-me" name="remember" type="checkbox" class="shrink-0 mt-0.5 border-gray-200 rounded text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800">
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<label for="remember-me" class="text-sm dark:text-white">記住我</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Checkbox -->
|
||||
|
||||
<button type="submit" class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">
|
||||
登入
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="block mt-4">
|
||||
<label for="remember_me" class="inline-flex items-center">
|
||||
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
|
||||
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
@if (Route::has('password.request'))
|
||||
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
{{ __('Log in') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user