[FEAT] 優化部署流程:加入 RoleSeeder 與 AdminUserSeeder,並實作權限系統基礎架構與多租戶隔離機制
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s

This commit is contained in:
2026-03-13 17:35:22 +08:00
parent 39d25ed1d4
commit 56daf8940b
41 changed files with 3052 additions and 358 deletions

View File

@@ -68,7 +68,10 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- [ ] 是否使用了正確的 `rounded-2xl` (或更圓) 的導角? - [ ] 是否使用了正確的 `rounded-2xl` (或更圓) 的導角?
- [ ] 所有的圖示是否一致使用 `lucide-react` 風格? - [ ] 所有的圖示是否一致使用 `lucide-react` 風格?
- [ ] 卡片是否有適當的間距 (通常為 `p-6`) - [ ] 卡片是否有適當的間距 (通常為 `p-6`)
- [ ] 文字色階是否符合:標題 (slate-900/white)、副標 (slate-500/slate-400) - [ ] 文字色階是否符合:
- **標題**: `text-slate-900` / `dark:text-white`
- **副標/標籤**: 最小應為 `text-slate-500` / `dark:text-slate-400`(避免使用 `slate-400` 以下等級,以確保對比度足以閱讀)。
- [ ] **字體大小**: 確保所有文字至少為 `text-xs`,重要的標籤建議為 `text-sm`
## 5. 開發注意事項 (Important Notes) ## 5. 開發注意事項 (Important Notes)
@@ -80,6 +83,102 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
- **格式**: `#機台編號 動作內容` (例如 `#V-001 執行出貨`)。 - **格式**: `#機台編號 動作內容` (例如 `#V-001 執行出貨`)。
- **脈絡**: 必須呈現相對時間與機台位置。 - **脈絡**: 必須呈現相對時間與機台位置。
## 6. 頁面佈局規範 (Page Layout)
### 標準寬版佈局 (Wide Layout)
```html
@section('content')
<div class="space-y-6">
<!-- Header: 標題與操作按鈕 -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Title') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Subtitle') }}</p>
</div>
<div class="flex items-center gap-3">
<button class="btn-luxury-primary">...</button>
</div>
</div>
<!-- Main Container: 卡片與表格 -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<!-- Table Content -->
</table>
</div>
</div>
</div>
@endsection
```
### 佈局核心原則:
1. **移除重複內距**: 根容器 `div` 應**禁止**使用 `p-6``p-10`,因為佈局基底已提供基礎間距。僅使用 `space-y-6` (或 `space-y-8`) 控制區塊間隙。
2. **主容器樣式**: 強制對齊為 `luxury-card rounded-3xl p-8`
3. **標題排版**:
- 主標題需應用 `font-display` (Outfit)。
- 描述文字需應用 `uppercase tracking-widest font-bold` 以呈現高級設計感。
## 7. 表單元件規範 (Form Elements)
針對輸入框與下拉選單,強制使用以下類別以確保深色模式質感。
### 輸入框與選單
- **類別**: `.luxury-input`, `.luxury-select`
- **特性**:
- 深色模式下具備半透明背景與背景模糊效果。
- 統一的 `rounded-xl` 圓角與 `font-bold` 字體。
- 聚焦時帶有青色 (`Cyan`) 發光邊框。
```html
<input type="text" class="luxury-input" placeholder="請輸入內容">
<select class="luxury-select">
<option value="1">啟用</option>
<option value="0">禁用</option>
</select>
```
## 8. 資料表格規範 (Data Tables)
為了確保管理後台資料的可讀性與精密感,表格內的所有文字級別必須對齊以下規範:
### 文字大小與權重 (Typography Hierarchy)
- **表頭 (Table Header)**:
- 類別: `text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest`
- 作用: 提供清晰的欄位定義而不奪取資料視覺焦點。
- **主標題 (Primary Item)**:
- 類別: `text-base font-extrabold text-slate-800 dark:text-slate-100`
- 範例: 公司名稱、機台標題。
- **次要資訊 (Secondary Info)**:
- 類別: `text-[11px] font-bold text-slate-400 dark:text-slate-500 tracking-[0.15em]`
- 範例: 機台序號 (SN)、公司代碼。
- **狀態標籤 (Status Badge)**:
- 類別: `text-[11px] font-black tracking-widest`
- 樣式: 在線 (`emerald`)、離線 (`rose`)。
- **時間訊號 (Signals/Time)**:
- 類別: `text-[13px] font-bold font-display tracking-widest`
- 作用: 解決數字黏滯感,提升判讀舒適度。
- **內距 (Padding)**: 單元格統一使用 `px-6 py-6` 以維持呼吸感。
- **懸停 (Hover)**: 表格行需具備 `hover:bg-slate-50/80` (深色: `dark:hover:bg-slate-800/40`) 動態反饋。
### 分頁與列表控制項 (Pagination & Controls)
為了維持操作一致性所有列表的分頁與切換組件必須遵循以下「Luxury Jump」模式
- **統一高度**: 所有控制項(按鈕、下拉選單)固定為 `h-9` (36px)。
- **筆數切換 (Limit Selector)**:
- 樣式: 使用 `bg-slate-50` (深色: `dark:bg-slate-800`) 配合 `text-[11px] font-black`
- 位置: 位於表格右上方。
- **分頁導航 (Luxury Jump)**:
- 模式: 捨棄傳統頁碼按鈕,全端統一使用「跳轉選單」。
- 寬度: 下拉選單內部 Padding 為 `pl-4 pr-10`
- 字體: 使用 `text-xs font-black tracking-widest`
- **指示文字**:
- 行動端隱藏多餘詞彙僅保留「1 - 10 / 50」格式。
- 數字顏色對齊 `text-slate-600` (深色: `text-slate-300`)。
```
--- ---
> [!IMPORTANT] > [!IMPORTANT]
> **開發新功能前,必須確認 `app.css` 中的 `.btn-luxury-*` 系列組件是否滿足需求。** > **開發新功能前,必須確認 `app.css` 中的 `.btn-luxury-*` 系列組件是否滿足需求。**

View File

@@ -94,7 +94,9 @@ jobs:
php artisan migrate --force && php artisan migrate --force &&
php artisan optimize:clear && php artisan optimize:clear &&
php artisan optimize && php artisan optimize &&
php artisan view:cache php artisan view:cache &&
php artisan db:seed --class=RoleSeeder --force &&
php artisan db:seed --class=AdminUserSeeder --force
" "
docker exec star-cloud-demo-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache docker exec star-cloud-demo-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\System\Company;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CompanyController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = Company::query()->withCount(['users', 'machines']);
// 搜尋
if ($search = $request->input('search')) {
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('code', 'like', "%{$search}%");
});
}
// 狀態篩選
if ($request->filled('status')) {
$query->where('status', $request->status);
}
$limit = $request->input('limit', 10);
$companies = $query->latest()->paginate($limit)->withQueryString();
return view('admin.companies.index', compact('companies'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'code' => 'required|string|max:50|unique:companies,code',
'tax_id' => 'nullable|string|max:50',
'contact_name' => 'nullable|string|max:255',
'contact_phone' => 'nullable|string|max:50',
'contact_email' => 'nullable|email|max:255',
'valid_until' => 'nullable|date',
'status' => 'required|boolean',
'note' => 'nullable|string',
// 帳號相關欄位 (可選)
'admin_username' => 'nullable|string|max:255|unique:users,username',
'admin_password' => 'nullable|string|min:8',
'admin_name' => 'nullable|string|max:255',
]);
DB::transaction(function () use ($validated) {
$company = Company::create([
'name' => $validated['name'],
'code' => $validated['code'],
'tax_id' => $validated['tax_id'] ?? null,
'contact_name' => $validated['contact_name'] ?? null,
'contact_phone' => $validated['contact_phone'] ?? null,
'contact_email' => $validated['contact_email'] ?? null,
'valid_until' => $validated['valid_until'] ?? null,
'status' => $validated['status'],
'note' => $validated['note'] ?? null,
]);
// 如果有填寫帳號資訊,則建立管理員帳號
if (!empty($validated['admin_username']) && !empty($validated['admin_password'])) {
$user = \App\Models\System\User::create([
'company_id' => $company->id,
'username' => $validated['admin_username'],
'password' => \Illuminate\Support\Facades\Hash::make($validated['admin_password']),
'name' => $validated['admin_name'] ?: ($validated['contact_name'] ?: $validated['name']),
'status' => 1,
]);
// 綁定客戶管理員角色
$user->assignRole('tenant-admin');
}
});
return redirect()->back()->with('success', __('Customer created successfully.'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Company $company)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'code' => 'required|string|max:50|unique:companies,code,' . $company->id,
'tax_id' => 'nullable|string|max:50',
'contact_name' => 'required|string|max:255',
'contact_phone' => 'nullable|string|max:50',
'contact_email' => 'nullable|email|max:255',
'valid_until' => 'nullable|date',
'status' => 'required|boolean',
'note' => 'nullable|string',
]);
$company->update($validated);
return redirect()->back()->with('success', __('Customer updated successfully.'));
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Company $company)
{
if ($company->users()->count() > 0) {
return redirect()->back()->with('error', __('Cannot delete company with active accounts.'));
}
$company->delete();
return redirect()->back()->with('success', __('Customer deleted successfully.'));
}
}

View File

@@ -8,26 +8,34 @@ use Illuminate\Http\Request;
class DashboardController extends Controller class DashboardController extends Controller
{ {
public function index() public function index(Request $request)
{ {
// 每頁顯示筆數限制 (預設為 10)
$perPage = $request->get('limit', 10);
// 從資料庫獲取真實統計數據 // 從資料庫獲取真實統計數據
$totalRevenue = \App\Models\Member\MemberWallet::sum('balance'); $totalRevenue = \App\Models\Member\MemberWallet::sum('balance');
$activeMachines = Machine::where('status', 'online')->count(); $activeMachines = Machine::where('status', 'online')->count();
$alertsPending = Machine::where('status', 'error')->count(); $alertsPending = Machine::where('status', 'error')->count();
$memberCount = \App\Models\Member\Member::count(); $memberCount = \App\Models\Member\Member::count();
// 獲取最新動態 (最近 3 筆機台日誌) // 獲取機台列表 (分頁)
$latestActivities = \App\Models\Machine\MachineLog::with('machine') $machines = Machine::when($request->search, function($query, $search) {
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('serial_no', 'like', "%{$search}%");
});
})
->latest() ->latest()
->limit(3) ->paginate($perPage)
->get(); ->withQueryString();
return view('admin.dashboard', compact( return view('admin.dashboard', compact(
'totalRevenue', 'totalRevenue',
'activeMachines', 'activeMachines',
'alertsPending', 'alertsPending',
'memberCount', 'memberCount',
'latestActivities' 'machines'
)); ));
} }
} }

View File

@@ -34,14 +34,6 @@ class DataConfigController extends Controller
]); ]);
} }
// 帳號管理
public function accounts()
{
return view('admin.placeholder', [
'title' => '帳號管理',
'description' => '主帳號管理',
]);
}
// 子帳號管理 // 子帳號管理
public function subAccounts() public function subAccounts()

View File

@@ -13,12 +13,14 @@ class MachineController extends AdminController
*/ */
public function index(Request $request): View public function index(Request $request): View
{ {
$limit = $request->input('limit', 10);
$machines = Machine::query() $machines = Machine::query()
->when($request->status, function ($query, $status) { ->when($request->status, function ($query, $status) {
return $query->where('status', $status); return $query->where('status', $status);
}) })
->latest() ->latest()
->paginate(10); ->paginate($limit)
->withQueryString();
return view('admin.machines.index', compact('machines')); return view('admin.machines.index', compact('machines'));
} }
@@ -40,6 +42,7 @@ class MachineController extends AdminController
*/ */
public function logs(Request $request): View public function logs(Request $request): View
{ {
$limit = $request->input('limit', 20);
$logs = \App\Models\Machine\MachineLog::with('machine') $logs = \App\Models\Machine\MachineLog::with('machine')
->when($request->level, function ($query, $level) { ->when($request->level, function ($query, $level) {
return $query->where('level', $level); return $query->where('level', $level);
@@ -48,7 +51,7 @@ class MachineController extends AdminController
return $query->where('machine_id', $machineId); return $query->where('machine_id', $machineId);
}) })
->latest() ->latest()
->paginate(20); ->paginate($limit)->withQueryString();
$machines = Machine::select('id', 'name')->get(); $machines = Machine::select('id', 'name')->get();

View File

@@ -91,10 +91,67 @@ class PermissionController extends Controller
// 權限角色設定 // 權限角色設定
public function roles() public function roles()
{ {
return view('admin.placeholder', [ $limit = request()->input('limit', 10);
'title' => '權限角色設定', $roles = \Spatie\Permission\Models\Role::withCount('users')->latest()->paginate($limit)->withQueryString();
'description' => '角色權限組合設定', return view('admin.permission.roles', compact('roles'));
}
/**
* Store a newly created role in storage.
*/
public function storeRole(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name',
]); ]);
\Spatie\Permission\Models\Role::create([
'name' => $validated['name'],
'guard_name' => 'web',
'is_system' => false,
]);
return redirect()->back()->with('success', __('Role created successfully.'));
}
/**
* Update the specified role in storage.
*/
public function updateRole(Request $request, $id)
{
$role = \Spatie\Permission\Models\Role::findOrFail($id);
if ($role->is_system) {
return redirect()->back()->with('error', __('System roles cannot be renamed.'));
}
$validated = $request->validate([
'name' => 'required|string|max:255|unique:roles,name,' . $id,
]);
$role->update(['name' => $validated['name']]);
return redirect()->back()->with('success', __('Role updated successfully.'));
}
/**
* Remove the specified role from storage.
*/
public function destroyRole($id)
{
$role = \Spatie\Permission\Models\Role::findOrFail($id);
if ($role->is_system) {
return redirect()->back()->with('error', __('System roles cannot be deleted.'));
}
if ($role->users()->count() > 0) {
return redirect()->back()->with('error', __('Cannot delete role with active users.'));
}
$role->delete();
return redirect()->back()->with('success', __('Role deleted successfully.'));
} }
// 其他功能管理 // 其他功能管理
@@ -106,6 +163,125 @@ class PermissionController extends Controller
]); ]);
} }
// 帳號管理
public function accounts(Request $request)
{
$query = \App\Models\System\User::query()->with(['company', 'roles']);
// 租戶隔離:如果不是系統管理員,則只看自己公司的成員
if (!auth()->user()->isSystemAdmin()) {
$query->where('company_id', auth()->user()->company_id);
}
// 搜尋
if ($search = $request->input('search')) {
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('username', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
// 公司篩選 (僅限 super-admin)
if (auth()->user()->isSystemAdmin() && $request->filled('company_id')) {
$query->where('company_id', $request->company_id);
}
$limit = $request->input('limit', 10);
$users = $query->latest()->paginate($limit)->withQueryString();
$companies = auth()->user()->isSystemAdmin() ? \App\Models\System\Company::all() : collect();
return view('admin.data-config.accounts', compact('users', 'companies'));
}
/**
* Store a newly created account in storage.
*/
public function storeAccount(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'email' => 'nullable|email|max:255|unique:users,email',
'password' => 'required|string|min:8',
'role' => 'required|string',
'status' => 'required|boolean',
'company_id' => 'nullable|exists:companies,id',
'phone' => 'nullable|string|max:20',
]);
$user = \App\Models\System\User::create([
'name' => $validated['name'],
'username' => $validated['username'],
'email' => $validated['email'],
'password' => \Illuminate\Support\Facades\Hash::make($validated['password']),
'status' => $validated['status'],
'company_id' => auth()->user()->isSystemAdmin() ? $validated['company_id'] : auth()->user()->company_id,
'phone' => $validated['phone'],
]);
$user->assignRole($validated['role']);
return redirect()->back()->with('success', __('Account created successfully.'));
}
/**
* Update the specified account in storage.
*/
public function updateAccount(Request $request, $id)
{
$user = \App\Models\System\User::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username,' . $id,
'email' => 'nullable|email|max:255|unique:users,email,' . $id,
'password' => 'nullable|string|min:8',
'role' => 'required|string',
'status' => 'required|boolean',
'company_id' => 'nullable|exists:companies,id',
'phone' => 'nullable|string|max:20',
]);
$updateData = [
'name' => $validated['name'],
'username' => $validated['username'],
'email' => $validated['email'],
'status' => $validated['status'],
'phone' => $validated['phone'],
];
if (auth()->user()->isSystemAdmin()) {
$updateData['company_id'] = $validated['company_id'];
}
if (!empty($validated['password'])) {
$updateData['password'] = \Illuminate\Support\Facades\Hash::make($validated['password']);
}
$user->update($updateData);
$user->syncRoles([$validated['role']]);
return redirect()->back()->with('success', __('Account updated successfully.'));
}
/**
* Remove the specified account from storage.
*/
public function destroyAccount($id)
{
$user = \App\Models\System\User::findOrFail($id);
if ($user->id === auth()->id()) {
return redirect()->back()->with('error', __('You cannot delete your own account.'));
}
$user->delete();
return redirect()->back()->with('success', __('Account deleted successfully.'));
}
// AI智能預測 // AI智能預測
public function aiPrediction() public function aiPrediction()
{ {

View File

@@ -65,5 +65,9 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class, 'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'tenant.access' => \App\Http\Middleware\EnsureTenantAccess::class,
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]; ];
} }

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureTenantAccess
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$user = auth()->user();
// 如果是租戶帳號,檢查公司狀態
if ($user && $user->isTenant()) {
$company = $user->company;
if (!$company || $company->status === 0) {
auth()->logout();
return redirect()->route('login')->with('error', __('Your account is associated with a deactivated company.'));
}
if ($company->valid_until && $company->valid_until->isPast()) {
auth()->logout();
return redirect()->route('login')->with('error', __('Your company contract has expired.'));
}
}
return $next($request);
}
}

View File

@@ -5,11 +5,14 @@ namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Traits\TenantScoped;
class Machine extends Model class Machine extends Model
{ {
use HasFactory; use HasFactory, TenantScoped;
protected $fillable = [ protected $fillable = [
'company_id',
'name', 'name',
'location', 'location',
'status', 'status',

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models\System;
use App\Models\Machine\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Permission\Models\Role;
class Company extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'code',
'tax_id',
'contact_name',
'contact_phone',
'contact_email',
'status',
'valid_until',
'note',
];
protected $casts = [
'valid_until' => 'date',
'status' => 'integer',
];
/**
* Get the users for the company.
*/
public function users(): HasMany
{
return $this->hasMany(User::class);
}
/**
* Get the machines for the company.
*/
public function machines(): HasMany
{
return $this->hasMany(Machine::class);
}
}

View File

@@ -8,9 +8,11 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasApiTokens, HasFactory, Notifiable; use HasApiTokens, HasFactory, Notifiable, HasRoles;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -18,6 +20,7 @@ class User extends Authenticatable
* @var array<int, string> * @var array<int, string>
*/ */
protected $fillable = [ protected $fillable = [
'company_id',
'username', 'username',
'name', 'name',
'email', 'email',
@@ -25,6 +28,7 @@ class User extends Authenticatable
'phone', 'phone',
'avatar', 'avatar',
'role', 'role',
'status',
]; ];
/** /**
@@ -56,14 +60,26 @@ class User extends Authenticatable
} }
/** /**
* Get the user's avatar URL. * Get the company that owns the user.
*/ */
public function getAvatarUrlAttribute(): string public function company()
{ {
if ($this->avatar) { return $this->belongsTo(Company::class);
return asset('storage/' . $this->avatar); }
}
return "https://ui-avatars.com/api/?name=" . urlencode($this->name) . "&background=0D8ABC&color=fff"; /**
* Check if the user is a system administrator.
*/
public function isSystemAdmin(): bool
{
return is_null($this->company_id);
}
/**
* Check if the user belongs to a tenant.
*/
public function isTenant(): bool
{
return !is_null($this->company_id);
} }
} }

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
trait TenantScoped
{
/**
* Boot the trait.
*/
public static function bootTenantScoped(): void
{
static::addGlobalScope('tenant', function (Builder $query) {
$user = auth()->user();
// 如果使用者已登入且有綁定公司,則自動注入過濾條件
if ($user && $user->company_id) {
$query->where((new static)->getTable() . '.company_id', $user->company_id);
}
});
// 建立資料時,自動填入當前使用者的 company_id
static::creating(function ($model) {
if (!$model->company_id) {
$user = auth()->user();
if ($user && $user->company_id) {
$model->company_id = $user->company_id;
}
}
});
}
/**
* Define the company relationship.
*/
public function company()
{
return $this->belongsTo(\App\Models\System\Company::class);
}
}

View File

@@ -13,7 +13,8 @@
"jenssegers/agent": "^2.6", "jenssegers/agent": "^2.6",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.3", "laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1" "laravel/tinker": "^2.10.1",
"spatie/laravel-permission": "^7.2"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",

150
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "0c5398ab8233c21548345608b86027cc", "content-hash": "a723334f883b537b67e4475890eb949e",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -3554,6 +3554,154 @@
}, },
"time": "2025-12-14T04:43:48+00:00" "time": "2025-12-14T04:43:48+00:00"
}, },
{
"name": "spatie/laravel-package-tools",
"version": "1.93.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
"reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7",
"reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.5",
"orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
"pestphp/pest": "^2.1|^3.1|^4.0",
"phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
"phpunit/phpunit": "^10.5|^11.5|^12.5",
"spatie/pest-plugin-test-time": "^2.2|^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\LaravelPackageTools\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Tools for creating Laravel packages",
"homepage": "https://github.com/spatie/laravel-package-tools",
"keywords": [
"laravel-package-tools",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
"source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-02-21T12:49:54+00:00"
},
{
"name": "spatie/laravel-permission",
"version": "7.2.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-permission.git",
"reference": "062b0cd8e3a1753fa7a53e468b918710004aa06b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/062b0cd8e3a1753fa7a53e468b918710004aa06b",
"reference": "062b0cd8e3a1753fa7a53e468b918710004aa06b",
"shasum": ""
},
"require": {
"illuminate/auth": "^12.0|^13.0",
"illuminate/container": "^12.0|^13.0",
"illuminate/contracts": "^12.0|^13.0",
"illuminate/database": "^12.0|^13.0",
"php": "^8.4",
"spatie/laravel-package-tools": "^1.0"
},
"require-dev": {
"larastan/larastan": "^3.9",
"laravel/passport": "^13.0",
"laravel/pint": "^1.0",
"orchestra/testbench": "^10.0|^11.0",
"pestphp/pest": "^3.0|^4.0",
"pestphp/pest-plugin-laravel": "^3.0|^4.1",
"phpstan/phpstan": "^2.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\Permission\\PermissionServiceProvider"
]
},
"branch-alias": {
"dev-main": "7.x-dev",
"dev-master": "7.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\Permission\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Permission handling for Laravel 12 and up",
"homepage": "https://github.com/spatie/laravel-permission",
"keywords": [
"acl",
"laravel",
"permission",
"permissions",
"rbac",
"roles",
"security",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/7.2.3"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-02-23T20:30:07+00:00"
},
{ {
"name": "symfony/clock", "name": "symfony/clock",
"version": "v8.0.0", "version": "v8.0.0",

202
config/permission.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Spatie\Permission\Models\Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttachedEvent
* \Spatie\Permission\Events\RoleDetachedEvent
* \Spatie\Permission\Events\PermissionAttachedEvent
* \Spatie\Permission\Events\PermissionDetachedEvent
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace Database\Factories; namespace Database\Factories\System;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;

View File

@@ -0,0 +1,137 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id(); // permission id
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id(); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->string('name'); // 公司名稱
$table->string('code', 20)->unique(); // 公司代碼(簡碼)
$table->string('tax_id', 20)->nullable(); // 統一編號
$table->string('contact_name', 100)->nullable(); // 聯絡人
$table->string('contact_phone', 50)->nullable(); // 聯絡電話
$table->string('contact_email')->nullable(); // 聯絡信箱
$table->tinyInteger('status')->default(1); // 1:啟用, 0:停用
$table->date('valid_until')->nullable(); // 合約期限
$table->text('note')->nullable(); // 備註
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('companies');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('machines', function (Blueprint $table) {
$table->foreignId('company_id')->nullable()->after('id')
->constrained('companies')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('machines', function (Blueprint $table) {
$table->dropConstrainedForeignId('company_id');
});
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignId('company_id')->nullable()->after('id')
->constrained('companies')->nullOnDelete();
$table->tinyInteger('status')->default(1)->after('role'); // 1:啟用, 0:停用
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropConstrainedForeignId('company_id');
$table->dropColumn('status');
});
}
};

View File

@@ -0,0 +1,28 @@
<?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::table('roles', function (Blueprint $table) {
$table->boolean('is_system')->default(false)->after('guard_name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('is_system');
});
}
};

View File

@@ -0,0 +1,28 @@
<?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::table('users', function (Blueprint $table) {
$table->string('email')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('email')->nullable(false)->change();
});
}
};

View File

@@ -27,19 +27,20 @@ class AdminUserSeeder extends Seeder
'name' => 'Admin', 'name' => 'Admin',
'email' => 'admin@star-cloud.com', 'email' => 'admin@star-cloud.com',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'admin',
]); ]);
$admin->assignRole('super-admin');
return; return;
} }
User::create([ $admin = User::create([
'username' => 'admin', 'username' => 'admin',
'name' => 'Admin', 'name' => 'Admin',
'email' => 'admin@star-cloud.com', 'email' => 'admin@star-cloud.com',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'admin',
]); ]);
$admin->assignRole('super-admin');
$this->command->info('Admin 帳號建立成功!'); $this->command->info('Admin 帳號建立成功!');
} }
} }

View File

@@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
$this->call([ $this->call([
RoleSeeder::class,
AdminUserSeeder::class, AdminUserSeeder::class,
MachineSeeder::class, MachineSeeder::class,
MemberSeeder::class, MemberSeeder::class,

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Illuminate\Support\Facades\Schema;
class RoleSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// 重設快取
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// 建立角色
Role::updateOrCreate(
['name' => 'super-admin'],
['is_system' => true]
);
Role::updateOrCreate(
['name' => 'tenant-admin'],
['is_system' => true]
);
}
}

248
lang/en.json Normal file
View File

@@ -0,0 +1,248 @@
{
"Account Settings": "Account Settings",
"Manage your profile information, security settings, and login history": "Manage your profile information, security settings, and login history",
"Profile Information": "Profile Information",
"Update your account's profile information and email address.": "Update your account's profile information and email address.",
"Update Password": "Update Password",
"Ensure your account is using a long, random password to stay secure.": "Ensure your account is using a long, random password to stay secure.",
"Delete Account": "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.": "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?": "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.": "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": "Login History",
"Name": "Name",
"Phone": "Phone",
"Email": "Email",
"Current Password": "Current Password",
"New Password": "New Password",
"Confirm Password": "Confirm Password",
"Save": "Save",
"Saved.": "Saved.",
"Update": "Update",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Danger Zone: Delete Account": "Danger Zone: Delete Account",
"Permanently Delete Account": "Permanently Delete Account",
"Password": "Password",
"Enter your password to confirm": "Enter your password to confirm",
"Dashboard": "Dashboard",
"Connectivity Status": "Connectivity Status",
"Real-time status monitoring": "Real-time status monitoring",
"LIVE": "LIVE",
"Online Machines": "Online Machines",
"Offline Machines": "Offline Machines",
"Alerts Pending": "Alerts Pending",
"Total Connected": "Total Connected",
"Monthly Transactions": "Monthly Transactions",
"Monthly cumulative revenue overview": "Monthly cumulative revenue overview",
"Today's Transactions": "Today's Transactions",
"vs Yesterday": "vs Yesterday",
"Yesterday": "Yesterday",
"Day Before": "Day Before",
"Machine Status List": "Machine Status List",
"Total items": "Total items: :count",
"Real-time monitoring across all machines": "Real-time monitoring across all machines",
"Quick search...": "Quick search...",
"Machine Info": "Machine Info",
"Running Status": "Running Status",
"Today Cumulative Sales": "Today Cumulative Sales",
"Current Stock": "Current Stock",
"Last Signal": "Last Signal",
"Alert Summary": "Alert Summary",
"Online": "Online",
"Offline": "Offline",
"Low Stock": "Low Stock",
"No alert summary": "No alert summary",
"No data available": "No data available",
"Showing :from to :to of :total items": "Showing :from to :to of :total items",
"Previous": "Previous",
"Next": "Next",
"Profile Settings": "Profile Settings",
"Profile": "Profile",
"Member Management": "Member Management",
"Member List": "Member List",
"Membership Tiers": "Membership Tiers",
"Deposit Bonus": "Deposit Bonus",
"Point Rules": "Point Rules",
"Gift Definitions": "Gift Definitions",
"Machine Management": "Machine Management",
"Machine Logs": "Machine Logs",
"Machine List": "Machine List",
"Machine Permissions": "Machine Permissions",
"Utilization Rate": "Utilization Rate",
"Expiry Management": "Expiry Management",
"Maintenance Records": "Maintenance Records",
"APP Management": "APP Management",
"UI Elements": "UI Elements",
"Helper": "Helper",
"Questionnaire": "Questionnaire",
"Games": "Games",
"Timer": "Timer",
"Warehouse Management": "Warehouse Management",
"Warehouse List (All)": "Warehouse List (All)",
"Warehouse List (Individual)": "Warehouse List (Individual)",
"Stock Management": "Stock Management",
"Transfers": "Transfers",
"Purchases": "Purchases",
"Replenishments": "Replenishments",
"Replenishment Records": "Replenishment Records",
"Machine Stock": "Machine Stock",
"Staff Stock": "Staff Stock",
"Returns": "Returns",
"Sales Management": "Sales Management",
"Sales Records": "Sales Records",
"Pickup Codes": "Pickup Codes",
"Orders": "Orders",
"Promotions": "Promotions",
"Pass Codes": "Pass Codes",
"Store Gifts": "Store Gifts",
"Analysis Management": "Analysis Management",
"Change Stock": "Change Stock",
"Machine Reports": "Machine Reports",
"Product Reports": "Product Reports",
"Survey Analysis": "Survey Analysis",
"Audit Management": "Audit Management",
"Purchase Audit": "Purchase Audit",
"Transfer Audit": "Transfer Audit",
"Replenishment Audit": "Replenishment Audit",
"Data Configuration": "Data Configuration",
"Product Management": "Product Management",
"Advertisement Management": "Advertisement Management",
"Admin Sellable Products": "Admin Sellable Products",
"Account Management": "Account Management",
"Sub Accounts": "Sub Accounts",
"Sub Account Roles": "Sub Account Roles",
"Point Settings": "Point Settings",
"Badge Settings": "Badge Settings",
"Remote Management": "Remote Management",
"Machine Restart": "Machine Restart",
"Card Reader Restart": "Card Reader Restart",
"Remote Checkout": "Remote Checkout",
"Remote Lock": "Remote Lock",
"Remote Change": "Remote Change",
"Remote Dispense": "Remote Dispense",
"Line Management": "Line Management",
"Line Members": "Line Members",
"Line Machines": "Line Machines",
"Line Products": "Line Products",
"Line Official Account": "Line Official Account",
"Line Orders": "Line Orders",
"Line Coupons": "Line Coupons",
"Reservation System": "Reservation System",
"Reservation Members": "Reservation Members",
"Store Management": "Store Management",
"Time Slots": "Time Slots",
"Venue Management": "Venue Management",
"Coupons": "Coupons",
"Reservations": "Reservations",
"Order Management": "Order Management",
"Special Permission": "Special Permission",
"Clear Stock": "Clear Stock",
"APK Versions": "APK Versions",
"Discord Notifications": "Discord Notifications",
"Permission Settings": "Permission Settings",
"APP Features": "APP Features",
"Sales": "Sales",
"Others": "Others",
"AI Prediction": "AI Prediction",
"Roles": "Roles",
"Role Management": "Role Management",
"Define and manage security roles for the system.": "Define and manage security roles for the system.",
"Add Role": "Add Role",
"Role Name": "Role Name",
"Type": "Type",
"Users": "Users",
"System Role": "System Role",
"System": "System",
"Custom": "Custom",
"Edit": "Edit",
"Are you sure you want to delete this role?": "Are you sure you want to delete this role?",
"Delete": "Delete",
"Protected": "Protected",
"No roles found.": "No roles found.",
"Create Role": "Create Role",
"Edit Role": "Edit Role",
"Enter role name": "Enter role name",
"No login history yet": "No login history yet",
"Signed in as": "Signed in as",
"Logout": "Logout",
"Joined": "Joined",
"Recent Login": "Recent Login",
"Total Logins": "Total Logins",
"Account Status": "Account Status",
"Active": "Active",
"Customer Management": "Customer Management",
"Manage all tenant accounts and validity": "Manage all tenant accounts and validity",
"Add Customer": "Add Customer",
"Total Customers": "Total Customers",
"Expired / Disabled": "Expired / Disabled",
"Search customers...": "Search customers...",
"All": "All",
"Disabled": "Disabled",
"Customer Info": "Customer Info",
"Accounts / Machines": "Accounts / Machines",
"Valid Until": "Valid Until",
"Actions": "Actions",
"Permanent": "Permanent",
"Are you sure to delete this customer?": "Are you sure to delete this customer?",
"No customers found": "No customers found",
"Edit Customer": "Edit Customer",
"Update Customer": "Update Customer",
"Create": "Create",
"Company Name": "Company Name",
"Company Code": "Company Code",
"Tax ID (Optional)": "Tax ID (Optional)",
"Status": "Status",
"Contact Name": "Contact Name",
"Contact Phone": "Contact Phone",
"Contact Email": "Contact Email",
"Notes": "Notes",
"Customer created successfully.": "Customer created successfully.",
"Customer updated successfully.": "Customer updated successfully.",
"Customer deleted successfully.": "Customer deleted successfully.",
"Cannot delete company with active accounts.": "Cannot delete company with active accounts.",
"Contract Until (Optional)": "Contract Until (Optional)",
"Company Information": "Company Information",
"Initial Admin Account": "Initial Admin Account",
"Optional": "Optional",
"Username": "Username",
"Enter login ID": "Enter login ID",
"Min 8 characters": "Min 8 characters",
"Admin display name": "Admin display name",
"Contact & Details": "Contact & Details",
"e.g. Taiwan Star": "e.g. Taiwan Star",
"e.g. TWSTAR": "e.g. TWSTAR",
"Manage administrative and tenant accounts": "Manage administrative and tenant accounts",
"Add Account": "Add Account",
"All Companies": "All Companies",
"User Info": "User Info",
"Belongs To": "Belongs To",
"Role": "Role",
"SYSTEM": "SYSTEM",
"No users found": "No users found",
"Data Configuration Permissions": "Data Configuration Permissions",
"Sales Permissions": "Sales Permissions",
"Machine Management Permissions": "Machine Management Permissions",
"Warehouse Permissions": "Warehouse Permissions",
"Analysis Permissions": "Analysis Permissions",
"Audit Permissions": "Audit Permissions",
"Remote Permissions": "Remote Permissions",
"Line Permissions": "Line Permissions",
"Company": "Company",
"Save Changes": "Save Changes",
"User": "User",
"Admin": "Admin",
"Super Admin": "Super Admin",
"e.g. John Doe": "e.g. John Doe",
"e.g. johndoe": "e.g. johndoe",
"Search users...": "Search users...",
"Admin Name": "Admin Name",
"New Password (leave blank to keep current)": "New Password (leave blank to keep current)",
"Are you sure you want to delete this account?": "Are you sure you want to delete this account?",
"Show": "Show",
"to": "to",
"of": "of",
"items": "items",
"Showing": "Showing"
}

View File

@@ -6,8 +6,8 @@
"Update Password": "パスワードの更新", "Update Password": "パスワードの更新",
"Ensure your account is using a long, random password to stay secure.": "セキュリティを維持するため、アカウントには長くランダムなパスワードを使用してください。", "Ensure your account is using a long, random password to stay secure.": "セキュリティを維持するため、アカウントには長くランダムなパスワードを使用してください。",
"Delete Account": "アカウントの削除", "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.": "アカウントが削除されると、そのすべてのリソースデータが永久に削除されます。アカウントを削除する前に、保持したいデータや情報をダウンロードしてください。", "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?": "本当にアカウントを削除してもよろしいですか?", "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.": "アカウントが削除されると、すべての関連データが永久に削除されます。アカウントの永久削除を確定するため、パスワードを入力してください。", "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": "ログイン履歴", "Login History": "ログイン履歴",
"Name": "氏名", "Name": "氏名",
@@ -27,34 +27,34 @@
"Enter your password to confirm": "確認のためパスワードを入力してください", "Enter your password to confirm": "確認のためパスワードを入力してください",
"Dashboard": "ダッシュボード", "Dashboard": "ダッシュボード",
"Connectivity Status": "接続ステータス", "Connectivity Status": "接続ステータス概況",
"Real-time status monitoring": "リアルタイムステータス監視", "Real-time status monitoring": "リアルタイムステータス監視",
"LIVE": "ライブ", "LIVE": "ライブ",
"Online Machines": "オンライン機台", "Online Machines": "オンライン機台",
"Offline Machines": "オフライン機台", "Offline Machines": "オフライン機台",
"Alerts Pending": "アラート待機中", "Alerts Pending": "アラート待機中",
"Total Connected": "接続数合計", "Total Connected": "接続数合計",
"Monthly Transactions": "今月の取引", "Monthly Transactions": "今月の取引統計",
"Monthly cumulative revenue overview": "今月の累計収益概要", "Monthly cumulative revenue overview": "今月の累計収益概要",
"Today's Transactions": "今日の取引", "Today's Transactions": "今日の取引",
"Yesterday's Transactions": "昨日の取引",
"Before Yesterday's Transactions": "一昨日の取引",
"vs Yesterday": "前日比", "vs Yesterday": "前日比",
"Machine Status List": "機台ステータスリスト", "Yesterday": "昨日",
"Day Before": "一昨日",
"Machine Status List": "機台稼働状況リスト",
"Total items": "合計 :count 件", "Total items": "合計 :count 件",
"Real-time monitoring across all machines": "全機台のリアルタイム監視", "Real-time monitoring across all machines": "全機台のリアルタイム監視",
"Quick search...": "クイック検索...", "Quick search...": "クイック検索...",
"Machine Info": "機台情報", "Machine Info": "機台情報",
"Running Status": "運行ステータス", "Running Status": "稼働状況",
"Today Cumulative Sales": "日累計売", "Today Cumulative Sales": "日累計売",
"Current Stock": "現在の在庫", "Current Stock": "現在の在庫",
"Last Communication": "最終信", "Last Signal": "最終信号時間",
"Alert Summary": "アラート概要", "Alert Summary": "アラート概要",
"Online": "オンライン", "Online": "オンライン",
"Offline": "オフライン", "Offline": "オフライン",
"Low Stock": "在庫少", "Low Stock": "在庫少",
"No alert summary": "アラートなし", "No alert summary": "アラートなし",
"No data available": "データがありません", "No data available": "データなし",
"Showing :from to :to of :total items": ":total 件中 :from から :to 件を表示", "Showing :from to :to of :total items": ":total 件中 :from から :to 件を表示",
"Previous": "前へ", "Previous": "前へ",
"Next": "次へ", "Next": "次へ",
@@ -108,7 +108,7 @@
"Replenishment Audit": "補充監査", "Replenishment Audit": "補充監査",
"Data Configuration": "データ設定", "Data Configuration": "データ設定",
"Product Management": "商品管理", "Product Management": "商品管理",
"Advertisement Management": "告管理", "Advertisement Management": "告管理",
"Admin Sellable Products": "管理者販売可能商品", "Admin Sellable Products": "管理者販売可能商品",
"Account Management": "アカウント管理", "Account Management": "アカウント管理",
"Sub Accounts": "サブアカウント", "Sub Accounts": "サブアカウント",
@@ -147,8 +147,23 @@
"Others": "その他", "Others": "その他",
"AI Prediction": "AI予測", "AI Prediction": "AI予測",
"Roles": "ロール", "Roles": "ロール",
"Yesterday": "昨日", "Role Management": "ロール管理",
"Day Before": "一昨日", "Define and manage security roles for the system.": "システムのセキュリティロールを定義および管理します。",
"Add Role": "ロールを追加",
"Role Name": "ロール名",
"Type": "タイプ",
"Users": "ユーザー数",
"System Role": "システムロール",
"System": "システム",
"Custom": "カスタム",
"Edit": "編集",
"Are you sure you want to delete this role?": "このロールを削除してもよろしいですか?",
"Delete": "削除",
"Protected": "保護済み",
"No roles found.": "ロールが見見つかりません。",
"Create Role": "ロールの作成",
"Edit Role": "ロールの編集",
"Enter role name": "ロール名を入力してください",
"No login history yet": "ログイン履歴はまだありません", "No login history yet": "ログイン履歴はまだありません",
"Signed in as": "ログイン中", "Signed in as": "ログイン中",
"Logout": "ログアウト", "Logout": "ログアウト",
@@ -156,5 +171,78 @@
"Recent Login": "最近のログイン", "Recent Login": "最近のログイン",
"Total Logins": "総ログイン数", "Total Logins": "総ログイン数",
"Account Status": "アカウント状態", "Account Status": "アカウント状態",
"Active": "アクティブ" "Active": "アクティブ",
"Customer Management": "客戶管理",
"Manage all tenant accounts and validity": "すべてのテナントアカウントと有効期限を管理します",
"Add Customer": "客戶を追加",
"Total Customers": "客戶総数",
"Expired / Disabled": "期限切れ / 停止中",
"Search customers...": "客戶を検索...",
"All": "すべて",
"Disabled": "停止中",
"Customer Info": "客戶情報",
"Accounts / Machines": "アカウント / 機台",
"Valid Until": "有効期限",
"Actions": "操作",
"Permanent": "永久認可",
"Are you sure to delete this customer?": "この客戶を削除してもよろしいですか?",
"No customers found": "客戶が見つかりません",
"Edit Customer": "客戶を編集",
"Update Customer": "客戶を更新",
"Create": "作成",
"Company Name": "会社名",
"Company Code": "会社コード",
"Tax ID (Optional)": "納税者番号 (任意)",
"Status": "ステータス",
"Contact Name": "連絡担当者名",
"Contact Phone": "連絡先電話番号",
"Contact Email": "連絡先メールアドレス",
"Notes": "備考",
"Customer created successfully.": "客戶が正常に作成されました。",
"Customer updated successfully.": "客戶が正常に更新されました。",
"Customer deleted successfully.": "客戶が正常に削除されました。",
"Cannot delete company with active accounts.": "アクティブなアカウントを持つ会社は削除できません。",
"Contract Until (Optional)": "契約期限 (任意)",
"Company Information": "会社情報",
"Initial Admin Account": "初期管理者アカウント",
"Optional": "任意",
"Username": "ユーザー名",
"Enter login ID": "ログインIDを入力してください",
"Min 8 characters": "最低8文字",
"Admin display name": "管理者表示名",
"Contact & Details": "連絡先と詳細",
"e.g. Taiwan Star": "例:台湾スター",
"e.g. TWSTAR": "例TWSTAR",
"Manage administrative and tenant accounts": "管理者およびテナントアカウントを管理します",
"Add Account": "アカウントを追加",
"All Companies": "すべての会社",
"User Info": "ユーザー情報",
"Belongs To": "所属",
"Role": "ロール",
"SYSTEM": "システムレベル",
"No users found": "ユーザーが見つかりません",
"Data Configuration Permissions": "データ設定権限",
"Sales Permissions": "販売管理権限",
"Machine Management Permissions": "機台管理權限",
"Warehouse Permissions": "倉庫管理權限",
"Analysis Permissions": "分析管理權限",
"Audit Permissions": "監査管理權限",
"Remote Permissions": "リモート管理權限",
"Line Permissions": "Line管理權限",
"Company": "所属客戶",
"Save Changes": "変更を保存",
"User": "一般ユーザー",
"Admin": "管理者",
"Super Admin": "スーパー管理者",
"e.g. John Doe": "例:山田太郎",
"e.g. johndoe": "例yamadataro",
"Search users...": "ユーザーを検索...",
"Admin Name": "管理者名",
"New Password (leave blank to keep current)": "新しいパスワード (変更しない場合は空欄)",
"Are you sure you want to delete this account?": "このアカウントを削除してもよろしいですか?",
"Show": "表示",
"to": "から",
"of": "件中",
"items": "個の項目",
"Showing": "表示中"
} }

View File

@@ -27,37 +27,35 @@
"Enter your password to confirm": "請輸入您的密碼以確認", "Enter your password to confirm": "請輸入您的密碼以確認",
"Dashboard": "儀表板", "Dashboard": "儀表板",
"Connectivity Status": "連狀態", "Connectivity Status": "連狀態概況",
"Real-time status monitoring": "即時運作狀態監控", "Real-time status monitoring": "即時監控機台連線動態",
"LIVE": "時", "LIVE": "時",
"Online Machines": "在線機台", "Online Machines": "在線機台",
"Offline Machines": "離線機台", "Offline Machines": "離線機台",
"Alerts Pending": "異常警報", "Alerts Pending": "待處理告警",
"Total Connected": "連線中總數", "Total Connected": "總計連線數",
"Monthly Transactions": "月交易", "Monthly Transactions": "月交易統計",
"Monthly cumulative revenue overview": "本月累計營收概況", "Monthly cumulative revenue overview": "本月累計營收概況",
"Today's Transactions": "今日交易", "Today's Transactions": "今日交易",
"Yesterday's Transactions": "昨日交易", "vs Yesterday": "昨日",
"Before Yesterday's Transactions": "前日交易",
"Yesterday": "昨日", "Yesterday": "昨日",
"Day Before": "前日", "Day Before": "前日",
"vs Yesterday": "比昨日", "Machine Status List": "機台運行狀態列表",
"Machine Status List": "機台狀態列表", "Total items": "總計 :count 項",
"Total items": "共 :count 筆", "Real-time monitoring across all machines": "跨機台即時狀態監控",
"Real-time monitoring across all machines": "全線機台即時監控",
"Quick search...": "快速搜尋...", "Quick search...": "快速搜尋...",
"Machine Info": "機台資訊", "Machine Info": "機台資訊",
"Running Status": "運行狀態", "Running Status": "運行狀態",
"Today Cumulative Sales": "日累積銷售", "Today Cumulative Sales": "日累積銷售",
"Current Stock": "前庫存", "Current Stock": "前庫存",
"Last Communication": "最後訊", "Last Signal": "最後訊號時間",
"Alert Summary": "警摘要", "Alert Summary": "警摘要",
"Online": "在線", "Online": "在線",
"Offline": "離線", "Offline": "離線",
"Low Stock": "庫存低", "Low Stock": "庫存低",
"No alert summary": "無異常摘要", "No alert summary": "暫無告警記錄",
"No data available": "目前尚無數據", "No data available": "暫無資料",
"Showing :from to :to of :total items": "顯示第 :from 到 :to ,共 :total ", "Showing :from to :to of :total items": "顯示第 :from 到 :to ,共 :total ",
"Previous": "上一頁", "Previous": "上一頁",
"Next": "下一頁", "Next": "下一頁",
"Profile Settings": "個人設定", "Profile Settings": "個人設定",
@@ -149,6 +147,23 @@
"Others": "其他功能", "Others": "其他功能",
"AI Prediction": "AI智能預測", "AI Prediction": "AI智能預測",
"Roles": "角色設定", "Roles": "角色設定",
"Role Management": "角色管理",
"Define and manage security roles for the system.": "定義與管理系統的安全角色。",
"Add Role": "新增角色",
"Role Name": "角色名稱",
"Type": "類型",
"Users": "使用者人數",
"System Role": "系統角色",
"System": "系統",
"Custom": "自定義",
"Edit": "編輯",
"Are you sure you want to delete this role?": "您確定要刪除此角色嗎?",
"Delete": "刪除",
"Protected": "受保護",
"No roles found.": "找不到角色資料。",
"Create Role": "建立角色",
"Edit Role": "編輯角色",
"Enter role name": "請輸入角色名稱",
"No login history yet": "尚無登入紀錄", "No login history yet": "尚無登入紀錄",
"Signed in as": "登入身份", "Signed in as": "登入身份",
"Logout": "登出", "Logout": "登出",
@@ -156,5 +171,78 @@
"Recent Login": "最近登入", "Recent Login": "最近登入",
"Total Logins": "總登入次數", "Total Logins": "總登入次數",
"Account Status": "帳號狀態", "Account Status": "帳號狀態",
"Active": "使用中" "Active": "使用中",
"Customer Management": "客戶管理",
"Manage all tenant accounts and validity": "管理所有租戶帳號與合約效期",
"Add Customer": "新增客戶",
"Total Customers": "客戶總數",
"Expired / Disabled": "已過期 / 停用",
"Search customers...": "搜尋客戶...",
"All": "全部",
"Disabled": "已停用",
"Customer Info": "客戶資訊",
"Accounts / Machines": "帳號 / 機台",
"Valid Until": "合約到期日",
"Actions": "操作",
"Permanent": "永久授權",
"Are you sure to delete this customer?": "您確定要刪除此客戶嗎?",
"No customers found": "找不到客戶資料",
"Edit Customer": "編輯客戶",
"Update Customer": "更新客戶",
"Create": "建立",
"Company Name": "公司名稱",
"Company Code": "公司代碼",
"Tax ID (Optional)": "統一編號 (選填)",
"Status": "狀態",
"Contact Name": "聯絡人姓名",
"Contact Phone": "聯絡人電話",
"Contact Email": "聯絡人信箱",
"Notes": "備註",
"Customer created successfully.": "客戶新增成功",
"Customer updated successfully.": "客戶更新成功",
"Customer deleted successfully.": "客戶刪除成功",
"Cannot delete company with active accounts.": "無法刪除仍有帳號的客戶",
"Contract Until (Optional)": "合約到期日 (選填)",
"Company Information": "公司資訊",
"Initial Admin Account": "初始管理帳號",
"Optional": "選填",
"Username": "使用者帳號",
"Enter login ID": "請輸入登入帳號",
"Min 8 characters": "至少 8 個字元",
"Admin display name": "管理員顯示名稱",
"Contact & Details": "聯絡資訊與詳情",
"e.g. Taiwan Star": "例如:台灣之星",
"e.g. TWSTAR": "例如TWSTAR",
"Manage administrative and tenant accounts": "管理系統管理者與租戶帳號",
"Add Account": "新增帳號",
"All Companies": "所有公司",
"User Info": "用戶資訊",
"Company": "所屬客戶",
"Belongs To": "所屬單位",
"Role": "角色",
"SYSTEM": "系統層級",
"No users found": "找不到用戶資料",
"Data Configuration Permissions": "資料設定權限",
"Sales Permissions": "銷售管理權限",
"Machine Management Permissions": "機台管理權限",
"Warehouse Permissions": "倉庫管理權限",
"Analysis Permissions": "分析管理權限",
"Audit Permissions": "稽核管理權限",
"Remote Permissions": "遠端管理權限",
"Line Permissions": "Line 管理權限",
"Save Changes": "儲存變更",
"User": "一般用戶",
"Admin": "管理員",
"Super Admin": "超級管理員",
"e.g. John Doe": "例如:張曉明",
"e.g. johndoe": "例如xiaoming",
"Search users...": "搜尋用戶...",
"Admin Name": "管理員姓名",
"New Password (leave blank to keep current)": "新密碼 (若不修改請留空)",
"Are you sure you want to delete this account?": "您確定要刪除此帳號嗎?",
"Show": "顯示",
"to": "至",
"of": "總計",
"items": "筆項目",
"Showing": "顯示第"
} }

View File

@@ -171,4 +171,34 @@
@apply text-slate-500 hover:bg-slate-100 hover:text-slate-900; @apply text-slate-500 hover:bg-slate-100 hover:text-slate-900;
@apply dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-cyan-400; @apply dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-cyan-400;
} }
/* Luxury Form Elements */
.luxury-input, .luxury-select {
@apply py-3 px-5 block w-full border-slate-200 rounded-xl text-sm font-bold text-slate-700 placeholder-slate-400 transition-all duration-200 outline-none border;
@apply dark:bg-slate-900/40 dark:border-slate-700 dark:text-slate-200 dark:placeholder-slate-500;
backdrop-filter: blur(4px);
}
.luxury-input:focus, .luxury-select:focus {
@apply ring-4 ring-cyan-500/10 border-cyan-500;
@apply dark:ring-cyan-500/20 dark:border-cyan-400/50;
}
/* Date Input Calendar Icon Optimization */
.luxury-input[type="date"]::-webkit-calendar-picker-indicator {
@apply cursor-pointer transition-opacity hover:opacity-100;
opacity: 0.6;
}
.dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.luxury-select {
@apply appearance-none pr-10;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2364748b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.85rem center;
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
} }

View File

@@ -0,0 +1,397 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
editing: false,
currentCompany: {
id: '',
name: '',
code: '',
tax_id: '',
contact_name: '',
contact_phone: '',
contact_email: '',
valid_until: '',
status: 1,
note: ''
},
openCreateModal() {
this.editing = false;
this.currentCompany = { id: '', name: '', code: '', tax_id: '', contact_name: '', contact_phone: '', contact_email: '', valid_until: '', status: 1, note: '' };
this.showModal = true;
},
openEditModal(company) {
this.editing = true;
this.currentCompany = { ...company };
this.showModal = true;
}
}">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Customer Management') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage all tenant accounts and validity') }}</p>
</div>
<button @click="openCreateModal()" class="btn-luxury-primary">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span>{{ __('Add Customer') }}</span>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
<p class="text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Total Customers') }}</p>
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->total() }}</p>
</div>
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 100ms">
<p class="text-sm font-black text-emerald-500 uppercase tracking-widest">{{ __('Active') }}</p>
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->where('status',
1)->count() }}</p>
</div>
<div class="luxury-card p-6 rounded-2xl animate-luxury-in" style="animation-delay: 200ms">
<p class="text-sm font-black text-rose-500 uppercase tracking-widest">{{ __('Expired / Disabled') }}</p>
<p class="text-3xl font-black text-slate-800 dark:text-white mt-2">{{ $companies->where('status',
0)->count() }}</p>
</div>
</div>
<!-- Table Section -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 300ms">
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-6">
<form action="{{ route('admin.permission.companies.index') }}" method="GET" 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" name="search" value="{{ request('search') }}"
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="{{ __('Search customers...') }}">
</form>
<div class="flex items-center gap-4">
<form action="{{ route('admin.permission.companies.index') }}" method="GET" class="flex items-center gap-2">
@if(request('search'))<input type="hidden" name="search" value="{{ request('search') }}">@endif
@if(request('status'))<input type="hidden" name="status" value="{{ request('status') }}">@endif
<select name="limit" onchange="this.form.submit()" class="luxury-select text-[11px] py-2 px-3">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
</select>
</form>
<div
class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50">
<a href="{{ route('admin.permission.companies.index') }}"
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300' }}">
{{ __('All') }}
</a>
<a href="{{ route('admin.permission.companies.index', ['status' => 1]) }}"
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === '1' ? 'bg-white dark:bg-emerald-500/10 text-emerald-500 shadow-lg shadow-emerald-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-emerald-500/80' }}">
{{ __('Active') }}
</a>
<a href="{{ route('admin.permission.companies.index', ['status' => 0]) }}"
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === '0' ? 'bg-white dark:bg-rose-500/10 text-rose-500 shadow-lg shadow-rose-500/10' : 'text-slate-400 dark:text-slate-500 hover:text-rose-500/80' }}">
{{ __('Disabled') }}
</a>
</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-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">
{{ __('Customer Info') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Status') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Accounts / Machines') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">
{{ __('Valid Until') }}</th>
<th
class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">
{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@forelse($companies as $company)
<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-4">
<div
class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 group-hover:bg-cyan-500 group-hover:text-white transition-all">
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2">
<path
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</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">{{
$company->name }}</span>
<span
class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-[0.15em]">{{
$company->code }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
@if($company->status)
<span
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
{{ __('Active') }}
</span>
@else
<span
class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-wider uppercase">
{{ __('Disabled') }}
</span>
@endif
</td>
<td class="px-6 py-6 text-center">
<div class="flex items-center justify-center gap-x-3">
<div class="flex flex-col items-center">
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $company->users_count }}</span>
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Users') }}</span>
</div>
<div class="w-px h-6 bg-slate-100 dark:bg-slate-800"></div>
<div class="flex flex-col items-center">
<span class="text-base font-extrabold text-slate-800 dark:text-white">{{ $company->machines_count }}</span>
<span class="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Machines') }}</span>
</div>
</div>
</td>
<td class="px-6 py-6 text-center">
<span
class="text-[13px] font-bold font-display tracking-widest {{ $company->valid_until && $company->valid_until->isPast() ? 'text-rose-500' : 'text-slate-600 dark:text-slate-300' }}">
{{ $company->valid_until ? $company->valid_until->format('Y/m/d') : __('Permanent') }}
</span>
</td>
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-x-2">
<button @click="openEditModal({{ json_encode($company) }})"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</button>
<form action="{{ route('admin.permission.companies.destroy', $company->id) }}"
method="POST"
onsubmit="return confirm('{{ addslashes(__('Are you sure to delete this customer?')) }}')"
class="inline">
@csrf @method('DELETE')
<button type="submit"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-24 text-center">
<div class="flex flex-col items-center">
<div
class="w-16 h-16 rounded-2xl bg-slate-50 dark:bg-slate-900 flex items-center justify-center text-slate-300 mb-4">
<svg class="size-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<p class="text-slate-400 font-bold">{{ __('No customers found') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $companies->links('vendor.pagination.luxury') }}
</div>
</div>
<!-- Create/Edit Modal (Simplified with standard Blade form for reliability) -->
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showModal = false">
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">&#8203;</span>
<div x-show="showModal" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block px-8 py-10 overflow-hidden text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight"
x-text="editing ? '{{ __('Edit Customer') }}' : '{{ __('Add Customer') }}'"></h3>
<button @click="showModal = false"
class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form
:action="editing ? '{{ route('admin.permission.companies.index') }}/' + currentCompany.id : '{{ route('admin.permission.companies.store') }}'"
method="POST" class="space-y-6">
@csrf
<template x-if="editing">
@method('PUT')
</template>
<!-- Profile Section -->
<div class="space-y-6">
<div class="flex items-center gap-3">
<div class="h-6 w-1 bg-cyan-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{
__('Company Information') }}</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Company Name') }}</label>
<input type="text" name="name" x-model="currentCompany.name" required
class="luxury-input w-full" placeholder="{{ __('e.g. Taiwan Star') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Company Code') }}</label>
<input type="text" name="code" x-model="currentCompany.code" required
class="luxury-input w-full" placeholder="{{ __('e.g. TWSTAR') }}">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Tax ID (Optional)') }}</label>
<input type="text" name="tax_id" x-model="currentCompany.tax_id"
class="luxury-input w-full">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Contract Until (Optional)') }}</label>
<input type="date" name="valid_until" x-model="currentCompany.valid_until"
class="luxury-input w-full">
</div>
</div>
</div>
<!-- Admin Account Section (Account Creation) - Only show when creating -->
<div x-show="!editing" class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-3">
<div class="h-6 w-1 bg-emerald-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">
{{ __('Initial Admin Account') }}
<span class="text-[10px] text-slate-400 font-normal normal-case tracking-normal ml-2">({{ __('Optional') }})</span>
</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Username') }}</label>
<input type="text" name="admin_username" class="luxury-input w-full"
placeholder="{{ __('Enter login ID') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Password') }}</label>
<input type="password" name="admin_password" class="luxury-input w-full"
placeholder="{{ __('Min 8 characters') }}">
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Admin Name') }}</label>
<input type="text" name="admin_name" class="luxury-input w-full" placeholder="{{ __('Admin display name') }}">
</div>
</div>
<!-- Contact Section -->
<div class="space-y-6 pt-6 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-3">
<div class="h-6 w-1 bg-amber-500 rounded-full"></div>
<h4 class="text-xs font-black text-slate-800 dark:text-white uppercase tracking-widest">{{
__('Contact & Details') }}</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Contact Name') }}</label>
<input type="text" name="contact_name" x-model="currentCompany.contact_name"
class="luxury-input w-full">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Contact Phone') }}</label>
<input type="text" name="contact_phone" x-model="currentCompany.contact_phone"
class="luxury-input w-full">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Status') }}</label>
<select name="status" x-model="currentCompany.status" class="luxury-select">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{
__('Notes') }}</label>
<textarea name="note" x-model="currentCompany.note" rows="2"
class="luxury-input w-full h-[46px] min-h-[46px] py-3"></textarea>
</div>
</div>
</div>
<div class="flex justify-end gap-x-4 pt-8">
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-12">
<span x-text="editing ? '{{ __('Update') }}' : '{{ __('Create') }}'"></span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -122,13 +122,23 @@
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<h2 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Status List') }}</h2> <h2 class="text-2xl font-black 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"> <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)]) }} {{ __('Total items', ['count' => $machines->total()]) }}
</span> </span>
</div> </div>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time monitoring across all machines') }}</p> <p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time monitoring across all machines') }}</p>
</div> </div>
<div class="flex items-center gap-x-4"> <form action="{{ route('admin.dashboard') }}" method="GET" class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-x-2">
<span class="text-[11px] font-black text-slate-400 uppercase tracking-widest hidden sm:inline">{{ __('Show') }}:</span>
<select name="limit" onchange="this.form.submit()" class="luxury-select py-3 min-w-[100px]">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
</select>
</div>
<div class="relative group"> <div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none"> <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"> <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">
@@ -136,25 +146,25 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg> </svg>
</span> </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...') }}"> <input type="text" name="search" value="{{ request('search') }}" 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> </form>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0"> <table class="w-full text-left border-separate border-spacing-y-0">
<thead> <thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/30"> <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-[12px] 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-[12px] 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-[12px] 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-[12px] 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-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Signal') }}</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> <th class="px-6 py-4 text-[12px] 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> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50"> <tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@forelse($latestActivities as $activity) @forelse($machines as $machine)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300"> <tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-6"> <td class="px-6 py-6">
<div class="flex items-center gap-x-5"> <div class="flex items-center gap-x-5">
@@ -162,18 +172,24 @@
<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> <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>
<div class="flex flex-col"> <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-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $machine->name }}</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> <span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $machine->serial_no }})</span>
</div> </div>
</div> </div>
</td> </td>
<td class="px-6 py-6 text-center"> <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"> @if($machine->status === 'online')
{{ $activity->machine->status === 'online' ? __('Online') : __('Offline') }} <span class="inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
</span> {{ __('Online') }}
</span>
@else
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">
{{ __('Offline') }}
</span>
@endif
</td> </td>
<td class="px-6 py-6 text-center"> <td class="px-6 py-6 text-center">
<span class="text-xl font-black text-slate-900 dark:text-slate-100">$ 0</span> <span class="text-base font-extrabold text-slate-900 dark:text-slate-100">$ 0</span>
</td> </td>
<td class="px-6 py-6"> <td class="px-6 py-6">
<div class="flex flex-col items-center gap-y-2.5"> <div class="flex flex-col items-center gap-y-2.5">
@@ -184,7 +200,9 @@
</div> </div>
</td> </td>
<td class="px-6 py-6 text-center"> <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> <span class="text-[13px] text-slate-600 dark:text-slate-200 font-bold font-display tracking-widest bg-slate-50 dark:bg-slate-800/50 px-4 py-1.5 rounded-lg border border-slate-100 dark:border-slate-700/50">
{{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') : '---' }}
</span>
</td> </td>
<td class="px-6 py-6 text-right"> <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> <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>
@@ -199,27 +217,8 @@
</table> </table>
</div> </div>
<!-- Table Footer: Pagination --> <div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
<div class="mt-8 flex items-center justify-between border-t border-slate-100 dark:border-slate-800 pt-6"> {{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
<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 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>
<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> </div>
</div> </div>

View File

@@ -0,0 +1,259 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
editing: false,
currentUser: {
id: '',
name: '',
username: '',
email: '',
phone: '',
company_id: '',
role: 'user',
status: 1
},
openCreateModal() {
this.editing = false;
this.currentUser = { id: '', name: '', username: '', email: '', phone: '', company_id: '', role: 'user', status: 1 };
this.showModal = true;
},
openEditModal(user) {
this.editing = true;
this.currentUser = { ...user };
this.showModal = true;
}
}">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Account Management') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage administrative and tenant accounts') }}</p>
</div>
<button @click="openCreateModal()" class="btn-luxury-primary">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
<span>{{ __('Add Account') }}</span>
</button>
</div>
<!-- Filters & Search -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.permission.accounts') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div class="flex flex-col md:flex-row items-start md:items-center gap-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" name="search" value="{{ request('search') }}" class="py-3 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search users...') }}">
</div>
@if(auth()->user()->isSystemAdmin())
<select name="company_id" onchange="this.form.submit()" class="luxury-select w-full md:w-auto min-w-[200px]">
<option value="">{{ __('All Companies') }}</option>
@foreach($companies as $company)
<option value="{{ $company->id }}" {{ request('company_id') == $company->id ? 'selected' : '' }}>{{ $company->name }}</option>
@endforeach
</select>
@endif
<select name="limit" onchange="this.form.submit()" class="luxury-select w-full md:w-auto text-[11px]">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
<option value="100" {{ request('limit') == 100 ? 'selected' : '' }}>100</option>
</select>
</div>
<div class="flex items-center gap-3">
<!-- 移除了冗餘的 Filter 按鈕,下拉選單具備自動提交功能 -->
</div>
</form>
<div class="overflow-x-auto mt-8">
<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-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('User Info') }}</th>
@if(auth()->user()->isSystemAdmin())
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Belongs To') }}</th>
@endif
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Role') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/50">
@forelse($users as $user)
<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-4">
<div class="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 overflow-hidden">
@if($user->avatar)
<img src="{{ Storage::url($user->avatar) }}" class="w-full h-full object-cover">
@else
<span class="text-sm font-black">{{ substr($user->name, 0, 1) }}</span>
@endif
</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">{{ $user->name }}</span>
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-0.5 tracking-[0.15em]">{{ $user->username }} @if($user->email) {{ $user->email }} @endif</span>
</div>
</div>
</td>
@if(auth()->user()->isSystemAdmin())
<td class="px-6 py-6">
@if($user->company)
<span class="text-xs font-bold text-slate-600 dark:text-slate-300 tracking-tight">{{ $user->company->name }}</span>
@else
<span class="text-xs font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{ __('SYSTEM') }}</span>
@endif
</td>
@endif
<td class="px-6 py-6 text-center">
@foreach($user->roles as $role)
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-[11px] 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-widest">
{{ $role->name }}
</span>
@endforeach
</td>
<td class="px-6 py-6 text-center">
@if($user->status)
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-wider uppercase">
{{ __('Active') }}
</span>
@else
<span class="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-black bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 border border-slate-200 dark:border-slate-700 tracking-wider uppercase">
{{ __('Disabled') }}
</span>
@endif
</td>
<td class="px-6 py-6 text-right">
<div class="flex justify-end items-center gap-2">
<button @click="openEditModal({{ json_encode($user) }})" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
</button>
<form action="{{ route('admin.permission.accounts.destroy', $user->id) }}" method="POST" onsubmit="return confirm('{{ addslashes(__('Are you sure you want to delete this account?')) }}')">
@csrf
@method('DELETE')
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="{{ auth()->user()->isSystemAdmin() ? 5 : 4 }}" class="px-6 py-24 text-center">
<p class="text-slate-400 font-bold">{{ __('No users found') }}</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $users->links('vendor.pagination.luxury') }}
</div>
</div>
<!-- User Modal -->
<div x-show="showModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showModal = false"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">&#8203;</span>
<div x-show="showModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block px-8 py-10 overflow-hidden text-left align-bottom transition-all transform luxury-card rounded-3xl dark:bg-slate-900 border-slate-200/50 dark:border-slate-700/50 shadow-2xl sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight" x-text="editing ? '{{ __('Edit Account') }}' : '{{ __('Add Account') }}'"></h3>
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<form :action="editing ? '{{ url('admin/permission/accounts') }}/' + currentUser.id : '{{ route('admin.permission.accounts.store') }}'" method="POST" class="space-y-6">
@csrf
<template x-if="editing">
<input type="hidden" name="_method" value="PUT">
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Full Name') }}</label>
<input type="text" name="name" x-model="currentUser.name" required class="luxury-input" placeholder="{{ __('e.g. John Doe') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Username') }}</label>
<input type="text" name="username" x-model="currentUser.username" required class="luxury-input" placeholder="{{ __('e.g. johndoe') }}">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Email') }}</label>
<input type="email" name="email" x-model="currentUser.email" class="luxury-input" placeholder="{{ __('john@example.com') }}">
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Phone') }}</label>
<input type="text" name="phone" x-model="currentUser.phone" class="luxury-input">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Role') }}</label>
<select name="role" x-model="currentUser.role" class="luxury-select">
<option value="user">{{ __('User') }}</option>
<option value="admin">{{ __('Admin') }}</option>
<option value="super-admin">{{ __('Super Admin') }}</option>
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Status') }}</label>
<select name="status" x-model="currentUser.status" class="luxury-select">
<option value="1">{{ __('Active') }}</option>
<option value="0">{{ __('Disabled') }}</option>
</select>
</div>
</div>
@if(auth()->user()->isSystemAdmin())
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Company') }}</label>
<select name="company_id" x-model="currentUser.company_id" class="luxury-select">
<option value="">{{ __('SYSTEM') }}</option>
@foreach($companies as $company)
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</select>
</div>
@endif
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">
<span x-text="editing ? '{{ __('New Password (leave blank to keep current)') }}' : '{{ __('Password') }}'"></span>
</label>
<input type="password" name="password" :required="!editing" class="luxury-input" placeholder="••••••••">
</div>
<div class="flex justify-end gap-x-4 pt-8">
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-12">
<span x-text="editing ? '{{ __('Update') }}' : '{{ __('Create') }}'"></span>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -13,10 +13,22 @@
<div class="p-6 text-gray-900"> <div class="p-6 text-gray-900">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-medium">機台列表</h3> <h3 class="text-lg font-medium">機台列表</h3>
<div class="flex space-x-2"> <div class="flex items-center gap-x-4">
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a> <form method="GET" action="{{ route('admin.machines.index') }}" class="flex items-center gap-x-2">
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a> @if(request('status'))
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a> <input type="hidden" name="status" value="{{ request('status') }}">
@endif
<select name="limit" onchange="this.form.submit()" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
@foreach([10, 25, 50, 100] as $size)
<option value="{{ $size }}" {{ request('limit') == $size ? 'selected' : '' }}>{{ $size }} {{ __('Items') }}</option>
@endforeach
</select>
</form>
<div class="flex space-x-2">
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a>
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a>
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a>
</div>
</div> </div>
</div> </div>
@@ -71,7 +83,7 @@
</div> </div>
<div class="mt-6"> <div class="mt-6">
{{ $machines->links() }} {{ $machines->links('vendor.pagination.luxury') }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -40,6 +40,14 @@
<option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>Error</option> <option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>Error</option>
</select> </select>
</div> </div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">筆數</label>
<select name="limit" class="h-9 text-[11px] font-black bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 rounded-lg focus:ring-cyan-500/20 focus:border-cyan-500 transition-all">
@foreach([10, 20, 50, 100] as $size)
<option value="{{ $size }}" {{ request('limit', 20) == $size ? 'selected' : '' }}>{{ $size }} </option>
@endforeach
</select>
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="submit" class="btn-luxury-primary"> <button type="submit" class="btn-luxury-primary">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg> <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
@@ -110,7 +118,7 @@
@if($logs->hasPages()) @if($logs->hasPages())
<div class="mt-6"> <div class="mt-6">
{{ $logs->appends(request()->query())->links() }} {{ $logs->links('vendor.pagination.luxury') }}
</div> </div>
@endif @endif
</div> </div>

View File

@@ -0,0 +1,163 @@
@extends('layouts.admin')
@section('content')
<div class="space-y-6" x-data="{
showModal: false,
isEdit: false,
roleId: '',
roleName: '',
modalTitle: '{{ __('Create Role') }}',
openModal(edit = false, id = '', name = '') {
this.isEdit = edit;
this.roleId = id;
this.roleName = name;
this.modalTitle = edit ? '{{ __('Edit Role') }}' : '{{ __('Create Role') }}';
this.showModal = true;
}
}">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display">{{ __('Roles') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Define and manage security roles for the system.') }}</p>
</div>
<button @click="openModal()" class="btn-luxury-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
<span>{{ __('Add Role') }}</span>
</button>
</div>
<!-- Toolbar -->
<div class="luxury-card rounded-3xl p-6 mb-6 animate-luxury-in" style="animation-delay: 100ms">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
<form action="{{ route('admin.permission.roles') }}" method="GET" 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" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full md:w-80 luxury-input" placeholder="{{ __('Search roles...') }}">
</form>
<form action="{{ route('admin.permission.roles') }}" method="GET" class="flex items-center gap-3">
@if(request('search'))<input type="hidden" name="search" value="{{ request('search') }}">@endif
<label class="text-[11px] font-black text-slate-400 uppercase tracking-widest">{{ __('Show') }}</label>
<select name="limit" onchange="this.form.submit()" class="luxury-select py-1.5 px-3 text-[11px] min-w-[70px]">
<option value="10" {{ request('limit') == 10 ? 'selected' : '' }}>10</option>
<option value="25" {{ request('limit') == 25 ? 'selected' : '' }}>25</option>
<option value="50" {{ request('limit') == 50 ? 'selected' : '' }}>50</option>
</select>
</form>
</div>
</div>
<!-- Roles List -->
<div class="luxury-card rounded-3xl p-8 animate-luxury-in" style="animation-delay: 200ms">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-slate-100 dark:border-slate-700">
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Role Name') }}</th>
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Type') }}</th>
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest text-center">{{ __('Users') }}</th>
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800">
@forelse($roles as $role)
<tr class="hover:bg-slate-50/80 dark:hover:bg-slate-800/50 transition-colors group">
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<span class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ $role->name }}</span>
@if($role->is_system)
<span class="p-1.5 bg-cyan-50 dark:bg-cyan-900/30 text-cyan-600 dark:text-cyan-400 rounded-lg tooltip" title="{{ __('System Role') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</span>
@endif
</div>
</td>
<td class="px-6 py-5">
@if($role->is_system)
<span class="px-2.5 py-1 text-[11px] font-black uppercase tracking-tight bg-slate-100 dark:bg-slate-700 text-slate-500 dark:text-slate-400 rounded-full">
{{ __('System') }}
</span>
@else
<span class="px-2.5 py-1 text-[11px] font-black uppercase tracking-tight bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 rounded-full">
{{ __('Custom') }}
</span>
@endif
</td>
<td class="px-6 py-5 text-center">
<span class="text-sm font-black text-slate-600 dark:text-slate-400">{{ $role->users_count }}</span>
</td>
<td class="px-6 py-5 text-right">
<div class="flex items-center justify-end gap-2">
@if(!$role->is_system)
<button @click="openModal(true, '{{ $role->id }}', '{{ $role->name }}')" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20 tooltip" title="{{ __('Edit') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</button>
<form action="{{ route('admin.permission.roles.destroy', $role->id) }}" method="POST" @submit.prevent="if(confirm('{{ __('Are you sure you want to delete this role?') }}')) $el.submit()" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-rose-500 hover:bg-rose-500/5 transition-all border border-transparent hover:border-rose-500/20 tooltip" title="{{ __('Delete') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4.5 h-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
</button>
</form>
@else
<span class="px-3 py-1 text-[11px] font-black uppercase tracking-widest text-slate-400 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200/50 dark:border-slate-700/50 italic">{{ __('Protected') }}</span>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-20 text-center">
<div class="flex flex-col items-center justify-center gap-4 text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 opacity-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>
<p class="text-lg font-bold">{{ __('No roles found.') }}</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
{{ $roles->links('vendor.pagination.luxury') }}
</div>
</div>
<!-- Modal -->
<template x-if="showModal">
<div class="fixed inset-0 z-[60] flex items-center justify-center p-4">
<div @click="showModal = false" class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"></div>
<div class="relative w-full max-w-lg bg-white dark:bg-slate-900 rounded-2xl shadow-2xl overflow-hidden animate-luxury-in">
<div class="flex items-center justify-between p-6 border-b border-slate-100 dark:border-slate-800">
<h3 class="text-xl font-black text-slate-800 dark:text-white" x-text="modalTitle"></h3>
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<form :action="isEdit ? '{{ route('admin.permission.roles') }}/' + roleId : '{{ route('admin.permission.roles.store') }}'" method="POST" class="p-6 space-y-6">
@csrf
<template x-if="isEdit"><input type="hidden" name="_method" value="PUT"></template>
<div class="space-y-2">
<label class="text-sm font-black text-slate-700 dark:text-slate-300">{{ __('Role Name') }}</label>
<input type="text" name="name" x-model="roleName" required class="luxury-input w-full" :placeholder="'{{ __('Enter role name') }}'">
</div>
<div class="flex items-center justify-end gap-3 mt-8">
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-6 py-2.5 rounded-xl font-bold">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-8 py-2.5 rounded-xl font-bold">{{ __('Save Changes') }}</button>
</div>
</form>
</div>
</div>
</template>
</div>
@endsection

View File

@@ -52,113 +52,136 @@
$links[] = $foundModule; $links[] = $foundModule;
} }
// 2. 處理具體頁面 // 2. 處理中間層級與具體頁面
$segments = explode('.', $routeName); $segments = explode('.', $routeName);
$lastSegment = end($segments); $lastSegment = end($segments);
// 嘗試翻譯最後一個片段作為頁面名稱 // 如果路由有四段以上 (如 admin.permission.companies.index),則處理中間一段
$pageLabel = match($lastSegment) { if (count($segments) >= 4) {
'edit' => __('Profile'), // 專門處理 profile.edit $midSegment = $segments[2];
'index' => null, // 通常 index 代表列表,如果在大模組下則不重複顯示 $midLabel = match($midSegment) {
'logs' => __('Machine Logs'), 'companies' => __('Customer Management'),
'permissions' => __('Machine Permissions'),
'utilization' => __('Utilization Rate'),
'expiry' => __('Expiry Management'),
'maintenance' => __('Maintenance Records'),
'ui-elements' => __('UI Elements'),
'helper' => __('Helper'),
'questionnaire' => __('Questionnaire'),
'games' => __('Games'),
'timer' => __('Timer'),
'personal' => __('Warehouse List (Individual)'),
'stock-management' => __('Stock Management'),
'transfers' => __('Transfers'),
'purchases' => __('Purchases'),
'replenishments' => __('Replenishments'),
'replenishment-records' => __('Replenishment Records'),
'machine-stock' => __('Machine Stock'),
'staff-stock' => __('Staff Stock'),
'returns' => __('Returns'),
'pickup-codes' => __('Pickup Codes'),
'orders' => __('Orders'),
'promotions' => __('Promotions'),
'pass-codes' => __('Pass Codes'),
'store-gifts' => __('Store Gifts'),
'change-stock' => __('Change Stock'),
'machine-reports' => __('Machine Reports'),
'product-reports' => __('Product Reports'),
'survey-analysis' => __('Survey Analysis'),
'products' => __('Product Management'),
'advertisements' => __('Advertisement Management'),
'admin-products' => __('Admin Sellable Products'),
'accounts' => __('Account Management'),
'sub-accounts' => __('Sub Accounts'),
'sub-account-roles' => __('Sub Account Roles'),
'points' => __('Point Settings'),
'badges' => __('Badge Settings'),
'restart' => __('Machine Restart'),
'restart-card-reader' => __('Card Reader Restart'),
'checkout' => __('Remote Checkout'),
'lock' => __('Remote Lock'),
'change' => __('Remote Change'),
'dispense' => __('Remote Dispense'),
'official-account' => __('Line Official Account'),
'coupons' => __('Line Coupons'),
'stores' => __('Store Management'),
'time-slots' => __('Time Slots'),
'venues' => __('Venue Management'),
'reservations' => __('Reservations'),
'clear-stock' => __('Clear Stock'),
'apk-versions' => __('APK Versions'),
'discord-notifications' => __('Discord Notifications'),
'app-features' => __('APP Features'),
'roles' => __('Roles'),
'others' => __('Others'),
'ai-prediction' => __('AI Prediction'),
'create' => __('Create'),
'show' => __('Show'),
'members' => __('Member List'), // 處理 admin.members.index 這種情況
default => null,
};
// 如果匹配不到,嘗試處理一些特殊的 index 標籤
if (!$pageLabel && $lastSegment === 'index') {
$pageLabel = match($segments[count($segments)-2] ?? '') {
'members' => __('Member List'), 'members' => __('Member List'),
'machines' => __('Machine List'), 'machines' => __('Machine List'),
'warehouses' => __('Warehouse List'), 'warehouses' => __('Warehouse List'),
'sales' => __('Sales Records'), 'sales' => __('Sales Records'),
default => null, default => null,
}; };
if ($midLabel) {
$links[] = [
'label' => $midLabel,
'url' => '#', // 如果有需要可以導向 index 路由
'active' => $lastSegment === 'index'
];
}
} }
if ($pageLabel) { // 3. 處理最後一個動作/頁面
$links[] = [ if ($lastSegment !== 'index') {
'label' => $pageLabel, $pageLabel = match($lastSegment) {
'url' => route($routeName), 'edit' => __('Edit'),
'active' => true 'create' => __('Create'),
]; 'show' => __('Detail'),
'logs' => __('Machine Logs'),
'permissions' => __('Machine Permissions'),
'utilization' => __('Utilization Rate'),
'expiry' => __('Expiry Management'),
'maintenance' => __('Maintenance Records'),
'ui-elements' => __('UI Elements'),
'helper' => __('Helper'),
'questionnaire' => __('Questionnaire'),
'games' => __('Games'),
'timer' => __('Timer'),
'personal' => __('Warehouse List (Individual)'),
'stock-management' => __('Stock Management'),
'transfers' => __('Transfers'),
'purchases' => __('Purchases'),
'replenishments' => __('Replenishments'),
'replenishment-records' => __('Replenishment Records'),
'machine-stock' => __('Machine Stock'),
'staff-stock' => __('Staff Stock'),
'returns' => __('Returns'),
'pickup-codes' => __('Pickup Codes'),
'orders' => __('Orders'),
'promotions' => __('Promotions'),
'pass-codes' => __('Pass Codes'),
'store-gifts' => __('Store Gifts'),
'change-stock' => __('Change Stock'),
'machine-reports' => __('Machine Reports'),
'product-reports' => __('Product Reports'),
'survey-analysis' => __('Survey Analysis'),
'products' => __('Product Management'),
'advertisements' => __('Advertisement Management'),
'admin-products' => __('Admin Sellable Products'),
'accounts' => __('Account Management'),
'sub-accounts' => __('Sub Accounts'),
'sub-account-roles' => __('Sub Account Roles'),
'points' => __('Point Settings'),
'badges' => __('Badge Settings'),
'restart' => __('Machine Restart'),
'restart-card-reader' => __('Card Reader Restart'),
'checkout' => __('Remote Checkout'),
'lock' => __('Remote Lock'),
'change' => __('Remote Change'),
'dispense' => __('Remote Dispense'),
'official-account' => __('Line Official Account'),
'coupons' => __('Line Coupons'),
'stores' => __('Store Management'),
'time-slots' => __('Time Slots'),
'venues' => __('Venue Management'),
'reservations' => __('Reservations'),
'clear-stock' => __('Clear Stock'),
'apk-versions' => __('APK Versions'),
'discord-notifications' => __('Discord Notifications'),
'app-features' => __('APP Features'),
'roles' => __('Roles'),
'others' => __('Others'),
'ai-prediction' => __('AI Prediction'),
'data-config' => __('Data Configuration Permissions'),
'sales' => __('Sales Permissions'),
'machines' => __('Machine Management Permissions'),
'warehouses' => __('Warehouse Permissions'),
'analysis' => __('Analysis Permissions'),
'audit' => __('Audit Permissions'),
'remote' => __('Remote Permissions'),
'line' => __('Line Permissions'),
default => null,
};
if ($pageLabel) {
$links[] = [
'label' => $pageLabel,
'url' => route($routeName),
'active' => true
];
}
} }
// 確保最後一個 link 是 active 的 // 確保最後一個 link 是 active 的
if (!empty($links)) { if (!empty($links)) {
$links[count($links) - 1]['active'] = true; $links[count($links) - 1]['active'] = true;
// 如果倒數第二個也是同個頁面(例如 Dashboard > Dashboard),則移除重複 // 檢查是否有相鄰重複的 Label (例如 Dashboard > Dashboard)
if (count($links) > 1 && $links[count($links)-1]['label'] === $links[count($links)-2]['label']) { if (count($links) > 1 && $links[count($links)-1]['label'] === $links[count($links)-2]['label']) {
array_pop($links); array_pop($links);
$links[count($links)-1]['active'] = true; $links[count($links)-1]['active'] = true;
} }
} }
} }
} }
@endphp @endphp
<nav {{ $attributes->merge(['class' => 'flex', 'aria-label' => 'Breadcrumb']) }}> <nav {{ $attributes->merge(['class' => 'flex', 'aria-label' => 'Breadcrumb']) }}>
<ol class="flex items-center whitespace-nowrap min-w-0 w-full"> <ol class="flex items-center whitespace-nowrap min-w-0 w-full">
@foreach($links as $link) @foreach($links as $link)
<li class="flex items-center text-sm {{ $link['active'] ? 'font-semibold text-slate-800 dark:text-slate-200' : 'text-slate-500 dark:text-slate-400' }} {{ !$loop->last ? 'shrink-0' : 'truncate' }}"> @php
@if(!$link['active'] && $link['url'] !== '#') $isActive = $link['active'] ?? false;
<a class="hover:text-cyan-600 transition-colors" href="{{ $link['url'] }}"> $url = $link['url'] ?? '#';
@endphp
<li class="flex items-center text-sm {{ $isActive ? 'font-semibold text-slate-800 dark:text-slate-200' : 'text-slate-500 dark:text-slate-400' }} {{ !$loop->last ? 'shrink-0' : 'truncate' }}">
@if(!$isActive && $url !== '#')
<a class="hover:text-cyan-600 transition-colors" href="{{ $url }}">
{{ $link['label'] }} {{ $link['label'] }}
</a> </a>
@else @else

View File

@@ -84,19 +84,19 @@
<li x-data="{ open: localStorage.getItem('menu_warehouses') === 'true' || {{ request()->routeIs('admin.warehouses.*') ? 'true' : 'false' }} }"> <li x-data="{ open: localStorage.getItem('menu_warehouses') === 'true' || {{ request()->routeIs('admin.warehouses.*') ? 'true' : 'false' }} }">
<button type="button" @click="open = !open; localStorage.setItem('menu_warehouses', open)" class="luxury-nav-item w-full text-start group"> <button type="button" @click="open = !open; localStorage.setItem('menu_warehouses', open)" class="luxury-nav-item w-full text-start group">
<svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.warehouses.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg> <svg class="w-4 h-4 shrink-0 transition-colors {{ request()->routeIs('admin.warehouses.*') ? 'text-cyan-500' : 'text-slate-400 group-hover:text-cyan-500 dark:text-slate-400 dark:group-hover:text-cyan-400' }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
倉庫管理 {{ __('Warehouse 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> <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> </button>
<div x-show="open" x-collapse> <div x-show="open" x-collapse>
<ul class="luxury-submenu"> <ul class="luxury-submenu">
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">倉庫列表()</a></li> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.index') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.index') }}">{{ __('Warehouse List (All)') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">倉庫列表()</a></li> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.personal') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.personal') }}">{{ __('Warehouse List (Individual)') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">庫存管理單</a></li> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.stock-management') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.stock-management') }}">{{ __('Stock Management') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">調撥單</a></li> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.transfers') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.transfers') }}">{{ __('Transfers') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">採購單</a></li> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.purchases') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.purchases') }}">{{ __('Purchases') }}</a></li>
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">機台補貨單</a></li> <li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.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') }}">{{ __('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.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') }}">{{ __('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.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') }}">{{ __('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') }}">{{ __('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.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> <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> </ul>
@@ -167,7 +167,6 @@
<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.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.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.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-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.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.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>
@@ -236,6 +235,7 @@
</div> </div>
</li> </li>
@role('super-admin')
{{-- 14. 特殊權限 --}} {{-- 14. 特殊權限 --}}
<li x-data="{ open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }} }"> <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"> <button type="button" @click="open = !open; localStorage.setItem('menu_special_permission', open)" class="luxury-nav-item w-full text-start group">
@@ -251,7 +251,9 @@
</ul> </ul>
</div> </div>
</li> </li>
@endrole
@role('super-admin')
{{-- 15. 權限設定 --}} {{-- 15. 權限設定 --}}
<li x-data="{ open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }} }"> <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"> <button type="button" @click="open = !open; localStorage.setItem('menu_permissions', open)" class="luxury-nav-item w-full text-start group">
@@ -261,6 +263,9 @@
</button> </button>
<div x-show="open" x-collapse> <div x-show="open" x-collapse>
<ul class="luxury-submenu"> <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.companies.*') ? '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.companies.index') }}">{{ __('Customer 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.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.permission.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.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.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.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.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.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>
@@ -270,9 +275,9 @@
<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.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.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.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.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> <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> </ul>
</div> </div>
</li> </li>
@endrole

View File

@@ -0,0 +1,59 @@
@if ($paginator->hasPages())
<div class="flex flex-col sm:flex-row items-center justify-between w-full gap-4">
{{-- Total Items Info --}}
<div class="order-2 sm:order-1">
<p class="text-[10px] sm:text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest text-center sm:text-left">
<span class="hidden xs:inline">{{ __('Showing') }}</span>
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->firstItem() }}</span>
{{ __('to') }}
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->lastItem() }}</span>
<span class="hidden xs:inline">{{ __('of') }}</span>
<span class="inline xs:hidden">/</span>
<span class="text-slate-600 dark:text-slate-300">{{ $paginator->total() }}</span>
{{ __('items') }}
</p>
</div>
<div class="order-1 sm:order-2 flex items-center gap-x-1.5 sm:gap-x-2">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-slate-50 dark:bg-slate-800 text-slate-300 dark:text-slate-600 border border-slate-100 dark:border-slate-800 cursor-not-allowed">
<svg class="size-3.5 sm:size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm: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 shadow-sm group">
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
<span class="hidden xxs:inline">{{ __('Previous') }}</span>
</a>
@endif
{{-- Unified Quick Jump Selection (Desktop & Mobile) --}}
<div class="relative group">
<select onchange="window.location.href = this.value" class="h-9 pl-4 pr-10 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-[11px] sm:text-xs font-black text-slate-600 dark:text-slate-300 appearance-none focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 outline-none transition-all cursor-pointer shadow-sm hover:border-slate-300 dark:hover:border-slate-600">
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
<option value="{{ $paginator->url($i) }}" {{ $i == $paginator->currentPage() ? 'selected' : '' }}>
{{ $i }} / {{ $paginator->lastPage() }}
</option>
@endfor
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none opacity-40 group-hover:opacity-100 transition-opacity">
<svg class="size-3.5 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7"/></svg>
</div>
</div>
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm: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 shadow-sm group">
<span class="hidden xxs:inline">{{ __('Next') }}</span>
<svg class="size-3.5 sm:size-4 text-cyan-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
</a>
@else
<span class="h-9 inline-flex items-center gap-x-1.5 sm:gap-x-2 px-3 sm:px-4 rounded-xl text-[10px] sm:text-xs font-black bg-slate-50 dark:bg-slate-800 text-slate-300 dark:text-slate-600 border border-slate-100 dark:border-slate-800 cursor-not-allowed">
<span class="hidden xxs:inline">{{ __('Next') }}</span>
<svg class="size-3.5 sm:size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"/></svg>
</span>
@endif
</div>
</div>
@endif

View File

@@ -4,18 +4,18 @@ use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Web Routes | Web Routes
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Here is where you can register web routes for your application. These | Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will | routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great! | be assigned to the "web" middleware group. Make something great!
| |
*/ */
// Multi-language switch // Multi-language switch
Route::get('lang/{locale}', [App\Http\Controllers\System\LanguageController::class, 'switch'])->name('lang.switch'); Route::get('lang/{locale}', [App\Http\Controllers\System\LanguageController::class , 'switch'])->name('lang.switch');
Route::get('/', function () { Route::get('/', function () {
return redirect()->route('login'); return redirect()->route('login');
@@ -25,159 +25,178 @@ Route::get('/dashboard', function () {
return redirect()->route('admin.dashboard'); return redirect()->route('admin.dashboard');
})->middleware(['auth', 'verified'])->name('dashboard'); })->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(function () { Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name('admin.')->group(function () {
// 1. 儀表板 // 1. 儀表板
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard'); Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class , 'index'])->name('dashboard');
// 2. 會員管理 // 2. 會員管理
Route::resource('members', App\Http\Controllers\MemberController::class)->only(['index']); Route::resource('members', App\Http\Controllers\MemberController::class)->only(['index']);
Route::resource('membership-tiers', App\Http\Controllers\Admin\MembershipTierController::class)->except(['show', 'create', 'edit']); Route::resource('membership-tiers', App\Http\Controllers\Admin\MembershipTierController::class)->except(['show', 'create', 'edit']);
Route::resource('deposit-bonus-rules', App\Http\Controllers\Admin\DepositBonusRuleController::class)->except(['show', 'create', 'edit']); Route::resource('deposit-bonus-rules', App\Http\Controllers\Admin\DepositBonusRuleController::class)->except(['show', 'create', 'edit']);
Route::resource('point-rules', App\Http\Controllers\Admin\PointRuleController::class)->except(['show', 'create', 'edit']); Route::resource('point-rules', App\Http\Controllers\Admin\PointRuleController::class)->except(['show', 'create', 'edit']);
Route::resource('gift-definitions', App\Http\Controllers\Admin\GiftDefinitionController::class)->except(['show', 'create', 'edit']); Route::resource('gift-definitions', App\Http\Controllers\Admin\GiftDefinitionController::class)->except(['show', 'create', 'edit']);
// 3. 機台管理 // 3. 機台管理
Route::prefix('machines')->name('machines.')->group(function () { Route::prefix('machines')->name('machines.')->group(function () {
Route::get('/logs', [App\Http\Controllers\Admin\MachineController::class, 'logs'])->name('logs'); Route::get('/logs', [App\Http\Controllers\Admin\MachineController::class , 'logs'])->name('logs');
Route::get('/permissions', [App\Http\Controllers\Admin\MachineController::class, 'permissions'])->name('permissions'); Route::get('/permissions', [App\Http\Controllers\Admin\MachineController::class , 'permissions'])->name('permissions');
Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class, 'utilization'])->name('utilization'); Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization');
Route::get('/expiry', [App\Http\Controllers\Admin\MachineController::class, 'expiry'])->name('expiry'); Route::get('/expiry', [App\Http\Controllers\Admin\MachineController::class , 'expiry'])->name('expiry');
Route::get('/maintenance', [App\Http\Controllers\Admin\MachineController::class, 'maintenance'])->name('maintenance'); Route::get('/maintenance', [App\Http\Controllers\Admin\MachineController::class , 'maintenance'])->name('maintenance');
}
);
Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);
// 4. APP管理
Route::prefix('app')->name('app.')->group(function () {
Route::get('/ui-elements', [App\Http\Controllers\Admin\AppConfigController::class , 'uiElements'])->name('ui-elements');
Route::get('/helper', [App\Http\Controllers\Admin\AppConfigController::class , 'helper'])->name('helper');
Route::get('/questionnaire', [App\Http\Controllers\Admin\AppConfigController::class , 'questionnaire'])->name('questionnaire');
Route::get('/games', [App\Http\Controllers\Admin\AppConfigController::class , 'games'])->name('games');
Route::get('/timer', [App\Http\Controllers\Admin\AppConfigController::class , 'timer'])->name('timer');
}
);
Route::get('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class , 'index'])->name('app-configs.index');
Route::put('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class , 'update'])->name('app-configs.update');
// 5. 倉庫管理
Route::prefix('warehouses')->name('warehouses.')->group(function () {
Route::get('/', [App\Http\Controllers\Admin\WarehouseController::class , 'index'])->name('index');
Route::get('/personal', [App\Http\Controllers\Admin\WarehouseController::class , 'personal'])->name('personal');
Route::get('/stock-management', [App\Http\Controllers\Admin\WarehouseController::class , 'stockManagement'])->name('stock-management');
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class , 'transfers'])->name('transfers');
Route::get('/purchases', [App\Http\Controllers\Admin\WarehouseController::class , 'purchases'])->name('purchases');
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class , 'replenishments'])->name('replenishments');
Route::get('/replenishment-records', [App\Http\Controllers\Admin\WarehouseController::class , 'replenishmentRecords'])->name('replenishment-records');
Route::get('/replenishment-records-all', [App\Http\Controllers\Admin\WarehouseController::class , 'replenishmentRecordsAll'])->name('replenishment-records-all');
Route::get('/machine-stock', [App\Http\Controllers\Admin\WarehouseController::class , 'machineStock'])->name('machine-stock');
Route::get('/staff-stock', [App\Http\Controllers\Admin\WarehouseController::class , 'staffStock'])->name('staff-stock');
Route::get('/returns', [App\Http\Controllers\Admin\WarehouseController::class , 'returns'])->name('returns');
}
);
// 6. 銷售管理
Route::prefix('sales')->name('sales.')->group(function () {
Route::get('/', [App\Http\Controllers\Admin\SalesController::class , 'index'])->name('index');
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class , 'pickupCodes'])->name('pickup-codes');
Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class , 'orders'])->name('orders');
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class , 'promotions'])->name('promotions');
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class , 'passCodes'])->name('pass-codes');
Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class , 'storeGifts'])->name('store-gifts');
}
);
// 7. 分析管理
Route::prefix('analysis')->name('analysis.')->group(function () {
Route::get('/change-stock', [App\Http\Controllers\Admin\AnalysisController::class , 'changeStock'])->name('change-stock');
Route::get('/machine-reports', [App\Http\Controllers\Admin\AnalysisController::class , 'machineReports'])->name('machine-reports');
Route::get('/product-reports', [App\Http\Controllers\Admin\AnalysisController::class , 'productReports'])->name('product-reports');
Route::get('/survey-analysis', [App\Http\Controllers\Admin\AnalysisController::class , 'surveyAnalysis'])->name('survey-analysis');
}
);
// 8. 稽核管理
Route::prefix('audit')->name('audit.')->group(function () {
Route::get('/purchases', [App\Http\Controllers\Admin\AuditController::class , 'purchases'])->name('purchases');
Route::get('/transfers', [App\Http\Controllers\Admin\AuditController::class , 'transfers'])->name('transfers');
Route::get('/replenishments', [App\Http\Controllers\Admin\AuditController::class , 'replenishments'])->name('replenishments');
}
);
// 9. 資料設定
Route::prefix('data-config')->name('data-config.')->group(function () {
Route::get('/products', [App\Http\Controllers\Admin\DataConfigController::class , 'products'])->name('products');
Route::get('/advertisements', [App\Http\Controllers\Admin\DataConfigController::class , 'advertisements'])->name('advertisements');
Route::get('/admin-products', [App\Http\Controllers\Admin\DataConfigController::class , 'adminProducts'])->name('admin-products');
Route::get('/sub-accounts', [App\Http\Controllers\Admin\DataConfigController::class , 'subAccounts'])->name('sub-accounts');
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\DataConfigController::class , 'subAccountRoles'])->name('sub-account-roles');
Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class , 'points'])->name('points');
Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class , 'badges'])->name('badges');
}
);
// 10. 遠端管理
Route::prefix('remote')->name('remote.')->group(function () {
Route::get('/stock', [App\Http\Controllers\Admin\RemoteController::class , 'stock'])->name('stock');
Route::get('/restart', [App\Http\Controllers\Admin\RemoteController::class , 'restart'])->name('restart');
Route::get('/restart-card-reader', [App\Http\Controllers\Admin\RemoteController::class , 'restartCardReader'])->name('restart-card-reader');
Route::get('/checkout', [App\Http\Controllers\Admin\RemoteController::class , 'checkout'])->name('checkout');
Route::get('/lock', [App\Http\Controllers\Admin\RemoteController::class , 'lock'])->name('lock');
Route::get('/change', [App\Http\Controllers\Admin\RemoteController::class , 'change'])->name('change');
Route::get('/dispense', [App\Http\Controllers\Admin\RemoteController::class , 'dispense'])->name('dispense');
}
);
// 11. Line管理
Route::prefix('line')->name('line.')->group(function () {
Route::get('/members', [App\Http\Controllers\Admin\LineController::class , 'members'])->name('members');
Route::get('/machines', [App\Http\Controllers\Admin\LineController::class , 'machines'])->name('machines');
Route::get('/products', [App\Http\Controllers\Admin\LineController::class , 'products'])->name('products');
Route::get('/official-account', [App\Http\Controllers\Admin\LineController::class , 'officialAccount'])->name('official-account');
Route::get('/orders', [App\Http\Controllers\Admin\LineController::class , 'orders'])->name('orders');
Route::get('/coupons', [App\Http\Controllers\Admin\LineController::class , 'coupons'])->name('coupons');
}
);
// 12. 預約系統
Route::prefix('reservation')->name('reservation.')->group(function () {
Route::get('/members', [App\Http\Controllers\Admin\ReservationController::class , 'members'])->name('members');
Route::get('/stores', [App\Http\Controllers\Admin\ReservationController::class , 'stores'])->name('stores');
Route::get('/time-slots', [App\Http\Controllers\Admin\ReservationController::class , 'timeSlots'])->name('time-slots');
Route::get('/venues', [App\Http\Controllers\Admin\ReservationController::class , 'venues'])->name('venues');
Route::get('/coupons', [App\Http\Controllers\Admin\ReservationController::class , 'coupons'])->name('coupons');
Route::get('/reservations', [App\Http\Controllers\Admin\ReservationController::class , 'reservations'])->name('reservations');
Route::get('/orders', [App\Http\Controllers\Admin\ReservationController::class , 'orders'])->name('orders');
}
);
// 13. 特殊權限管理
Route::prefix('special-permission')->name('special-permission.')->group(function () {
Route::get('/clear-stock', [App\Http\Controllers\Admin\SpecialPermissionController::class , 'clearStock'])->name('clear-stock');
Route::get('/apk-versions', [App\Http\Controllers\Admin\SpecialPermissionController::class , 'apkVersions'])->name('apk-versions');
Route::get('/discord-notifications', [App\Http\Controllers\Admin\SpecialPermissionController::class , 'discordNotifications'])->name('discord-notifications');
}
);
// 14. 權限設定
Route::prefix('permission')->name('permission.')->group(function () {
Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit']);
Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'accounts'])->name('accounts');
Route::post('/accounts', [App\Http\Controllers\Admin\PermissionController::class , 'storeAccount'])->name('accounts.store');
Route::put('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateAccount'])->name('accounts.update');
Route::delete('/accounts/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyAccount'])->name('accounts.destroy');
Route::get('/app-features', [App\Http\Controllers\Admin\PermissionController::class , 'appFeatures'])->name('app-features');
Route::get('/data-config', [App\Http\Controllers\Admin\PermissionController::class , 'dataConfig'])->name('data-config');
Route::get('/sales', [App\Http\Controllers\Admin\PermissionController::class , 'sales'])->name('sales');
Route::get('/machines', [App\Http\Controllers\Admin\PermissionController::class , 'machines'])->name('machines');
Route::get('/warehouses', [App\Http\Controllers\Admin\PermissionController::class , 'warehouses'])->name('warehouses');
Route::get('/analysis', [App\Http\Controllers\Admin\PermissionController::class , 'analysis'])->name('analysis');
Route::get('/audit', [App\Http\Controllers\Admin\PermissionController::class , 'audit'])->name('audit');
Route::get('/remote', [App\Http\Controllers\Admin\PermissionController::class , 'remote'])->name('remote');
Route::get('/line', [App\Http\Controllers\Admin\PermissionController::class , 'line'])->name('line');
Route::get('/roles', [App\Http\Controllers\Admin\PermissionController::class , 'roles'])->name('roles');
Route::post('/roles', [App\Http\Controllers\Admin\PermissionController::class , 'storeRole'])->name('roles.store');
Route::put('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'updateRole'])->name('roles.update');
Route::delete('/roles/{id}', [App\Http\Controllers\Admin\PermissionController::class , 'destroyRole'])->name('roles.destroy');
Route::get('/others', [App\Http\Controllers\Admin\PermissionController::class , 'others'])->name('others');
Route::get('/ai-prediction', [App\Http\Controllers\Admin\PermissionController::class , 'aiPrediction'])->name('ai-prediction');
}
);
// 主題設定
Route::post('/theme', [App\Http\Controllers\Admin\ThemeController::class , 'update'])->name('theme.update');
}); });
Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);
// 4. APP管理
Route::prefix('app')->name('app.')->group(function () {
Route::get('/ui-elements', [App\Http\Controllers\Admin\AppConfigController::class, 'uiElements'])->name('ui-elements');
Route::get('/helper', [App\Http\Controllers\Admin\AppConfigController::class, 'helper'])->name('helper');
Route::get('/questionnaire', [App\Http\Controllers\Admin\AppConfigController::class, 'questionnaire'])->name('questionnaire');
Route::get('/games', [App\Http\Controllers\Admin\AppConfigController::class, 'games'])->name('games');
Route::get('/timer', [App\Http\Controllers\Admin\AppConfigController::class, 'timer'])->name('timer');
});
Route::get('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class, 'index'])->name('app-configs.index');
Route::put('/app-configs', [App\Http\Controllers\Admin\AppConfigController::class, 'update'])->name('app-configs.update');
// 5. 倉庫管理
Route::prefix('warehouses')->name('warehouses.')->group(function () {
Route::get('/', [App\Http\Controllers\Admin\WarehouseController::class, 'index'])->name('index');
Route::get('/personal', [App\Http\Controllers\Admin\WarehouseController::class, 'personal'])->name('personal');
Route::get('/stock-management', [App\Http\Controllers\Admin\WarehouseController::class, 'stockManagement'])->name('stock-management');
Route::get('/transfers', [App\Http\Controllers\Admin\WarehouseController::class, 'transfers'])->name('transfers');
Route::get('/purchases', [App\Http\Controllers\Admin\WarehouseController::class, 'purchases'])->name('purchases');
Route::get('/replenishments', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishments'])->name('replenishments');
Route::get('/replenishment-records', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentRecords'])->name('replenishment-records');
Route::get('/replenishment-records-all', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentRecordsAll'])->name('replenishment-records-all');
Route::get('/machine-stock', [App\Http\Controllers\Admin\WarehouseController::class, 'machineStock'])->name('machine-stock');
Route::get('/staff-stock', [App\Http\Controllers\Admin\WarehouseController::class, 'staffStock'])->name('staff-stock');
Route::get('/returns', [App\Http\Controllers\Admin\WarehouseController::class, 'returns'])->name('returns');
});
// 6. 銷售管理
Route::prefix('sales')->name('sales.')->group(function () {
Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index');
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'pickupCodes'])->name('pickup-codes');
Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders');
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions');
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes');
Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class, 'storeGifts'])->name('store-gifts');
});
// 7. 分析管理
Route::prefix('analysis')->name('analysis.')->group(function () {
Route::get('/change-stock', [App\Http\Controllers\Admin\AnalysisController::class, 'changeStock'])->name('change-stock');
Route::get('/machine-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'machineReports'])->name('machine-reports');
Route::get('/product-reports', [App\Http\Controllers\Admin\AnalysisController::class, 'productReports'])->name('product-reports');
Route::get('/survey-analysis', [App\Http\Controllers\Admin\AnalysisController::class, 'surveyAnalysis'])->name('survey-analysis');
});
// 8. 稽核管理
Route::prefix('audit')->name('audit.')->group(function () {
Route::get('/purchases', [App\Http\Controllers\Admin\AuditController::class, 'purchases'])->name('purchases');
Route::get('/transfers', [App\Http\Controllers\Admin\AuditController::class, 'transfers'])->name('transfers');
Route::get('/replenishments', [App\Http\Controllers\Admin\AuditController::class, 'replenishments'])->name('replenishments');
});
// 9. 資料設定
Route::prefix('data-config')->name('data-config.')->group(function () {
Route::get('/products', [App\Http\Controllers\Admin\DataConfigController::class, 'products'])->name('products');
Route::get('/advertisements', [App\Http\Controllers\Admin\DataConfigController::class, 'advertisements'])->name('advertisements');
Route::get('/admin-products', [App\Http\Controllers\Admin\DataConfigController::class, 'adminProducts'])->name('admin-products');
Route::get('/accounts', [App\Http\Controllers\Admin\DataConfigController::class, 'accounts'])->name('accounts');
Route::get('/sub-accounts', [App\Http\Controllers\Admin\DataConfigController::class, 'subAccounts'])->name('sub-accounts');
Route::get('/sub-account-roles', [App\Http\Controllers\Admin\DataConfigController::class, 'subAccountRoles'])->name('sub-account-roles');
Route::get('/points', [App\Http\Controllers\Admin\DataConfigController::class, 'points'])->name('points');
Route::get('/badges', [App\Http\Controllers\Admin\DataConfigController::class, 'badges'])->name('badges');
});
// 10. 遠端管理
Route::prefix('remote')->name('remote.')->group(function () {
Route::get('/stock', [App\Http\Controllers\Admin\RemoteController::class, 'stock'])->name('stock');
Route::get('/restart', [App\Http\Controllers\Admin\RemoteController::class, 'restart'])->name('restart');
Route::get('/restart-card-reader', [App\Http\Controllers\Admin\RemoteController::class, 'restartCardReader'])->name('restart-card-reader');
Route::get('/checkout', [App\Http\Controllers\Admin\RemoteController::class, 'checkout'])->name('checkout');
Route::get('/lock', [App\Http\Controllers\Admin\RemoteController::class, 'lock'])->name('lock');
Route::get('/change', [App\Http\Controllers\Admin\RemoteController::class, 'change'])->name('change');
Route::get('/dispense', [App\Http\Controllers\Admin\RemoteController::class, 'dispense'])->name('dispense');
});
// 11. Line管理
Route::prefix('line')->name('line.')->group(function () {
Route::get('/members', [App\Http\Controllers\Admin\LineController::class, 'members'])->name('members');
Route::get('/machines', [App\Http\Controllers\Admin\LineController::class, 'machines'])->name('machines');
Route::get('/products', [App\Http\Controllers\Admin\LineController::class, 'products'])->name('products');
Route::get('/official-account', [App\Http\Controllers\Admin\LineController::class, 'officialAccount'])->name('official-account');
Route::get('/orders', [App\Http\Controllers\Admin\LineController::class, 'orders'])->name('orders');
Route::get('/coupons', [App\Http\Controllers\Admin\LineController::class, 'coupons'])->name('coupons');
});
// 12. 預約系統
Route::prefix('reservation')->name('reservation.')->group(function () {
Route::get('/members', [App\Http\Controllers\Admin\ReservationController::class, 'members'])->name('members');
Route::get('/stores', [App\Http\Controllers\Admin\ReservationController::class, 'stores'])->name('stores');
Route::get('/time-slots', [App\Http\Controllers\Admin\ReservationController::class, 'timeSlots'])->name('time-slots');
Route::get('/venues', [App\Http\Controllers\Admin\ReservationController::class, 'venues'])->name('venues');
Route::get('/coupons', [App\Http\Controllers\Admin\ReservationController::class, 'coupons'])->name('coupons');
Route::get('/reservations', [App\Http\Controllers\Admin\ReservationController::class, 'reservations'])->name('reservations');
Route::get('/orders', [App\Http\Controllers\Admin\ReservationController::class, 'orders'])->name('orders');
});
// 13. 特殊權限管理
Route::prefix('special-permission')->name('special-permission.')->group(function () {
Route::get('/clear-stock', [App\Http\Controllers\Admin\SpecialPermissionController::class, 'clearStock'])->name('clear-stock');
Route::get('/apk-versions', [App\Http\Controllers\Admin\SpecialPermissionController::class, 'apkVersions'])->name('apk-versions');
Route::get('/discord-notifications', [App\Http\Controllers\Admin\SpecialPermissionController::class, 'discordNotifications'])->name('discord-notifications');
});
// 14. 權限設定
Route::prefix('permission')->name('permission.')->group(function () {
Route::get('/app-features', [App\Http\Controllers\Admin\PermissionController::class, 'appFeatures'])->name('app-features');
Route::get('/data-config', [App\Http\Controllers\Admin\PermissionController::class, 'dataConfig'])->name('data-config');
Route::get('/sales', [App\Http\Controllers\Admin\PermissionController::class, 'sales'])->name('sales');
Route::get('/machines', [App\Http\Controllers\Admin\PermissionController::class, 'machines'])->name('machines');
Route::get('/warehouses', [App\Http\Controllers\Admin\PermissionController::class, 'warehouses'])->name('warehouses');
Route::get('/analysis', [App\Http\Controllers\Admin\PermissionController::class, 'analysis'])->name('analysis');
Route::get('/audit', [App\Http\Controllers\Admin\PermissionController::class, 'audit'])->name('audit');
Route::get('/remote', [App\Http\Controllers\Admin\PermissionController::class, 'remote'])->name('remote');
Route::get('/line', [App\Http\Controllers\Admin\PermissionController::class, 'line'])->name('line');
Route::get('/roles', [App\Http\Controllers\Admin\PermissionController::class, 'roles'])->name('roles');
Route::get('/others', [App\Http\Controllers\Admin\PermissionController::class, 'others'])->name('others');
Route::get('/ai-prediction', [App\Http\Controllers\Admin\PermissionController::class, 'aiPrediction'])->name('ai-prediction');
});
// 主題設定
Route::post('/theme', [App\Http\Controllers\Admin\ThemeController::class, 'update'])->name('theme.update');
});
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::get('/profile', [ProfileController::class , 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::patch('/profile', [ProfileController::class , 'update'])->name('profile.update');
Route::post("/profile/avatar", [ProfileController::class, "updateAvatar"])->name("profile.avatar"); Route::post("/profile/avatar", [ProfileController::class , "updateAvatar"])->name("profile.avatar");
}); });
require __DIR__.'/auth.php'; require __DIR__ . '/auth.php';
// 測試路由 (需非正式環境或有特別權限控管) // 測試路由 (需非正式環境或有特別權限控管)
Route::prefix('test')->name('test.')->group(function () { Route::prefix('test')->name('test.')->group(function () {
Route::get('/social-login', [App\Http\Controllers\SocialLoginTestController::class, 'index'])->name('social-login'); Route::get('/social-login', [App\Http\Controllers\SocialLoginTestController::class , 'index'])->name('social-login');
Route::get('/line/callback', [App\Http\Controllers\SocialLoginTestController::class, 'lineCallback'])->name('line.callback'); Route::get('/line/callback', [App\Http\Controllers\SocialLoginTestController::class , 'lineCallback'])->name('line.callback');
}); });

View File

@@ -0,0 +1,68 @@
<?php
namespace Tests\Feature;
use App\Models\Machine\Machine;
use App\Models\System\Company;
use App\Models\System\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TenantIsolationTest extends TestCase
{
use RefreshDatabase;
public function test_tenant_can_only_see_their_own_machines()
{
// 建立兩家公司
$companyA = Company::create(['name' => 'Company A', 'code' => 'COA']);
$companyB = Company::create(['name' => 'Company B', 'code' => 'COB']);
// 建立各自的機台
Machine::create(['name' => 'Machine A', 'company_id' => $companyA->id]);
Machine::create(['name' => 'Machine B', 'company_id' => $companyB->id]);
// 建立租戶 A 的使用者並登入
$userA = User::factory()->create(['company_id' => $companyA->id]);
$this->actingAs($userA);
// 驗證 Eloquent 查詢是否被過濾
$machines = Machine::all();
$this->assertCount(1, $machines);
$this->assertEquals('Machine A', $machines->first()->name);
$this->assertFalse($machines->contains('name', 'Machine B'));
}
public function test_system_admin_can_see_all_machines()
{
// 建立兩家公司
$companyA = Company::create(['name' => 'Company A', 'code' => 'COA']);
$companyB = Company::create(['name' => 'Company B', 'code' => 'COB']);
// 建立各自的機台
Machine::create(['name' => 'Machine A', 'company_id' => $companyA->id]);
Machine::create(['name' => 'Machine B', 'company_id' => $companyB->id]);
// 建立系統管理員 (company_id = null) 並登入
$admin = User::factory()->create(['company_id' => null]);
$this->actingAs($admin);
// 驗證 Eloquent 查詢未被過濾
$machines = Machine::all();
$this->assertCount(2, $machines);
}
public function test_creating_machine_automatically_sets_company_id_for_tenant()
{
$companyA = Company::create(['name' => 'Company A', 'code' => 'COA']);
$userA = User::factory()->create(['company_id' => $companyA->id]);
$this->actingAs($userA);
// 建立新機台(不指定 company_id
$machine = Machine::create(['name' => 'New Machine']);
$this->assertEquals($companyA->id, $machine->company_id);
}
}