Compare commits

16 Commits
main ... demo

Author SHA1 Message Date
74b6c71c95 feat: Preline UI 改版與深色模式修復
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 35s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-13 13:28:58 +08:00
88c3678a4d chore: 加入 Docker 8.5 Dockerfile 並恢復 deploy.yaml 原始版本
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 1m22s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
- 加入 docker/8.5 目錄(從 Laravel Sail 8.4 複製)
- 恢復 deploy.yaml 到原始版本
2026-01-13 11:04:58 +08:00
649cbaab02 fix: 修正部署腳本用戶權限問題
Some checks failed
Star-Cloud-Deploy-System / deploy-demo (push) Failing after 38s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
- 將 docker exec 用戶從 -u 1000:1000 改為 -u sail(容器內 sail UID 是 1337)
- 簡化權限修正為 chown -R sail:sail /var/www/html
- 同步更新 Demo 與正式環境的部署腳本

問題根因:容器內 sail 用戶 UID 是 1337,非預期的 1000,導致 npm install 無權寫入
2026-01-13 10:54:29 +08:00
9c2ef60463 fix: 更新部署腳本,完全清除 node_modules 後重建以解決權限問題
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 47s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
- 在 npm install 前先 rm -rf node_modules 再重建
- 確保 /.npm 和 node_modules 都有正確的 sail 擁有者
- 同步更新 Demo 與正式環境的部署腳本
2026-01-13 10:48:20 +08:00
f67a1dc11e style: 調整 deploy.yaml 格式
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 33s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-13 10:45:13 +08:00
a578c7f261 fix: 修正部署流程中的 npm 權限問題
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 40s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
- 在 npm install 前先用 root 身份修正 /.npm 和 node_modules 權限
- 加入 npm cache clean --force 避免快取權限衝突
- 同時修正 Demo 與正式環境的部署腳本
- 解決 EACCES 與 ENOTEMPTY 錯誤
2026-01-13 10:39:05 +08:00
84ef0c24e2 feat: 整合 Preline UI 3.x 與重寫 README 為 Docker 架構
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 44s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
- 新增 Preline UI 3.2.3 作為 UI 組件庫
- 更新 tailwind.config.js 整合 Preline
- 更新 app.js 初始化 Preline
- 完全重寫 README.md 以 Docker 容器化架構為核心
- 新增 Docker 常用指令大全
- 新增故障排除與生產部署指南
- 新增會員系統相關功能(會員、錢包、點數、會籍、禮物)
- 新增社交登入測試功能
2026-01-13 10:17:37 +08:00
55ba08c88f chore: 更新 deploy.yaml 部署流程
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 39s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 13:21:07 +08:00
11491e07aa fix: 修正空白的 add_username_to_users_table migration 檔案
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 47s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 13:11:01 +08:00
d3684385b2 fix: 修正 AdminUserSeeder 欄位結構與資料庫一致
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 44s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 13:08:00 +08:00
7db3ee3a05 feat: 分離 AdminUserSeeder 並重構 DatabaseSeeder 支援單獨執行
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 39s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 13:04:59 +08:00
a0d107ca79 feat: 更新 DatabaseSeeder 添加 admin 帳號的 username 和 role 欄位
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 56s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 13:02:34 +08:00
96b22cd577 fix: 修正 Step 4 部署錯誤,改用 SSH action 在 demo 伺服器執行 docker exec
All checks were successful
Star-Cloud-Deploy-System / deploy-demo (push) Successful in 50s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 11:46:56 +08:00
3ed8b00cab chore: 移除 p.bat 檔案
Some checks failed
Star-Cloud-Deploy-System / deploy-demo (push) Failing after 2m18s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 11:39:59 +08:00
2aff99fc76 add deploy
Some checks failed
Star-Cloud-Deploy-System / deploy-demo (push) Failing after 28s
Star-Cloud-Deploy-System / deploy-production (push) Has been skipped
2026-01-12 11:27:34 +08:00
2ed0ee272e 登入修正 2026-01-12 11:23:41 +08:00
71 changed files with 4865 additions and 792 deletions

View File

@@ -1,5 +1,5 @@
APP_NAME=startCloud
COMPOSE_PROJECT_NAME=start-cloud
APP_NAME=starCloud
COMPOSE_PROJECT_NAME=star-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=start-cloud
DB_DATABASE=star-cloud
DB_USERNAME=sail
DB_PASSWORD=password
# FORWARD_DB_PORT=3308

View File

@@ -0,0 +1,198 @@
name: Star-Cloud-Deploy-System
on:
push:
branches:
- demo
- main
jobs:
# --- 1. Demo 環境部署 (103 本機) ---
deploy-demo:
if: github.ref == 'refs/heads/demo'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
github-server-url: http://192.168.0.103:3000
repository: ${{ github.repository }}
- name: Step 1 - Push Code to Demo
run: |
apt-get update && apt-get install -y rsync openssh-client
mkdir -p ~/.ssh
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' \
-e "ssh -i ~/.ssh/id_rsa_demo -o StrictHostKeyChecking=no" \
./ amba@192.168.0.103:/home/amba/star-cloud/
rm ~/.ssh/id_rsa_demo
# 2. 檢查是否需要重建容器(只有 Dockerfile 或 compose.yaml 變動時才重建)
- name: Step 2 - Check if Rebuild Needed
id: check_rebuild
uses: appleboy/ssh-action@master
with:
host: 192.168.0.103
port: 22
username: amba
key: ${{ secrets.DEMO_SSH_KEY }}
script: |
cd /home/amba/star-cloud
# 檢查最近的 commit 是否包含 Dockerfile 或 compose.yaml 的變更
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -qE '(Dockerfile|compose\.yaml|docker-compose\.yaml)'; then
echo "REBUILD_NEEDED=true"
else
echo "REBUILD_NEEDED=false"
fi
# 3. 啟動或重建容器(根據檢查結果決定是否加 --build
- name: Step 3 - Container Up & Health Check
uses: appleboy/ssh-action@master
with:
host: 192.168.0.103
port: 22
username: amba
key: ${{ secrets.DEMO_SSH_KEY }}
script: |
cd /home/amba/star-cloud
chown -R 1000:1000 .
# 檢查是否需要重建
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -qE '(Dockerfile|compose\.yaml|docker-compose\.yaml)'; then
echo "🔄 偵測到 Docker 相關檔案變更,執行完整重建..."
WWWGROUP=1000 WWWUSER=1000 docker compose up -d --build --wait
else
echo "⚡ 無 Docker 檔案變更,僅重載服務..."
# 確保容器正在運行(若未運行則啟動)
if ! docker ps --format '{{.Names}}' | grep -q 'star-cloud-laravel'; then
echo "容器未運行,正在啟動..."
WWWGROUP=1000 WWWUSER=1000 docker compose up -d --wait
else
echo "容器已運行,跳過 docker compose直接進行程式碼部署..."
fi
fi
echo "容器狀態:" && docker ps --filter "name=star-cloud-laravel"
- name: Step 4 - Composer & NPM Build
uses: appleboy/ssh-action@master
with:
host: 192.168.0.103
port: 22
username: amba
key: ${{ secrets.DEMO_SSH_KEY }}
command_timeout: 10m
script: |
cd /home/amba/star-cloud
docker exec -u 1000:1000 -w /var/www/html star-cloud-laravel sh -c "
# 1. 後端依賴 (Demo 環境建議加上 --no-interaction 避免卡住)
composer install --no-dev --optimize-autoloader --no-interaction &&
# 2. 前端編譯
npm install &&
npm run build &&
# 3. Laravel 初始化與優化
php artisan migrate --force &&
php artisan optimize:clear &&
php artisan optimize &&
php artisan view:cache
"
docker exec star-cloud-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
# --- 2. 正式環境部署 (erp.koori.tw:2224) ---
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
github-server-url: http://192.168.0.103:3000
repository: ${{ github.repository }}
- name: Step 1 - Push Code to Production
run: |
apt-get update && apt-get install -y rsync openssh-client
mkdir -p ~/.ssh
echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_rsa_prod
chmod 600 ~/.ssh/id_rsa_prod
rsync -avz --delete \
--exclude='.git' \
--exclude='.env' \
--exclude='node_modules' \
--exclude='vendor' \
--exclude='public/build' \
-e "ssh -p 2224 -i ~/.ssh/id_rsa_prod -o StrictHostKeyChecking=no" \
./ root@erp.koori.tw:/var/www/star-cloud-prod/
rm ~/.ssh/id_rsa_prod
# 2. 檢查是否需要重建容器(只有 Dockerfile 或 compose.yaml 變動時才重建)
- name: Step 2 - Check if Rebuild Needed
id: check_rebuild_prod
uses: appleboy/ssh-action@master
with:
host: erp.koori.tw
port: 2224
username: root
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /var/www/star-cloud-prod
# 檢查最近的 commit 是否包含 Dockerfile 或 compose.yaml 的變更
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -qE '(Dockerfile|compose\.yaml|docker-compose\.yaml)'; then
echo "REBUILD_NEEDED=true"
else
echo "REBUILD_NEEDED=false"
fi
# 3. 啟動或重建容器(根據檢查結果決定是否加 --build
- name: Step 3 - Container Up & Health Check
uses: appleboy/ssh-action@master
with:
host: erp.koori.tw
port: 2224
username: root
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /var/www/star-cloud-prod
chown -R 1000:1000 .
# 檢查是否需要重建
if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -qE '(Dockerfile|compose\.yaml|docker-compose\.yaml)'; then
echo "🔄 偵測到 Docker 相關檔案變更,執行完整重建..."
WWWGROUP=1000 WWWUSER=1000 docker compose up -d --build --wait
else
echo "⚡ 無 Docker 檔案變更,僅重載服務..."
# 確保容器正在運行(若未運行則啟動)
if ! docker ps --format '{{.Names}}' | grep -q 'star-cloud-laravel'; then
echo "容器未運行,正在啟動..."
WWWGROUP=1000 WWWUSER=1000 docker compose up -d --wait
else
echo "容器已運行,跳過 docker compose直接進行程式碼部署..."
fi
fi
echo "容器狀態:" && docker ps --filter "name=star-cloud-laravel"
docker exec -u 1000:1000 -w /var/www/html star-cloud-laravel sh -c "
composer install --no-dev --optimize-autoloader &&
npm install &&
npm run build
php artisan migrate --force &&
php artisan optimize:clear &&
php artisan optimize &&
php artisan view:cache
"
docker exec star-cloud-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache

