feat: 整合 Preline UI 3.x 與重寫 README 為 Docker 架構
- 新增 Preline UI 3.2.3 作為 UI 組件庫 - 更新 tailwind.config.js 整合 Preline - 更新 app.js 初始化 Preline - 完全重寫 README.md 以 Docker 容器化架構為核心 - 新增 Docker 常用指令大全 - 新增故障排除與生產部署指南 - 新增會員系統相關功能(會員、錢包、點數、會籍、禮物) - 新增社交登入測試功能
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
APP_NAME=startCloud
|
APP_NAME=starCloud
|
||||||
COMPOSE_PROJECT_NAME=start-cloud
|
COMPOSE_PROJECT_NAME=star-cloud
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
@@ -25,7 +25,7 @@ LOG_LEVEL=debug
|
|||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=mysql
|
DB_HOST=mysql
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=start-cloud
|
DB_DATABASE=star-cloud
|
||||||
DB_USERNAME=sail
|
DB_USERNAME=sail
|
||||||
DB_PASSWORD=password
|
DB_PASSWORD=password
|
||||||
# FORWARD_DB_PORT=3308
|
# FORWARD_DB_PORT=3308
|
||||||
|
|||||||
414
README.md
414
README.md
@@ -1,112 +1,392 @@
|
|||||||
# Star Cloud 智能販賣機管理平台
|
# Star Cloud 智能販賣機管理平台
|
||||||
|
|
||||||
## 專案簡介 (Project Description)
|
> 基於 Docker 的全方位智能販賣機後台管理系統
|
||||||
Star Cloud 是一個專為智能販賣機設計的後台管理系統,旨在提供全方位的機台監控、庫存管理、銷售分析與會員管理功能。透過此平台,管理者可以即時掌握機台運營狀態,優化補貨流程,並透過數據分析提升營運效益。
|
|
||||||
|
|
||||||
## 技術棧 (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
|
- **Framework**: Laravel 10.x
|
||||||
- **Language**: PHP 8.1+
|
- **Language**: PHP 8.5+
|
||||||
- **Database**: MySQL 8.0+
|
- **Database**: MySQL 8.0
|
||||||
- **Authentication**: Laravel Sanctum (API Token Authentication)
|
- **Cache/Session**: Redis
|
||||||
- **Tools**: Composer
|
- **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
|
- **CSS Framework**: Tailwind CSS 3.x
|
||||||
- **JavaScript**: Alpine.js 3.x
|
- **JavaScript**: Alpine.js 3.x (輕量級互動框架)
|
||||||
- **Build Tool**: Vite 5.x
|
- **Build Tool**: Vite 5.x
|
||||||
- **HTTP Client**: Axios
|
- **HTTP Client**: Axios
|
||||||
|
|
||||||
## 安裝與使用說明 (Installation & Usage)
|
---
|
||||||
|
|
||||||
請依照以下步驟將專案 Clone 至本地端並開始運行:
|
## 快速開始
|
||||||
|
|
||||||
|
### 前置需求
|
||||||
|
|
||||||
### 0. 前置需求 (Prerequisites)
|
|
||||||
確保您的系統已安裝以下軟體:
|
確保您的系統已安裝以下軟體:
|
||||||
- PHP 8.1+
|
|
||||||
- Composer
|
|
||||||
- Node.js & npm
|
|
||||||
- MySQL 8.0+
|
|
||||||
|
|
||||||
若您尚未安裝 MySQL,Windows 使用者可至 [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
|
```bash
|
||||||
git clone <repository_url>
|
git clone <repository_url>
|
||||||
cd star-cloud
|
cd star-cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 安裝依賴套件 (Install Dependencies)
|
#### 2. 環境設定
|
||||||
|
|
||||||
安裝後端 PHP 套件:
|
複製環境變數範例檔案:
|
||||||
```bash
|
|
||||||
composer install
|
|
||||||
```
|
|
||||||
|
|
||||||
安裝前端 Node.js 套件:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 環境變數設定 (Environment Setup)
|
|
||||||
複製範例環境設定檔:
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
請開啟 `.env` 檔案,並依照您的本地環境設定資料庫連線資訊:
|
**重要設定**(`.env` 檔案):
|
||||||
```dotenv
|
|
||||||
|
```env
|
||||||
|
# 應用程式設定
|
||||||
|
APP_NAME=Star Cloud
|
||||||
|
APP_ENV=local
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost:8090
|
||||||
|
|
||||||
|
# 資料庫設定(對應 Docker Compose 服務)
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=mysql
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=star_cloud
|
DB_DATABASE=star_cloud
|
||||||
DB_USERNAME=root
|
DB_USERNAME=sail
|
||||||
DB_PASSWORD=your_password
|
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
|
```bash
|
||||||
php artisan key:generate
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 資料庫遷移 (Database Migration)
|
> **說明**:`-d` 參數表示背景執行
|
||||||
執行 Migration 以建立資料庫結構:
|
|
||||||
|
檢查容器狀態:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan migrate
|
docker compose ps
|
||||||
```
|
|
||||||
php artisan migrate --seed
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.1 預設管理員帳號 (Default Admin Account)
|
預期輸出:
|
||||||
執行上述指令後,系統會建立一組預設管理員帳號:
|
```
|
||||||
- **Email**: `admin@star-cloud.com`
|
NAME STATUS PORTS
|
||||||
- **Password**: `password`
|
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
|
```bash
|
||||||
npm run dev
|
docker compose exec laravel.test composer install
|
||||||
```
|
|
||||||
或編譯生產環境檔案:
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 啟動伺服器 (Start Server)
|
**4.2 產生應用程式金鑰**
|
||||||
啟動 Laravel 開發伺服器:
|
|
||||||
```bash
|
|
||||||
php artisan serve --port=8001
|
|
||||||
```
|
|
||||||
預設網址為:http://localhost:8001
|
|
||||||
|
|
||||||
## 主要功能模組
|
```bash
|
||||||
- **儀錶板 (Dashboard)**: 銷售數據概覽、機台狀態監控
|
docker compose exec laravel.test php artisan key:generate
|
||||||
- **機台管理 (Machine Management)**: 機台列表、遠端控制、日誌查詢
|
```
|
||||||
- **商品與庫存 (Inventory)**: 商品管理、進銷存、補貨單
|
|
||||||
- **銷售管理 (Sales)**: 交易紀錄、營收報表
|
**4.3 執行資料庫遷移與種子**
|
||||||
- **權限設定 (Permissions)**: 角色與權限分配
|
|
||||||
|
```bash
|
||||||
|
docker compose exec laravel.test php artisan migrate --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
> **預設管理員帳號**:
|
||||||
|
> - Email: `admin@star-cloud.com`
|
||||||
|
> - 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.
|
© Star Cloud. All Rights Reserved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技術支援
|
||||||
|
|
||||||
|
如有問題或建議,請聯繫開發團隊或提交 Issue。
|
||||||
|
|||||||
56
app/Http/Controllers/Admin/DepositBonusRuleController.php
Normal file
56
app/Http/Controllers/Admin/DepositBonusRuleController.php
Normal 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', '儲值回饋規則已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Http/Controllers/Admin/GiftDefinitionController.php
Normal file
58
app/Http/Controllers/Admin/GiftDefinitionController.php
Normal 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', '禮品已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Http/Controllers/Admin/MembershipTierController.php
Normal file
62
app/Http/Controllers/Admin/MembershipTierController.php
Normal 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', '會員等級已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Http/Controllers/Admin/PointRuleController.php
Normal file
54
app/Http/Controllers/Admin/PointRuleController.php
Normal 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', '點數規則已刪除');
|
||||||
|
}
|
||||||
|
}
|
||||||
260
app/Http/Controllers/Api/MemberController.php
Normal file
260
app/Http/Controllers/Api/MemberController.php
Normal 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' => '登出成功',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Http/Controllers/MemberController.php
Normal file
25
app/Http/Controllers/MemberController.php
Normal 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Http/Controllers/SocialLoginTestController.php
Normal file
33
app/Http/Controllers/SocialLoginTestController.php
Normal 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
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Models/DepositBonusRule.php
Normal file
60
app/Models/DepositBonusRule.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Models/GiftDefinition.php
Normal file
53
app/Models/GiftDefinition.php
Normal 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
147
app/Models/Member.php
Normal 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
56
app/Models/MemberGift.php
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Models/MemberMembership.php
Normal file
65
app/Models/MemberMembership.php
Normal 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Models/MemberPoint.php
Normal file
44
app/Models/MemberPoint.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Models/MemberWallet.php
Normal file
48
app/Models/MemberWallet.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Models/MembershipTier.php
Normal file
62
app/Models/MembershipTier.php
Normal 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
47
app/Models/PointRule.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Models/PointTransaction.php
Normal file
48
app/Models/PointTransaction.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Models/SocialAccount.php
Normal file
53
app/Models/SocialAccount.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Models/WalletTransaction.php
Normal file
38
app/Models/WalletTransaction.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
198
docs/members.md
Normal file
198
docs/members.md
Normal 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) | ✅ 完成 |
|
||||||
|
|
||||||
239
docs/postman/Star_Cloud_Members_API.postman_collection.json
Normal file
239
docs/postman/Star_Cloud_Members_API.postman_collection.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
214
package-lock.json
generated
214
package-lock.json
generated
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "star-cloud",
|
"name": "html",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@alpinejs/collapse": "^3.15.3",
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"alpinejs": "^3.4.2",
|
"alpinejs": "^3.4.2",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.4",
|
||||||
"laravel-vite-plugin": "^1.0.0",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
|
"preline": "^3.2.0",
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
@@ -28,6 +30,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
@@ -419,6 +428,34 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -804,6 +841,74 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@tailwindcss/forms": {
|
||||||
"version": "0.5.10",
|
"version": "0.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
||||||
@@ -841,6 +946,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/alpinejs": {
|
||||||
"version": "3.15.2",
|
"version": "3.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.2.tgz",
|
||||||
@@ -872,6 +984,21 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
@@ -992,7 +1119,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -1126,6 +1252,27 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -1150,6 +1297,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"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==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/laravel-vite-plugin": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
|
||||||
@@ -1745,6 +1916,13 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -1832,7 +2010,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -1976,6 +2153,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -2177,7 +2369,6 @@
|
|||||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -2274,7 +2465,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2340,13 +2530,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@alpinejs/collapse": "^3.15.3",
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"alpinejs": "^3.4.2",
|
"alpinejs": "^3.4.2",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.4",
|
||||||
"laravel-vite-plugin": "^1.0.0",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
|
"preline": "^3.2.0",
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
|
||||||
import Alpine from 'alpinejs';
|
import Alpine from 'alpinejs';
|
||||||
|
import collapse from '@alpinejs/collapse';
|
||||||
|
|
||||||
|
Alpine.plugin(collapse);
|
||||||
|
|
||||||
window.Alpine = Alpine;
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
Alpine.start();
|
Alpine.start();
|
||||||
|
|
||||||
|
// 初始化 Preline UI
|
||||||
|
import 'preline';
|
||||||
|
|||||||
132
resources/views/admin/deposit-bonus-rules/index.blade.php
Normal file
132
resources/views/admin/deposit-bonus-rules/index.blade.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@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';
|
||||||
|
$thBg = $isLight ? 'bg-gray-50' : 'bg-gray-700';
|
||||||
|
$inputBg = $isLight ? 'bg-white' : 'bg-gray-700';
|
||||||
|
$inputBorder = $isLight ? 'border-gray-300' : 'border-gray-600';
|
||||||
|
@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="{{ $textPrimary }} 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 {{ $cardBg }} rounded-lg overflow-hidden">
|
||||||
|
<thead class="{{ $thBg }}">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">名稱</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">最低儲值</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">回饋類型</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">回饋值</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">狀態</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y {{ $borderColor }}">
|
||||||
|
@forelse($rules as $rule)
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $rule->name }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">${{ number_format($rule->min_amount) }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $rule->bonus_type == 'fixed' ? '固定金額' : '百分比' }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $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 {{ $textSecondary }}">尚無資料</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 {{ $cardBg }} rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||||
|
<div class="px-6 py-4 border-b {{ $borderColor }}">
|
||||||
|
<h3 class="{{ $textPrimary }} 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="{{ $textSecondary }} text-sm block mb-1">名稱</label>
|
||||||
|
<input type="text" name="name" required class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">最低儲值金額</label>
|
||||||
|
<input type="number" name="min_amount" value="0" step="0.01" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">回饋類型</label>
|
||||||
|
<select name="bonus_type" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
<option value="fixed">固定金額</option>
|
||||||
|
<option value="percentage">百分比</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">回饋值</label>
|
||||||
|
<input type="number" name="bonus_value" value="0" step="0.01" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} 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="{{ $textSecondary }} text-sm">啟用</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t {{ $borderColor }} flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 {{ $textSecondary }} border {{ $inputBorder }} 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
|
||||||
168
resources/views/admin/gift-definitions/index.blade.php
Normal file
168
resources/views/admin/gift-definitions/index.blade.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
@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';
|
||||||
|
$thBg = $isLight ? 'bg-gray-50' : 'bg-gray-700';
|
||||||
|
$inputBg = $isLight ? 'bg-white' : 'bg-gray-700';
|
||||||
|
$inputBorder = $isLight ? 'border-gray-300' : 'border-gray-600';
|
||||||
|
|
||||||
|
$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="{{ $textPrimary }} 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 {{ $cardBg }} rounded-lg overflow-hidden">
|
||||||
|
<thead class="{{ $thBg }}">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">名稱</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">類型</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">數值</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">適用等級</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">觸發條件</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">狀態</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y {{ $borderColor }}">
|
||||||
|
@forelse($gifts as $gift)
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $gift->name }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $typeLabels[$gift->type] ?? $gift->type }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $gift->value }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $gift->tier?->name ?? '全部' }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $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 {{ $textSecondary }}">尚無資料</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 {{ $cardBg }} rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||||
|
<div class="px-6 py-4 border-b {{ $borderColor }}">
|
||||||
|
<h3 class="{{ $textPrimary }} 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="{{ $textSecondary }} text-sm block mb-1">名稱</label>
|
||||||
|
<input type="text" name="name" required class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">類型</label>
|
||||||
|
<select name="type" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
@foreach($typeLabels as $key => $label)
|
||||||
|
<option value="{{ $key }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">數值</label>
|
||||||
|
<input type="number" name="value" value="0" step="0.01" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">適用等級</label>
|
||||||
|
<select name="tier_id" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} 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="{{ $textSecondary }} text-sm block mb-1">觸發條件</label>
|
||||||
|
<select name="trigger" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
@foreach($triggerLabels as $key => $label)
|
||||||
|
<option value="{{ $key }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">有效天數</label>
|
||||||
|
<input type="number" name="validity_days" value="30" min="1" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} 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="{{ $textSecondary }} text-sm">啟用</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t {{ $borderColor }} flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 {{ $textSecondary }} border {{ $inputBorder }} 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
|
||||||
93
resources/views/admin/members/index.blade.php
Normal file
93
resources/views/admin/members/index.blade.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
@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';
|
||||||
|
$thBg = $isLight ? 'bg-gray-50' : 'bg-gray-700';
|
||||||
|
@endphp
|
||||||
|
<div class="container mx-auto px-6 py-8">
|
||||||
|
<h3 class="{{ $textPrimary }} 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 {{ $borderColor }}">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $thBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">
|
||||||
|
UUID
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $thBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">
|
||||||
|
姓名
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $thBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $thBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">
|
||||||
|
手機
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $thBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">
|
||||||
|
狀態
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 border-b {{ $borderColor }} {{ $thBg }} text-left text-xs leading-4 font-medium {{ $textSecondary }} uppercase tracking-wider">
|
||||||
|
註冊時間
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="{{ $cardBg }}">
|
||||||
|
@forelse ($members as $member)
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||||
|
<div class="text-sm leading-5 font-medium {{ $textSecondary }}">{{ $member->uuid }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||||
|
<div class="text-sm leading-5 font-bold {{ $textPrimary }}">{{ $member->name }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||||
|
<div class="text-sm leading-5 {{ $textPrimary }}">{{ $member->email ?? '-' }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||||
|
<div class="text-sm leading-5 {{ $textPrimary }}">{{ $member->phone ?? '-' }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-no-wrap border-b {{ $borderColor }}">
|
||||||
|
@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 {{ $borderColor }} text-sm leading-5 {{ $textSecondary }}">
|
||||||
|
{{ $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 {{ $borderColor }} text-center {{ $textSecondary }}">
|
||||||
|
尚無會員資料
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
{{ $members->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
129
resources/views/admin/membership-tiers/index.blade.php
Normal file
129
resources/views/admin/membership-tiers/index.blade.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
@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';
|
||||||
|
$thBg = $isLight ? 'bg-gray-50' : 'bg-gray-700';
|
||||||
|
$inputBg = $isLight ? 'bg-white' : 'bg-gray-700';
|
||||||
|
$inputBorder = $isLight ? 'border-gray-300' : 'border-gray-600';
|
||||||
|
@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="{{ $textPrimary }} 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 {{ $cardBg }} rounded-lg overflow-hidden">
|
||||||
|
<thead class="{{ $thBg }}">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">名稱</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">年費</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">折扣</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">點數倍率</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">預設</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y {{ $borderColor }}">
|
||||||
|
@forelse($tiers as $tier)
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $tier->name }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $tier->annual_fee == 0 ? '免費' : '$'.number_format($tier->annual_fee) }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $tier->discount_rate * 100 }}%</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $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 {{ $textSecondary }}">尚無資料</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 {{ $cardBg }} rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||||
|
<div class="px-6 py-4 border-b {{ $borderColor }}">
|
||||||
|
<h3 class="{{ $textPrimary }} 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="{{ $textSecondary }} text-sm block mb-1">名稱</label>
|
||||||
|
<input type="text" name="name" required class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">年費</label>
|
||||||
|
<input type="number" name="annual_fee" value="0" step="0.01" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} 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 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} 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 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} 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="{{ $textSecondary }} text-sm">設為預設等級</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t {{ $borderColor }} flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 {{ $textSecondary }} border {{ $inputBorder }} 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
|
||||||
147
resources/views/admin/point-rules/index.blade.php
Normal file
147
resources/views/admin/point-rules/index.blade.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
@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';
|
||||||
|
$thBg = $isLight ? 'bg-gray-50' : 'bg-gray-700';
|
||||||
|
$inputBg = $isLight ? 'bg-white' : 'bg-gray-700';
|
||||||
|
$inputBorder = $isLight ? 'border-gray-300' : 'border-gray-600';
|
||||||
|
|
||||||
|
$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="{{ $textPrimary }} 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 {{ $cardBg }} rounded-lg overflow-hidden">
|
||||||
|
<thead class="{{ $thBg }}">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">名稱</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">觸發條件</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">每單位點數</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">單位金額</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">有效天數</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">狀態</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium {{ $textSecondary }} uppercase">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y {{ $borderColor }}">
|
||||||
|
@forelse($rules as $rule)
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $rule->name }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $triggerLabels[$rule->trigger] ?? $rule->trigger }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $rule->points_per_unit }} 點</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">${{ number_format($rule->unit_amount) }}</td>
|
||||||
|
<td class="px-6 py-4 {{ $textPrimary }}">{{ $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 {{ $textSecondary }}">尚無資料</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 {{ $cardBg }} rounded-lg shadow-xl transform transition-all sm:max-w-lg sm:w-full mx-4">
|
||||||
|
<div class="px-6 py-4 border-b {{ $borderColor }}">
|
||||||
|
<h3 class="{{ $textPrimary }} 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="{{ $textSecondary }} text-sm block mb-1">名稱</label>
|
||||||
|
<input type="text" name="name" required class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">觸發條件</label>
|
||||||
|
<select name="trigger" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
@foreach($triggerLabels as $key => $label)
|
||||||
|
<option value="{{ $key }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">每單位獲得點數</label>
|
||||||
|
<input type="number" name="points_per_unit" value="1" min="1" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">單位金額</label>
|
||||||
|
<input type="number" name="unit_amount" value="100" step="0.01" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} focus:ring-2 focus:ring-indigo-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="{{ $textSecondary }} text-sm block mb-1">有效天數</label>
|
||||||
|
<input type="number" name="validity_days" value="365" min="1" class="w-full px-3 py-2 {{ $inputBg }} {{ $inputBorder }} border rounded-md {{ $textPrimary }} 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="{{ $textSecondary }} text-sm">啟用</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t {{ $borderColor }} flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="document.getElementById('createModal').classList.add('hidden')" class="px-4 py-2 {{ $textSecondary }} border {{ $inputBorder }} 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
|
||||||
@@ -80,14 +80,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- 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"
|
<aside class="fixed inset-y-0 left-0 z-50 w-64 {{ $currentTheme['sidebar'] }} border-r overflow-y-auto transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0"
|
||||||
:class="{'translate-x-0': sidebarOpen, '-translate-x-full': !sidebarOpen}"
|
:class="{'translate-x-0': sidebarOpen, '-translate-x-full': !sidebarOpen}"
|
||||||
x-transition:enter="transition-transform ease-in-out duration-300"
|
x-cloak>
|
||||||
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">
|
<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>
|
<span class="text-2xl font-bold text-{{ $currentTheme['accent'] }}-500">Star Cloud</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,11 +24,36 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 會員管理 (新增) --}}
|
||||||
|
<div x-data="{
|
||||||
|
open: localStorage.getItem('menu_members') === 'true' || {{ request()->routeIs('admin.members.*') ? 'true' : 'false' }},
|
||||||
|
init() {
|
||||||
|
this.$watch('open', value => localStorage.setItem('menu_members', 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="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>
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
<div x-show="open" x-collapse.duration.500ms class="mt-1 space-y-1">
|
||||||
|
<a href="{{ route('admin.members.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.members.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.membership-tiers.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.membership-tiers.*') ? '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.deposit-bonus-rules.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.deposit-bonus-rules.*') ? '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.point-rules.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.point-rules.*') ? '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.gift-definitions.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.gift-definitions.*') ? '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>
|
||||||
|
|
||||||
{{-- 3. 機台管理 --}}
|
{{-- 3. 機台管理 --}}
|
||||||
<div x-data="{
|
<div x-data="{
|
||||||
open: localStorage.getItem('menu_machines') === 'true' || {{ request()->routeIs('admin.machines.*') ? 'true' : 'false' }},
|
open: localStorage.getItem('menu_machines') === 'true' || {{ request()->routeIs('admin.machines.*') ? 'true' : 'false' }},
|
||||||
@@ -45,7 +70,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -71,7 +96,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -96,7 +121,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -127,7 +152,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -153,7 +178,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -177,7 +202,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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>
|
<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>
|
||||||
@@ -201,7 +226,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -229,7 +254,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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') }}" @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.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>
|
||||||
@@ -256,7 +281,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -282,7 +307,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
@@ -309,7 +334,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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>
|
<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>
|
||||||
@@ -332,7 +357,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div x-show="open" x-collapse class="mt-1 space-y-1">
|
<div x-show="open" x-collapse.duration.500ms 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.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.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.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>
|
||||||
|
|||||||
174
resources/views/test/social-login.blade.php
Normal file
174
resources/views/test/social-login.blade.php
Normal 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>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
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) {
|
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
||||||
return $request->user();
|
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']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(
|
|||||||
// 1. 儀表板
|
// 1. 儀表板
|
||||||
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard');
|
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. 機台管理
|
// 3. 機台管理
|
||||||
Route::prefix('machines')->name('machines.')->group(function () {
|
Route::prefix('machines')->name('machines.')->group(function () {
|
||||||
Route::get('/logs', [App\Http\Controllers\Admin\MachineController::class, 'logs'])->name('logs');
|
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';
|
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');
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default {
|
|||||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||||
'./storage/framework/views/*.php',
|
'./storage/framework/views/*.php',
|
||||||
'./resources/views/**/*.blade.php',
|
'./resources/views/**/*.blade.php',
|
||||||
|
'./node_modules/preline/preline.js',
|
||||||
],
|
],
|
||||||
|
|
||||||
safelist: [
|
safelist: [
|
||||||
|
|||||||
Reference in New Issue
Block a user