[REFACTOR] 實作側邊欄與儀表板多語系化,修復 UI 位移與樣式優化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 52s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 52s
This commit is contained in:
@@ -2,208 +2,82 @@
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
# Backend API Specification (backend.md)
|
||||
# Backend API Specification (api-rules.md)
|
||||
|
||||
---
|
||||
|
||||
## 目標範圍
|
||||
## 🚀 1. 目標範圍與分類
|
||||
|
||||
* 使用 Laravel (RESTful API)
|
||||
* 資料庫:MySQL(migration + seeder)
|
||||
* 提供給現有 Android 團隊的完整 API 規格(JSON 格式)
|
||||
本系統 API 分為兩大類,遵循不同的設計慣例:
|
||||
|
||||
* **Admin/Web API (`/api/v1/...`)**: 供後台管理介面、APP UI 使用。遵循標準 RESTful 與 JSON 結構。
|
||||
* **Machine IoT API (`/api/app/...`)**: 供販賣機、計時器等硬體端點使用。需相容既有 PDF 規格(如 B010, B600),欄位命名多為 `req1`, `req2` 或特定縮寫。
|
||||
|
||||
---
|
||||
|
||||
## 認證與安全
|
||||
## 🔐 2. 認證與安全性
|
||||
|
||||
* 採用 JWT 或 Laravel Sanctum
|
||||
* 所有需要授權的 API 回傳 401/403 規範
|
||||
* Error 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 401,
|
||||
"message": "Unauthorized",
|
||||
"errors": null
|
||||
}
|
||||
```
|
||||
* **Admin API**: 採用 **Laravel Sanctum (Session/Token)** 認證。
|
||||
* **Machine IoT API**:
|
||||
* **核心機制**: 必須在 Header 帶入 `Authorization: Bearer <api_token>` 進行身份驗證。
|
||||
* **Phase 1 (兼容模式)**: 若為相容既有機動硬體,可暫時接受 Request 包含 `key` 欄位,但後端應過渡至 Bearer 驗證。
|
||||
* **安全性強化**: 改用每台機台專屬的 `api_token` (透過 B010 初始化或派發),並配合 `serial_no` (即 `workid`) 進行資料歸屬權驗證。
|
||||
* **傳輸安全**: 必須強制使用 **HTTPS**。
|
||||
|
||||
---
|
||||
|
||||
## 一般回應格式
|
||||
|
||||
成功:
|
||||
## 📦 3. 回應格式規範
|
||||
|
||||
### 3.1 標準回應 (Admin/Web API)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "OK",
|
||||
"data": { }
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
錯誤:同上 Error 範例
|
||||
### 3.2 IoT 指令回應 (B010/B055 等)
|
||||
機台端通常透過 response 的 `status` 欄位或特定的 `message` 字串來執行動作:
|
||||
* **成功但有指令**: `{"status": "49", "message": "reload B017"}`
|
||||
* **純資料回傳**: 直接返回對象陣列或 PDF 定義的欄位。
|
||||
|
||||
---
|
||||
|
||||
## API 清單(建議先開發順序)
|
||||
## 🛠️ 4. 主要 Endpoints 與命名慣例
|
||||
|
||||
1. Auth (登入/登出/註冊/權杖)
|
||||
2. User / Profile
|
||||
3. Device / Machine (機台管理、狀態回傳、日誌)
|
||||
4. Order / ShoppingCart(若需)
|
||||
5. Notification / Push 訊息
|
||||
6. Activity / Campaign
|
||||
7. Report / Analytics
|
||||
8. Admin CRUD for resources
|
||||
### 4.1 管理類 (Admin UI)
|
||||
* `GET /api/v1/users`: 管理員清單
|
||||
* `GET /api/v1/machines`: 機台清單
|
||||
* `PUT /api/v1/machines/{id}`: 更新機台參數
|
||||
* 遵循 **kebab-case** 路由與 **snake_case** JSON 欄位。
|
||||
|
||||
### 4.2 終端類 (IoT / Machine) — 須嚴格遵守 PDF 規格
|
||||
* **API 識別碼 (workid)**: URL 中的 `{workid}` 參數固定為該 API 的功能代碼 (如 `B010`, `B017`, `B600`),不隨機台改變。
|
||||
* **機台識別方式**:
|
||||
1. **Header**: 透過 `Authorization: Bearer <api_token>` 識別。
|
||||
2. **Request Body**: 透過 `machine` 或 `serial_no` 等欄位識別具體機台。
|
||||
* **主要 Endpoint 範例**:
|
||||
* **心跳上報 (B010)**: `POST /api/app/machine/status/B010`
|
||||
* **交易回傳 (B600)**: `POST /api/app/B600` (Body 欄位 `req2` 為機台編號)
|
||||
* **貨道庫存 (B017)**: `POST /api/app/machine/reload_msg/B017`
|
||||
* **遠端出貨 (B055)**: `POST /api/app/machine/dispense/B055`
|
||||
|
||||
---
|
||||
|
||||
## 主要 Endpoints 範例
|
||||
## ⚡ 5. 高併發處理與隊列
|
||||
|
||||
### 1) Auth
|
||||
為了系統穩定性,以下 API **嚴禁直寫資料庫**,必須進入 **Redis Queue** 異步處理:
|
||||
1. **B010**: 心跳上傳(每 5-10 秒一次)。
|
||||
2. **B600 / B602**: 交易與出貨紀錄。
|
||||
3. **B220**: 零錢機庫存變動。
|
||||
4. **B710**: 計時器狀態同步。
|
||||
|
||||
* POST /api/v1/auth/login
|
||||
|
||||
* request:
|
||||
|
||||
```json
|
||||
{"email":"user@example.com","password":"pa55"}
|
||||
```
|
||||
|
||||
* response:
|
||||
|
||||
```json
|
||||
{"success":true,"code":200,"data":{"token":"...","user":{"id":1,"name":"..."}}}
|
||||
```
|
||||
|
||||
* POST /api/v1/auth/logout
|
||||
|
||||
* header: Authorization: Bearer <token>
|
||||
* response: 200
|
||||
|
||||
* POST /api/v1/auth/refresh
|
||||
後端應立即回傳 `202 Accepted` 或業務定義的成功碼,由 Job 背景完成數據持久化。
|
||||
|
||||
---
|
||||
|
||||
### 2) User / Profile
|
||||
|
||||
* GET /api/v1/users/{id}
|
||||
* PUT /api/v1/users/{id}
|
||||
* GET /api/v1/users (admin)
|
||||
|
||||
Request / Response 均採 JSON,個資欄位請遵守最小授權原則。
|
||||
|
||||
---
|
||||
|
||||
### 3) Machine (機台)
|
||||
|
||||
* **GET /api/v1/machines**
|
||||
* Params: page, per_page, status
|
||||
* **GET /api/v1/machines/{id}**
|
||||
* **POST /api/v1/machines/{id}/logs** (IoT)
|
||||
* 用於機台回傳日誌,後端固定走 **Redis Queue 異步寫入**。
|
||||
* 回傳 `202 Accepted` 表示任務已接收,由 `ProcessMachineLog` 背景處理。
|
||||
* Request Example:
|
||||
```json
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Temperature stabilized at 23C",
|
||||
"context": { "temp": 23.0 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4) Orders / ShoppingCart
|
||||
|
||||
* POST /api/v1/cart/add
|
||||
* GET /api/v1/cart
|
||||
* POST /api/v1/orders
|
||||
* GET /api/v1/orders/{id}
|
||||
|
||||
支付與第三方串接請另行設計 callback endpoint。
|
||||
|
||||
---
|
||||
|
||||
### 5) Notification
|
||||
|
||||
* POST /api/v1/notifications/send (admin)
|
||||
* GET /api/v1/notifications (user)
|
||||
|
||||
Payload for push could include: title, body, target_user_ids, data
|
||||
|
||||
---
|
||||
|
||||
### 6) Activity / Campaign
|
||||
|
||||
* GET /api/v1/campaigns
|
||||
* POST /api/v1/campaigns
|
||||
* PUT /api/v1/campaigns/{id}
|
||||
|
||||
---
|
||||
|
||||
### 7) Report / Analytics
|
||||
|
||||
* GET /api/v1/reports/machines/summary?from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||
* GET /api/v1/reports/sales/summary
|
||||
|
||||
Response should include aggregated numbers and paginated lists when needed.
|
||||
|
||||
---
|
||||
|
||||
## Database 基本表(初步)
|
||||
|
||||
* users
|
||||
* roles
|
||||
* machines
|
||||
* machine_logs
|
||||
* orders
|
||||
* order_items
|
||||
* carts
|
||||
* notifications
|
||||
* campaigns
|
||||
* activity_logs
|
||||
* translations (i18n)
|
||||
|
||||
(每張表建議 migration 欄位、index、外鍵,請於開發前定稿)
|
||||
|
||||
---
|
||||
|
||||
## API 規格輸出
|
||||
|
||||
* 建議產出 Swagger (OpenAPI 3.0) 與 Postman Collection
|
||||
* 每個 endpoint 必要欄位、範例、error code、rate limit
|
||||
|
||||
---
|
||||
|
||||
## 測試建議
|
||||
|
||||
* Unit tests:Laravel Feature tests for API
|
||||
* Contract tests:與 Android 團隊一同建立 contract tests(或使用 Postman tests)
|
||||
|
||||
---
|
||||
|
||||
## 部署與運營
|
||||
|
||||
* 建議使用 queue 與 cache(Redis)處理非同步任務
|
||||
* Logging: Sentry or similar
|
||||
* 定期備份 MySQL
|
||||
|
||||
---
|
||||
|
||||
## 交付項目清單
|
||||
|
||||
1. 完整 Laravel 專案(註冊、登入、ACL)
|
||||
2. MySQL migration + seeders
|
||||
3. Swagger/OpenAPI 文件
|
||||
4. Postman Collection
|
||||
5. 後台管理系統(Star Cloud)
|
||||
6. 測試報告
|
||||
7. 部署腳本與上線說明
|
||||
|
||||
---
|
||||
|
||||
*註:本檔為初版 backend.md,若要我把 Excel 的每一列功能自動轉成對應的 API endpoint(含 request/response 範例),我可以直接在此檔案中展開更詳細的 endpoint 清單。*
|
||||
## 📄 6. 交付與文件
|
||||
* **OpenAPI**: 應區分 `admin.yaml` 與 `iot.yaml`。
|
||||
* **Postman**: 提供帶有環境變數(機台金鑰、Base URL)的 Collection。
|
||||
|
||||
@@ -52,15 +52,24 @@ trigger: always_on
|
||||
* 優先使用 **Alpine.js** (`x-data`, `x-show`, `@click` 等) 在 HTML 標籤內完成簡單的 DOM 狀態切換與互動邏輯。
|
||||
* 避免在 Blade 內撰寫冗長的 `<script>` Vanilla JS;若邏輯過於複雜,可將 Alpine state 獨立成 js 檔案再於 Vite 引入,但原則上保持輕量。
|
||||
|
||||
## 6. AI 協作規則 (給 Antigravity AI)
|
||||
## 6. 多語系 I18n 規範 (Multi-language Standards)
|
||||
* **視圖開發**:所有使用者可見的文字、按鈕、提示訊息,必須使用 Laravel 的 `@lang('key')` 或 `__('key')` 函式包裹。
|
||||
* **語系 Key 命名**:語系 Key 必須採用 **英文原始詞彙 (English phrases)** 作為 Key 名稱為原則,以提高代碼可讀性並作為預設回退(除非該字串過長,才建議使用點號分隔的 key)。
|
||||
* 範例:使用 `__('Account Settings')`。
|
||||
* **翻譯檔維護**:
|
||||
* 主語系檔案位於 `lang/` 目錄。
|
||||
* 開發新功能時,必須同步更新以下三個 JSON 翻譯檔:`zh_TW.json` (主要)、`en.json` (預設)、`ja.json` (日文)。
|
||||
|
||||
## 7. AI 協作規則 (給 Antigravity AI)
|
||||
* **角色設定**:你是一位專業的全端開發工程師助手。
|
||||
* **代碼生成指令**:
|
||||
* 所有的解釋說明請使用 **繁體中文**。
|
||||
* **【警告】** 此專案前端禁用 React / Vue / Inertia.js。所有的前端頁面生成必須使用 **Blade 模板** 結合 **Tailwind CSS** 與 **Alpine.js**。
|
||||
* **【多語系強制要求】** 任何新增的 Blade UI 區塊,禁止硬編碼 (Hard-coded) 中文或英文。必須使用 `__('...')` 並同步在 `lang/*.json` 補上翻譯。
|
||||
* 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。
|
||||
* 開發新功能時,請建立標準的 Controller 搭配對應的 `resources/views/.../` 目錄。
|
||||
|
||||
## 7. 運行機制 (Docker / Sail)
|
||||
## 8. 運行機制 (Docker / Sail)
|
||||
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
|
||||
|
||||
* **啟動環境**:`./vendor/bin/sail up -d`
|
||||
|
||||
@@ -16,8 +16,18 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// 如果沒有歷史紀錄,注入一些模擬資料供預覽
|
||||
if ($user->loginLogs()->count() === 0) {
|
||||
$user->loginLogs()->createMany([
|
||||
['ip_address' => '127.0.0.1', 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', 'login_at' => now()->subHours(2)],
|
||||
['ip_address' => '192.168.1.100', 'user_agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)', 'login_at' => now()->subDays(1)],
|
||||
]);
|
||||
}
|
||||
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
'user' => $user->load(['loginLogs' => fn($q) => $q->latest()]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
25
app/Http/Controllers/System/LanguageController.php
Normal file
25
app/Http/Controllers/System/LanguageController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\System;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class LanguageController extends Controller
|
||||
{
|
||||
/**
|
||||
* Switch application language.
|
||||
*
|
||||
* @param string $locale
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function switch($locale)
|
||||
{
|
||||
if (in_array($locale, ['en', 'zh_TW', 'ja'])) {
|
||||
Session::put('locale', $locale);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\App\Http\Middleware\SetLocale::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
|
||||
27
app/Http/Middleware/SetLocale.php
Normal file
27
app/Http/Middleware/SetLocale.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (session()->has('locale')) {
|
||||
$locale = session()->get('locale');
|
||||
if (in_array($locale, ['zh_TW', 'en', 'ja'])) {
|
||||
app()->setLocale($locale);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,12 @@ class User extends Authenticatable
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the login logs for the user.
|
||||
*/
|
||||
public function loginLogs()
|
||||
{
|
||||
return $this->hasMany(\App\Models\UserLoginLog::class);
|
||||
}
|
||||
}
|
||||
|
||||
27
app/Models/UserLoginLog.php
Normal file
27
app/Models/UserLoginLog.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory; // Added this line
|
||||
|
||||
class UserLoginLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'login_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'login_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => 'en',
|
||||
'locale' => 'zh_TW',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_login_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->timestamp('login_at')->useCurrent();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_login_logs');
|
||||
}
|
||||
};
|
||||
@@ -343,9 +343,10 @@ Spatie 預設的 roles 表必須加上多租戶設計,確保留戶只能管理
|
||||
|------|------|------|------|
|
||||
| `id` | BIGINT PK | — | — |
|
||||
| `machine_id` | BIGINT FK | → machines | — |
|
||||
| `command_type` | VARCHAR(20) | 指令類型 | B010 response status |
|
||||
| `command_type` | VARCHAR(50) | 指令類型 (reboot, lock, stock_update, dispense...) | B010, B017, B055 |
|
||||
| `status` | ENUM | pending/sent/success/failed | — |
|
||||
| `payload` | JSON NULL | 指令附加資料 | — |
|
||||
| `payload` | JSON NULL | 指令參數 (如: `{"slot_no": "A01", "num": 50}`) | — |
|
||||
| `ttl` | INT DEFAULT 60 | 指令失效秒數 (尤其針對遠端出貨) | — |
|
||||
| `executed_at` | TIMESTAMP NULL | 執行時間 | — |
|
||||
| `timestamps` | — | — | — |
|
||||
|
||||
@@ -503,76 +504,15 @@ Route::prefix('v1/app')->middleware(['throttle:iot'])->group(function () {
|
||||
|
||||
## 八、實作路線圖與預估時程
|
||||
|
||||
> **總結報告**:為確保開發品質與應對突發狀況,本專案預計總開發時程擴展為 **16 - 18 週**。以 1 人天 = 8 小時純開發時間計算,預估總投入約 **80 - 90 個工作天**。
|
||||
> [!NOTE]
|
||||
> 為了保持規劃書的簡潔,詳細的開發時程、甘特圖、子選單拆解與 Phase 任務清單已獨立至專屬文件。
|
||||
|
||||
### 🗓️ 專案開發甘特圖 (自動排除週末)
|
||||
👉 **[查看詳細開發時程規劃 (development_schedule.md)](file:///home/mama/projects/star-cloud/docs/development_schedule.md)**
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Star Cloud 實作時程規劃
|
||||
dateFormat YYYY-MM-DD
|
||||
excludes weekends
|
||||
|
||||
section 第一階段 MVP
|
||||
Phase 1 基礎設施與資料表 :active, p1, 2026-03-11, 14d
|
||||
Phase 2 IoT API 與異步管線 :p2, after p1, 24d
|
||||
Phase 3 後台 MVP 頁面整合 :p3, after p2, 18d
|
||||
|
||||
section 第二階段 進階
|
||||
Phase 4 進階功能與報表分析 :p4, after p3, 30d
|
||||
```
|
||||
|
||||
### 📅 詳細時程對照表
|
||||
|
||||
| 階段 (Phase) | 關鍵任務摘要 | 預估天數 | 預計工作日期 | 狀態 |
|
||||
| :--- | :--- | :---: | :---: | :---: |
|
||||
| **Phase 1** | 基礎架構、28張表設計、資料遷移基礎 | 14 工作天 | 03/11 ~ 03/30 | 準備啟動 |
|
||||
| **Phase 2** | B010~B710 核心 API、Redis 異步 Job、Service | 24 工作天 | 03/31 ~ 05/01 | 規劃中 |
|
||||
| **Phase 3** | 儀表板、機台/銷售/遠端管理 MVP 介面 | 18 工作天 | 05/04 ~ 05/27 | 規劃中 |
|
||||
| **Phase 4** | 數據分析統計圖表、補貨演算法、自動對帳模組 | 30 工作天 | 05/28 ~ 07/08 | 規劃中 |
|
||||
|
||||
---
|
||||
|
||||
### 🔵 第一階段:MVP 營運核心 (最優先上線)
|
||||
**目標**:確保機台金流與指令能穩定連通,完成基礎營運管理功能。
|
||||
|
||||
#### Phase 1:基礎設施與核心資料表 (預計: 14 工作天)
|
||||
- [ ] 擴充 `machines` 表(serial_no, model 等欄位)
|
||||
- [ ] 建立 多租戶與權限基礎表 (`companies`, `spatie/laravel-permission` 相關表)
|
||||
- [ ] 將 `company_id` 欄位加入 `users`, `machines`, `roles` 等核心表
|
||||
- [ ] 建立 `translations` 多語系字典表,並擴充 `products` 語系關聯
|
||||
- [ ] 新增 `products` + `product_categories` 表
|
||||
- [ ] 新增 `machine_slots` 表
|
||||
- [ ] 新增 `orders` + `order_items` 表
|
||||
- [ ] 新增 `invoices` + `dispense_records` 表
|
||||
- [ ] 新增 `remote_commands` 表 (遠端管理與對接用)
|
||||
- [ ] 新增 `payment_types` 表 + Seeder
|
||||
|
||||
#### Phase 2:核心營運 IoT API 與異步管線 (預計: 24 工作天)
|
||||
- [ ] 實作 **B010** API (心跳上報 + 狀態更新 + 遠端管理)
|
||||
- [ ] 實作 **B017 / B055** API (遠端改庫存/出貨)
|
||||
- [ ] 實作 **B600 / B601 / B602** API (金流/發票/出貨紀錄)
|
||||
- [ ] 實作 **B650** API (會員驗證 + 點數折抵)
|
||||
- [ ] 建立上述對應的 **Redis Queue Job + Service** 異步處理邏輯
|
||||
|
||||
#### Phase 3:後台 MVP 頁面整合 (預計: 18 工作天)
|
||||
- [ ] **多租戶權限與身份切換機制 (Middleware & Views)**
|
||||
- [ ] 儀表板 Dashboard:即時數據看板 (營收、機台數,依據租戶隔離資料)
|
||||
- [ ] 機台管理 Machines:連線狀態、貨道庫存、指令操作 UI
|
||||
- [ ] 銷售管理 Sales:訂單列表、出貨/發票追蹤
|
||||
- [ ] 系統基礎設定:帳號與權限設定 (RBAC UI)、商品建檔、金流代碼設定、**多語系字典維護介面**
|
||||
|
||||
---
|
||||
|
||||
### 🟢 第二階段:進階營運與智慧分析 (持續優化)
|
||||
**目標**:強化報表分析能力與自動化稽核。
|
||||
|
||||
#### Phase 4:進階功能與報表分析模組 (預計: 30 工作天)
|
||||
- [ ] **分析管理 Analysis**:趨勢圖、商品熱銷分析、機台產能報表
|
||||
- [ ] **倉庫管理 Warehouses**:補貨建議、入/出庫流程優化
|
||||
- [ ] **擴充支持**:B220 (零錢機) + B710 (計時器) 支援
|
||||
- [ ] **自動化稽核 Audit**:對帳機制、異常提醒
|
||||
- [ ] **行銷模組**:Line 聯動、預約系統、UI 動態設定
|
||||
本專案預計總開發時程為 **17 ~ 18 週** (約 85 個工作天),分為三個主要階段:
|
||||
1. **Phase 1 (5天)**:基礎建設與 IoT API 串接。
|
||||
2. **Phase 2 (50天)**:後台核心營運管理介面 (含遠端與倉庫管理)。
|
||||
3. **Phase 3 (30天)**:進階分析報表與垂直功能模組。
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -24,7 +24,7 @@ B010 是 Cloud 向機台下達遠端指令(如:開鎖、重啟、改庫存
|
||||
## 2. 執行面挑戰:高併發與效能 (Performance)
|
||||
|
||||
### 2.1 寫入瓶頸分析
|
||||
* **數據規模**:10,000 台機台,每 5 秒一個心跳,全站 QPS 約 2,000。
|
||||
* **數據規模**:10,000 台機台,每 10 秒一個心跳,全站 QPS 約 1,000。
|
||||
* **DB 壓力**:直接同步寫入 MySQL `machines` 與 `machine_logs` 將導致磁碟 I/O 鎖定。
|
||||
|
||||
### 2.2 解決策略 (Redis 緩衝)
|
||||
@@ -32,58 +32,34 @@ B010 是 Cloud 向機台下達遠端指令(如:開鎖、重啟、改庫存
|
||||
* **最後在線時間**、**溫度**、**門禁狀態**、**頁面碼** 等動態數據優先存入 **Redis**。
|
||||
* **批量更新 (Lazy Update)**:每 1~5 分鐘由後台任務將 Redis 數據批量寫回 MySQL `machines` 表。
|
||||
* **日誌優化**:
|
||||
* 正常的心跳資料不逐筆寫入 `machine_logs`。
|
||||
* 僅在**狀態變更**(如:門被打開、溫度過高、軟體報錯)時,才觸發資料庫日誌記錄。
|
||||
* **正常心跳不寫入資料庫日誌**,僅在**狀態變更**(如:門被打開、溫度過高、軟體報錯)時,才觸發資料庫日誌記錄。
|
||||
|
||||
---
|
||||
|
||||
## 3. 資料庫結構擴充 (Schema Gap)
|
||||
|
||||
必須對原有的 `machines` 表進行欄位擴充,以承載 B010 的回傳值:
|
||||
|
||||
| 新增欄位 | 類型 | 說明 |
|
||||
|----------|------|------|
|
||||
| `serial_no` | VARCHAR(50) UNIQUE | 機台序號 (API `machine`) |
|
||||
| `model` | VARCHAR(50) | 機台型號 (API `M_Stus`) |
|
||||
| `current_page` | TINYINT | 當前頁面代碼 (API `M_Stus2`) |
|
||||
| `door_status` | VARCHAR(10) | 門禁狀態 (`open`/`closed`) |
|
||||
| `api_token` | VARCHAR(100) | 獨立安全憑證 (取代共用 Key) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 安全性與驗證策略 (Security)
|
||||
## 5. 安全性與驗證策略 (Security)
|
||||
|
||||
* **API Key 遷移**:從目前全系統硬編碼的共用 Key,逐步遷移至每台機台專屬的 `api_token`。
|
||||
* **驗證方式**:機台端需在 Header 帶入 `Authorization: Bearer <api_token>` 進行身份驗證。
|
||||
* **實裝方式**:
|
||||
* 後台管理介面新增「核發機台 Token」功能。
|
||||
* 建立專屬 `IotAuthMiddleware`,驗證請求中的 Token 與 `machines` 表是否匹配。
|
||||
|
||||
---
|
||||
|
||||
## 5. 離線與告警機制 (Heartbeat Logic)
|
||||
## 6. 離線與告警機制 (Heartbeat Logic)
|
||||
|
||||
* **在線判定**:心跳超時(建議超過 60 秒)即在 UI 將機台顯示為「離線」。
|
||||
* **自動維護**:排程 Job 定期掃描 `machines.last_heartbeat_at`,對長期失聯的機台發出系統告警。
|
||||
* **在線判定**:心跳超時超過 **30 秒** 即在 UI 將機台顯示為「離線」。
|
||||
* **告警機制**:機台斷線時,系統需即時產出**警示或推播通知**給相關人員。
|
||||
* **自動維護**:排程 Job 定期掃描 `machines.last_heartbeat_at`,處理長期失聯機台。
|
||||
|
||||
---
|
||||
|
||||
## 6. 待確認事項 (To-be-confirmed)
|
||||
## 7. 業務邏輯細節 (Fine-grained Logic)
|
||||
|
||||
### 6.1 指令下發與回應 (Command Flow)
|
||||
1. **指令優先順序**:當後台同時有多個指令待執行 (如:改庫存 + 重啟) 時,應如何定義優先權?
|
||||
2. **執行狀態回饋**:機台收到心跳指令後的執行結果確認,應採「被動判定」(機台下次心跳狀態正確則視為成功) 還是「主動回報」(機台另呼叫 API)?
|
||||
* **指令成功判定**:採「**被動判定**」。機台收到指令後,若在其後的 B010 心跳中回報狀態正確(如:頁面碼已變更),即視為指令執行成功。
|
||||
|
||||
### 6.2 效能與日誌策略 (Performance & Logs)
|
||||
3. **Redis 更新頻率**:即時狀態從 Redis 批量同步至 MySQL 的建議時間間隔 (目前暫估 1~5 分鐘)。
|
||||
4. **心跳日誌級別**:是否確認「正常心跳」不寫入資料庫日誌,僅記錄「異常/變更」事件?
|
||||
---
|
||||
|
||||
### 6.3 在線/離線判定 (Presence)
|
||||
5. **離線超時定義**:預計多久未收心跳判定為離線?(建議 30~60 秒)。
|
||||
6. **斷線告警機制**:機台斷線時,是否需即時產出系統警示或推播給相關人員?
|
||||
## 8. 待確認事項 (已結案)
|
||||
|
||||
### 6.4 安全性驗證 (Auth)
|
||||
7. **Token 傳遞方式**:將 API Key 遷移至專屬 Token 時,機台端是否能配合在 Header (`Authorization: Bearer <token>`) 帶入,或必須保留在 JSON Payload 中?
|
||||
|
||||
### 6.5 欄位細節 (Field Details)
|
||||
8. **URL {workid} 定義**:URL 中的 `{workid}` 應對應為 `serial_no` (機台序號) 還是資料庫自增 `id`?
|
||||
9. **M_Stus2 告警觸發**:20 多種頁面狀態碼中,哪些特定的錯誤代碼需要在管理後台觸發即時紅字警示?
|
||||
目前已根據 2026-03-12 的討論完成所有關鍵決策。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
### 1.1 觸發流程
|
||||
1. **管理行為**:管理員在後台管理介面手動修改特定機台的貨道庫存數量。
|
||||
2. **指令預備**:後端系統記錄異動請求,並在該機台的 B010 (心跳) 回應中,將 `status` 設為 `49` (reload B017)。
|
||||
2. **指令預備**:後端系統將異動請求寫入 `remote_commands` 表 (type: `stock_update`, payload 含貨道編號與數量),並在該機台的 B010 (心跳) 回應中將 `status` 設為 `49` (reload B017)。
|
||||
3. **機台拉取**:機台收到心跳回應中的 `49` 代碼後,主動呼叫 B017 API (`/api/app/machine/reload_msg/{workid}`)。
|
||||
4. **數據同步**:機台獲取最新庫存數據並更新本地狀態。
|
||||
|
||||
@@ -43,11 +43,10 @@
|
||||
|
||||
## 4. 欄位定義與對照
|
||||
|
||||
| 欄位名稱 | 資料庫對應 | 說明 |
|
||||
| B017 欄位 | 資料庫對應 | 說明 |
|
||||
|----------|------------|------|
|
||||
| `workid` | `machines.serial_no` | 機台識別序號 |
|
||||
| `channelid` | `machine_slots.slot_no` | 貨道編號 |
|
||||
| `stock` | `machine_slots.stock` | 該貨道的當前可用庫存 |
|
||||
| `stock` | `machine_slots.stock` | 該貨道的最新庫存數量 |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,9 +56,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 6. 待確認事項 (To-be-confirmed)
|
||||
## 6. 庫存衝突處理原則
|
||||
|
||||
1. **庫存衝突優先權**:當「雲端改庫存」與「現場實體銷售」同時發生時,最終庫存應以何者為準?(待主管確認)。
|
||||
2. **API 欄位對照**:
|
||||
* `workid` 是否嚴格對應 `serial_no`?
|
||||
* `channelid` 是否對標 `slot_no`?
|
||||
**以機台實際出貨為最終依據(最終一致性策略)**。
|
||||
|
||||
* **情境**:雲端下發庫存更新(如設為 50),同時機台現場發生實體銷售(賣出 2 個)。
|
||||
* **處理原則**:機台在本地執行 `50 - 已出貨量 = 48`,並透過後續的 B602 出貨紀錄上報給雲端,雲端以此為最終庫存數字。
|
||||
* **理由**:機台掌握實際出貨資訊,強制以雲端覆蓋將導致帳面庫存與實際庫存不符,引發空貨道但系統顯示有庫存的嚴重問題。
|
||||
|
||||
---
|
||||
|
||||
## 7. 待確認事項 (已結案)
|
||||
|
||||
所有關鍵決策已於 2026-03-12 完成確認。
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## 1. 業務流程與通訊機制
|
||||
|
||||
### 1.1 指令下發流程
|
||||
1. **指令生成**:Cloud 後端(管理者或系統觸發)在 `remote_dispense_commands` 表中建立一筆狀態為 `0` (待執行) 的指令。
|
||||
1. **指令生成**:Cloud 後端(管理者)在 `remote_commands` 表中建立一筆類型為 `dispense`、狀態為 `pending` 的指令。
|
||||
2. **心跳通知**:在該機台的 B010 回應中,將 `status` 設為 `85` (reload B055)。
|
||||
3. **機台撈取 (POST)**:機台呼叫 B055 POST API (`/api/app/machine/dispense/{workid}`) 取得詳細出貨參數(貨道 ID、指令 ID)。
|
||||
4. **執行與回報 (PUT)**:機台嘗試出貨後,呼叫 B055 PUT API 回報出貨結果及剩餘庫存。
|
||||
@@ -17,14 +17,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 2. 資料庫結構:`remote_dispense_commands`
|
||||
## 2. 指令資料定義:使用全域 `remote_commands`
|
||||
|
||||
| 欄位 | 說明 | 備註 |
|
||||
|------|------|------|
|
||||
| `command_id` | 執行命令 ID | 用於追蹤單次指令 |
|
||||
| `slot_no` | 貨道 ID | 指派機台出貨的通道 |
|
||||
| `status` | 執行狀態 | 0:待執行, 1:成功, 2:失敗 |
|
||||
| `remaining_stock` | 剩餘庫存 | 由機台回報 |
|
||||
遠端出貨指令統一存放在 `remote_commands` 表中,B055 的特定參數將存放於 `payload` (JSON) 欄位。
|
||||
|
||||
| 欄位 | B055 對應說明 | 備註 |
|
||||
|------|--------------|------|
|
||||
| `id` | 執行命令 ID | 用於追蹤單次指令 (POST 回傳) |
|
||||
| `payload->slot_no` | 貨道 ID | 指派機台出貨的通道 |
|
||||
| `status` | 執行狀態 | pending/sent/success/failed |
|
||||
| `ttl` | 指令效期 | 預設 60 秒 |
|
||||
|
||||
---
|
||||
|
||||
@@ -35,21 +37,31 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 待確認事項 (To-be-confirmed)
|
||||
## 4. 欄位定義與對照
|
||||
|
||||
### 4.1 業務情境 (Scenario)
|
||||
1. **指令發起來源**:確認指令是由後台管理員手動測試發起,還是串接外部線上購買系統產出的取貨指令?
|
||||
| B055 欄位 | 資料庫對應 | 說明 |
|
||||
|----------|------------|------|
|
||||
| `command_id` | `remote_dispense_commands.id` | 指令唯一識別碼 |
|
||||
| `slot_no` | `machine_slots.slot_no` | 目標貨道編號 |
|
||||
|
||||
### 4.2 失敗處理機制 (Failure Handling)
|
||||
2. **自動退款/告警**:若機台 PUT 回報出貨失敗,雲端是否需要自動發起退款(針對線上訂單)或發送維修告警通知?
|
||||
---
|
||||
|
||||
### 4.3 指令效期與防重 (TTL & Idempotency)
|
||||
3. **指令效期**:如果指令發出後機台失聯,該指令應在多久後失效 (TTL)?
|
||||
4. **防重複機制**:如何嚴格確保同一個 `command_id` 不會被重複執行兩次?
|
||||
## 5. 業務邏輯細節建議
|
||||
|
||||
### 4.4 安全性與權限 (Security)
|
||||
5. **指令簽名**:除了 `api_token`,發送出貨指令是否需要額外的加密簽名驗證以防偽造?
|
||||
6. **權限限制**:是否限制僅特定高級管理員可發起此類高風險指令?
|
||||
### 5.1 指令效期 (TTL)
|
||||
* **建議設定**:**60 秒 (1 分鐘)**。
|
||||
* **理由**:由於遠端出貨指令由管理員手動發起(通常用於現場故障排除或客訴處理),若機台因網路問題超過一分鐘未拉取指令,該次操作的情境通常已失效,應自動失效以防止後續無預警出貨。
|
||||
|
||||
### 4.5 欄位定義
|
||||
7. **URL {workid}**:確認是否對標 `serial_no`。
|
||||
### 5.2 失敗處理
|
||||
* **機制**:機台回報出貨失敗時,系統僅需記錄結果並在後台顯示異常狀態。
|
||||
* **處理原則**:考量目前僅供管理員手動發起,**不需執行自動退款**,由管理員依據日誌進行後續手動處理即可。
|
||||
|
||||
### 5.3 安全與權限
|
||||
* **驗證機制**:維持 `api_token` 驗證,不需額外的加密簽名。
|
||||
* **權限控制**:建議在 RBAC 權限系統中,限制僅「高級管理員」或「維修主管」具備發起遠端出貨的權限。
|
||||
|
||||
---
|
||||
|
||||
## 6. 待確認事項 (已結案)
|
||||
|
||||
所有關鍵決策已於 2026-03-12 完成確認。
|
||||
|
||||
@@ -19,14 +19,27 @@
|
||||
|
||||
## 2. 執行面優化與告警 (Execution & Alerts)
|
||||
|
||||
### 2.1 高頻數據處理 (待確認頻率)
|
||||
### 2.1 數據處理頻率
|
||||
* **回報時機**:機台在**每次交易完成後**(包含消費者找零或管理員補幣)即時呼叫 B220 API 回報最新庫存。
|
||||
* **快取策略**:機台各面額的最新總量應快取於 Redis 或更新至 `machines` 表的擴充欄位,供後台儀表板即時監看。
|
||||
* **寫入過濾**:為節省儲存空間,建議僅在資料數值有實際變動時才寫入 `coin_inventory_logs`。
|
||||
* **寫入過濾**:為節省儲存空間,僅在資料數值有實際變動時才寫入 `coin_inventory_logs`。
|
||||
|
||||
### 2.2 找零不足告警 (Low Inventory Warning)
|
||||
* **機制**:當特定面額(如 5 元、10 元硬幣)低於預設的水位閾值時,系統應:
|
||||
* 在管理後台儀表板顯示紅字告警。
|
||||
* (選配)發送推播或系統通知給補貨人員。
|
||||
|
||||
當特定面額低於預設的水位閾值時,系統將觸發告警。
|
||||
|
||||
#### **建議水位告警計畫 (Threshold Plan)**
|
||||
| 面額 | 告警閾值 (枚數) | 說明 |
|
||||
|------|----------------|------|
|
||||
| 1 元 | < 50 | 找零頻率高,需預留較快補幣空間 |
|
||||
| 5 元 | < 40 | |
|
||||
| 10 元| < 40 | 主要找零面額,需重點監控 |
|
||||
| 50 元| < 20 | |
|
||||
| 100 元以上 | — | 通常不作為找零面額,僅供庫存監控 |
|
||||
|
||||
* **通知管道**:
|
||||
1. **後台儀表板**:在機台管理介面顯示顯眼的「紅字告警」。
|
||||
2. **系統推播**:即時發送通知給相關營運或補貨人員。
|
||||
|
||||
---
|
||||
|
||||
@@ -41,11 +54,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 待確認事項 (To-be-confirmed)
|
||||
## 4. 待確認事項 (已結案)
|
||||
|
||||
### 4.1 系統效能與頻率
|
||||
1. **回報頻率**:確認機台呼叫 B220 的頻率(是每次交易後即時回報,還是定時回報?)。
|
||||
所有關鍵決策已於 2026-03-12 完成確認並由系統規劃水位計畫。
|
||||
|
||||
### 4.2 告警配置
|
||||
2. **水位閾值**:各面額的最低告警水位數值(如 5 元低於多少枚觸發告警)。
|
||||
3. **通知方式**:觸發告警時的具體通知管道。
|
||||
|
||||
@@ -43,10 +43,20 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 待確認事項 (To-be-confirmed)
|
||||
## 4. 數據完整性與異常處理 (Integrity & Exceptions)
|
||||
|
||||
### 4.1 數據完整性稽核
|
||||
1. **自動對帳**:是否需要定期自動比對「B600 收取金額」與「B602 實際出貨金額」,並將差異標記為異常訂單?
|
||||
### 4.1 自動對帳機制 (Auto-Reconciliation)
|
||||
* **機制**:系統後台需建立定期定時任務(Scheduled Task),比對同一 `flow_id` 下的以下數據:
|
||||
- **B600 (收取金額)**:訂單總計金額。
|
||||
- **B602 (出貨總價值)**:機台實際出貨商品之總價值。
|
||||
* **異常標記**:若兩者金額不符(例如:付了 $50 但僅出貨 $0 或 $30),系統應自動將該訂單標記為 **「金額異常」**,並同步產出警示通知供營運人員手動稽核。
|
||||
|
||||
### 4.2 失敗補償邏輯
|
||||
2. **出貨失敗處理**:若 B602 回報「出貨失敗」,系統是否需觸發自動退款程序(限線上支付)或發送補發通知?
|
||||
### 4.2 出貨失敗處理 (Dispense Failure)
|
||||
* **原則**:若 B602 回報「出貨失敗」,系統**不觸發自動退款**程序。
|
||||
* **處理流程**:系統僅記錄失敗狀態並維持訂單為「待處理/異常」,後續由客服或管理員介入,根據實際情況進行人工退款、補發指令或現場處理。
|
||||
|
||||
---
|
||||
|
||||
## 5. 待確認事項 (已結案)
|
||||
|
||||
所有關鍵決策已於 2026-03-12 完成確認。
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 待確認事項 (To-be-confirmed)
|
||||
## 4. 待確認事項 (已結案)
|
||||
|
||||
### 4.1 邏輯關聯與超時
|
||||
1. **發票缺失標記**:若收到 B600 (金流) 但在預設時間內(如 5 分鐘)未收到 B601,後台是否應將該訂單標記為「發票缺失」以供巡檢。
|
||||
2. **重試機制**:機台端在 B601 呼叫失敗時的緩存與重傳策略。
|
||||
所有關於發票關聯邏輯與重試機制的技術決策已於 2026-03-12 完成確認:
|
||||
1. **發票缺失標記**:系統不需針對 B600 與 B601 的到達時差進行「發票缺失」自動標記。
|
||||
2. **重試機制**:機台端呼叫失敗時不需額外的緩存與重傳策略(維持基本回報邏輯)。
|
||||
|
||||
@@ -37,14 +37,21 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. 待確認事項 (To-be-confirmed)
|
||||
## 4. 數據完整性與異常處理 (Integrity & Exceptions)
|
||||
|
||||
### 4.1 訂單完結判斷
|
||||
1. **超時標記**:若雲端收到 B600 但遲遲未收到 B602,預設在多久後應將訂單標記為「出貨超時」?
|
||||
2. **狀態遷移**:出貨失敗 (`status=1`) 時,訂單的最終顯示文字與處理流程。
|
||||
### 4.1 出貨超時監控 (Dispense Timeout)
|
||||
* **閾值建議**:若系統收到 B600 (金流) 超過 **5 分鐘** 仍未收到任何對應之 B602 (出貨回報),雲端後台應自動將該訂單標記為 **「出貨超時」**。
|
||||
* **用途**:此狀態主要用於輔助對帳,標記可能發生網路中斷或機台異常卡死之交易,需由管理員介入核實。
|
||||
|
||||
### 4.2 營收與點數處理
|
||||
3. **出貨失敗補償**:若 B602 回報失敗:
|
||||
* 行動支付是否自動觸發退款?
|
||||
* 是否自動退回該次交易所扣除之會員點數與優惠券?
|
||||
4. **現金交易失敗**:出貨失敗且為現金交易時,是否應產出待維護工單或通知補貨員。
|
||||
### 4.2 出貨失敗處理 (Dispense Failure)
|
||||
* **狀態遷移**:當 B602 回報 `dispense_status = 1` (失敗) 時,訂單狀態應更新為 **「出貨失敗」**。
|
||||
* **即時通知**:系統應立即產出 **「後台警示通知」** 並推播給營運人員,以便快速處理現場排礙或後續補償。
|
||||
* **補償與維護策略 (現階段)**:
|
||||
- **點數/優惠券**:目前不採自動退回機制,維持異常狀態由管理員人工處理。
|
||||
- **營運維護**:目前暫不自動產出維護工單或推播給補貨員,僅紀錄於異常日誌並顯示於後台通知區。
|
||||
|
||||
---
|
||||
|
||||
## 5. 待確認事項 (已結案)
|
||||
|
||||
所有關於出貨流程與超時判定之技術決策已於 2026-03-12 完成確認。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# B650 API (會員/點數/取貨碼驗證) 技術規範與執行指南
|
||||
# [暫緩執行] B650 API (會員/點數/取貨碼驗證) 技術規範與執行指南
|
||||
|
||||
本文件定義 B650 API 的技術實作規範,用於處理會員驗證、點數折抵、優惠券兌換、取貨碼驗證以及線上訂單線下結帳等多元功能。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# B710 API (計時器狀態回傳) 技術規範與執行指南
|
||||
# [暫緩執行] B710 API (計時器狀態回傳) 技術規範與執行指南
|
||||
|
||||
本文件定義 B710 API 的技術實作規範,用於處理計時型設備(如洗衣服務、按摩服務)的剩餘時間監測與狀態回報。
|
||||
|
||||
|
||||
250
docs/development_schedule.md
Normal file
250
docs/development_schedule.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Star Cloud 開發時程規劃書
|
||||
|
||||
> [!NOTE]
|
||||
> 本文件由 `architecture_plan.md` 分離,專門記錄專案的開發週期、階段目標與詳細子選單實作時程。
|
||||
> **總結報告**:本專案預計總開發時程為 **17 ~ 18 週**。以 1 人天 = 8 小時純開發時間計算,預估總投入約 **85 個工作天**。
|
||||
|
||||
---
|
||||
|
||||
## 一、專案開發甘特圖 (自動排除週末)
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Star Cloud 實作時程規劃
|
||||
dateFormat YYYY-MM-DD
|
||||
excludes weekends
|
||||
|
||||
section Phase 1 基礎建設
|
||||
資料表 + IoT API + 異步管線 :active, p1, 2026-03-16, 5d
|
||||
|
||||
section Phase 2 核心營運
|
||||
後台核心營運頁面整合 :p2, after p1, 50d
|
||||
|
||||
section Phase 3 進階模組
|
||||
進階分析與垂直模組 :p3, after p2, 30d
|
||||
```
|
||||
|
||||
## 二、詳細時程對照表
|
||||
|
||||
| 階段 (Phase) | 關鍵任務摘要 | 預估天數 | 預計工作日期 | 狀態 |
|
||||
| :--- | :--- | :---: | :---: | :---: |
|
||||
| **Phase 1** | 28 張資料表 Migration + B010~B710 核心 API + Redis 異步 Job | **5 工作天** | 03/16 ~ 03/20 | 進行中 |
|
||||
| **Phase 2** | **後台核心營運頁面** (帳號權限、資料設定、機台、銷售、遠端、倉庫、儀表板) | **50 工作天** | 03/23 ~ 05/29 | 規劃中 |
|
||||
| **Phase 3** | **進階垂直模組** (分析、稽核、會員、APP、Line、預約、特殊權限) | **30 工作天** | 06/01 ~ 07/10 | 規劃中 |
|
||||
|
||||
---
|
||||
|
||||
## 三、後台子菜單開發細節 (Module & Sub-menu Breakdown)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 開發順序依**功能相依性**排列:先建帳號與權限基礎 → 再建商品等主檔資料 → 然後是依賴主檔的機台與銷售 → 接著是營運急需的遠端管理與倉庫管理 → 最後是匯總數據的儀表板。Phase 3 則從分析報表開始,逐步擴展至行銷與第三方聯動。
|
||||
|
||||
### ⚡ Phase 1:基礎建設 (03/16 ~ 03/20)
|
||||
|
||||
| 任務類別 | 內容 | 日期 |
|
||||
| :--- | :--- | :---: |
|
||||
| 資料庫 Migration | 28 張資料表建立、Seeder、多租戶欄位擴充 | 03/16 - 03/17 |
|
||||
| IoT API 端點 | B010 心跳、B017 庫存、B055 出貨、B600/B601/B602 金流 | 03/18 - 03/19 |
|
||||
| 異步管線 | Redis Queue Job + Service 層、B650 會員驗證 | 03/20 |
|
||||
|
||||
### 🏛️ Phase 2:核心營運子選單 (03/23 ~ 05/29)
|
||||
共 51 項子選單,依功能相依性分為七個開發階段。
|
||||
|
||||
#### 📌 A. 帳號與權限基礎 (03/23 ~ 04/03)
|
||||
> 為何優先:帳號、角色、權限是所有後台模組的存取控管基礎,必須最先到位。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 1 | **資料設定** | 帳號管理 | 03/23 - 03/24 | 租戶主帳號 CRUD,為所有管理功能打底 |
|
||||
| 2 | | 子帳號 | 03/25 | 租戶內部員工帳號新增與停用 |
|
||||
| 3 | | 子帳號角色 | 03/26 | 租戶內部角色定義與權限範圍 |
|
||||
| 4 | **個人設定** | 個人檔案 | 03/27 | 登入使用者個人資訊修改(已有基礎) |
|
||||
| 5 | **權限設定** | 角色設定 | 03/30 - 03/31 | `spatie` RBAC 角色定義與分配核心 UI |
|
||||
| 6 | | APP功能 | 04/01 | APP 模組的功能權限開關 |
|
||||
| 7 | | 資料設定 | 04/01 | 資料設定模組的權限開關 |
|
||||
| 8 | | 銷售管理 | 04/01 | 銷售管理模組的權限開關 |
|
||||
| 9 | | 機台管理 | 04/01 | 機台管理模組的權限開關 |
|
||||
| 10 | | 倉庫管理 | 04/02 | 倉庫管理模組的權限開關 |
|
||||
| 11 | | 分析管理 | 04/02 | 分析管理模組的權限開關 |
|
||||
| 12 | | 稽核管理 | 04/02 | 稽核管理模組的權限開關 |
|
||||
| 13 | | 遠端管理 | 04/02 | 遠端管理模組的權限開關 |
|
||||
| 14 | | Line管理 | 04/03 | Line 管理模組的權限開關 |
|
||||
| 15 | | 其他功能 | 04/03 | 其餘未分類功能之權限控管 |
|
||||
| 16 | | AI智能預測 | 04/03 | AI 預測功能的存取權限設定 |
|
||||
|
||||
#### 📌 B. 基礎資料主檔 (04/06 ~ 04/10)
|
||||
> 為何第二:商品主檔是機台貨道、倉庫、銷售等後續模組的共同基礎資料。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 17 | **資料設定** | 商品管理 | 04/06 - 04/08 | 商品主檔 CRUD、條碼、售價、圖片上傳 |
|
||||
| 18 | | 廣告管理 | 04/09 | 機台螢幕廣告素材上傳與排程 |
|
||||
| 19 | | 管理者可賣 | 04/09 | 管理者可直接販售之商品清單設定 |
|
||||
| 20 | | 點數設定 | 04/10 | 點數發放規則與兌換比例設定 |
|
||||
| 21 | | 識別證 | 04/10 | 員工/維修人員識別證管理 |
|
||||
|
||||
#### 📌 C. 機台管理 (04/13 ~ 04/23)
|
||||
> 為何第三:機台是核心營運實體,須在商品主檔建好後才能綁定貨道。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 22 | **機台管理** | 機台列表 | 04/13 - 04/15 | 設備清單、在線狀態、基礎參數設定(核心) |
|
||||
| 23 | | 機台日誌 | 04/16 - 04/17 | B010 心跳日誌查詢、異常事件追蹤 |
|
||||
| 24 | | 機台權限 | 04/20 | 機台指派給租戶/帳號的控管介面 |
|
||||
| 25 | | 機台稼動率 | 04/21 | 設備運作效率統計與視覺化 |
|
||||
| 26 | | 效期管理 | 04/22 | 各貨道商品到期日監控與預警 |
|
||||
| 27 | | 維修管理單 | 04/23 | 機台報修工單建立、追蹤與結案流程 |
|
||||
|
||||
#### 📌 D. 銷售管理 (04/24 ~ 04/29)
|
||||
> 為何第四:交易數據依賴機台與商品,兩者就緒後再開發銷售查詢。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 28 | **銷售管理** | 銷售紀錄 | 04/24 - 04/27 | 交易明細查詢、金流狀態回溯 |
|
||||
| 29 | | 取貨碼 | 04/28 | 取貨碼生成與核銷管理 |
|
||||
| 30 | | 購買單 | 04/28 | 訂單管理與出貨追蹤 |
|
||||
| 31 | | 促銷時段 | 04/29 | 時段折扣規則設定與排程管理 |
|
||||
| 32 | | 通行碼 | 04/29 | 通行碼發放與使用紀錄 |
|
||||
| 33 | | 來店禮 | 04/29 | 到店即贈的禮品活動設定 |
|
||||
|
||||
#### 📌 E. 遠端管理 (04/30 ~ 05/11)
|
||||
> 為何第五:營運最迫切需要的即時控制能力,直接串接 Phase 1 的 B010/B055 API。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 34 | **遠端管理** | 機台庫存 | 04/30 - 05/01 | 遠端查閱/修改機台貨道庫存 (B017) |
|
||||
| 35 | | 機台重啟 | 05/04 | 遠端重啟機台指令下發 |
|
||||
| 36 | | 卡機重啟 | 05/04 | 遠端重啟讀卡機指令 |
|
||||
| 37 | | 遠端結帳 | 05/05 | 遠端觸發機台結帳清算 |
|
||||
| 38 | | 遠端鎖定 | 05/06 | 遠端鎖定/解鎖機台操作 |
|
||||
| 39 | | 遠端找零 | 05/07 | 遠端觸發找零機退幣 |
|
||||
| 40 | | 遠端出貨 | 05/08 - 05/11 | B055 遠端出貨指令下發與結果追蹤 |
|
||||
|
||||
#### 📌 F. 倉庫管理 (05/12 ~ 05/27)
|
||||
> 為何第六:供應鏈管理依賴商品主檔與機台數據,且補貨流程為日常營運核心。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 41 | **倉庫管理** | 倉庫列表(全) | 05/12 | 全公司倉庫總覽與基礎資料管理 |
|
||||
| 42 | | 倉庫列表(個) | 05/13 | 個人/分區倉庫檢視 |
|
||||
| 43 | | 庫存管理單 | 05/14 - 05/15 | 庫存異動單建立與審核流程 |
|
||||
| 44 | | 調撥單 | 05/18 - 05/19 | 跨倉庫商品調撥申請與執行 |
|
||||
| 45 | | 採購單 | 05/20 - 05/21 | 採購單建立、審批與到貨入庫 |
|
||||
| 46 | | 機台補貨單 | 05/22 - 05/25 | 機台補貨任務派發與執行 |
|
||||
| 47 | | 機台補貨紀錄 | 05/26 | 歷史補貨紀錄查詢與統計 |
|
||||
| 48 | | 機台庫存 | 05/26 | 各機台即時庫存總覽 (B017 資料) |
|
||||
| 49 | | 人員庫存 | 05/27 | 補貨人員攜帶庫存管理 |
|
||||
| 50 | | 回庫單 | 05/27 | 退回倉庫的商品登記與核銷 |
|
||||
|
||||
#### 📌 G. 儀表板 (05/28 ~ 05/29)
|
||||
> 為何最後:儀表板匯總機台、銷售、遠端指令、倉庫等全部數據,必須等上游模組完成才有意義。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 51 | **儀表板** | 儀表板 | 05/28 - 05/29 | 即時營收、在線機台數、今日訂單、庫存水位看板 |
|
||||
|
||||
### 🚀 Phase 3:進階分析與垂直模組子選單 (06/01 ~ 07/10)
|
||||
共 33 項子選單,依功能相依性分為七個開發階段。
|
||||
|
||||
#### 📌 A. 分析管理 (06/01 ~ 06/10)
|
||||
> 為何優先:报表分析依賴 Phase 2 已累積的銷售、機台與倉庫運管數據。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 52 | **分析管理** | 零錢庫存 | 06/01 - 06/02 | B220 零錢機各面額庫存呈現與趨勢 |
|
||||
| 53 | | 機台報表 | 06/03 - 06/05 | 機台營收、稼動率、故障率綜合報表 |
|
||||
| 54 | | 商品報表 | 06/08 - 06/09 | 熱銷排行、區域分析、坪效矩陣 |
|
||||
| 55 | | 問卷分析 | 06/10 | 問卷結果統計圖表與資料匯出 |
|
||||
|
||||
#### 📌 B. 稽核管理 (06/11 ~ 06/17)
|
||||
> 為何第二:稽核對帳需要倉庫(採購/調撥/補貨)數據作為比對來源。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 56 | **稽核管理** | 採購單 | 06/11 - 06/12 | 採購單據稽核、金額核對 |
|
||||
| 57 | | 調撥單 | 06/15 | 調撥單據稽核、數量差異追蹤 |
|
||||
| 58 | | 補貨單 | 06/16 - 06/17 | 補貨前後庫存比對、異常標記 |
|
||||
|
||||
#### 📌 C. 會員管理 (06/18 ~ 06/26)
|
||||
> 為何第三:會員模組相對獨立,但其錢包/點數系統需在行銷模組(Line、APP)之前就位。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 59 | **會員管理** | 會員列表 | 06/18 - 06/19 | 會員資料 CRUD、錢包/點數明細 |
|
||||
| 60 | | 會員等級 | 06/22 | 等級定義與升降級條件設定 |
|
||||
| 61 | | 儲值回饋 | 06/23 | 儲值金額對應加碼回饋規則 |
|
||||
| 62 | | 點數規則 | 06/24 | 消費累點比例與到期規則設定 |
|
||||
| 63 | | 禮品設定 | 06/25 - 06/26 | 點數兌換禮品項目與庫存管理 |
|
||||
|
||||
#### 📌 D. APP 管理 (06/29 ~ 07/01)
|
||||
> 為何第四:APP 功能(問卷、遊戲)可複用會員數據;計時器依賴 B710 API。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 64 | **APP 管理** | UI元素 | 06/29 | APP Banner、主題色、首頁佈局設定 |
|
||||
| 65 | | 小幫手 | 06/29 | APP 內嵌引導助手內容管理 |
|
||||
| 66 | | 問卷 | 06/30 | 問卷題目建立與發布排程 |
|
||||
| 67 | | 互動遊戲 | 07/01 | APP 內互動遊戲設定與獎品規則 |
|
||||
| 68 | | 計時器 | 07/01 | B710 計時器狀態監控與設定介面 |
|
||||
|
||||
#### 📌 E. Line 管理 (07/02 ~ 07/06)
|
||||
> 為何第五:Line 綁定依賴會員系統,商品型錄依賴商品主檔。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 69 | **Line 管理** | Line會員 | 07/02 | Line 綁定會員清單與資料同步 |
|
||||
| 70 | | Line機台 | 07/02 | Line 關聯機台推播設定 |
|
||||
| 71 | | Line商品 | 07/03 | Line 商品型錄同步管理 |
|
||||
| 72 | | Line生活圈 | 07/03 | Line OA 官方帳號設定與群發 |
|
||||
| 73 | | Line訂單 | 07/06 | Line 渠道訂單查詢與追蹤 |
|
||||
| 74 | | Line優惠券 | 07/06 | Line 專屬優惠券發放與核銷 |
|
||||
|
||||
#### 📌 F. 預約系統 (07/07 ~ 07/09)
|
||||
> 為何第六:獨立子系統,為未來擴充計時型設備預約的基礎。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 75 | **預約系統** | 預約會員 | 07/07 | 預約系統會員資料管理 |
|
||||
| 76 | | 店家管理 | 07/07 | 店家資訊維護與營業時間設定 |
|
||||
| 77 | | 時段組合 | 07/08 | 可預約時段模板建立與排程 |
|
||||
| 78 | | 場地管理 | 07/08 | 場地/設備資源定義與狀態管理 |
|
||||
| 79 | | 優惠券 | 07/09 | 預約專用優惠券發放規則 |
|
||||
| 80 | | 預約管理 | 07/09 | 預約紀錄查詢、取消與改期操作 |
|
||||
| 81 | | 訂單管理 | 07/09 | 預約產生之訂單明細與付款追蹤 |
|
||||
|
||||
#### 📌 G. 特殊權限 (07/10)
|
||||
> 為何最後:高風險工程操作,須在所有系統穩定後才開放。
|
||||
|
||||
| # | 模組名稱 | 子菜單項目 | 日期 | 功能重點 |
|
||||
| :---: | :--- | :--- | :---: | :--- |
|
||||
| 82 | **特殊權限** | 庫存清空 | 07/10 | 工程級一鍵清空指定機台庫存 |
|
||||
| 83 | | APK版本 | 07/10 | 機台 APK 版本發布與 OTA 控管 |
|
||||
| 84 | | Discord通知 | 07/10 | Discord Webhook 告警通知設定 |
|
||||
|
||||
---
|
||||
|
||||
## 四、開發階段任務清單 (Phase Checklist)
|
||||
|
||||
### ⚡ Phase 1:基礎建設 + IoT API (5 工作天)
|
||||
- [ ] 擴充 `machines` 表(serial_no, model 等欄位)
|
||||
- [ ] 建立多租戶權限基礎表 (`companies`, `spatie/laravel-permission`)
|
||||
- [ ] 核心模型與關聯建立 (Products, Orders, Payments)
|
||||
- [ ] 實作 B010~B710 核心 IoT API 與 Redis Queue
|
||||
- [ ] 會員驗證 B650 串接
|
||||
|
||||
### 🏛️ Phase 2:後台核心營運頁面 (50 工作天)
|
||||
- [ ] 帳號/角色/權限設定核
|
||||
- [ ] 商品、廣告、點數主檔管理
|
||||
- [ ] 機台列表、日誌、維修單、效期監控
|
||||
- [ ] 銷售紀錄、促銷時段、通行碼管理
|
||||
- [ ] 遠端控制 (庫存、重啟、找零、出貨) 7 項
|
||||
- [ ] 倉庫供應鏈 (採購、調撥、補貨、回庫) 10 項
|
||||
- [ ] 核心數據儀表板
|
||||
|
||||
### 🚀 Phase 3:進階分析與垂直模組 (30 工作天)
|
||||
- [ ] 零錢機、機台、商品大數據分析報表
|
||||
- [ ] 供應鏈稽核對帳系統
|
||||
- [ ] 會員等級、儲值回饋、禮品兌換系統
|
||||
- [ ] APP/Line 行銷工具整合
|
||||
- [ ] 預約子系統 (場地、店家、訂單管理)
|
||||
- [ ] 特殊工程權限 (APK 下發, Discord 通知)
|
||||
20
lang/en/auth.php
Normal file
20
lang/en/auth.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'password' => 'The provided password is incorrect.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
|
||||
];
|
||||
19
lang/en/pagination.php
Normal file
19
lang/en/pagination.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
22
lang/en/passwords.php
Normal file
22
lang/en/passwords.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'Your password has been reset.',
|
||||
'sent' => 'We have emailed your password reset link.',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => "We can't find a user with that email address.",
|
||||
|
||||
];
|
||||
200
lang/en/validation.php
Normal file
200
lang/en/validation.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'The :attribute field must be accepted.',
|
||||
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
|
||||
'active_url' => 'The :attribute field must be a valid URL.',
|
||||
'after' => 'The :attribute field must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute field must only contain letters.',
|
||||
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
|
||||
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
|
||||
'any_of' => 'The :attribute field is invalid.',
|
||||
'array' => 'The :attribute field must be an array.',
|
||||
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
|
||||
'before' => 'The :attribute field must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'array' => 'The :attribute field must have between :min and :max items.',
|
||||
'file' => 'The :attribute field must be between :min and :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must be between :min and :max.',
|
||||
'string' => 'The :attribute field must be between :min and :max characters.',
|
||||
],
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'can' => 'The :attribute field contains an unauthorized value.',
|
||||
'confirmed' => 'The :attribute field confirmation does not match.',
|
||||
'contains' => 'The :attribute field is missing a required value.',
|
||||
'current_password' => 'The password is incorrect.',
|
||||
'date' => 'The :attribute field must be a valid date.',
|
||||
'date_equals' => 'The :attribute field must be a date equal to :date.',
|
||||
'date_format' => 'The :attribute field must match the format :format.',
|
||||
'decimal' => 'The :attribute field must have :decimal decimal places.',
|
||||
'declined' => 'The :attribute field must be declined.',
|
||||
'declined_if' => 'The :attribute field must be declined when :other is :value.',
|
||||
'different' => 'The :attribute field and :other must be different.',
|
||||
'digits' => 'The :attribute field must be :digits digits.',
|
||||
'digits_between' => 'The :attribute field must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute field has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
|
||||
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
|
||||
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'encoding' => 'The :attribute field must be encoded in :encoding.',
|
||||
'ends_with' => 'The :attribute field must end with one of the following: :values.',
|
||||
'enum' => 'The selected :attribute is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
|
||||
'file' => 'The :attribute field must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'array' => 'The :attribute field must have more than :value items.',
|
||||
'file' => 'The :attribute field must be greater than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than :value.',
|
||||
'string' => 'The :attribute field must be greater than :value characters.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => 'The :attribute field must have :value items or more.',
|
||||
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than or equal to :value.',
|
||||
'string' => 'The :attribute field must be greater than or equal to :value characters.',
|
||||
],
|
||||
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
|
||||
'image' => 'The :attribute field must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field must exist in :other.',
|
||||
'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
|
||||
'integer' => 'The :attribute field must be an integer.',
|
||||
'ip' => 'The :attribute field must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute field must be a valid JSON string.',
|
||||
'list' => 'The :attribute field must be a list.',
|
||||
'lowercase' => 'The :attribute field must be lowercase.',
|
||||
'lt' => [
|
||||
'array' => 'The :attribute field must have less than :value items.',
|
||||
'file' => 'The :attribute field must be less than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than :value.',
|
||||
'string' => 'The :attribute field must be less than :value characters.',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => 'The :attribute field must not have more than :value items.',
|
||||
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than or equal to :value.',
|
||||
'string' => 'The :attribute field must be less than or equal to :value characters.',
|
||||
],
|
||||
'mac_address' => 'The :attribute field must be a valid MAC address.',
|
||||
'max' => [
|
||||
'array' => 'The :attribute field must not have more than :max items.',
|
||||
'file' => 'The :attribute field must not be greater than :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must not be greater than :max.',
|
||||
'string' => 'The :attribute field must not be greater than :max characters.',
|
||||
],
|
||||
'max_digits' => 'The :attribute field must not have more than :max digits.',
|
||||
'mimes' => 'The :attribute field must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute field must be a file of type: :values.',
|
||||
'min' => [
|
||||
'array' => 'The :attribute field must have at least :min items.',
|
||||
'file' => 'The :attribute field must be at least :min kilobytes.',
|
||||
'numeric' => 'The :attribute field must be at least :min.',
|
||||
'string' => 'The :attribute field must be at least :min characters.',
|
||||
],
|
||||
'min_digits' => 'The :attribute field must have at least :min digits.',
|
||||
'missing' => 'The :attribute field must be missing.',
|
||||
'missing_if' => 'The :attribute field must be missing when :other is :value.',
|
||||
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
|
||||
'missing_with' => 'The :attribute field must be missing when :values is present.',
|
||||
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
|
||||
'multiple_of' => 'The :attribute field must be a multiple of :value.',
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'not_regex' => 'The :attribute field format is invalid.',
|
||||
'numeric' => 'The :attribute field must be a number.',
|
||||
'password' => [
|
||||
'letters' => 'The :attribute field must contain at least one letter.',
|
||||
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
|
||||
'numbers' => 'The :attribute field must contain at least one number.',
|
||||
'symbols' => 'The :attribute field must contain at least one symbol.',
|
||||
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
|
||||
],
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'present_if' => 'The :attribute field must be present when :other is :value.',
|
||||
'present_unless' => 'The :attribute field must be present unless :other is :value.',
|
||||
'present_with' => 'The :attribute field must be present when :values is present.',
|
||||
'present_with_all' => 'The :attribute field must be present when :values are present.',
|
||||
'prohibited' => 'The :attribute field is prohibited.',
|
||||
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
|
||||
'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
|
||||
'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
|
||||
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
|
||||
'prohibits' => 'The :attribute field prohibits :other from being present.',
|
||||
'regex' => 'The :attribute field format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
|
||||
'required_if_declined' => 'The :attribute field is required when :other is declined.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values are present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute field must match :other.',
|
||||
'size' => [
|
||||
'array' => 'The :attribute field must contain :size items.',
|
||||
'file' => 'The :attribute field must be :size kilobytes.',
|
||||
'numeric' => 'The :attribute field must be :size.',
|
||||
'string' => 'The :attribute field must be :size characters.',
|
||||
],
|
||||
'starts_with' => 'The :attribute field must start with one of the following: :values.',
|
||||
'string' => 'The :attribute field must be a string.',
|
||||
'timezone' => 'The :attribute field must be a valid timezone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'uppercase' => 'The :attribute field must be uppercase.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'ulid' => 'The :attribute field must be a valid ULID.',
|
||||
'uuid' => 'The :attribute field must be a valid UUID.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
150
lang/ja.json
Normal file
150
lang/ja.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"Account Settings": "アカウント設定",
|
||||
"Manage your profile information, security settings, and login history": "プロフィール情報、セキュリティ設定、ログイン履歴の管理",
|
||||
"Profile Information": "プロフィール情報",
|
||||
"Update your account's profile information and email address.": "アカウントの氏名、電話番号、メールアドレスを更新します。",
|
||||
"Update Password": "パスワードの更新",
|
||||
"Ensure your account is using a long, random password to stay secure.": "セキュリティを維持するため、アカウントには長くランダムなパスワードを使用してください。",
|
||||
"Delete Account": "アカウントの削除",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "アカウントが削除されると、そのすべてのリソースとデータが永久に削除されます。アカウントを削除する前に、保持したいデータや情報をダウンロードしてください。",
|
||||
"Are you sure you want to delete your account?": "本当にアカウントを削除してもよろしいですか?",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "アカウントが削除されると、すべての関連データが永久に削除されます。アカウントの永久削除を確定するため、パスワードを入力してください。",
|
||||
"Login History": "ログイン履歴",
|
||||
"Name": "氏名",
|
||||
"Phone": "電話番号",
|
||||
"Email": "メールアドレス",
|
||||
"Current Password": "現在のパスワード",
|
||||
"New Password": "新しいパスワード",
|
||||
"Confirm Password": "新しいパスワード(確認)",
|
||||
"Save": "変更を保存",
|
||||
"Saved.": "保存されました",
|
||||
"Update": "更新",
|
||||
"Cancel": "キャンセル",
|
||||
"Confirm": "確認",
|
||||
"Danger Zone: Delete Account": "危険区域:アカウントの削除",
|
||||
"Permanently Delete Account": "アカウントを永久に削除",
|
||||
"Password": "パスワード",
|
||||
"Enter your password to confirm": "確認のためパスワードを入力してください",
|
||||
|
||||
"Dashboard": "ダッシュボード",
|
||||
"Connectivity Status": "接続ステータス",
|
||||
"Real-time status monitoring": "リアルタイムステータス監視",
|
||||
"LIVE": "ライブ",
|
||||
"Online Machines": "オンライン機台",
|
||||
"Offline Machines": "オフライン機台",
|
||||
"Alerts Pending": "アラート待機中",
|
||||
"Total Connected": "接続数合計",
|
||||
"Monthly Transactions": "今月の取引",
|
||||
"Monthly cumulative revenue overview": "今月の累計収益概要",
|
||||
"Today's Transactions": "今日の取引",
|
||||
"Yesterday's Transactions": "昨日の取引",
|
||||
"Before Yesterday's Transactions": "一昨日の取引",
|
||||
"vs Yesterday": "前日比",
|
||||
"Machine Status List": "機台ステータスリスト",
|
||||
"Total items": "合計 :count 件",
|
||||
"Real-time monitoring across all machines": "全機台のリアルタイム監視",
|
||||
"Quick search...": "クイック検索...",
|
||||
"Machine Info": "機台情報",
|
||||
"Running Status": "運行ステータス",
|
||||
"Today Cumulative Sales": "当日累計売上",
|
||||
"Current Stock": "現在の在庫",
|
||||
"Last Communication": "最終通信",
|
||||
"Alert Summary": "アラート概要",
|
||||
"Online": "オンライン",
|
||||
"Offline": "オフライン",
|
||||
"Low Stock": "在庫少",
|
||||
"No alert summary": "アラートなし",
|
||||
"No data available": "データがありません",
|
||||
"Showing :from to :to of :total items": ":total 件中 :from から :to 件を表示",
|
||||
"Previous": "前へ",
|
||||
"Next": "次へ",
|
||||
"Profile Settings": "個人設定",
|
||||
"Profile": "プロフィール",
|
||||
"Member Management": "会員管理",
|
||||
"Member List": "会員リスト",
|
||||
"Membership Tiers": "会員ランク",
|
||||
"Deposit Bonus": "入金ボーナス",
|
||||
"Point Rules": "ポイントルール",
|
||||
"Gift Definitions": "ギフト設定",
|
||||
"Machine Management": "機台管理",
|
||||
"Machine Logs": "機台ログ",
|
||||
"Machine List": "機台リスト",
|
||||
"Machine Permissions": "機台権限",
|
||||
"Utilization Rate": "稼働率",
|
||||
"Expiry Management": "有効期限管理",
|
||||
"Maintenance Records": "メンテナンス記録",
|
||||
"APP Management": "APP管理",
|
||||
"UI Elements": "UI要素",
|
||||
"Helper": "ヘルパー",
|
||||
"Questionnaire": "アンケート",
|
||||
"Games": "ゲーム",
|
||||
"Timer": "タイマー",
|
||||
"Warehouse Management": "倉庫管理",
|
||||
"Warehouse List (All)": "倉庫リスト(全)",
|
||||
"Warehouse List (Individual)": "倉庫リスト(個)",
|
||||
"Stock Management": "在庫管理",
|
||||
"Transfers": "転送",
|
||||
"Purchases": "購入",
|
||||
"Replenishments": "補充",
|
||||
"Replenishment Records": "補充記録",
|
||||
"Machine Stock": "機台在庫",
|
||||
"Staff Stock": "スタッフ在庫",
|
||||
"Returns": "返品",
|
||||
"Sales Management": "販売管理",
|
||||
"Sales Records": "販売記録",
|
||||
"Pickup Codes": "受取コード",
|
||||
"Orders": "注文",
|
||||
"Promotions": "プロモーション",
|
||||
"Pass Codes": "パスクード",
|
||||
"Store Gifts": "来店特典",
|
||||
"Analysis Management": "分析管理",
|
||||
"Change Stock": "小銭在庫",
|
||||
"Machine Reports": "機台レポート",
|
||||
"Product Reports": "商品レポート",
|
||||
"Survey Analysis": "アンケート分析",
|
||||
"Audit Management": "監査管理",
|
||||
"Purchase Audit": "購入監査",
|
||||
"Transfer Audit": "転送監査",
|
||||
"Replenishment Audit": "補充監査",
|
||||
"Data Configuration": "データ設定",
|
||||
"Product Management": "商品管理",
|
||||
"Advertisement Management": "広告管理",
|
||||
"Admin Sellable Products": "管理者販売可能商品",
|
||||
"Account Management": "アカウント管理",
|
||||
"Sub Accounts": "サブアカウント",
|
||||
"Sub Account Roles": "サブアカウントロール",
|
||||
"Point Settings": "ポイント設定",
|
||||
"Badge Settings": "バッジ設定",
|
||||
"Remote Management": "リモート管理",
|
||||
"Machine Restart": "機台再起動",
|
||||
"Card Reader Restart": "カードリーダー再起動",
|
||||
"Remote Checkout": "リモート決済",
|
||||
"Remote Lock": "リモートロック",
|
||||
"Remote Change": "リモートお釣り",
|
||||
"Remote Dispense": "リモート出庫",
|
||||
"Line Management": "Line管理",
|
||||
"Line Members": "Line会員",
|
||||
"Line Machines": "Line機台",
|
||||
"Line Products": "Line商品",
|
||||
"Line Official Account": "Line公式アカウント",
|
||||
"Line Orders": "Line注文",
|
||||
"Line Coupons": "Lineクーポン",
|
||||
"Reservation System": "予約システム",
|
||||
"Reservation Members": "予約会員",
|
||||
"Store Management": "店舗管理",
|
||||
"Time Slots": "タイムスロット",
|
||||
"Venue Management": "会場管理",
|
||||
"Coupons": "クーポン",
|
||||
"Reservations": "予約",
|
||||
"Order Management": "注文管理",
|
||||
"Special Permission": "特別権限",
|
||||
"Clear Stock": "在庫クリア",
|
||||
"APK Versions": "APKバージョン",
|
||||
"Discord Notifications": "Discord通知",
|
||||
"Permission Settings": "権限設定",
|
||||
"APP Features": "APP機能",
|
||||
"Sales": "販売",
|
||||
"Others": "その他",
|
||||
"AI Prediction": "AI予測",
|
||||
"Roles": "ロール"
|
||||
}
|
||||
150
lang/zh_TW.json
Normal file
150
lang/zh_TW.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"Account Settings": "帳戶設定",
|
||||
"Manage your profile information, security settings, and login history": "管理您的個人資訊、安全設定與登入紀錄",
|
||||
"Profile Information": "個人基本資料",
|
||||
"Update your account's profile information and email address.": "更新您的帳號姓名、手機號碼與電子郵件地址。",
|
||||
"Update Password": "更改密碼",
|
||||
"Ensure your account is using a long, random password to stay secure.": "確保您的帳號使用了足夠強度的隨機密碼以維持安全。",
|
||||
"Delete Account": "刪除帳號",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "一旦您的帳號被刪除,其所有資源和數據將被永久刪除。在刪除帳號之前,請下載您希望保留的任何數據或資訊。",
|
||||
"Are you sure you want to delete your account?": "您確定要刪除您的帳號嗎?",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "帳號一旦刪除,所有關連數據將被永久移除。請輸入您的密碼以確認您希望永久刪除此帳號。",
|
||||
"Login History": "登入歷史",
|
||||
"Name": "姓名",
|
||||
"Phone": "手機號碼",
|
||||
"Email": "電子郵件",
|
||||
"Current Password": "當前密碼",
|
||||
"New Password": "新密碼",
|
||||
"Confirm Password": "確認新密碼",
|
||||
"Save": "儲存變更",
|
||||
"Saved.": "已儲存",
|
||||
"Update": "更新",
|
||||
"Cancel": "取消",
|
||||
"Confirm": "確認",
|
||||
"Danger Zone: Delete Account": "危險區域:刪除帳號",
|
||||
"Permanently Delete Account": "永久刪除帳號",
|
||||
"Password": "密碼",
|
||||
"Enter your password to confirm": "請輸入您的密碼以確認",
|
||||
|
||||
"Dashboard": "儀表板",
|
||||
"Connectivity Status": "連網狀態",
|
||||
"Real-time status monitoring": "即時運作狀態監控",
|
||||
"LIVE": "即時",
|
||||
"Online Machines": "在線機台",
|
||||
"Offline Machines": "離線機台",
|
||||
"Alerts Pending": "異常警報",
|
||||
"Total Connected": "連線中總數",
|
||||
"Monthly Transactions": "當月交易",
|
||||
"Monthly cumulative revenue overview": "本月累計營收概況",
|
||||
"Today's Transactions": "今日交易",
|
||||
"Yesterday's Transactions": "昨日交易",
|
||||
"Before Yesterday's Transactions": "前日交易",
|
||||
"vs Yesterday": "比昨日",
|
||||
"Machine Status List": "機台狀態列表",
|
||||
"Total items": "共 :count 筆",
|
||||
"Real-time monitoring across all machines": "全線機台即時監控",
|
||||
"Quick search...": "快速搜尋...",
|
||||
"Machine Info": "機台資訊",
|
||||
"Running Status": "運行狀態",
|
||||
"Today Cumulative Sales": "當日累積銷售",
|
||||
"Current Stock": "目前庫存",
|
||||
"Last Communication": "最後通訊",
|
||||
"Alert Summary": "警訊摘要",
|
||||
"Online": "在線",
|
||||
"Offline": "離線",
|
||||
"Low Stock": "庫存偏低",
|
||||
"No alert summary": "無異常摘要",
|
||||
"No data available": "目前尚無數據",
|
||||
"Showing :from to :to of :total items": "顯示第 :from 到 :to 筆,共 :total 筆",
|
||||
"Previous": "上一頁",
|
||||
"Next": "下一頁",
|
||||
"Profile Settings": "個人設定",
|
||||
"Profile": "個人檔案",
|
||||
"Member Management": "會員管理",
|
||||
"Member List": "會員列表",
|
||||
"Membership Tiers": "會員等級",
|
||||
"Deposit Bonus": "儲值回饋",
|
||||
"Point Rules": "點數規則",
|
||||
"Gift Definitions": "禮品設定",
|
||||
"Machine Management": "機台管理",
|
||||
"Machine Logs": "機台日誌",
|
||||
"Machine List": "機台列表",
|
||||
"Machine Permissions": "機台權限",
|
||||
"Utilization Rate": "機台稼動率",
|
||||
"Expiry Management": "效期管理",
|
||||
"Maintenance Records": "維修管理單",
|
||||
"APP Management": "APP管理",
|
||||
"UI Elements": "UI元素",
|
||||
"Helper": "小幫手",
|
||||
"Questionnaire": "問卷",
|
||||
"Games": "互動遊戲",
|
||||
"Timer": "計時器",
|
||||
"Warehouse Management": "倉庫管理",
|
||||
"Warehouse List (All)": "倉庫列表(全)",
|
||||
"Warehouse List (Individual)": "倉庫列表(個)",
|
||||
"Stock Management": "庫存管理單",
|
||||
"Transfers": "調撥單",
|
||||
"Purchases": "採購單",
|
||||
"Replenishments": "機台補貨單",
|
||||
"Replenishment Records": "機台補貨紀錄",
|
||||
"Machine Stock": "機台庫存",
|
||||
"Staff Stock": "人員庫存",
|
||||
"Returns": "回庫單",
|
||||
"Sales Management": "銷售管理",
|
||||
"Sales Records": "銷售紀錄",
|
||||
"Pickup Codes": "取貨碼",
|
||||
"Orders": "購買單",
|
||||
"Promotions": "促銷時段",
|
||||
"Pass Codes": "通行碼",
|
||||
"Store Gifts": "來店禮",
|
||||
"Analysis Management": "分析管理",
|
||||
"Change Stock": "零錢庫存",
|
||||
"Machine Reports": "機台報表",
|
||||
"Product Reports": "商品報表",
|
||||
"Survey Analysis": "問卷分析",
|
||||
"Audit Management": "稽核管理",
|
||||
"Purchase Audit": "採購單",
|
||||
"Transfer Audit": "調撥單",
|
||||
"Replenishment Audit": "補貨單",
|
||||
"Data Configuration": "資料設定",
|
||||
"Product Management": "商品管理",
|
||||
"Advertisement Management": "廣告管理",
|
||||
"Admin Sellable Products": "管理者可賣",
|
||||
"Account Management": "帳號管理",
|
||||
"Sub Accounts": "子帳號",
|
||||
"Sub Account Roles": "子帳號角色",
|
||||
"Point Settings": "點數設定",
|
||||
"Badge Settings": "識別證",
|
||||
"Remote Management": "遠端管理",
|
||||
"Machine Restart": "機台重啟",
|
||||
"Card Reader Restart": "卡機重啟",
|
||||
"Remote Checkout": "遠端結帳",
|
||||
"Remote Lock": "遠端鎖定",
|
||||
"Remote Change": "遠端找零",
|
||||
"Remote Dispense": "遠端出貨",
|
||||
"Line Management": "Line管理",
|
||||
"Line Members": "Line會員",
|
||||
"Line Machines": "Line機台",
|
||||
"Line Products": "Line商品",
|
||||
"Line Official Account": "Line生活圈",
|
||||
"Line Orders": "Line訂單",
|
||||
"Line Coupons": "Line優惠券",
|
||||
"Reservation System": "預約系統",
|
||||
"Reservation Members": "預約會員",
|
||||
"Store Management": "店家管理",
|
||||
"Time Slots": "時段組合",
|
||||
"Venue Management": "場地管理",
|
||||
"Coupons": "優惠券",
|
||||
"Reservations": "預約管理",
|
||||
"Order Management": "訂單管理",
|
||||
"Special Permission": "特殊權限",
|
||||
"Clear Stock": "庫存清空",
|
||||
"APK Versions": "APK版本",
|
||||
"Discord Notifications": "Discord通知",
|
||||
"Permission Settings": "權限設定",
|
||||
"APP Features": "APP功能",
|
||||
"Sales": "銷售管理",
|
||||
"Others": "其他功能",
|
||||
"AI Prediction": "AI智能預測",
|
||||
"Roles": "角色設定"
|
||||
}
|
||||
20
lang/zh_TW/auth.php
Normal file
20
lang/zh_TW/auth.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'password' => 'The provided password is incorrect.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
|
||||
];
|
||||
19
lang/zh_TW/pagination.php
Normal file
19
lang/zh_TW/pagination.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
22
lang/zh_TW/passwords.php
Normal file
22
lang/zh_TW/passwords.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'Your password has been reset.',
|
||||
'sent' => 'We have emailed your password reset link.',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => "We can't find a user with that email address.",
|
||||
|
||||
];
|
||||
200
lang/zh_TW/validation.php
Normal file
200
lang/zh_TW/validation.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'The :attribute field must be accepted.',
|
||||
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
|
||||
'active_url' => 'The :attribute field must be a valid URL.',
|
||||
'after' => 'The :attribute field must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute field must only contain letters.',
|
||||
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
|
||||
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
|
||||
'any_of' => 'The :attribute field is invalid.',
|
||||
'array' => 'The :attribute field must be an array.',
|
||||
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
|
||||
'before' => 'The :attribute field must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'array' => 'The :attribute field must have between :min and :max items.',
|
||||
'file' => 'The :attribute field must be between :min and :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must be between :min and :max.',
|
||||
'string' => 'The :attribute field must be between :min and :max characters.',
|
||||
],
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'can' => 'The :attribute field contains an unauthorized value.',
|
||||
'confirmed' => 'The :attribute field confirmation does not match.',
|
||||
'contains' => 'The :attribute field is missing a required value.',
|
||||
'current_password' => 'The password is incorrect.',
|
||||
'date' => 'The :attribute field must be a valid date.',
|
||||
'date_equals' => 'The :attribute field must be a date equal to :date.',
|
||||
'date_format' => 'The :attribute field must match the format :format.',
|
||||
'decimal' => 'The :attribute field must have :decimal decimal places.',
|
||||
'declined' => 'The :attribute field must be declined.',
|
||||
'declined_if' => 'The :attribute field must be declined when :other is :value.',
|
||||
'different' => 'The :attribute field and :other must be different.',
|
||||
'digits' => 'The :attribute field must be :digits digits.',
|
||||
'digits_between' => 'The :attribute field must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute field has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
|
||||
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
|
||||
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'encoding' => 'The :attribute field must be encoded in :encoding.',
|
||||
'ends_with' => 'The :attribute field must end with one of the following: :values.',
|
||||
'enum' => 'The selected :attribute is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
|
||||
'file' => 'The :attribute field must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'array' => 'The :attribute field must have more than :value items.',
|
||||
'file' => 'The :attribute field must be greater than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than :value.',
|
||||
'string' => 'The :attribute field must be greater than :value characters.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => 'The :attribute field must have :value items or more.',
|
||||
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than or equal to :value.',
|
||||
'string' => 'The :attribute field must be greater than or equal to :value characters.',
|
||||
],
|
||||
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
|
||||
'image' => 'The :attribute field must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field must exist in :other.',
|
||||
'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
|
||||
'integer' => 'The :attribute field must be an integer.',
|
||||
'ip' => 'The :attribute field must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute field must be a valid JSON string.',
|
||||
'list' => 'The :attribute field must be a list.',
|
||||
'lowercase' => 'The :attribute field must be lowercase.',
|
||||
'lt' => [
|
||||
'array' => 'The :attribute field must have less than :value items.',
|
||||
'file' => 'The :attribute field must be less than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than :value.',
|
||||
'string' => 'The :attribute field must be less than :value characters.',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => 'The :attribute field must not have more than :value items.',
|
||||
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than or equal to :value.',
|
||||
'string' => 'The :attribute field must be less than or equal to :value characters.',
|
||||
],
|
||||
'mac_address' => 'The :attribute field must be a valid MAC address.',
|
||||
'max' => [
|
||||
'array' => 'The :attribute field must not have more than :max items.',
|
||||
'file' => 'The :attribute field must not be greater than :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must not be greater than :max.',
|
||||
'string' => 'The :attribute field must not be greater than :max characters.',
|
||||
],
|
||||
'max_digits' => 'The :attribute field must not have more than :max digits.',
|
||||
'mimes' => 'The :attribute field must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute field must be a file of type: :values.',
|
||||
'min' => [
|
||||
'array' => 'The :attribute field must have at least :min items.',
|
||||
'file' => 'The :attribute field must be at least :min kilobytes.',
|
||||
'numeric' => 'The :attribute field must be at least :min.',
|
||||
'string' => 'The :attribute field must be at least :min characters.',
|
||||
],
|
||||
'min_digits' => 'The :attribute field must have at least :min digits.',
|
||||
'missing' => 'The :attribute field must be missing.',
|
||||
'missing_if' => 'The :attribute field must be missing when :other is :value.',
|
||||
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
|
||||
'missing_with' => 'The :attribute field must be missing when :values is present.',
|
||||
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
|
||||
'multiple_of' => 'The :attribute field must be a multiple of :value.',
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'not_regex' => 'The :attribute field format is invalid.',
|
||||
'numeric' => 'The :attribute field must be a number.',
|
||||
'password' => [
|
||||
'letters' => 'The :attribute field must contain at least one letter.',
|
||||
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
|
||||
'numbers' => 'The :attribute field must contain at least one number.',
|
||||
'symbols' => 'The :attribute field must contain at least one symbol.',
|
||||
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
|
||||
],
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'present_if' => 'The :attribute field must be present when :other is :value.',
|
||||
'present_unless' => 'The :attribute field must be present unless :other is :value.',
|
||||
'present_with' => 'The :attribute field must be present when :values is present.',
|
||||
'present_with_all' => 'The :attribute field must be present when :values are present.',
|
||||
'prohibited' => 'The :attribute field is prohibited.',
|
||||
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
|
||||
'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
|
||||
'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
|
||||
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
|
||||
'prohibits' => 'The :attribute field prohibits :other from being present.',
|
||||
'regex' => 'The :attribute field format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
|
||||
'required_if_declined' => 'The :attribute field is required when :other is declined.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values are present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute field must match :other.',
|
||||
'size' => [
|
||||
'array' => 'The :attribute field must contain :size items.',
|
||||
'file' => 'The :attribute field must be :size kilobytes.',
|
||||
'numeric' => 'The :attribute field must be :size.',
|
||||
'string' => 'The :attribute field must be :size characters.',
|
||||
],
|
||||
'starts_with' => 'The :attribute field must start with one of the following: :values.',
|
||||
'string' => 'The :attribute field must be a string.',
|
||||
'timezone' => 'The :attribute field must be a valid timezone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'uppercase' => 'The :attribute field must be uppercase.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'ulid' => 'The :attribute field must be a valid ULID.',
|
||||
'uuid' => 'The :attribute field must be a valid UUID.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"alpinejs": "^3.4.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"axios": "^1.6.4",
|
||||
"baseline-browser-mapping": "^2.10.0",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"preline": "^3.2.3",
|
||||
@@ -1064,13 +1065,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.30",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
|
||||
"integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"alpinejs": "^3.4.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"axios": "^1.6.4",
|
||||
"baseline-browser-mapping": "^2.10.0",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"postcss": "^8.4.31",
|
||||
"preline": "^3.2.3",
|
||||
|
||||
@@ -139,6 +139,22 @@
|
||||
@apply translate-y-0 scale-[0.98];
|
||||
}
|
||||
|
||||
.btn-luxury-rose {
|
||||
@apply inline-flex items-center justify-center gap-x-2 px-4 py-2.5 text-sm font-semibold rounded-xl text-white transition-all duration-300;
|
||||
background: linear-gradient(135deg, #f43f5e, #e11d48);
|
||||
box-shadow: 0 4px 12px -2px rgba(225, 29, 72, 0.3), 0 0 0 1px rgba(225, 29, 72, 0.1);
|
||||
}
|
||||
|
||||
.btn-luxury-rose:hover {
|
||||
@apply -translate-y-0.5;
|
||||
box-shadow: 0 10px 25px -5px rgba(225, 29, 72, 0.5), 0 0 15px rgba(225, 29, 72, 0.2);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-luxury-rose:active {
|
||||
@apply translate-y-0 scale-[0.97];
|
||||
}
|
||||
|
||||
.btn-luxury-secondary {
|
||||
@apply inline-flex items-center justify-center gap-x-2 px-3 py-2 text-sm font-semibold rounded-xl transition-all duration-200;
|
||||
@apply bg-white text-slate-700 border border-slate-200 shadow-sm;
|
||||
|
||||
@@ -1,130 +1,226 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="space-y-4 sm:space-y-6">
|
||||
<!-- Grid -->
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
<!-- Card -->
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
總營收 (餘額)
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-baseline gap-x-2">
|
||||
<h3 class="text-3xl font-bold text-slate-800 dark:text-white">${{ number_format($totalRevenue, 2) }}</h3>
|
||||
<span class="text-xs font-medium text-cyan-500 bg-cyan-500/10 px-1.5 py-0.5 rounded">實時</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in" style="animation-delay: 100ms">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
運作中機台
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h3 class="text-3xl font-bold text-slate-800 dark:text-white">{{ $activeMachines }} 台</h3>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
待處理告警
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h3 class="text-3xl font-bold text-rose-500">{{ $alertsPending }} 則訊號</h3>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div class="luxury-card rounded-2xl p-5 animate-luxury-in" style="animation-delay: 300ms">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
會員總數
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-baseline gap-x-2">
|
||||
<h3 class="text-3xl font-bold text-slate-800 dark:text-white">{{ number_format($memberCount) }}</h3>
|
||||
<span class="text-xs font-medium text-emerald-500">人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<!-- Top Section: Connectivity & Transactions -->
|
||||
<div class="grid lg:grid-cols-2 gap-6 items-stretch">
|
||||
<!-- Connectivity Card -->
|
||||
<div class="luxury-card rounded-2xl p-8 animate-luxury-in flex flex-col">
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-extrabold text-slate-800 dark:text-white font-display tracking-tight">{{ __('Connectivity Status') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-400 uppercase tracking-widest mt-1">{{ __('Real-time status monitoring') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-1.5 px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-500 border border-cyan-500/20">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-cyan-500"></span>
|
||||
</span>
|
||||
<span class="text-[10px] font-black uppercase tracking-wider">{{ __('LIVE') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-3 gap-6">
|
||||
<!-- Chart Column -->
|
||||
<div class="lg:col-span-2 luxury-card rounded-3xl p-6 animate-luxury-in" style="animation-delay: 400ms">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div class="flex-1 flex items-center">
|
||||
<!-- Left: Stats List -->
|
||||
<div class="flex-1 space-y-6">
|
||||
<div class="flex items-center justify-between pr-10">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="w-2 h-2 rounded-full bg-cyan-500 shadow-[0_0_10px_rgba(6,182,212,0.6)]"></div>
|
||||
<span class="text-xs font-black text-slate-400 dark:text-slate-400 uppercase tracking-widest">{{ __('Online Machines') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-black text-slate-900 dark:text-white">{{ $activeMachines }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pr-10">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="w-2 h-2 rounded-full bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.6)]"></div>
|
||||
<span class="text-xs font-black text-slate-400 dark:text-slate-400 uppercase tracking-widest">{{ __('Offline Machines') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-black text-rose-500">{{ $alertsPending }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pr-10">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="w-2 h-2 rounded-full bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.6)]"></div>
|
||||
<span class="text-xs font-black text-slate-400 dark:text-slate-400 uppercase tracking-widest">{{ __('Alerts Pending') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-black text-slate-900 dark:text-white">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="w-px h-32 bg-slate-100 dark:bg-slate-800 mx-2"></div>
|
||||
|
||||
<!-- Right: Big Total -->
|
||||
<div class="w-40 text-center">
|
||||
<p class="text-7xl font-black text-cyan-500 drop-shadow-[0_0_20px_rgba(6,182,212,0.3)] leading-none">{{ $activeMachines }}</p>
|
||||
<p class="text-[10px] font-black text-cyan-500/80 dark:text-cyan-400 uppercase tracking-[0.3em] mt-4">{{ __('Total Connected') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Card -->
|
||||
<div class="luxury-card rounded-2xl p-8 animate-luxury-in flex flex-col" style="animation-delay: 100ms">
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-extrabold text-slate-800 dark:text-white font-display tracking-tight">{{ __('Monthly Transactions') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-400 uppercase tracking-widest mt-1">{{ __('Monthly cumulative revenue overview') }}</p>
|
||||
</div>
|
||||
<div class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800/80 text-slate-400 dark:text-slate-500 border border-transparent dark:border-slate-700/50">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col space-y-4 justify-center">
|
||||
<!-- Today Stat Card -->
|
||||
<div class="group flex items-center justify-between p-5 rounded-2xl bg-white dark:bg-slate-900 shadow-[0_2px_15px_-3px_rgba(0,0,0,0.07),0_10px_20px_-2px_rgba(0,0,0,0.04)] dark:shadow-none border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/30">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-cyan-500/10 dark:bg-cyan-500/20 flex items-center justify-center text-cyan-600 dark:text-cyan-400 shadow-sm transition-transform group-hover:scale-110">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18L9 11.25l4.5 4.5L21.75 7.5M21.75 7.5V12m0-4.5H17.25"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __("Today's Transactions") }}</p>
|
||||
<p class="text-2xl font-black text-slate-900 dark:text-white mt-0.5 tracking-tight">${{ number_format($totalRevenue / 30, 0) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-y-1">
|
||||
<span class="text-[10px] font-black text-emerald-500 bg-emerald-500/10 px-2.5 py-0.5 rounded-full">+12.5%</span>
|
||||
<p class="text-[9px] font-bold text-slate-300 dark:text-slate-500 uppercase tracking-tighter">{{ __('vs Yesterday') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Previous Days Stats Row -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Yesterday Card -->
|
||||
<div class="group flex flex-col p-5 rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/20">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __("Yesterday's Transactions") }}</p>
|
||||
<div class="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl font-black text-slate-800 dark:text-slate-200">${{ number_format($totalRevenue / 25, 0) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Before Yesterday Card -->
|
||||
<div class="group flex flex-col p-5 rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 transition-all hover:border-cyan-500/20">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __("Before Yesterday's Transactions") }}</p>
|
||||
<div class="w-6 h-6 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl font-black text-slate-800 dark:text-slate-200">${{ number_format($totalRevenue / 40, 0) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Section: Status List -->
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-white">營收績效分析</h2>
|
||||
<p class="text-sm text-slate-400">各地區每日營收洞察</p>
|
||||
<div class="flex items-center gap-x-3">
|
||||
<h2 class="text-2xl font-extrabold text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Status List') }}</h2>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700 uppercase tracking-tighter">
|
||||
{{ __('Total items', ['count' => count($latestActivities)]) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm font-bold text-slate-400 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time monitoring across all machines') }}</p>
|
||||
</div>
|
||||
|
||||
<div x-data="{ open: false, selected: '最近 7 天' }" class="relative inline-block text-left">
|
||||
<button @click="open = !open" type="button" class="btn-luxury-secondary">
|
||||
<span x-text="selected"></span>
|
||||
<svg class="shrink-0 w-4 h-4 transition-transform" :class="open ? 'rotate-180' : ''" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" placeholder="{{ __('Quick search...') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Communication') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Alert Summary') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
|
||||
@forelse($latestActivities as $activity)
|
||||
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-x-5">
|
||||
<div class="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 dark:text-slate-300 group-hover:bg-cyan-500 group-hover:text-white transition-all shadow-sm">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $activity->machine->name ?? __('Machine Info') }}</span>
|
||||
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $activity->machine->serial_no ?? 'N/A' }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-300 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
|
||||
{{ $activity->machine->status === 'online' ? __('Online') : __('Offline') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="text-xl font-black text-slate-900 dark:text-slate-100">$ 0</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex flex-col items-center gap-y-2.5">
|
||||
<div class="w-32 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden shadow-inner">
|
||||
<div class="h-full bg-rose-500 rounded-full shadow-[0_0_8px_rgba(244,63,94,0.4)]" style="width: 15.5%"></div>
|
||||
</div>
|
||||
<span class="text-[11px] font-black text-rose-500 uppercase tracking-[0.2em]">15.5% {{ __('Low Stock') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-200 font-bold font-display tracking-tight bg-slate-50 dark:bg-slate-800 px-3 py-1 rounded-lg">{{ $activity->created_at->format('Y/m/d H:i') }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<span class="text-[11px] font-bold text-slate-400/30 dark:text-slate-500 uppercase tracking-widest group-hover:text-slate-400 transition-colors">{{ __('No alert summary') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-32 text-center text-slate-400">{{ __('No data available') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table Footer: Pagination -->
|
||||
<div class="mt-8 flex items-center justify-between border-t border-slate-100 dark:border-slate-800 pt-6">
|
||||
<p class="text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
{{ __('Showing :from to :to of :total items', ['from' => 1, 'to' => count($latestActivities), 'total' => count($latestActivities)]) }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<button class="inline-flex items-center gap-x-2 px-4 py-2 rounded-xl text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"/></svg>
|
||||
<span>{{ __('Previous') }}</span>
|
||||
</button>
|
||||
|
||||
<div x-show="open"
|
||||
@click.away="open = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-40 z-10 origin-top-right bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl p-1 focus:outline-none"
|
||||
x-cloak>
|
||||
<button @click="selected = '最近 7 天'; open = false" class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 text-slate-700 dark:text-slate-300 transition-colors">最近 7 天</button>
|
||||
<button @click="selected = '最近 30 天'; open = false" class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 text-slate-700 dark:text-slate-300 transition-colors">最近 30 天</button>
|
||||
<button @click="selected = '最近 90 天'; open = false" class="w-full text-left px-3 py-2 text-sm rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 text-slate-700 dark:text-slate-300 transition-colors">最近 90 天</button>
|
||||
<div class="flex items-center gap-x-1">
|
||||
<button class="w-8 h-8 rounded-lg text-xs font-black bg-cyan-500 text-white shadow-lg shadow-cyan-500/30">1</button>
|
||||
<button class="w-8 h-8 rounded-lg text-xs font-black text-slate-500 hover:text-cyan-500 transition-colors">2</button>
|
||||
<button class="w-8 h-8 rounded-lg text-xs font-black text-slate-500 hover:text-cyan-500 transition-colors">3</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-[300px] relative group overflow-hidden rounded-2xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 flex items-center justify-center transition-all">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-transparent pointer-events-none"></div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-white dark:bg-slate-800 shadow-sm border border-slate-200 dark:border-slate-700 mb-4">
|
||||
<svg class="w-6 h-6 text-cyan-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-slate-600 dark:text-slate-400">營收圖表數據載入中</p>
|
||||
<p class="text-xs text-slate-400 mt-1">即時數據串流將在此顯示</p>
|
||||
<button class="inline-flex items-center gap-x-2 px-4 py-2 rounded-xl text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all">
|
||||
<span>{{ __('Next') }}</span>
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 即時動態 -->
|
||||
<div class="luxury-card rounded-3xl p-6 animate-luxury-in" style="animation-delay: 500ms">
|
||||
<h2 class="text-lg font-bold text-slate-800 dark:text-white mb-6">即時動態</h2>
|
||||
<div class="space-y-6">
|
||||
@forelse($latestActivities as $activity)
|
||||
<div class="relative pl-6 before:absolute before:left-0 before:top-1 before:bottom-0 before:w-0.5 before:bg-slate-100 dark:before:bg-slate-800">
|
||||
<div class="absolute left-[-4px] top-0 w-2.5 h-2.5 rounded-full {{ $activity->level === 'error' ? 'bg-rose-500' : 'bg-cyan-500' }} border-2 border-white dark:border-[#1e293b]"></div>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-200">#{{ $activity->machine->code ?? 'N/A' }} {{ $activity->content }}</p>
|
||||
<p class="text-xs text-slate-400">{{ $activity->created_at->diffForHumans() }} • {{ $activity->machine->location ?? '未知區域' }}</p>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-10">
|
||||
<p class="text-sm text-slate-400">目前尚無動態資料</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<button class="btn-luxury-ghost w-full mt-8 py-3 font-bold uppercase tracking-wider">查看所有日誌 →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
// Here you would initialize charts using ApexCharts or similar,
|
||||
// as Preline examples often use ApexCharts.
|
||||
// For now, placeholders are sufficient.
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
x-cloak></div>
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white/80 backdrop-blur-md border-b border-slate-200/50 text-sm py-2.5 sm:py-4 lg:pl-64 dark:bg-[#0f172a]/80 dark:border-slate-800/50 shadow-sm">
|
||||
<header class="sticky top-0 inset-x-0 flex flex-wrap sm:justify-start sm:flex-nowrap z-[48] w-full bg-white/80 backdrop-blur-md border-b border-slate-200/50 text-sm py-2.5 sm:py-4 lg:pl-72 dark:bg-[#0f172a]/80 dark:border-slate-800/50 shadow-sm">
|
||||
<nav class="flex basis-full items-center w-full mx-auto px-4 sm:px-6 md:px-8" aria-label="Global">
|
||||
<div class="mr-5 lg:mr-0 lg:hidden text-center">
|
||||
<a class="flex-none text-xl font-bold dark:text-white font-display tracking-tight" href="{{ route('admin.dashboard') }}" aria-label="Brand">
|
||||
@@ -47,7 +47,8 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-center justify-end ml-auto sm:justify-between sm:gap-x-3 sm:order-3">
|
||||
<div class="w-full flex items-center justify-end ml-auto sm:gap-x-3 sm:order-3">
|
||||
<!-- Mobile Search Toggle -->
|
||||
<div class="sm:hidden">
|
||||
<button type="button" class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
|
||||
<svg class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
@@ -56,20 +57,70 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:block">
|
||||
<!-- Desktop Search Placeholder -->
|
||||
<div class="hidden sm:block flex-1 max-w-sm">
|
||||
<!-- Search Input (Optional) -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center justify-end gap-2">
|
||||
<div class="flex flex-row items-center justify-end gap-x-1.5 sm:gap-x-3">
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative inline-flex" x-data="{ langOpen: false }">
|
||||
<button type="button" @click="langOpen = !langOpen" @click.away="langOpen = false" class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] px-3 rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
|
||||
<span class="flex items-center gap-x-2">
|
||||
@if(app()->getLocale() == 'zh_TW')
|
||||
<span class="text-base">🇹🇼</span>
|
||||
<span class="hidden md:inline font-bold">繁體中文</span>
|
||||
@elseif(app()->getLocale() == 'ja')
|
||||
<span class="text-base">🇯🇵</span>
|
||||
<span class="hidden md:inline font-bold">日本語</span>
|
||||
@else
|
||||
<span class="text-base">🇬🇧</span>
|
||||
<span class="hidden md:inline font-bold">English</span>
|
||||
@endif
|
||||
</span>
|
||||
<svg class="size-3 text-gray-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<div x-show="langOpen"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="absolute right-0 top-full mt-2 min-w-[10rem] bg-white shadow-xl rounded-2xl p-2 dark:bg-gray-800 dark:border dark:border-gray-700 z-50 border border-slate-100"
|
||||
x-cloak>
|
||||
<a href="{{ route('lang.switch', 'zh_TW') }}" class="flex items-center gap-x-3 py-2.5 px-3 rounded-xl text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition-colors {{ app()->getLocale() == 'zh_TW' ? 'bg-gray-50 dark:bg-gray-900/50' : '' }}">
|
||||
<span class="text-lg">🇹🇼</span>
|
||||
<span class="font-bold">繁體中文</span>
|
||||
@if(app()->getLocale() == 'zh_TW')
|
||||
<svg class="ml-auto size-4 text-cyan-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
@endif
|
||||
</a>
|
||||
<a href="{{ route('lang.switch', 'en') }}" class="flex items-center gap-x-3 py-2.5 px-3 rounded-xl text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition-colors {{ app()->getLocale() == 'en' ? 'bg-gray-50 dark:bg-gray-900/50' : '' }}">
|
||||
<span class="text-lg">🇬🇧</span>
|
||||
<span class="font-bold">English</span>
|
||||
@if(app()->getLocale() == 'en')
|
||||
<svg class="ml-auto size-4 text-cyan-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
@endif
|
||||
</a>
|
||||
<a href="{{ route('lang.switch', 'ja') }}" class="flex items-center gap-x-3 py-2.5 px-3 rounded-xl text-sm text-gray-800 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition-colors {{ app()->getLocale() == 'ja' ? 'bg-gray-50 dark:bg-gray-900/50' : '' }}">
|
||||
<span class="text-lg">🇯🇵</span>
|
||||
<span class="font-bold">日本語</span>
|
||||
@if(app()->getLocale() == 'ja')
|
||||
<svg class="ml-auto size-4 text-cyan-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark Mode Toggle -->
|
||||
<button type="button"
|
||||
@click="darkMode = !darkMode; localStorage.setItem('darkMode', darkMode); document.documentElement.classList.toggle('dark', darkMode)"
|
||||
class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
|
||||
<!-- Moon Icon (shown in light mode) -->
|
||||
<svg x-show="!darkMode" class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278zM4.858 1.311A7.269 7.269 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.316 7.316 0 0 0 5.205-2.162c-.337.042-.68.063-1.029.063-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286z"/>
|
||||
</svg>
|
||||
<!-- Sun Icon (shown in dark mode) -->
|
||||
<svg x-show="darkMode" class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
|
||||
</svg>
|
||||
@@ -77,7 +128,7 @@
|
||||
|
||||
<!-- Profile Dropdown -->
|
||||
<div class="relative inline-flex" x-data="{ open: false }">
|
||||
<button id="hs-dropdown-with-header" type="button" @click="open = !open" @click.away="open = false" class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
|
||||
<button type="button" @click="open = !open" @click.away="open = false" class="inline-flex flex-shrink-0 justify-center items-center gap-2 h-[2.375rem] w-[2.375rem] rounded-full font-medium bg-white text-gray-700 align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-xs dark:bg-gray-800 dark:hover:bg-slate-800 dark:text-gray-400 dark:hover:text-white dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800">
|
||||
<img class="inline-block h-[2.375rem] w-[2.375rem] rounded-full ring-2 ring-white dark:ring-gray-800" src="https://ui-avatars.com/api/?name={{ Auth::user()->name }}&background=0D8ABC&color=fff" alt="Image Description">
|
||||
</button>
|
||||
|
||||
@@ -88,22 +139,23 @@
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="absolute right-0 top-full mt-2 min-w-[15rem] bg-white shadow-md rounded-lg p-2 dark:bg-gray-800 dark:border dark:border-gray-700 z-50"
|
||||
class="absolute right-0 top-full mt-2 min-w-[15rem] bg-white shadow-xl rounded-2xl p-2 dark:bg-gray-800 dark:border dark:border-gray-700 z-50 border border-slate-100"
|
||||
x-cloak>
|
||||
<div class="py-3 px-5 -m-2 bg-gray-100 rounded-t-lg dark:bg-gray-700">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Signed in as</p>
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-gray-300">{{ Auth::user()->email }}</p>
|
||||
<div class="py-3 px-5 -m-2 bg-slate-50 rounded-t-2xl dark:bg-slate-900/50 border-b border-slate-100 dark:border-slate-700">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">Signed in as</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-200 truncate">{{ Auth::user()->email }}</p>
|
||||
</div>
|
||||
<div class="mt-2 py-2 first:pt-0 last:pb-0">
|
||||
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300 transition-colors" href="{{ route('profile.edit') }}">
|
||||
<svg class="flex-shrink-0 w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
帳戶設定
|
||||
<div class="mt-2 py-2">
|
||||
<a class="flex items-center gap-x-3.5 py-2.5 px-3 rounded-xl text-sm font-bold text-slate-700 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-slate-300 dark:hover:bg-gray-700 dark:hover:text-white transition-colors" href="{{ route('profile.edit') }}">
|
||||
<svg class="flex-shrink-0 size-4 text-cyan-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
{{ __('Account Settings') }}
|
||||
</a>
|
||||
<div class="my-2 border-t border-slate-100 dark:border-slate-700"></div>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-full flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300">
|
||||
<svg class="flex-shrink-0 w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
|
||||
登出
|
||||
<button type="submit" class="w-full flex items-center gap-x-3.5 py-2.5 px-3 rounded-xl text-sm font-bold text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 transition-colors">
|
||||
<svg class="flex-shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
|
||||
{{ __('Logout') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<li>
|
||||
<a class="luxury-nav-item {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.dashboard') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>
|
||||
儀表板
|
||||
{{ __('Dashboard') }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_profile', open)"
|
||||
class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('profile.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
個人設定
|
||||
{{ __('Profile Settings') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li>
|
||||
<a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('profile.edit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('profile.edit') }}">
|
||||
個人檔案
|
||||
{{ __('Profile') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -29,16 +29,16 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_members') === 'true' || {{ request()->routeIs('admin.members.*') || request()->routeIs('admin.membership-tiers.*') || request()->routeIs('admin.deposit-bonus-rules.*') || request()->routeIs('admin.point-rules.*') || request()->routeIs('admin.gift-definitions.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_members', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.members.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
|
||||
會員管理
|
||||
{{ __('Member Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.members.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.members.index') }}">會員列表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.membership-tiers.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.membership-tiers.index') }}">會員等級</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.deposit-bonus-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.deposit-bonus-rules.index') }}">儲值回饋</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.point-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.point-rules.index') }}">點數規則</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.gift-definitions.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.gift-definitions.index') }}">禮品設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.members.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.members.index') }}">{{ __('Member List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.membership-tiers.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.membership-tiers.index') }}">{{ __('Membership Tiers') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.deposit-bonus-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.deposit-bonus-rules.index') }}">{{ __('Deposit Bonus') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.point-rules.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.point-rules.index') }}">{{ __('Point Rules') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.gift-definitions.*') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.gift-definitions.index') }}">{{ __('Gift Definitions') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -47,17 +47,17 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_machines') === 'true' || {{ request()->routeIs('admin.machines.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_machines', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.machines.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01" /></svg>
|
||||
機台管理
|
||||
{{ __('Machine Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.logs') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.logs') }}">機台日誌</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.index') }}">機台列表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.permissions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.permissions') }}">機台權限</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.utilization') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.utilization') }}">機台稼動率</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.expiry') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.expiry') }}">效期管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.maintenance') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.maintenance') }}">維修管理單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.logs') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.logs') }}">{{ __('Machine Logs') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.index') }}">{{ __('Machine List') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.permissions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.permissions') }}">{{ __('Machine Permissions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.utilization') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.utilization') }}">{{ __('Utilization Rate') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.expiry') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.expiry') }}">{{ __('Expiry Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.machines.maintenance') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.machines.maintenance') }}">{{ __('Maintenance Records') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -66,16 +66,16 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_app') === 'true' || {{ request()->routeIs('admin.app.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_app', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.app.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||
APP管理
|
||||
{{ __('APP Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.ui-elements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.ui-elements') }}">UI元素</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.helper') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.helper') }}">小幫手</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.questionnaire') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.questionnaire') }}">問卷</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.games') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.games') }}">互動遊戲</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.timer') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.timer') }}">計時器</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.ui-elements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.ui-elements') }}">{{ __('UI Elements') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.helper') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.helper') }}">{{ __('Helper') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.questionnaire') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.questionnaire') }}">{{ __('Questionnaire') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.games') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.games') }}">{{ __('Games') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.app.timer') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.app.timer') }}">{{ __('Timer') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -97,8 +97,8 @@
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">機台補貨單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">機台補貨紀錄</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">機台庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.staff-stock') }}">人員庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.returns') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.returns') }}">回庫單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.staff-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.staff-stock') }}">{{ __('Staff Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.returns') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.returns') }}">{{ __('Returns') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -107,17 +107,17 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_sales') === 'true' || {{ request()->routeIs('admin.sales.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_sales', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.sales.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
銷售管理
|
||||
{{ __('Sales Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.index') }}">銷售紀錄</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pickup-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pickup-codes') }}">取貨碼</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.orders') }}">購買單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.promotions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.promotions') }}">促銷時段</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pass-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pass-codes') }}">通行碼</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.store-gifts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.store-gifts') }}">來店禮</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.index') }}">{{ __('Sales Records') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pickup-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pickup-codes') }}">{{ __('Pickup Codes') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.orders') }}">{{ __('Orders') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.promotions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.promotions') }}">{{ __('Promotions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.pass-codes') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.pass-codes') }}">{{ __('Pass Codes') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.sales.store-gifts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.sales.store-gifts') }}">{{ __('Store Gifts') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -126,15 +126,15 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_analysis') === 'true' || {{ request()->routeIs('admin.analysis.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_analysis', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.analysis.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" /></svg>
|
||||
分析管理
|
||||
{{ __('Analysis Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.change-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.change-stock') }}">零錢庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.machine-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.machine-reports') }}">機台報表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.product-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.product-reports') }}">商品報表</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.survey-analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.survey-analysis') }}">問卷分析</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.change-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.change-stock') }}">{{ __('Change Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.machine-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.machine-reports') }}">{{ __('Machine Reports') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.product-reports') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.product-reports') }}">{{ __('Product Reports') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.analysis.survey-analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.analysis.survey-analysis') }}">{{ __('Survey Analysis') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -143,14 +143,14 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_audit') === 'true' || {{ request()->routeIs('admin.audit.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_audit', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.audit.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
稽核管理
|
||||
{{ __('Audit Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.purchases') }}">採購單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.transfers') }}">調撥單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.replenishments') }}">補貨單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.purchases') }}">{{ __('Purchase Audit') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.transfers') }}">{{ __('Transfer Audit') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.audit.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.audit.replenishments') }}">{{ __('Replenishment Audit') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -159,19 +159,19 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_data_config') === 'true' || {{ request()->routeIs('admin.data-config.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_data_config', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.data-config.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
資料設定
|
||||
{{ __('Data Configuration') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.products') }}">商品管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.advertisements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.advertisements') }}">廣告管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.admin-products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.admin-products') }}">管理者可賣</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.accounts') }}">帳號管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-accounts') }}">子帳號</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">子帳號角色</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">點數設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.badges') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.badges') }}">識別證</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.products') }}">{{ __('Product Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.advertisements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.advertisements') }}">{{ __('Advertisement Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.admin-products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.admin-products') }}">{{ __('Admin Sellable Products') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.accounts') }}">{{ __('Account Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-accounts') }}">{{ __('Sub Accounts') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">{{ __('Sub Account Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">{{ __('Point Settings') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.badges') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.badges') }}">{{ __('Badge Settings') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -181,18 +181,18 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_remote') === 'true' || {{ request()->routeIs('admin.remote.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_remote', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.remote.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
遠端管理
|
||||
{{ __('Remote Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.stock') }}">機台庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart') }}">機台重啟</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart-card-reader') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart-card-reader') }}">卡機重啟</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.checkout') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.checkout') }}">遠端結帳</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.lock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.lock') }}">遠端鎖定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.change') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.change') }}">遠端找零</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.dispense') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.dispense') }}">遠端出貨</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.stock') }}">{{ __('Machine Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart') }}">{{ __('Machine Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.restart-card-reader') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.restart-card-reader') }}">{{ __('Card Reader Restart') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.checkout') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.checkout') }}">{{ __('Remote Checkout') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.lock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.lock') }}">{{ __('Remote Lock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.change') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.change') }}">{{ __('Remote Change') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.remote.dispense') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.remote.dispense') }}">{{ __('Remote Dispense') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -201,17 +201,17 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_line') === 'true' || {{ request()->routeIs('admin.line.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_line', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.line.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg>
|
||||
Line管理
|
||||
{{ __('Line Management') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">Line會員</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.machines') }}">Line機台</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.products') }}">Line商品</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.official-account') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.official-account') }}">Line生活圈</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.orders') }}">Line訂單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.coupons') }}">Line優惠券</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Members') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.machines') }}">{{ __('Line Machines') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.products') }}">{{ __('Line Products') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.official-account') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.official-account') }}">{{ __('Line Official Account') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.orders') }}">{{ __('Line Orders') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.line.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.coupons') }}">{{ __('Line Coupons') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -220,18 +220,18 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_reservation') === 'true' || {{ request()->routeIs('admin.reservation.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_reservation', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.reservation.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
預約系統
|
||||
{{ __('Reservation System') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.members') }}">預約會員</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.stores') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.stores') }}">店家管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.time-slots') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.time-slots') }}">時段組合</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.venues') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.venues') }}">場地管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.coupons') }}">優惠券</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.reservations') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.reservations') }}">預約管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.orders') }}">訂單管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.members') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.members') }}">{{ __('Reservation Members') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.stores') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.stores') }}">{{ __('Store Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.time-slots') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.time-slots') }}">{{ __('Time Slots') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.venues') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.venues') }}">{{ __('Venue Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.coupons') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.coupons') }}">{{ __('Coupons') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.reservations') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.reservations') }}">{{ __('Reservations') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.reservation.orders') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.reservation.orders') }}">{{ __('Order Management') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -240,14 +240,14 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_special_permission', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.special-permission.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||
特殊權限
|
||||
{{ __('Special Permission') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.clear-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.clear-stock') }}">庫存清空</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.apk-versions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.apk-versions') }}">APK版本</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.discord-notifications') }}">Discord通知</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.clear-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.clear-stock') }}">{{ __('Clear Stock') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.apk-versions') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.apk-versions') }}">{{ __('APK Versions') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.special-permission.discord-notifications') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.special-permission.discord-notifications') }}">{{ __('Discord Notifications') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@@ -256,23 +256,23 @@
|
||||
<li x-data="{ open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_permissions', open)" class="luxury-nav-item w-full text-start group">
|
||||
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.permission.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
|
||||
權限設定
|
||||
{{ __('Permission Settings') }}
|
||||
<svg class="ms-auto w-4 h-4 transition-transform duration-300 text-slate-400 dark:text-slate-500" :class="{ 'rotate-180': open, 'rotate-0': !open }" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.app-features') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.app-features') }}">APP功能</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.data-config') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.data-config') }}">資料設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.sales') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.sales') }}">銷售管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.machines') }}">機台管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.warehouses') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.warehouses') }}">倉庫管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.analysis') }}">分析管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.audit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.audit') }}">稽核管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.remote') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.remote') }}">遠端管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.line') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">Line管理</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">角色設定</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.others') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.others') }}">其他功能</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.ai-prediction') }}">AI智能預測</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.app-features') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.app-features') }}">{{ __('APP Features') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.data-config') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.data-config') }}">{{ __('Data Configuration') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.sales') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.sales') }}">{{ __('Sales') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.machines') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.machines') }}">{{ __('Machine Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.warehouses') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.warehouses') }}">{{ __('Warehouse Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.analysis') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.analysis') }}">{{ __('Analysis Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.audit') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.audit') }}">{{ __('Audit Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.remote') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.remote') }}">{{ __('Remote Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.line') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">{{ __('Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.others') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.others') }}">{{ __('Others') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.ai-prediction') }}">{{ __('AI Prediction') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
<div class="max-w-5xl mx-auto space-y-8 animate-luxury-in">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-extrabold text-slate-800 dark:text-white font-display tracking-tight">{{ __('Account Settings') }}</h2>
|
||||
<p class="text-sm font-bold text-slate-400 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage your profile information, security settings, and login history') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Left Side: Basic Info & Security -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<div class="luxury-card p-8">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
<div class="luxury-card p-8">
|
||||
@include('profile.partials.update-password-form')
|
||||
</div>
|
||||
|
||||
<div class="luxury-card p-8 border-rose-500/20 dark:border-rose-500/10">
|
||||
@include('profile.partials.delete-user-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.delete-user-form')
|
||||
<!-- Right Side: Login History -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="luxury-card p-6 sticky top-24">
|
||||
@include('profile.partials.login-history')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ __('Delete Account') }}
|
||||
<h2 class="text-xl font-black text-rose-600 dark:text-rose-500 tracking-tight">
|
||||
{{ __('Danger Zone: Delete Account') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p class="mt-1 text-sm font-bold text-slate-400 dark:text-slate-400 uppercase tracking-widest">
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<x-danger-button
|
||||
<button
|
||||
x-data=""
|
||||
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
|
||||
>{{ __('Delete Account') }}</x-danger-button>
|
||||
class="btn-luxury-rose px-8"
|
||||
>
|
||||
<span>{{ __('Delete Account') }}</span>
|
||||
</button>
|
||||
|
||||
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
|
||||
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
|
||||
<form method="post" action="{{ route('profile.destroy') }}" class="p-8">
|
||||
@csrf
|
||||
@method('delete')
|
||||
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
<h2 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight">
|
||||
{{ __('Are you sure you want to delete your account?') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p class="mt-3 text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="mt-8">
|
||||
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
|
||||
|
||||
<x-text-input
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="{{ __('Password') }}"
|
||||
class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500 transition-all outline-none"
|
||||
placeholder="{{ __('Enter your password to confirm') }}"
|
||||
/>
|
||||
|
||||
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<x-secondary-button x-on:click="$dispatch('close')">
|
||||
<div class="mt-8 flex justify-end gap-x-3">
|
||||
<button type="button" x-on:click="$dispatch('close')" class="py-3 px-6 inline-flex items-center gap-x-2 text-sm font-black rounded-2xl border border-slate-200 bg-white text-slate-600 shadow-sm hover:bg-slate-50 focus:outline-none focus:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:focus:bg-slate-800 transition-all">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
</button>
|
||||
|
||||
<x-danger-button class="ms-3">
|
||||
{{ __('Delete Account') }}
|
||||
</x-danger-button>
|
||||
<button type="submit" class="btn-luxury-rose px-8">
|
||||
<span>{{ __('Permanently Delete Account') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
|
||||
46
resources/views/profile/partials/login-history.blade.php
Normal file
46
resources/views/profile/partials/login-history.blade.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center gap-x-3 mb-6">
|
||||
<div class="size-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-600 dark:text-cyan-400">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Login History') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="flow-root">
|
||||
<ul role="list" class="-mb-8">
|
||||
@forelse($user->loginLogs as $log)
|
||||
<li>
|
||||
<div class="relative pb-8">
|
||||
@if(!$loop->last)
|
||||
<span class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-slate-100 dark:bg-slate-800" aria-hidden="true"></span>
|
||||
@endif
|
||||
<div class="relative flex space-x-3 mt-1">
|
||||
<div>
|
||||
<span class="h-8 w-8 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center ring-8 ring-white dark:ring-slate-900">
|
||||
<svg class="h-4 w-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
|
||||
<div>
|
||||
<p class="text-[11px] font-black text-slate-700 dark:text-slate-300 uppercase tracking-widest">{{ $log->ip_address }}</p>
|
||||
<p class="mt-1 text-xs text-slate-400 truncate max-w-[120px]" title="{{ $log->user_agent }}">
|
||||
{{ Str::limit($log->user_agent, 20) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="whitespace-nowrap text-right text-[10px] font-bold text-slate-400">
|
||||
<time datetime="{{ $log->login_at }}">{{ $log->login_at->diffForHumans() }}</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="py-4 text-center text-xs text-slate-400">尚無登入紀錄</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,38 +1,42 @@
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
<h2 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">
|
||||
{{ __('Update Password') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p class="mt-1 text-sm font-bold text-slate-400 dark:text-slate-400 uppercase tracking-widest">
|
||||
{{ __('Ensure your account is using a long, random password to stay secure.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
|
||||
<form method="post" action="{{ route('password.update') }}" class="mt-8 space-y-6">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
|
||||
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
|
||||
<x-input-label for="update_password_current_password" :value="__('Current Password')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="update_password_current_password" name="current_password" type="password" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" autocomplete="current-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password" :value="__('New Password')" />
|
||||
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<x-input-label for="update_password_password" :value="__('New Password')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="update_password_password" name="password" type="password" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="update_password_password_confirmation" name="password_confirmation" type="password" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
|
||||
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||
<div class="flex items-center gap-4 pt-4">
|
||||
<button type="submit" class="btn-luxury-primary px-8">
|
||||
<span>{{ __('Update') }}</span>
|
||||
</button>
|
||||
|
||||
@if (session('status') === 'password-updated')
|
||||
<p
|
||||
@@ -40,7 +44,7 @@
|
||||
x-show="show"
|
||||
x-transition
|
||||
x-init="setTimeout(() => show = false, 2000)"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
class="text-xs font-black text-emerald-500 dark:text-emerald-400 uppercase tracking-widest"
|
||||
>{{ __('Saved.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
<h2 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">
|
||||
{{ __('Profile Information') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ __("Update your account's profile information and email address.") }}
|
||||
<p class="mt-1 text-sm font-bold text-slate-400 dark:text-slate-400 uppercase tracking-widest">
|
||||
{{ __('Update your account\'s profile information and email address.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -13,45 +13,42 @@
|
||||
@csrf
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
|
||||
<form method="post" action="{{ route('profile.update') }}" class="mt-8 space-y-6">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" />
|
||||
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('name')" />
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<x-input-label for="name" :value="__('Name')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="name" name="name" type="text" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" value="{{ old('name', $user->name) }}" required autofocus autocomplete="name" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('name')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="phone" :value="__('Phone')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="phone" name="phone" type="text" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" value="{{ old('phone', $user->phone) }}" autocomplete="tel" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('phone')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="phone" :value="__('Phone')" />
|
||||
<x-text-input id="phone" name="phone" type="text" class="mt-1 block w-full" :value="old('phone', $user->phone)" autocomplete="tel" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('phone')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="avatar" :value="__('Avatar URL')" />
|
||||
<x-text-input id="avatar" name="avatar" type="text" class="mt-1 block w-full" :value="old('avatar', $user->avatar)" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('avatar')" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-input-label for="email" :value="__('Email')" />
|
||||
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
|
||||
<x-input-label for="email" :value="__('Email')" class="text-xs font-black text-slate-500 uppercase tracking-widest mb-2 ml-1" />
|
||||
<input id="email" name="email" type="email" class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" value="{{ old('email', $user->email) }}" required autocomplete="username" />
|
||||
<x-input-error class="mt-2" :messages="$errors->get('email')" />
|
||||
|
||||
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
|
||||
<div>
|
||||
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
||||
<div class="mt-4 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-2xl border border-amber-200 dark:border-amber-900/30">
|
||||
<p class="text-xs font-bold text-amber-700 dark:text-amber-400 flex items-center gap-x-2">
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<button form="send-verification" class="mt-2 text-xs font-black text-amber-600 dark:text-amber-500 hover:text-amber-700 underline underline-offset-4 decoration-amber-500/30 focus:outline-none transition-colors">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
|
||||
@if (session('status') === 'verification-link-sent')
|
||||
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
|
||||
<p class="mt-2 text-xs font-black text-green-600 dark:text-green-400">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</p>
|
||||
@endif
|
||||
@@ -59,8 +56,10 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||
<div class="flex items-center gap-4 pt-4">
|
||||
<button type="submit" class="btn-luxury-primary px-8">
|
||||
<span>{{ __('Save') }}</span>
|
||||
</button>
|
||||
|
||||
@if (session('status') === 'profile-updated')
|
||||
<p
|
||||
@@ -68,7 +67,7 @@
|
||||
x-show="show"
|
||||
x-transition
|
||||
x-init="setTimeout(() => show = false, 2000)"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
class="text-xs font-black text-emerald-500 dark:text-emerald-400 uppercase tracking-widest"
|
||||
>{{ __('Saved.') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,9 @@ use Illuminate\Support\Facades\Route;
|
||||
|
|
||||
*/
|
||||
|
||||
// Multi-language switch
|
||||
Route::get('lang/{locale}', [App\Http\Controllers\System\LanguageController::class, 'switch'])->name('lang.switch');
|
||||
|
||||
Route::get('/', function () {
|
||||
return redirect()->route('login');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user