414
README.md
View File

@@ -1,112 +1,392 @@
# Star Cloud 智能販賣機管理平台
## 專案簡介 (Project Description)
Star Cloud 是一個專為智能販賣機設計的後台管理系統,旨在提供全方位的機台監控、庫存管理、銷售分析與會員管理功能。透過此平台,管理者可以即時掌握機台運營狀態,優化補貨流程,並透過數據分析提升營運效益。
> 基於 Docker 的全方位智能販賣機後台管理系統
## 技術棧 (Technology Stack)
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 |
### 後端技術棧
### 後端 (Backend)
- **Framework**: Laravel 10.x
- **Language**: PHP 8.1+
- **Database**: MySQL 8.0+
- **Authentication**: Laravel Sanctum (API Token Authentication)
- **Tools**: Composer
- **Language**: PHP 8.5+
- **Database**: MySQL 8.0
- **Cache/Session**: Redis
- **Authentication**: Laravel Sanctum (API Token)
- **Package Manager**: Composer 2.x
### 前端 (Frontend)
- **Framework**: Blade Templates (Laravel 預設樣板引擎)
### 前端技術棧
- **Template Engine**: Blade Templates
- **UI Library**: Preline UI 3.x (Tailwind CSS 組件庫)
- **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+
若您尚未安裝 MySQLWindows 使用者可至 [MySQL 官網](https://dev.mysql.com/downloads/installer/) 下載 Installer或使用 XAMPP / Laragon 等整合環境。
- **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 專案
### 1. 下載專案 (Clone Repository)
```bash
git clone <repository_url>
cd star-cloud
```
### 2. 安裝依賴套件 (Install Dependencies)
#### 2. 環境設定
安裝後端 PHP 套件
```bash
composer install
```
複製環境變數範例檔案
安裝前端 Node.js 套件:
```bash
npm install
```
### 3. 環境變數設定 (Environment Setup)
複製範例環境設定檔:
```bash
cp .env.example .env
```
請開啟 `.env` 檔案,並依照您的本地環境設定資料庫連線資訊
```dotenv
**重要設定**`.env` 檔案
```env
# 應用程式設定
APP_NAME=Star Cloud
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8090
# 資料庫設定(對應 Docker Compose 服務)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=star_cloud
DB_USERNAME=root
DB_PASSWORD=your_password
DB_USERNAME=sail
DB_PASSWORD=password
# Redis 設定(對應 Docker Compose 服務)
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
# Vite 開發伺服器
VITE_PORT=5175
```
產生應用程式金鑰 (Application Key)
#### 3. 啟動 Docker 容器
啟動所有服務應用程式、資料庫、Redis
```bash
php artisan key:generate
docker compose up -d
```
### 4. 資料庫遷移 (Database Migration)
執行 Migration 以建立資料庫結構:
> **說明**`-d` 參數表示背景執行
檢查容器狀態:
```bash
php artisan migrate
```
php artisan migrate --seed
docker compose ps
```
### 4.1 預設管理員帳號 (Default Admin Account)
執行上述指令後,系統會建立一組預設管理員帳號:
- **Email**: `admin@star-cloud.com`
- **Password**: `password`
預期輸出:
```
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 安裝後端依賴**
### 5. 編譯前端資源 (Build Frontend Assets)
啟動開發模式 (Hot Module Replacement)
```bash
npm run dev
```
或編譯生產環境檔案:
```bash
npm run build
docker compose exec laravel.test composer install
```
### 6. 啟動伺服器 (Start Server)
啟動 Laravel 開發伺服器:
```bash
php artisan serve --port=8001
```
預設網址為http://localhost:8001
**4.2 產生應用程式金鑰**
## 主要功能模組
- **儀錶板 (Dashboard)**: 銷售數據概覽、機台狀態監控
- **機台管理 (Machine Management)**: 機台列表、遠端控制、日誌查詢
- **商品與庫存 (Inventory)**: 商品管理、進銷存、補貨單
- **銷售管理 (Sales)**: 交易紀錄、營收報表
- **權限設定 (Permissions)**: 角色與權限分配
```bash
docker compose exec laravel.test php artisan key:generate
```
**4.3 執行資料庫遷移與種子**
```bash
docker compose exec laravel.test php artisan migrate --seed
```
> **預設管理員帳號**
> - 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
```
---
## 主要功能模組
### 核心功能
| 模組 | 功能描述 |
|------|---------|
| **儀錶板** | 銷售數據總覽、機台狀態即時監控、營收統計圖表 |
| **機台管理** | 機台列表、遠端控制、日誌查詢、維修管理、效期控制 |
| **倉庫管理** | 倉庫列表、庫存管理、調撥單、採購單、補貨單 |
| **商品管理** | 商品資料、分類管理、商品報表分析 |
| **銷售管理** | 交易紀錄、金流管理、促銷設定、營收報表 |
| **會員系統** | 會員管理、點數系統、來店禮、Line 整合 |
| **權限控制** | 角色管理、權限分配、功能權限設定 |
| **遠端管理** | 機台重啟、遠端出貨、遠端結帳、庫存調整 |
---
## 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。

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\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', '儲值回饋規則已刪除');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\GiftDefinition;
use App\Models\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', '禮品已刪除');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\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', '會員等級已刪除');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\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', '點數規則已刪除');
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Member;
use App\Models\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' => '登出成功',
]);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\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,
]);
}
}

View File

@@ -0,0 +1,33 @@
<?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
]
]);
}
}

View File

@@ -27,11 +27,22 @@ class LoginRequest extends FormRequest
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'username' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
/**
* 取得驗證規則的自訂錯誤訊息
*/
public function messages(): array
{
return [
'username.required' => '請輸入帳號',
'password.required' => '請輸入密碼',
];
}
/**
* Attempt to authenticate the request's credentials.
*
@@ -41,11 +52,11 @@ class LoginRequest extends FormRequest
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
if (! Auth::attempt($this->only('username', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
'username' => trans('auth.failed'),
]);
}
@@ -68,7 +79,7 @@ class LoginRequest extends FormRequest
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'username' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
@@ -80,6 +91,6 @@ class LoginRequest extends FormRequest
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
return Str::transliterate(Str::lower($this->string('username')).'|'.$this->ip());
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Models;
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);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Models;
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);
}
}

147
app/Models/Member.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
namespace App\Models;
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,
]);
}
}

56
app/Models/MemberGift.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
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());
});
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Models;
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());
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
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');
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
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;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Models;
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;
}
}

47
app/Models/PointRule.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
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;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
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();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Models;
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);
}
}

View File

@@ -18,6 +18,7 @@ class User extends Authenticatable
* @var array<int, string>
*/
protected $fillable = [
'username',
'name',
'email',
'password',

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
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);
}
}

View File

@@ -6,8 +6,8 @@ services:
args:
WWWGROUP: '${WWWGROUP}'
image: 'sail-8.5/app'
container_name: start-cloud-laravel
hostname: start-cloud-laravel
container_name: star-cloud-laravel
hostname: star-cloud-laravel
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
@@ -29,8 +29,8 @@ services:
mysql:
image: 'mysql/mysql-server:8.0'
container_name: start-cloud-mysql
hostname: start-cloud-mysql
container_name: star-cloud-mysql
hostname: star-cloud-mysql
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
@@ -56,8 +56,8 @@ services:
timeout: 5s
redis:
image: 'redis:alpine'
container_name: start-cloud-redis
hostname: start-cloud-redis
container_name: star-cloud-redis
hostname: star-cloud-redis
ports:
- '${FORWARD_REDIS_PORT:-6379}:6379'
volumes:

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('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');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
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');
});
}
}
};

View File

@@ -0,0 +1,38 @@
<?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');
}
};

View File

@@ -0,0 +1,37 @@
<?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');
}
};

View File

@@ -0,0 +1,29 @@
<?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');
}
};

View File

@@ -0,0 +1,34 @@
<?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');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 儲值回饋規則
*/
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');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 會員點數帳戶
*/
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');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 點數異動紀錄
*/
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');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 點數規則
*/
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');
}
};

View File

@@ -0,0 +1,34 @@
<?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');
}
};

View File

@@ -0,0 +1,34 @@
<?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');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 禮品/福利定義
*/
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');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 會員禮品發放紀錄
*/
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');
}
};

View File

@@ -0,0 +1,45 @@
<?php
namespace Database\Seeders;
use App\Models\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 帳號建立成功!');
}
}

View File

@@ -9,15 +9,14 @@ class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* 執行全部 Seederphp artisan db:seed
* 執行單一 Seederphp artisan db:seed --class=AdminUserSeeder
*/
public function run(): void
{
// \App\Models\User::factory(10)->create();
\App\Models\User::factory()->create([
'name' => 'Admin',
'email' => 'admin@star-cloud.com',
'password' => bcrypt('password'),
$this->call([
AdminUserSeeder::class,
]);
}
}

74
docker/8.5/Dockerfile Normal file
View File

@@ -0,0 +1,74 @@
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"]

5
docker/8.5/php.ini Normal file
View File

@@ -0,0 +1,5 @@
[PHP]
post_max_size = 100M
upload_max_filesize = 100M
variables_order = EGPCS
pcov.directory = .

View File

@@ -0,0 +1,26 @@
#!/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

View File

@@ -0,0 +1,14 @@
[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 Normal file
View File

@@ -0,0 +1,198 @@
# 會員系統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) | ✅ 完成 |

View File

@@ -0,0 +1,239 @@
{
"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"
]
}
}
}
]
}

2
p.bat
View File

@@ -1,2 +0,0 @@
@ECHO OFF
D:\php81\php.exe %*

214
package-lock.json generated
View File

@@ -1,16 +1,18 @@
{
"name": "star-cloud",
"name": "html",
"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"
}
@@ -28,6 +30,13 @@
"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",
@@ -419,6 +428,34 @@
"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",
@@ -804,6 +841,74 @@
"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",
@@ -841,6 +946,13 @@
"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",
@@ -872,6 +984,21 @@
"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",
@@ -992,7 +1119,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -1126,6 +1252,27 @@
"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",
@@ -1150,6 +1297,17 @@
"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",
@@ -1575,11 +1733,24 @@
"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",
@@ -1745,6 +1916,13 @@
"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",
@@ -1832,7 +2010,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -1976,6 +2153,21 @@
"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",
@@ -2177,7 +2369,6 @@
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -2274,7 +2465,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2340,13 +2530,23 @@
"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",

View File

@@ -6,12 +6,14 @@
"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"
}

View File

@@ -1,7 +1,13 @@
import './bootstrap';
import Alpine from 'alpinejs';
import collapse from '@alpinejs/collapse';
Alpine.plugin(collapse);
window.Alpine = Alpine;
Alpine.start();
// 初始化 Preline UI
import 'preline';

View File

@@ -2,17 +2,10 @@
@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="{{ $textPrimary }} text-3xl font-medium">APP 管理設定</h3>
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">APP 管理設定</h3>
<div class="mt-8">
<form action="{{ route('admin.app-configs.update') }}" method="POST">
@@ -21,44 +14,44 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- UI Settings -->
<div class="{{ $cardBg }} rounded-lg shadow-xl overflow-hidden p-6">
<h4 class="text-xl font-semibold {{ $textPrimary }} mb-4">UI 元素設定</h4>
<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="space-y-4">
<div>
<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">
<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">
</div>
<div>
<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">
<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">
</div>
</div>
</div>
<!-- Helper Settings -->
<div class="{{ $cardBg }} rounded-lg shadow-xl overflow-hidden p-6">
<h4 class="text-xl font-semibold {{ $textPrimary }} mb-4">小幫手設定</h4>
<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="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 {{ $textPrimary }}">啟用小幫手</label>
<label for="helper_enabled" class="ml-2 block text-sm text-gray-900 dark:text-gray-200">啟用小幫手</label>
</div>
</div>
</div>
<!-- Game Settings -->
<div class="{{ $cardBg }} rounded-lg shadow-xl overflow-hidden p-6">
<h4 class="text-xl font-semibold {{ $textPrimary }} mb-4">問卷與互動遊戲</h4>
<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="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 {{ $textPrimary }}">啟用互動遊戲</label>
<label for="game_enabled" class="ml-2 block text-sm text-gray-900 dark:text-gray-200">啟用互動遊戲</label>
</div>
<div>
<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">
<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">
</div>
</div>
</div>

View File

@@ -1,111 +1,165 @@
@extends('layouts.admin')
@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';
@endphp
<div class="container mx-auto px-6 py-8">
<h3 class="{{ $textPrimary }} text-3xl font-medium">儀表板</h3>
<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 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="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
總銷售額
</p>
<div class="hs-tooltip">
<div class="hs-tooltip-toggle">
<svg class="flex-shrink-0 w-4 h-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded shadow-sm dark:bg-slate-700" role="tooltip">
本月累計銷售總額
</span>
</div>
</div>
</div>
<!-- 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 class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
$72,540
</h3>
<span class="flex items-center gap-x-1 text-green-600">
<svg class="inline-block w-4 h-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
<span class="inline-block text-sm">
1.7%
</span>
</span>
</div>
</div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
活躍機台
</p>
</div>
<!-- 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 class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
124
</h3>
<span class="flex items-center gap-x-1 text-red-600">
<svg class="inline-block w-4 h-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/></svg>
<span class="inline-block text-sm">
0.3%
</span>
</span>
</div>
</div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
庫存警告
</p>
</div>
<!-- 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 class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
12
</h3>
</div>
</div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
本月新增會員
</p>
</div>
<div class="mt-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 class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
28
</h3>
</div>
</div>
</div>
<!-- End Card -->
</div>
<!-- End Grid -->
<div class="grid lg:grid-cols-2 gap-4 sm:gap-6">
<!-- Card -->
<div class="p-4 md:p-5 min-h-[410px] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h2 class="text-sm text-gray-500 dark:text-gray-400">
營收趨勢
</h2>
<p class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
$123,450
</p>
</div>
<div>
<span class="py-[5px] px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-md bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500">
<svg class="inline-block w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
25%
</span>
</div>
</div>
<!-- End Header -->
<div id="hs-multiple-bar-charts"></div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="p-4 md:p-5 min-h-[410px] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h2 class="text-sm text-gray-500 dark:text-gray-400">
訪客分析
</h2>
<p class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
92,913
</p>
</div>
<div>
<span class="py-[5px] px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-md bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500">
<svg class="inline-block w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
11%
</span>
</div>
</div>
<!-- End Header -->
<div id="hs-single-area-chart"></div>
</div>
<!-- End Card -->
</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

View File

@@ -0,0 +1,123 @@
@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

View File

@@ -0,0 +1,158 @@
@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

View File

@@ -2,27 +2,27 @@
@section('content')
<div class="container mx-auto px-6 py-8">
<h3 class="text-gray-300 text-3xl font-medium">新增機台</h3>
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">新增機台</h3>
<div class="mt-8">
<form action="{{ route('admin.machines.store') }}" method="POST" class="bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
<form action="{{ route('admin.machines.store') }}" method="POST" class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
@csrf
<div>
<label for="name" class="block text-sm font-medium text-gray-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>
<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>
@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-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">
<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">
@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-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">
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">狀態</label>
<select name="status" id="status" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline">離線</option>
<option value="online">連線中</option>
<option value="error">異常</option>
@@ -31,19 +31,19 @@
</div>
<div>
<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">
<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">
@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-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">
<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">
@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-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded mr-2">取消</a>
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded 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>

View File

@@ -2,28 +2,28 @@
@section('content')
<div class="container mx-auto px-6 py-8">
<h3 class="text-gray-300 text-3xl font-medium">編輯機台</h3>
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">編輯機台</h3>
<div class="mt-8">
<form action="{{ route('admin.machines.update', $machine) }}" method="POST" class="bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
<form action="{{ route('admin.machines.update', $machine) }}" method="POST" class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6 space-y-6">
@csrf
@method('PUT')
<div>
<label for="name" class="block text-sm font-medium text-gray-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>
<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>
@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-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">
<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">
@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-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">
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-400">狀態</label>
<select name="status" id="status" class="mt-1 block w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 text-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="offline" {{ $machine->status == 'offline' ? 'selected' : '' }}>離線</option>
<option value="online" {{ $machine->status == 'online' ? 'selected' : '' }}>連線中</option>
<option value="error" {{ $machine->status == 'error' ? 'selected' : '' }}>異常</option>
@@ -32,19 +32,19 @@
</div>
<div>
<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">
<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">
@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-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">
<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">
@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-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded mr-2">取消</a>
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded 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>

View File

@@ -2,16 +2,10 @@
@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';
@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>
<h3 class="text-gray-900 dark:text-gray-200 text-3xl font-medium">機台管理</h3>
<a href="{{ route('admin.machines.create') }}" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
新增機台
</a>
@@ -20,28 +14,28 @@
<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 }}">
<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 {{ $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>
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">名稱</th>
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">位置</th>
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">狀態</th>
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">溫度</th>
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">最後心跳</th>
<th class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left text-xs leading-4 font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="{{ $cardBg }}">
<tbody class="bg-white dark:bg-gray-800">
@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 class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
<div class="text-sm leading-5 font-medium text-gray-900 dark:text-gray-200">{{ $machine->name }}</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
<div class="text-sm leading-5 {{ $textSecondary }}">{{ $machine->location ?? '-' }}</div>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
<div class="text-sm leading-5 text-gray-600 dark:text-gray-400">{{ $machine->location ?? '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
@if($machine->status === 'online')
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">連線中</span>
@elseif($machine->status === 'offline')
@@ -50,13 +44,13 @@
<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 {{ $borderColor }}">
<div class="text-sm leading-5 {{ $textSecondary }}">{{ $machine->temperature ? $machine->temperature . '°C' : '-' }}</div>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
<div class="text-sm leading-5 text-gray-600 dark:text-gray-400">{{ $machine->temperature ? $machine->temperature . '°C' : '-' }}</div>
</td>
<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 class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700">
<div class="text-sm leading-5 text-gray-600 dark:text-gray-400">{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '-' }}</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }} text-sm leading-5 font-medium">
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 dark:border-gray-700 text-sm leading-5 font-medium">
<a href="{{ route('admin.machines.show', $machine) }}" class="text-indigo-400 hover:text-indigo-600 mr-3">查看</a>
<a href="{{ route('admin.machines.edit', $machine) }}" class="text-yellow-400 hover:text-yellow-600 mr-3">編輯</a>
<form action="{{ route('admin.machines.destroy', $machine) }}" method="POST" class="inline-block" onsubmit="return confirm('確定要刪除嗎?');">

View File

@@ -3,12 +3,12 @@
@section('content')
<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>
<h3 class="text-gray-900 dark:text-gray-300 text-3xl font-medium">機台詳情:{{ $machine->name }}</h3>
<div>
<a href="{{ route('admin.machines.edit', $machine) }}" class="bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded mr-2">
編輯
</a>
<a href="{{ route('admin.machines.index') }}" class="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded">
<a href="{{ route('admin.machines.index') }}" class="bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded">
返回列表
</a>
</div>
@@ -16,15 +16,15 @@
<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">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
<h4 class="text-xl font-semibold text-gray-200 mb-4">基本資訊</h4>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm text-gray-400">位置</p>
<p class="text-sm text-gray-700 dark:text-gray-400">位置</p>
<p class="text-lg text-gray-200">{{ $machine->location ?? '-' }}</p>
</div>
<div>
<p class="text-sm text-gray-400">狀態</p>
<p class="text-sm text-gray-700 dark:text-gray-400">狀態</p>
@if($machine->status === 'online')
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">連線中</span>
@elseif($machine->status === 'offline')
@@ -34,22 +34,22 @@
@endif
</div>
<div>
<p class="text-sm text-gray-400">溫度</p>
<p class="text-sm text-gray-700 dark:text-gray-400">溫度</p>
<p class="text-lg text-gray-200">{{ $machine->temperature ? $machine->temperature . '°C' : '-' }}</p>
</div>
<div>
<p class="text-sm text-gray-400">韌體版本</p>
<p class="text-sm text-gray-700 dark:text-gray-400">韌體版本</p>
<p class="text-lg text-gray-200">{{ $machine->firmware_version ?? '-' }}</p>
</div>
<div>
<p class="text-sm text-gray-400">最後心跳</p>
<p class="text-sm text-gray-700 dark:text-gray-400">最後心跳</p>
<p class="text-lg text-gray-200">{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->diffForHumans() : '-' }}</p>
</div>
</div>
</div>
<!-- Logs -->
<div class="bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden p-6">
<h4 class="text-xl font-semibold text-gray-200 mb-4">最近日誌</h4>
<div class="overflow-y-auto max-h-64">
<ul class="divide-y divide-gray-700">
@@ -64,7 +64,7 @@
@else
<span class="h-2 w-2 rounded-full bg-blue-500 mr-2"></span>
@endif
<p class="text-sm text-gray-300">{{ $log->message }}</p>
<p class="text-sm text-gray-900 dark:text-gray-300">{{ $log->message }}</p>
</div>
<span class="text-xs text-gray-500">{{ $log->created_at->format('m/d H:i') }}</span>
</div>

View File

@@ -0,0 +1,86 @@
@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

View File

@@ -0,0 +1,120 @@
@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

View File

@@ -1,39 +1,26 @@
@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="{{ $currentTheme['card'] }} rounded-lg shadow-lg p-8 text-center border {{ $isLight ? 'border-gray-200' : 'border-gray-700' }}">
<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="mb-6">
<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">
<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">
<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 {{ $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">
<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">
🚧 功能開發中
</div>
@if(isset($features) && count($features) > 0)
<div class="mt-8 text-left max-w-2xl mx-auto">
<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' }}">
<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">
@foreach($features as $feature)
<li class="flex items-start">
<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">
<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">
<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>

View File

@@ -0,0 +1,138 @@
@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

View File

@@ -1,47 +1,123 @@
<x-guest-layout>
<!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);
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>
</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>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
@if (session('status'))
<div class="mb-4 text-sm font-medium text-green-600">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div class="grid gap-y-4">
<!-- Form Group -->
<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" />
<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>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- 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>
@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 -->
<div class="flex items-center justify-end mt-4">
<!-- 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="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 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 -->
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
<!-- 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>
</x-guest-layout>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,17 +1,165 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
@extends('layouts.admin')
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __("You're logged in!") }}
@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="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
總銷售額
</p>
<div class="hs-tooltip">
<div class="hs-tooltip-toggle">
<svg class="flex-shrink-0 w-4 h-4 text-gray-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded shadow-sm dark:bg-slate-700" role="tooltip">
本月累計銷售總額
</span>
</div>
</div>
</div>
<div class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
$72,540
</h3>
<span class="flex items-center gap-x-1 text-green-600">
<svg class="inline-block w-4 h-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
<span class="inline-block text-sm">
1.7%
</span>
</span>
</div>
</x-app-layout>
</div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
活躍機台
</p>
</div>
<div class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
124
</h3>
<span class="flex items-center gap-x-1 text-red-600">
<svg class="inline-block w-4 h-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/></svg>
<span class="inline-block text-sm">
0.3%
</span>
</span>
</div>
</div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
庫存警告
</p>
</div>
<div class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
12
</h3>
</div>
</div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<div class="p-4 md:p-5">
<div class="flex items-center gap-x-2">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
本月新增會員
</p>
</div>
<div class="mt-1 flex items-center gap-x-2">
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
28
</h3>
</div>
</div>
</div>
<!-- End Card -->
</div>
<!-- End Grid -->
<div class="grid lg:grid-cols-2 gap-4 sm:gap-6">
<!-- Card -->
<div class="p-4 md:p-5 min-h-[410px] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h2 class="text-sm text-gray-500 dark:text-gray-400">
營收趨勢
</h2>
<p class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
$123,450
</p>
</div>
<div>
<span class="py-[5px] px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-md bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500">
<svg class="inline-block w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
25%
</span>
</div>
</div>
<!-- End Header -->
<div id="hs-multiple-bar-charts"></div>
</div>
<!-- End Card -->
<!-- Card -->
<div class="p-4 md:p-5 min-h-[410px] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h2 class="text-sm text-gray-500 dark:text-gray-400">
訪客分析
</h2>
<p class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
92,913
</p>
</div>
<div>
<span class="py-[5px] px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-md bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500">
<svg class="inline-block w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg>
11%
</span>
</div>
</div>
<!-- End Header -->
<div id="hs-single-area-chart"></div>
</div>
<!-- End Card -->
</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

View File

@@ -1,55 +1,5 @@
@php
$theme = request()->cookie('theme', 'dark-blue');
// 主題配置
$themes = [
'dark-blue' => [
'name' => '深色藍',
'body' => 'bg-gray-900 text-gray-100',
'sidebar' => 'bg-gray-800 border-gray-700',
'header' => 'bg-gray-800 border-gray-700',
'card' => 'bg-gray-800',
'accent' => 'indigo',
],
'dark-purple' => [
'name' => '深色紫',
'body' => 'bg-slate-900 text-slate-100',
'sidebar' => 'bg-slate-800 border-slate-700',
'header' => 'bg-slate-800 border-slate-700',
'card' => 'bg-slate-800',
'accent' => 'purple',
],
'dark-green' => [
'name' => '深色綠',
'body' => 'bg-zinc-900 text-zinc-100',
'sidebar' => 'bg-zinc-800 border-zinc-700',
'header' => 'bg-zinc-800 border-zinc-700',
'card' => 'bg-zinc-800',
'accent' => 'emerald',
],
'light-blue' => [
'name' => '亮色藍',
'body' => 'bg-gray-50 text-gray-900',
'sidebar' => 'bg-white border-gray-200',
'header' => 'bg-white border-gray-200',
'card' => 'bg-white',
'accent' => 'blue',
],
'light-green' => [
'name' => '亮色綠',
'body' => 'bg-green-50 text-gray-900',
'sidebar' => 'bg-white border-green-200',
'header' => 'bg-white border-green-200',
'card' => 'bg-white',
'accent' => 'green',
],
];
$currentTheme = $themes[$theme] ?? $themes['dark-blue'];
$isLight = in_array($theme, ['light-blue', 'light-green']);
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="{{ $isLight ? '' : 'dark' }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full" x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' }" :class="{ 'dark': darkMode }">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -62,93 +12,168 @@
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap" rel="stylesheet" />
<!-- Scripts -->
<script>
// Dark Mode Initialization (before Alpine loads)
if (localStorage.getItem('darkMode') === 'true' || (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
document.documentElement.classList.remove('dark');
}
</script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased {{ $currentTheme['body'] }}" x-data="{ sidebarOpen: false, themeOpen: false, dropdownOpen: false }" x-cloak>
<div class="min-h-screen flex">
<!-- Mobile Sidebar Backdrop -->
<body class="bg-gray-50 dark:bg-slate-900 antialiased font-sans h-full" x-data="{ sidebarOpen: false, userDropdownOpen: false }">
<!-- Sidebar Overlay (Mobile) -->
<div x-show="sidebarOpen"
x-cloak
@click="sidebarOpen = false"
x-transition:enter="transition-opacity ease-linear duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity ease-linear duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden">
@click="sidebarOpen = false"
class="fixed inset-0 z-[55] bg-gray-900/50 lg:hidden"
x-cloak></div>
<!-- ========== HEADER ========== -->
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white border-b text-sm py-2.5 sm:py-4 lg:pl-64 dark:bg-gray-800 dark:border-gray-700">
<nav class="flex basis-full items-center w-full mx-auto px-4 sm:px-6 md:px-8" aria-label="Global">
<div class="mr-5 lg:mr-0 lg:hidden">
<a class="flex-none text-xl font-semibold dark:text-white" href="#" aria-label="Brand">Star Cloud</a>
</div>
<!-- Sidebar -->
<aside class="fixed inset-y-0 left-0 z-50 w-64 {{ $currentTheme['sidebar'] }} border-r lg:translate-x-0 lg:static lg:inset-0 overflow-y-auto"
:class="{'translate-x-0': sidebarOpen, '-translate-x-full': !sidebarOpen}"
x-transition:enter="transition-transform ease-in-out duration-300"
x-transition:enter-start="-translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transition-transform ease-in-out duration-300"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="-translate-x-full">
<div class="flex items-center justify-center h-16 {{ $currentTheme['header'] }} border-b">
<span class="text-2xl font-bold text-{{ $currentTheme['accent'] }}-500">Star Cloud</span>
</div>
<nav class="mt-5 px-2 space-y-1 pb-4">
@include('layouts.partials.sidebar-menu')
</nav>
</aside>
<!-- Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Topbar -->
<header class="flex items-center justify-between h-16 {{ $currentTheme['header'] }} border-b px-6">
<div class="flex items-center">
<button @click="sidebarOpen = !sidebarOpen" class="{{ $isLight ? 'text-gray-600' : 'text-gray-400' }} focus:outline-none lg:hidden">
<svg class="h-6 w-6" 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="M4 6h16M4 12h16M4 18h16" />
<div class="w-full flex items-center justify-end ml-auto sm:justify-between sm:gap-x-3 sm:order-3">
<div class="sm:hidden">
<button type="button" class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
<svg class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
</button>
</div>
<div class="flex items-center space-x-4">
<!-- Theme Selector -->
<div class="relative">
<button @click="themeOpen = !themeOpen" class="relative block h-8 w-8 rounded-full overflow-hidden shadow focus:outline-none {{ $isLight ? 'bg-gray-200' : 'bg-gray-700' }}">
<svg class="h-5 w-5 m-auto mt-1.5 {{ $isLight ? 'text-gray-600' : 'text-gray-300' }}" 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="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
</button>
<div x-show="themeOpen" x-cloak @click.away="themeOpen = false" class="absolute right-0 w-48 mt-2 {{ $currentTheme['card'] }} rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50">
@foreach($themes as $key => $themeData)
<form method="POST" action="{{ route('admin.theme.update') }}">
@csrf
<input type="hidden" name="theme" value="{{ $key }}">
<button type="submit" class="block w-full text-left px-4 py-2 text-sm {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700' }} {{ $theme === $key ? 'font-bold' : '' }}">
{{ $themeData['name'] }} {{ $theme === $key ? '✓' : '' }}
</button>
</form>
@endforeach
</div>
<div class="hidden sm:block">
<!-- Search Input (Optional) -->
</div>
<!-- User Menu -->
<div class="ml-3 relative">
<button @click="dropdownOpen = !dropdownOpen" class="relative block h-8 w-8 rounded-full overflow-hidden shadow focus:outline-none">
<img class="h-full w-full object-cover" src="https://ui-avatars.com/api/?name={{ Auth::user()->name }}&background=random" alt="Avatar">
<div class="flex flex-row items-center justify-end gap-2">
<!-- Dark Mode Toggle -->
<button type="button"
@click="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode); document.documentElement.classList.toggle('dark', darkMode)"
class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
<!-- Moon Icon (shown in light mode) -->
<svg x-show="!darkMode" class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"/>
</svg>
<!-- Sun Icon (shown in dark mode) -->
<svg x-show="darkMode" class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</svg>
</button>
<div x-show="dropdownOpen" x-cloak @click.away="dropdownOpen = false" class="absolute right-0 w-48 mt-2 {{ $currentTheme['card'] }} rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50">
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700' }}">個人檔案</a>
<!-- Profile Dropdown -->
<div class="relative inline-flex" x-data="{ open: false }">
<button id="hs-dropdown-with-header" type="button" @click="open = !open" @click.away="open = false" class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
<img class="inline-block h-[2.375rem] w-[2.375rem] rounded-full ring-2 ring-white dark:ring-gray-800" src="https://ui-avatars.com/api/?name={{ Auth::user()->name }}&background=0D8ABC&color=fff" alt="Image Description">
</button>
<div x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute right-0 top-full mt-2 min-w-[15rem] bg-white shadow-md rounded-lg p-2 dark:bg-gray-800 dark:border dark:border-gray-700 z-50"
x-cloak>
<div class="py-3 px-5 -m-2 bg-gray-100 rounded-t-lg dark:bg-gray-700">
<p class="text-sm text-gray-500 dark:text-gray-400">Signed in as</p>
<p class="text-sm font-medium text-gray-800 dark:text-gray-300">{{ Auth::user()->email }}</p>
</div>
<div class="mt-2 py-2 first:pt-0 last:pb-0">
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="{{ route('profile.edit') }}">
<svg class="flex-shrink-0 w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
個人檔案
</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="block w-full text-left px-4 py-2 text-sm {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700' }}">登出</button>
<button type="submit" class="w-full flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300">
<svg class="flex-shrink-0 w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
登出
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<!-- ========== END HEADER ========== -->
<!-- Main Content -->
<main class="flex-1 overflow-x-hidden overflow-y-auto {{ $currentTheme['body'] }} p-6">
<!-- ========== MAIN CONTENT ========== -->
<!-- Sidebar Toggle (Mobile) -->
<div class="sticky top-[3.75rem] inset-x-0 z-20 bg-white border-y px-4 sm:px-6 md:px-8 lg:hidden dark:bg-gray-800 dark:border-gray-700">
<div class="flex items-center py-4">
<!-- Navigation Toggle -->
<button type="button" class="text-gray-500 hover:text-gray-600" @click="sidebarOpen = !sidebarOpen">
<span class="sr-only">Toggle Navigation</span>
<svg class="w-5 h-5" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
</svg>
</button>
<!-- End Navigation Toggle -->
<!-- Breadcrumb -->
<ol class="ms-3 flex items-center whitespace-nowrap" aria-label="Breadcrumb">
<li class="flex items-center text-sm text-gray-800 dark:text-gray-400">
Star Cloud
<svg class="flex-shrink-0 mx-3 overflow-visible h-2.5 w-2.5 text-gray-400 dark:text-gray-600" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 1L10.6869 7.16086C10.8637 7.35239 10.8637 7.64761 10.6869 7.83914L5 14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</li>
<li class="text-sm font-semibold text-gray-800 truncate dark:text-gray-200" aria-current="page">
Dashboard
</li>
</ol>
<!-- End Breadcrumb -->
</div>
</div>
<!-- End Sidebar Toggle -->
<!-- Sidebar -->
<div id="application-sidebar"
class="fixed top-0 left-0 bottom-0 z-[60] w-64 bg-white border-r border-gray-200 pt-7 pb-10 overflow-y-auto transition-transform duration-300 transform lg:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'">
<!-- Close Button (Mobile) -->
<button type="button" @click="sidebarOpen = false" class="absolute top-4 right-4 text-gray-500 hover:text-gray-700 lg:hidden dark:text-gray-400 dark:hover:text-gray-200">
<span class="sr-only">Close sidebar</span>
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="px-6">
<a class="flex-none text-xl font-semibold dark:text-white" href="#" aria-label="Brand">Star Cloud</a>
</div>
<nav class="p-6 w-full flex flex-col flex-wrap">
<ul class="space-y-1.5">
@include('layouts.partials.sidebar-menu')
</ul>
</nav>
</div>
<!-- End Sidebar -->
<!-- Content -->
<div class="w-full pt-10 px-4 sm:px-6 md:px-8 lg:pl-72">
<main>
@yield('content')
</main>
</div>
</div>
<!-- End Content -->
@yield('scripts')
</body>
</html>

View File

@@ -1,349 +1,278 @@
{{-- 1. 儀表板 (獨立項目) --}}
<a href="{{ route('admin.dashboard') }}"
@click="if (window.innerWidth < 1024) sidebarOpen = false"
class="group flex items-center px-2 py-2 text-sm font-medium rounded-md {{ request()->routeIs('admin.dashboard') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}">
<svg class="mr-3 h-5 w-5 {{ request()->routeIs('admin.dashboard') ? 'text-white' : ($isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300') }}" 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="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
{{-- 1. 儀表板 --}}
<li>
<a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.dashboard') ? 'bg-gray-100 dark:bg-gray-900 text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.dashboard') }}">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>
儀表板
</a>
</li>
{{-- 2. 應用程式 --}}
<div x-data="{
open: localStorage.getItem('menu_profile') === 'true' || {{ request()->routeIs('profile.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_profile', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="flex-1 text-left">應用程式</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 2. 應用程式 (個人) --}}
<li x-data="{ open: localStorage.getItem('menu_profile') === 'true' || {{ request()->routeIs('profile.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_profile', open)"
class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
個人設定
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('profile.edit') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('profile.edit') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">個人檔案</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li>
<a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('profile.edit') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('profile.edit') }}">
個人檔案
</a>
</li>
</ul>
</div>
</li>
{{-- 3. 機台管理 --}}
<div x-data="{
open: localStorage.getItem('menu_machines') === 'true' || {{ request()->routeIs('admin.machines.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_machines', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01" />
</svg>
<span class="flex-1 text-left">機台管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 3. 會員管理 --}}
<li x-data="{ open: localStorage.getItem('menu_members') === 'true' || {{ request()->routeIs('admin.members.*') || request()->routeIs('admin.membership-tiers.*') || request()->routeIs('admin.deposit-bonus-rules.*') || request()->routeIs('admin.point-rules.*') || request()->routeIs('admin.gift-definitions.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_members', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
會員管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.machines.logs') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.machines.logs') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台日誌</a>
<a href="{{ route('admin.machines.index') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.machines.index') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台列表</a>
<a href="{{ route('admin.machines.permissions') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.machines.permissions') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台權限</a>
<a href="{{ route('admin.machines.utilization') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.machines.utilization') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台稼動率</a>
<a href="{{ route('admin.machines.expiry') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.machines.expiry') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">效期管理</a>
<a href="{{ route('admin.machines.maintenance') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.machines.maintenance') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">維修管理單</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.members.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.members.index') }}">會員列表</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.membership-tiers.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.membership-tiers.index') }}">會員等級</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.deposit-bonus-rules.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.deposit-bonus-rules.index') }}">儲值回饋</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.point-rules.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.point-rules.index') }}">點數規則</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.gift-definitions.*') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.gift-definitions.index') }}">禮品設定</a></li>
</ul>
</div>
</li>
{{-- 4. APP管理 --}}
<div x-data="{
open: localStorage.getItem('menu_app') === 'true' || {{ request()->routeIs('admin.app.*') || request()->routeIs('admin.app-configs.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_app', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<span class="flex-1 text-left">APP管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 4. 機台管理 --}}
<li x-data="{ open: localStorage.getItem('menu_machines') === 'true' || {{ request()->routeIs('admin.machines.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_machines', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01" /></svg>
機台管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.app.ui-elements') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.app.ui-elements') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">UI元素設定</a>
<a href="{{ route('admin.app.helper') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.app.helper') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">小幫手設定</a>
<a href="{{ route('admin.app.questionnaire') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.app.questionnaire') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">問卷設定</a>
<a href="{{ route('admin.app.games') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.app.games') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">互動遊戲設定</a>
<a href="{{ route('admin.app.timer') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.app.timer') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">計時器</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.logs') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.logs') }}">機台日誌</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.index') }}">機台列表</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.permissions') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.permissions') }}">機台權限</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.utilization') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.utilization') }}">機台稼動率</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.expiry') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.expiry') }}">效期管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.machines.maintenance') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.machines.maintenance') }}">維修管理單</a></li>
</ul>
</div>
</li>
{{-- 5. 倉庫管理 --}}
<div x-data="{
open: localStorage.getItem('menu_warehouses') === 'true' || {{ request()->routeIs('admin.warehouses.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_warehouses', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<span class="flex-1 text-left">倉庫管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 5. APP管理 --}}
<li x-data="{ open: localStorage.getItem('menu_app') === 'true' || {{ request()->routeIs('admin.app.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_app', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
APP管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.warehouses.index') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.index') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">倉庫列表(全部)</a>
<a href="{{ route('admin.warehouses.personal') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.personal') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">倉庫列表(個人)</a>
<a href="{{ route('admin.warehouses.stock-management') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.stock-management') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">庫存管理單</a>
<a href="{{ route('admin.warehouses.transfers') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.transfers') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">調撥單</a>
<a href="{{ route('admin.warehouses.purchases') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.purchases') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">採購單</a>
<a href="{{ route('admin.warehouses.replenishments') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.replenishments') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台補貨單</a>
<a href="{{ route('admin.warehouses.replenishment-records') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台補貨紀錄</a>
<a href="{{ route('admin.warehouses.replenishment-records-all') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.replenishment-records-all') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台補貨紀錄()</a>
<a href="{{ route('admin.warehouses.machine-stock') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.machine-stock') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台庫存</a>
<a href="{{ route('admin.warehouses.staff-stock') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.staff-stock') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">人員庫存</a>
<a href="{{ route('admin.warehouses.returns') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.warehouses.returns') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">回庫單</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.ui-elements') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.ui-elements') }}">UI元素</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.helper') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.helper') }}">小幫手</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.questionnaire') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.questionnaire') }}">問卷</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.games') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.games') }}">互動遊戲</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.app.timer') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.app.timer') }}">計時器</a></li>
</ul>
</div>
</li>
{{-- 6. 銷售管理 --}}
<div x-data="{
open: localStorage.getItem('menu_sales') === 'true' || {{ request()->routeIs('admin.sales.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_sales', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="flex-1 text-left">銷售管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 6. 倉庫管理 --}}
<li x-data="{ open: localStorage.getItem('menu_warehouses') === 'true' || {{ request()->routeIs('admin.warehouses.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_warehouses', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
倉庫管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.sales.index') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.sales.index') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">銷售&金流紀錄</a>
<a href="{{ route('admin.sales.pickup-codes') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.sales.pickup-codes') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">取貨碼設定</a>
<a href="{{ route('admin.sales.orders') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.sales.orders') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">購買單</a>
<a href="{{ route('admin.sales.promotions') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.sales.promotions') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">促銷時段設定</a>
<a href="{{ route('admin.sales.pass-codes') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.sales.pass-codes') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">通行碼設定</a>
<a href="{{ route('admin.sales.store-gifts') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.sales.store-gifts') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">來店禮設定</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.index') }}">倉庫列表()</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.personal') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.personal') }}">倉庫列表()</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.stock-management') }}">庫存管理單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.transfers') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.transfers') }}">調撥單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.purchases') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.purchases') }}">採購單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.replenishments') }}">機台補貨單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.replenishment-records') }}">機台補貨紀錄</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.machine-stock') }}">機台庫存</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.staff-stock') }}">人員庫存</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.warehouses.returns') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.warehouses.returns') }}">回庫單</a></li>
</ul>
</div>
</li>
{{-- 7. 分析管理 --}}
<div x-data="{
open: localStorage.getItem('menu_analysis') === 'true' || {{ request()->routeIs('admin.analysis.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_analysis', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
</svg>
<span class="flex-1 text-left">分析管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 7. 銷售管理 --}}
<li x-data="{ open: localStorage.getItem('menu_sales') === 'true' || {{ request()->routeIs('admin.sales.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_sales', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
銷售管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.analysis.change-stock') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.analysis.change-stock') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">零錢庫存分析</a>
<a href="{{ route('admin.analysis.machine-reports') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.analysis.machine-reports') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台報表分析</a>
<a href="{{ route('admin.analysis.product-reports') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.analysis.product-reports') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">商品報表分析</a>
<a href="{{ route('admin.analysis.survey-analysis') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.analysis.survey-analysis') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">互動問卷分析</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.index') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.index') }}">銷售紀錄</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.pickup-codes') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.pickup-codes') }}">取貨碼</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.orders') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.orders') }}">購買單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.promotions') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.promotions') }}">促銷時段</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.pass-codes') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.pass-codes') }}">通行碼</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.sales.store-gifts') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.sales.store-gifts') }}">來店禮</a></li>
</ul>
</div>
</li>
{{-- 8. 稽核管理 --}}
<div x-data="{
open: localStorage.getItem('menu_audit') === 'true' || {{ request()->routeIs('admin.audit.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_audit', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span class="flex-1 text-left">稽核管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 8. 分析管理 --}}
<li x-data="{ open: localStorage.getItem('menu_analysis') === 'true' || {{ request()->routeIs('admin.analysis.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_analysis', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" /></svg>
分析管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.audit.purchases') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.audit.purchases') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">採購單稽核</a>
<a href="{{ route('admin.audit.transfers') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.audit.transfers') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">調撥單稽核</a>
<a href="{{ route('admin.audit.replenishments') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.audit.replenishments') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">補貨單稽核</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.change-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.change-stock') }}">零錢庫存</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.machine-reports') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.machine-reports') }}">機台報表</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.product-reports') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.product-reports') }}">商品報表</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.analysis.survey-analysis') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.analysis.survey-analysis') }}">問卷分析</a></li>
</ul>
</div>
</li>
{{-- 9. 資料設定 --}}
<div x-data="{
open: localStorage.getItem('menu_data_config') === 'true' || {{ request()->routeIs('admin.data-config.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_data_config', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="flex-1 text-left">資料設定</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 9. 稽核管理 --}}
<li x-data="{ open: localStorage.getItem('menu_audit') === 'true' || {{ request()->routeIs('admin.audit.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_audit', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
稽核管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.data-config.products') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.products') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">商品管理</a>
<a href="{{ route('admin.data-config.advertisements') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.advertisements') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">廣告管理</a>
<a href="{{ route('admin.data-config.admin-products') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.admin-products') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">管理者可賣商品</a>
<a href="{{ route('admin.data-config.accounts') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.accounts') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">帳號管理</a>
<a href="{{ route('admin.data-config.sub-accounts') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.sub-accounts') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">子帳號管理</a>
<a href="{{ route('admin.data-config.sub-account-roles') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">子帳號角色管理</a>
<a href="{{ route('admin.data-config.points') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.points') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">點數設定</a>
<a href="{{ route('admin.data-config.badges') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.data-config.badges') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">識別證管理</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.audit.purchases') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.audit.purchases') }}">採購單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.audit.transfers') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.audit.transfers') }}">調撥單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.audit.replenishments') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.audit.replenishments') }}">補貨單</a></li>
</ul>
</div>
</li>
{{-- 10. 遠端管理 --}}
<div x-data="{
open: localStorage.getItem('menu_remote') === 'true' || {{ request()->routeIs('admin.remote.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_remote', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="flex-1 text-left">遠端管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 10. 資料設定 --}}
<li x-data="{ open: localStorage.getItem('menu_data_config') === 'true' || {{ request()->routeIs('admin.data-config.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_data_config', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
資料設定
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.remote.stock') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.stock') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台庫存</a>
<a href="{{ route('admin.remote.restart') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.restart') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台重啟</a>
<a href="{{ route('admin.remote.restart-card-reader') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.restart-card-reader') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">卡機重啟</a>
<a href="{{ route('admin.remote.checkout') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.checkout') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">遠端結帳</a>
<a href="{{ route('admin.remote.lock') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.lock') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">遠端鎖定頁</a>
<a href="{{ route('admin.remote.change') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.change') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">遠端找零</a>
<a href="{{ route('admin.remote.dispense') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.remote.dispense') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">遠端出貨</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.products') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.products') }}">商品管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.advertisements') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.advertisements') }}">廣告管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.admin-products') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.admin-products') }}">管理者可賣</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.accounts') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.accounts') }}">帳號管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.sub-accounts') }}">子帳號</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.sub-account-roles') }}">子帳號角色</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.points') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.points') }}">點數設定</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.data-config.badges') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.data-config.badges') }}">識別證</a></li>
</ul>
</div>
</li>
{{-- 11. Line管理 --}}
<div x-data="{
open: localStorage.getItem('menu_line') === 'true' || {{ request()->routeIs('admin.line.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_line', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span class="flex-1 text-left">Line管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.line.members') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.line.members') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line會員管理</a>
<a href="{{ route('admin.line.machines') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.line.machines') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line機台管理</a>
<a href="{{ route('admin.line.products') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.line.products') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line商城商品</a>
<a href="{{ route('admin.line.official-account') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.line.official-account') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line生活圈</a>
<a href="{{ route('admin.line.orders') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.line.orders') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line商城訂單</a>
<a href="{{ route('admin.line.coupons') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.line.coupons') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line優惠券</a>
</div>
</div>
{{-- 12. 預約系統 --}}
<div x-data="{
open: localStorage.getItem('menu_reservation') === 'true' || {{ request()->routeIs('admin.reservation.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_reservation', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span class="flex-1 text-left">預約系統</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 11. 遠端管理 --}}
<li x-data="{ open: localStorage.getItem('menu_remote') === 'true' || {{ request()->routeIs('admin.remote.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_remote', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
遠端管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.reservation.members') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.members') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line會員管理</a>
<a href="{{ route('admin.reservation.stores') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.stores') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line店家管理</a>
<a href="{{ route('admin.reservation.time-slots') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.time-slots') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line時段組合</a>
<a href="{{ route('admin.reservation.venues') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.venues') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line場地管理</a>
<a href="{{ route('admin.reservation.coupons') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.coupons') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line優惠券管理</a>
<a href="{{ route('admin.reservation.reservations') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.reservations') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line預約管理</a>
<a href="{{ route('admin.reservation.orders') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.reservation.orders') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line訂單管理</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.stock') }}">機台庫存</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.restart') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.restart') }}">機台重啟</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.restart-card-reader') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.restart-card-reader') }}">卡機重啟</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.checkout') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.checkout') }}">遠端結帳</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.lock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.lock') }}">遠端鎖定</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.change') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.change') }}">遠端找零</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.remote.dispense') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.remote.dispense') }}">遠端出貨</a></li>
</ul>
</div>
</li>
{{-- 13. 特殊權限管理 --}}
<div x-data="{
open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_special_permission', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span class="flex-1 text-left">特殊權限管理</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 12. Line管理 --}}
<li x-data="{ open: localStorage.getItem('menu_line') === 'true' || {{ request()->routeIs('admin.line.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_line', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg>
Line管理
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.special-permission.clear-stock') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.special-permission.clear-stock') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">庫存清空</a>
<a href="{{ route('admin.special-permission.apk-versions') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.special-permission.apk-versions') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">APK版本管理</a>
<a href="{{ route('admin.special-permission.discord-notifications') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Discord通知設定</a>
</div>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.members') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.members') }}">Line會員</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.machines') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.machines') }}">Line機台</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.products') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.products') }}">Line商品</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.official-account') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.official-account') }}">Line生活圈</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.orders') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.orders') }}">Line訂單</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.line.coupons') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.line.coupons') }}">Line優惠券</a></li>
</ul>
</div>
</li>
{{-- 14. 權限設定 --}}
<div x-data="{
open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }},
init() {
this.$watch('open', value => localStorage.setItem('menu_permissions', value))
}
}">
<button @click="open = !open" class="group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md {{ $isLight ? 'text-gray-700 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white' }}">
<svg class="mr-3 h-5 w-5 {{ $isLight ? 'text-gray-400 group-hover:text-gray-500' : 'text-gray-400 group-hover:text-gray-300' }}" 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="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<span class="flex-1 text-left">權限設定</span>
<svg class="ml-auto h-5 w-5 transform transition-transform duration-200" :class="{'rotate-90': open}" 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 5l7 7-7 7" />
</svg>
{{-- 13. 預約系統 --}}
<li x-data="{ open: localStorage.getItem('menu_reservation') === 'true' || {{ request()->routeIs('admin.reservation.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_reservation', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
預約系統
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="mt-1 space-y-1">
<a href="{{ route('admin.permission.app-features') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.app-features') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">APP功能管理</a>
<a href="{{ route('admin.permission.data-config') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.data-config') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">資料設定</a>
<a href="{{ route('admin.permission.sales') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.sales') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">銷售管理</a>
<a href="{{ route('admin.permission.machines') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.machines') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">機台管理</a>
<a href="{{ route('admin.permission.warehouses') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.warehouses') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">倉庫管理</a>
<a href="{{ route('admin.permission.analysis') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.analysis') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">分析管理</a>
<a href="{{ route('admin.permission.audit') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.audit') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">稽核管理</a>
<a href="{{ route('admin.permission.remote') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.remote') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">遠端管理</a>
<a href="{{ route('admin.permission.line') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.line') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">Line管理</a>
<a href="{{ route('admin.permission.roles') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.roles') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">權限角色設定</a>
<a href="{{ route('admin.permission.others') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.others') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">其他功能管理</a>
<a href="{{ route('admin.permission.ai-prediction') }}" @click="if (window.innerWidth < 1024) sidebarOpen = false" class="group flex items-center pl-14 pr-2 py-2 text-sm rounded-md {{ request()->routeIs('admin.permission.ai-prediction') ? 'bg-'.$currentTheme['accent'].'-600 text-white' : ($isLight ? 'text-gray-600 hover:bg-gray-100' : 'text-gray-300 hover:bg-gray-700 hover:text-white') }}" style="padding-left: 3.5rem;">AI智能預測</a>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.members') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.members') }}">預約會員</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.stores') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.stores') }}">店家管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.time-slots') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.time-slots') }}">時段組合</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.venues') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.venues') }}">場地管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.coupons') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.coupons') }}">優惠券</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.reservations') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.reservations') }}">預約管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.reservation.orders') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.reservation.orders') }}">訂單管理</a></li>
</ul>
</div>
</li>
{{-- 14. 特殊權限 --}}
<li x-data="{ open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_special_permission', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
特殊權限
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.special-permission.clear-stock') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.special-permission.clear-stock') }}">庫存清空</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.special-permission.apk-versions') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.special-permission.apk-versions') }}">APK版本</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.special-permission.discord-notifications') }}">Discord通知</a></li>
</ul>
</div>
</li>
{{-- 15. 權限設定 --}}
<li x-data="{ open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_permissions', open)" class="w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
權限設定
<svg class="ms-auto w-4 h-4 transition-transform duration-300" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="open" x-collapse class="w-full overflow-hidden transition-[height] duration-300">
<ul class="pt-2 ps-2">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.app-features') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.app-features') }}">APP功能</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.data-config') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.data-config') }}">資料設定</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.sales') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.sales') }}">銷售管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.machines') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.machines') }}">機台管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.warehouses') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.warehouses') }}">倉庫管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.analysis') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.analysis') }}">分析管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.audit') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.audit') }}">稽核管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.remote') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.remote') }}">遠端管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.line') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.line') }}">Line管理</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.roles') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.roles') }}">角色設定</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.others') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.others') }}">其他功能</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-gray-400 dark:hover:text-gray-300 {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-blue-600 dark:text-blue-500' : '' }}" href="{{ route('admin.permission.ai-prediction') }}">AI智能預測</a></li>
</ul>
</div>
</li>

View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>社群登入測試</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.2/axios.min.js"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center p-6">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-2xl">
<h1 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-4">Star Cloud 社群登入實測</h1>
@if(isset($line_data))
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded text-sm break-all">
<h3 class="font-bold text-green-700 mb-2">Line Callback Data</h3>
<pre>{{ json_encode($line_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
<p class="mt-2 text-gray-600">請將上方 code 透過後端 API 交換 access_token再呼叫 /social-login API。</p>
</div>
@endif
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Google Section -->
<div class="bg-gray-50 p-4 rounded border">
<h2 class="font-semibold text-lg mb-4 text-blue-600">Google Login</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Client ID</label>
<input type="text" id="google-client-id" class="w-full p-2 border rounded text-sm" placeholder="YOUR_GOOGLE_CLIENT_ID">
</div>
<div id="g_id_onload"
data-context="signin"
data-ux_mode="popup"
data-callback="handleGoogleCredentialResponse"
data-auto_prompt="false">
</div>
<div class="g_id_signin"
data-type="standard"
data-shape="rectangular"
data-theme="outline"
data-text="signin_with"
data-size="large"
data-logo_alignment="left">
</div>
<button onclick="initGoogle()" class="mt-4 w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 text-sm">
初始化 Google 按鈕
</button>
</div>
<!-- Line Section -->
<div class="bg-gray-50 p-4 rounded border">
<h2 class="font-semibold text-lg mb-4 text-green-600">Line Login</h2>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Channel ID</label>
<input type="text" id="line-channel-id" class="w-full p-2 border rounded text-sm" placeholder="1234567890">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Redirect URI</label>
<input type="text" id="line-redirect-uri" class="w-full p-2 border rounded text-sm" value="{{ url('/test/line/callback') }}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">State (Random)</label>
<input type="text" id="line-state" class="w-full p-2 border rounded text-sm" value="{{ Str::random(10) }}" readonly>
</div>
</div>
<button onclick="startLineLogin()" class="mt-6 w-full bg-green-500 text-white py-2 rounded hover:bg-green-600 font-bold">
Log in with Line
</button>
</div>
</div>
<!-- API Result -->
<div class="mt-8 border-t pt-6">
<h2 class="font-semibold text-lg mb-4 text-gray-800">API 執行結果 (/api/members/social-login)</h2>
<div id="api-result" class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-64 overflow-y-auto">
Waiting for action...
</div>
</div>
</div>
<script>
// Google Initialization
function initGoogle() {
const clientId = document.getElementById('google-client-id').value;
if (!clientId) {
alert('請輸入 Google Client ID');
return;
}
const wrapper = document.getElementById('g_id_onload');
wrapper.setAttribute('data-client_id', clientId);
// Re-render button if SDK already loaded
if (window.google) {
google.accounts.id.initialize({
client_id: clientId,
callback: handleGoogleCredentialResponse
});
google.accounts.id.renderButton(
document.querySelector(".g_id_signin"),
{ theme: "outline", size: "large" }
);
}
}
function handleGoogleCredentialResponse(response) {
console.log("Google JWT ID Token: " + response.credential);
logResult("收到 Google ID Token...");
// 解析 JWT (簡單解碼,正式環境應由後端驗證)
const payload = parseJwt(response.credential);
logResult("解析 JWT:\n" + JSON.stringify(payload, null, 2));
// 呼叫後端 API
callSocialLoginApi({
provider: 'google',
provider_id: payload.sub,
email: payload.email,
name: payload.name,
avatar: payload.picture,
access_token: response.credential // 這裡暫傳 id_token
});
}
// Line Login Logic
function startLineLogin() {
const channelId = document.getElementById('line-channel-id').value;
const redirectUri = document.getElementById('line-redirect-uri').value;
const state = document.getElementById('line-state').value;
if (!channelId) {
alert('請輸入 Line Channel ID');
return;
}
const url = `https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=${channelId}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&scope=profile%20openid%20email`;
window.location.href = url;
}
// API Call
async function callSocialLoginApi(data) {
logResult("呼叫 API: /api/members/social-login...");
try {
const response = await axios.post('/api/members/social-login', data);
logResult("API 回傳成功:\n" + JSON.stringify(response.data, null, 2));
} catch (error) {
logResult("API 錯誤:\n" + JSON.stringify(error.response ? error.response.data : error.message, null, 2));
}
}
// Utilities
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
function logResult(msg) {
const el = document.getElementById('api-result');
el.innerText = msg + "\n\n" + el.innerText;
}
</script>
</body>
</html>

View File

@@ -2,6 +2,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\MemberController;
/*
|--------------------------------------------------------------------------
@@ -17,3 +18,23 @@ use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
/*
|--------------------------------------------------------------------------
| 會員 API Routes
|--------------------------------------------------------------------------
*/
// 公開路由(無需認證)
Route::prefix('members')->group(function () {
Route::post('/register', [MemberController::class, 'register']);
Route::post('/login', [MemberController::class, 'login']);
Route::post('/social-login', [MemberController::class, 'socialLogin']);
});
// 需認證路由
Route::prefix('members')->middleware('auth:sanctum')->group(function () {
Route::get('/profile', [MemberController::class, 'profile']);
Route::put('/profile', [MemberController::class, 'updateProfile']);
Route::post('/logout', [MemberController::class, 'logout']);
});

View File

@@ -26,6 +26,13 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(
// 1. 儀表板
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard');
// 2. 會員管理
Route::resource('members', App\Http\Controllers\MemberController::class)->only(['index']);
Route::resource('membership-tiers', App\Http\Controllers\Admin\MembershipTierController::class)->except(['show', 'create', 'edit']);
Route::resource('deposit-bonus-rules', App\Http\Controllers\Admin\DepositBonusRuleController::class)->except(['show', 'create', 'edit']);
Route::resource('point-rules', App\Http\Controllers\Admin\PointRuleController::class)->except(['show', 'create', 'edit']);
Route::resource('gift-definitions', App\Http\Controllers\Admin\GiftDefinitionController::class)->except(['show', 'create', 'edit']);
// 3. 機台管理
Route::prefix('machines')->name('machines.')->group(function () {
Route::get('/logs', [App\Http\Controllers\Admin\MachineController::class, 'logs'])->name('logs');
@@ -165,3 +172,9 @@ Route::middleware('auth')->group(function () {
});
require __DIR__.'/auth.php';
// 測試路由 (需非正式環境或有特別權限控管)
Route::prefix('test')->name('test.')->group(function () {
Route::get('/social-login', [App\Http\Controllers\SocialLoginTestController::class, 'index'])->name('social-login');
Route::get('/line/callback', [App\Http\Controllers\SocialLoginTestController::class, 'lineCallback'])->name('line.callback');
});

View File

@@ -8,6 +8,7 @@ export default {
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
'./node_modules/preline/dist/*.js',
],
safelist: [
@@ -29,5 +30,7 @@ export default {
},
},
plugins: [forms],
plugins: [
forms,
],
};