[FEAT] 優化部署流程:加入 RoleSeeder 與 AdminUserSeeder,並實作權限系統基礎架構與多租戶隔離機制
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 48s
This commit is contained in:
@@ -68,7 +68,10 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
- [ ] 是否使用了正確的 `rounded-2xl` (或更圓) 的導角?
|
||||
- [ ] 所有的圖示是否一致使用 `lucide-react` 風格?
|
||||
- [ ] 卡片是否有適當的間距 (通常為 `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)
|
||||
|
||||
@@ -80,6 +83,102 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
|
||||
- **格式**: `#機台編號 動作內容` (例如 `#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]
|
||||
> **開發新功能前,必須確認 `app.css` 中的 `.btn-luxury-*` 系列組件是否滿足需求。**
|
||||
|
||||
@@ -94,7 +94,9 @@ jobs:
|
||||
php artisan migrate --force &&
|
||||
php artisan optimize:clear &&
|
||||
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
|
||||
|
||||
|
||||
125
app/Http/Controllers/Admin/CompanyController.php
Normal file
125
app/Http/Controllers/Admin/CompanyController.php
Normal 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.'));
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,34 @@ use Illuminate\Http\Request;
|
||||
|
||||
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');
|
||||
$activeMachines = Machine::where('status', 'online')->count();
|
||||
$alertsPending = Machine::where('status', 'error')->count();
|
||||
$memberCount = \App\Models\Member\Member::count();
|
||||
|
||||
// 獲取最新動態 (最近 3 筆機台日誌)
|
||||
$latestActivities = \App\Models\Machine\MachineLog::with('machine')
|
||||
// 獲取機台列表 (分頁)
|
||||
$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()
|
||||
->limit(3)
|
||||
->get();
|
||||
->paginate($perPage)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.dashboard', compact(
|
||||
'totalRevenue',
|
||||
'activeMachines',
|
||||
'alertsPending',
|
||||
'memberCount',
|
||||
'latestActivities'
|
||||
'machines'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,6 @@ class DataConfigController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
// 帳號管理
|
||||
public function accounts()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '帳號管理',
|
||||
'description' => '主帳號管理',
|
||||
]);
|
||||
}
|
||||
|
||||
// 子帳號管理
|
||||
public function subAccounts()
|
||||
|
||||
@@ -13,12 +13,14 @@ class MachineController extends AdminController
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$limit = $request->input('limit', 10);
|
||||
$machines = Machine::query()
|
||||
->when($request->status, function ($query, $status) {
|
||||
return $query->where('status', $status);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10);
|
||||
->paginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.machines.index', compact('machines'));
|
||||
}
|
||||
@@ -40,6 +42,7 @@ class MachineController extends AdminController
|
||||
*/
|
||||
public function logs(Request $request): View
|
||||
{
|
||||
$limit = $request->input('limit', 20);
|
||||
$logs = \App\Models\Machine\MachineLog::with('machine')
|
||||
->when($request->level, function ($query, $level) {
|
||||
return $query->where('level', $level);
|
||||
@@ -48,7 +51,7 @@ class MachineController extends AdminController
|
||||
return $query->where('machine_id', $machineId);
|
||||
})
|
||||
->latest()
|
||||
->paginate(20);
|
||||
->paginate($limit)->withQueryString();
|
||||
|
||||
$machines = Machine::select('id', 'name')->get();
|
||||
|
||||
|
||||
@@ -91,10 +91,67 @@ class PermissionController extends Controller
|
||||
// 權限角色設定
|
||||
public function roles()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '權限角色設定',
|
||||
'description' => '角色權限組合設定',
|
||||
$limit = request()->input('limit', 10);
|
||||
$roles = \Spatie\Permission\Models\Role::withCount('users')->latest()->paginate($limit)->withQueryString();
|
||||
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智能預測
|
||||
public function aiPrediction()
|
||||
{
|
||||
|
||||
@@ -65,5 +65,9 @@ class Kernel extends HttpKernel
|
||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::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,
|
||||
];
|
||||
}
|
||||
|
||||
37
app/Http/Middleware/EnsureTenantAccess.php
Normal file
37
app/Http/Middleware/EnsureTenantAccess.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,14 @@ namespace App\Models\Machine;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
use App\Traits\TenantScoped;
|
||||
|
||||
class Machine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasFactory, TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'name',
|
||||
'location',
|
||||
'status',
|
||||
|
||||
48
app/Models/System/Company.php
Normal file
48
app/Models/System/Company.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,11 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable, HasRoles;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -18,6 +20,7 @@ class User extends Authenticatable
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'username',
|
||||
'name',
|
||||
'email',
|
||||
@@ -25,6 +28,7 @@ class User extends Authenticatable
|
||||
'phone',
|
||||
'avatar',
|
||||
'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 asset('storage/' . $this->avatar);
|
||||
}
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Traits/TenantScoped.php
Normal file
41
app/Traits/TenantScoped.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@
|
||||
"jenssegers/agent": "^2.6",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"spatie/laravel-permission": "^7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
150
composer.lock
generated
150
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0c5398ab8233c21548345608b86027cc",
|
||||
"content-hash": "a723334f883b537b67e4475890eb949e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -3554,6 +3554,154 @@
|
||||
},
|
||||
"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",
|
||||
"version": "v8.0.0",
|
||||
|
||||
202
config/permission.php
Normal file
202
config/permission.php
Normal 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',
|
||||
],
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
namespace Database\Factories\System;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -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']);
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -27,19 +27,20 @@ class AdminUserSeeder extends Seeder
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@star-cloud.com',
|
||||
'password' => Hash::make('password'),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
$admin->assignRole('super-admin');
|
||||
return;
|
||||
}
|
||||
|
||||
User::create([
|
||||
$admin = User::create([
|
||||
'username' => 'admin',
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@star-cloud.com',
|
||||
'password' => Hash::make('password'),
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
$admin->assignRole('super-admin');
|
||||
|
||||
$this->command->info('Admin 帳號建立成功!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
RoleSeeder::class,
|
||||
AdminUserSeeder::class,
|
||||
MachineSeeder::class,
|
||||
MemberSeeder::class,
|
||||
|
||||
31
database/seeders/RoleSeeder.php
Normal file
31
database/seeders/RoleSeeder.php
Normal 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
248
lang/en.json
Normal 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"
|
||||
}
|
||||
120
lang/ja.json
120
lang/ja.json
@@ -6,8 +6,8 @@
|
||||
"Update Password": "パスワードの更新",
|
||||
"Ensure your account is using a long, random password to stay secure.": "セキュリティを維持するため、アカウントには長くランダムなパスワードを使用してください。",
|
||||
"Delete Account": "アカウントの削除",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "アカウントが削除されると、そのすべてのリソースとデータが永久に削除されます。アカウントを削除する前に、保持したいデータや情報をダウンロードしてください。",
|
||||
"Are you sure you want to delete your account?": "本当にアカウントを削除してもよろしいですか?",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "アカウントが削除されると、そのすべてのリソース和データが永久に削除されます。アカウントを削除する前に、保持したいデータや情報をダウンロードしてください。",
|
||||
"Are you sure you want to delete your account?": "真的にアカウントを削除してもよろしいですか?",
|
||||
"Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "アカウントが削除されると、すべての関連データが永久に削除されます。アカウントの永久削除を確定するため、パスワードを入力してください。",
|
||||
"Login History": "ログイン履歴",
|
||||
"Name": "氏名",
|
||||
@@ -27,34 +27,34 @@
|
||||
"Enter your password to confirm": "確認のためパスワードを入力してください",
|
||||
|
||||
"Dashboard": "ダッシュボード",
|
||||
"Connectivity Status": "接続ステータス",
|
||||
"Connectivity Status": "接続ステータス概況",
|
||||
"Real-time status monitoring": "リアルタイムステータス監視",
|
||||
"LIVE": "ライブ",
|
||||
"Online Machines": "オンライン機台",
|
||||
"Offline Machines": "オフライン機台",
|
||||
"Alerts Pending": "アラート待機中",
|
||||
"Total Connected": "接続数合計",
|
||||
"Monthly Transactions": "今月の取引",
|
||||
"Monthly Transactions": "今月の取引統計",
|
||||
"Monthly cumulative revenue overview": "今月の累計収益概要",
|
||||
"Today's Transactions": "今日の取引",
|
||||
"Yesterday's Transactions": "昨日の取引",
|
||||
"Before Yesterday's Transactions": "一昨日の取引",
|
||||
"Today's Transactions": "今日の取引額",
|
||||
"vs Yesterday": "前日比",
|
||||
"Machine Status List": "機台ステータスリスト",
|
||||
"Yesterday": "昨日",
|
||||
"Day Before": "一昨日",
|
||||
"Machine Status List": "機台稼働状況リスト",
|
||||
"Total items": "合計 :count 件",
|
||||
"Real-time monitoring across all machines": "全機台のリアルタイム監視",
|
||||
"Quick search...": "クイック検索...",
|
||||
"Machine Info": "機台情報",
|
||||
"Running Status": "運行ステータス",
|
||||
"Today Cumulative Sales": "当日累計売上",
|
||||
"Running Status": "稼働状況",
|
||||
"Today Cumulative Sales": "本日累計販売",
|
||||
"Current Stock": "現在の在庫",
|
||||
"Last Communication": "最終通信",
|
||||
"Last Signal": "最終信号時間",
|
||||
"Alert Summary": "アラート概要",
|
||||
"Online": "オンライン",
|
||||
"Offline": "オフライン",
|
||||
"Low Stock": "在庫少",
|
||||
"No alert summary": "アラートなし",
|
||||
"No data available": "データがありません",
|
||||
"No data available": "データなし",
|
||||
"Showing :from to :to of :total items": ":total 件中 :from から :to 件を表示",
|
||||
"Previous": "前へ",
|
||||
"Next": "次へ",
|
||||
@@ -108,7 +108,7 @@
|
||||
"Replenishment Audit": "補充監査",
|
||||
"Data Configuration": "データ設定",
|
||||
"Product Management": "商品管理",
|
||||
"Advertisement Management": "広告管理",
|
||||
"Advertisement Management": "廣告管理",
|
||||
"Admin Sellable Products": "管理者販売可能商品",
|
||||
"Account Management": "アカウント管理",
|
||||
"Sub Accounts": "サブアカウント",
|
||||
@@ -147,8 +147,23 @@
|
||||
"Others": "その他",
|
||||
"AI Prediction": "AI予測",
|
||||
"Roles": "ロール",
|
||||
"Yesterday": "昨日",
|
||||
"Day Before": "一昨日",
|
||||
"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": "ログイン履歴はまだありません",
|
||||
"Signed in as": "ログイン中",
|
||||
"Logout": "ログアウト",
|
||||
@@ -156,5 +171,78 @@
|
||||
"Recent Login": "最近のログイン",
|
||||
"Total Logins": "総ログイン数",
|
||||
"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": "表示中"
|
||||
}
|
||||
|
||||
132
lang/zh_TW.json
132
lang/zh_TW.json
@@ -27,37 +27,35 @@
|
||||
"Enter your password to confirm": "請輸入您的密碼以確認",
|
||||
|
||||
"Dashboard": "儀表板",
|
||||
"Connectivity Status": "連網狀態",
|
||||
"Real-time status monitoring": "即時運作狀態監控",
|
||||
"LIVE": "即時",
|
||||
"Connectivity Status": "連線狀態概況",
|
||||
"Real-time status monitoring": "即時監控機台連線動態",
|
||||
"LIVE": "實時",
|
||||
"Online Machines": "在線機台",
|
||||
"Offline Machines": "離線機台",
|
||||
"Alerts Pending": "異常警報",
|
||||
"Total Connected": "連線中總數",
|
||||
"Monthly Transactions": "當月交易",
|
||||
"Alerts Pending": "待處理告警",
|
||||
"Total Connected": "總計連線數",
|
||||
"Monthly Transactions": "本月交易統計",
|
||||
"Monthly cumulative revenue overview": "本月累計營收概況",
|
||||
"Today's Transactions": "今日交易",
|
||||
"Yesterday's Transactions": "昨日交易",
|
||||
"Before Yesterday's Transactions": "前日交易",
|
||||
"Today's Transactions": "今日交易額",
|
||||
"vs Yesterday": "較昨日",
|
||||
"Yesterday": "昨日",
|
||||
"Day Before": "前日",
|
||||
"vs Yesterday": "比昨日",
|
||||
"Machine Status List": "機台狀態列表",
|
||||
"Total items": "共 :count 筆",
|
||||
"Real-time monitoring across all machines": "全線機台即時監控",
|
||||
"Machine Status List": "機台運行狀態列表",
|
||||
"Total items": "總計 :count 項",
|
||||
"Real-time monitoring across all machines": "跨機台即時狀態監控",
|
||||
"Quick search...": "快速搜尋...",
|
||||
"Machine Info": "機台資訊",
|
||||
"Running Status": "運行狀態",
|
||||
"Today Cumulative Sales": "當日累積銷售",
|
||||
"Current Stock": "目前庫存",
|
||||
"Last Communication": "最後通訊",
|
||||
"Alert Summary": "警訊摘要",
|
||||
"Today Cumulative Sales": "今日累積銷售",
|
||||
"Current Stock": "當前庫存",
|
||||
"Last Signal": "最後訊號時間",
|
||||
"Alert Summary": "告警摘要",
|
||||
"Online": "在線",
|
||||
"Offline": "離線",
|
||||
"Low Stock": "庫存偏低",
|
||||
"No alert summary": "無異常摘要",
|
||||
"No data available": "目前尚無數據",
|
||||
"Showing :from to :to of :total items": "顯示第 :from 到 :to 筆,共 :total 筆",
|
||||
"Low Stock": "庫存低",
|
||||
"No alert summary": "暫無告警記錄",
|
||||
"No data available": "暫無資料",
|
||||
"Showing :from to :to of :total items": "顯示第 :from 到 :to 項,共 :total 項",
|
||||
"Previous": "上一頁",
|
||||
"Next": "下一頁",
|
||||
"Profile Settings": "個人設定",
|
||||
@@ -149,6 +147,23 @@
|
||||
"Others": "其他功能",
|
||||
"AI Prediction": "AI智能預測",
|
||||
"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": "尚無登入紀錄",
|
||||
"Signed in as": "登入身份",
|
||||
"Logout": "登出",
|
||||
@@ -156,5 +171,78 @@
|
||||
"Recent Login": "最近登入",
|
||||
"Total Logins": "總登入次數",
|
||||
"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": "顯示第"
|
||||
}
|
||||
|
||||
@@ -171,4 +171,34 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
397
resources/views/admin/companies/index.blade.php
Normal file
397
resources/views/admin/companies/index.blade.php
Normal 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">​</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
|
||||
@@ -122,13 +122,23 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1">{{ __('Real-time monitoring across all machines') }}</p>
|
||||
</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">
|
||||
<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">
|
||||
@@ -136,25 +146,25 @@
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" class="py-3 pl-12 pr-6 block w-64 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-4 focus:ring-cyan-500/10 focus:border-cyan-500 transition-all outline-none" placeholder="{{ __('Quick search...') }}">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Machine Info') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Running Status') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Today Cumulative Sales') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Current Stock') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Last Communication') }}</th>
|
||||
<th class="px-6 py-4 text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Alert Summary') }}</th>
|
||||
<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-[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-[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-[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-[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-[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>
|
||||
</thead>
|
||||
<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">
|
||||
<td class="px-6 py-6">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">{{ $activity->machine->name ?? __('Machine Info') }}</span>
|
||||
<span class="text-[11px] font-bold text-slate-400 dark:text-slate-500 mt-1 uppercase tracking-[0.15em]">(SN: {{ $activity->machine->serial_no ?? 'N/A' }})</span>
|
||||
<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: {{ $machine->serial_no }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="inline-flex items-center px-4 py-1.5 rounded-full text-[10px] font-black bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-300 border border-slate-200 dark:border-slate-700 tracking-widest uppercase">
|
||||
{{ $activity->machine->status === 'online' ? __('Online') : __('Offline') }}
|
||||
</span>
|
||||
@if($machine->status === 'online')
|
||||
<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">
|
||||
{{ __('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 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 class="px-6 py-6">
|
||||
<div class="flex flex-col items-center gap-y-2.5">
|
||||
@@ -184,7 +200,9 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-center">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-200 font-bold font-display tracking-tight bg-slate-50 dark:bg-slate-800 px-3 py-1 rounded-lg">{{ $activity->created_at->format('Y/m/d H:i') }}</span>
|
||||
<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 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>
|
||||
@@ -199,27 +217,8 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Table Footer: Pagination -->
|
||||
<div class="mt-8 flex items-center justify-between border-t border-slate-100 dark:border-slate-800 pt-6">
|
||||
<p class="text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
||||
{{ __('Showing :from to :to of :total items', ['from' => 1, 'to' => count($latestActivities), 'total' => count($latestActivities)]) }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<button class="inline-flex items-center gap-x-2 px-4 py-2 rounded-xl text-xs font-black bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
||||
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"/></svg>
|
||||
<span>{{ __('Previous') }}</span>
|
||||
</button>
|
||||
<div 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 class="mt-8 border-t border-slate-100 dark:border-slate-800 pt-6">
|
||||
{{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
259
resources/views/admin/data-config/accounts.blade.php
Normal file
259
resources/views/admin/data-config/accounts.blade.php
Normal 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">​</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
|
||||
@@ -13,10 +13,22 @@
|
||||
<div class="p-6 text-gray-900">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-lg font-medium">機台列表</h3>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ route('admin.machines.index') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">全部</a>
|
||||
<a href="{{ route('admin.machines.index', ['status' => 'online']) }}" class="px-4 py-2 bg-green-100 text-green-700 hover:bg-green-200 rounded-md text-sm transition">線上</a>
|
||||
<a href="{{ route('admin.machines.index', ['status' => 'error']) }}" class="px-4 py-2 bg-red-100 text-red-700 hover:bg-red-200 rounded-md text-sm transition">異常</a>
|
||||
<div class="flex items-center gap-x-4">
|
||||
<form method="GET" action="{{ route('admin.machines.index') }}" class="flex items-center gap-x-2">
|
||||
@if(request('status'))
|
||||
<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>
|
||||
|
||||
@@ -71,7 +83,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
{{ $machines->links() }}
|
||||
{{ $machines->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,14 @@
|
||||
<option value="error" {{ request('level') == 'error' ? 'selected' : '' }}>Error</option>
|
||||
</select>
|
||||
</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">
|
||||
<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>
|
||||
@@ -110,7 +118,7 @@
|
||||
|
||||
@if($logs->hasPages())
|
||||
<div class="mt-6">
|
||||
{{ $logs->appends(request()->query())->links() }}
|
||||
{{ $logs->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
163
resources/views/admin/permission/roles.blade.php
Normal file
163
resources/views/admin/permission/roles.blade.php
Normal 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
|
||||
@@ -52,113 +52,136 @@
|
||||
$links[] = $foundModule;
|
||||
}
|
||||
|
||||
// 2. 處理具體頁面
|
||||
// 2. 處理中間層級與具體頁面
|
||||
$segments = explode('.', $routeName);
|
||||
$lastSegment = end($segments);
|
||||
|
||||
// 嘗試翻譯最後一個片段作為頁面名稱
|
||||
$pageLabel = match($lastSegment) {
|
||||
'edit' => __('Profile'), // 專門處理 profile.edit
|
||||
'index' => null, // 通常 index 代表列表,如果在大模組下則不重複顯示
|
||||
'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'),
|
||||
'create' => __('Create'),
|
||||
'show' => __('Show'),
|
||||
'members' => __('Member List'), // 處理 admin.members.index 這種情況
|
||||
default => null,
|
||||
};
|
||||
|
||||
// 如果匹配不到,嘗試處理一些特殊的 index 標籤
|
||||
if (!$pageLabel && $lastSegment === 'index') {
|
||||
$pageLabel = match($segments[count($segments)-2] ?? '') {
|
||||
// 如果路由有四段以上 (如 admin.permission.companies.index),則處理中間一段
|
||||
if (count($segments) >= 4) {
|
||||
$midSegment = $segments[2];
|
||||
$midLabel = match($midSegment) {
|
||||
'companies' => __('Customer Management'),
|
||||
'members' => __('Member List'),
|
||||
'machines' => __('Machine List'),
|
||||
'warehouses' => __('Warehouse List'),
|
||||
'sales' => __('Sales Records'),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($midLabel) {
|
||||
$links[] = [
|
||||
'label' => $midLabel,
|
||||
'url' => '#', // 如果有需要可以導向 index 路由
|
||||
'active' => $lastSegment === 'index'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($pageLabel) {
|
||||
$links[] = [
|
||||
'label' => $pageLabel,
|
||||
'url' => route($routeName),
|
||||
'active' => true
|
||||
];
|
||||
// 3. 處理最後一個動作/頁面
|
||||
if ($lastSegment !== 'index') {
|
||||
$pageLabel = match($lastSegment) {
|
||||
'edit' => __('Edit'),
|
||||
'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 的
|
||||
if (!empty($links)) {
|
||||
$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']) {
|
||||
array_pop($links);
|
||||
$links[count($links)-1]['active'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@endphp
|
||||
|
||||
<nav {{ $attributes->merge(['class' => 'flex', 'aria-label' => 'Breadcrumb']) }}>
|
||||
<ol class="flex items-center whitespace-nowrap min-w-0 w-full">
|
||||
@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' }}">
|
||||
@if(!$link['active'] && $link['url'] !== '#')
|
||||
<a class="hover:text-cyan-600 transition-colors" href="{{ $link['url'] }}">
|
||||
@php
|
||||
$isActive = $link['active'] ?? false;
|
||||
$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'] }}
|
||||
</a>
|
||||
@else
|
||||
|
||||
@@ -84,19 +84,19 @@
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.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.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.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.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.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.replenishments') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishments') }}">機台補貨單</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.replenishment-records') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.replenishment-records') }}">機台補貨紀錄</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.machine-stock') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.warehouses.machine-stock') }}">機台庫存</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.warehouses.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') }}">{{ __('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') }}">{{ __('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') }}">{{ __('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') }}">{{ __('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') }}">{{ __('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') }}">{{ __('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') }}">{{ __('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.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>
|
||||
@@ -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.advertisements') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.advertisements') }}">{{ __('Advertisement Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.admin-products') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.admin-products') }}">{{ __('Admin Sellable Products') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.accounts') }}">{{ __('Account Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-accounts') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-accounts') }}">{{ __('Sub Accounts') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.sub-account-roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.sub-account-roles') }}">{{ __('Sub Account Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.data-config.points') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.data-config.points') }}">{{ __('Point Settings') }}</a></li>
|
||||
@@ -236,6 +235,7 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@role('super-admin')
|
||||
{{-- 14. 特殊權限 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_special_permission') === 'true' || {{ request()->routeIs('admin.special-permission.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_special_permission', open)" class="luxury-nav-item w-full text-start group">
|
||||
@@ -251,7 +251,9 @@
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endrole
|
||||
|
||||
@role('super-admin')
|
||||
{{-- 15. 權限設定 --}}
|
||||
<li x-data="{ open: localStorage.getItem('menu_permissions') === 'true' || {{ request()->routeIs('admin.permission.*') ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open; localStorage.setItem('menu_permissions', open)" class="luxury-nav-item w-full text-start group">
|
||||
@@ -261,6 +263,9 @@
|
||||
</button>
|
||||
<div x-show="open" x-collapse>
|
||||
<ul class="luxury-submenu">
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.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.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>
|
||||
@@ -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.remote') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.remote') }}">{{ __('Remote Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.line') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.line.members') }}">{{ __('Line Management') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.roles') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.roles') }}">{{ __('Roles') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.others') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.others') }}">{{ __('Others') }}</a></li>
|
||||
<li><a class="flex items-center gap-x-3.5 py-2 px-2.5 text-sm transition-colors rounded-lg {{ request()->routeIs('admin.permission.ai-prediction') ? 'text-slate-900 dark:text-white bg-slate-100 dark:bg-white/5' : 'text-slate-500 hover:text-slate-900 dark:hover:text-white' }}" href="{{ route('admin.permission.ai-prediction') }}">{{ __('AI Prediction') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
@endrole
|
||||
|
||||
59
resources/views/vendor/pagination/luxury.blade.php
vendored
Normal file
59
resources/views/vendor/pagination/luxury.blade.php
vendored
Normal 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
|
||||
289
routes/web.php
289
routes/web.php
@@ -4,18 +4,18 @@ use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Web Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register web routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider and all of them will
|
||||
| be assigned to the "web" middleware group. Make something great!
|
||||
|
|
||||
*/
|
||||
|--------------------------------------------------------------------------
|
||||
| Web Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register web routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider and all of them will
|
||||
| be assigned to the "web" middleware group. Make something great!
|
||||
|
|
||||
*/
|
||||
|
||||
// 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 () {
|
||||
return redirect()->route('login');
|
||||
@@ -25,9 +25,9 @@ Route::get('/dashboard', function () {
|
||||
return redirect()->route('admin.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. 儀表板
|
||||
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard');
|
||||
Route::get('/dashboard', [App\Http\Controllers\Admin\DashboardController::class , 'index'])->name('dashboard');
|
||||
|
||||
// 2. 會員管理
|
||||
Route::resource('members', App\Http\Controllers\MemberController::class)->only(['index']);
|
||||
@@ -38,146 +38,165 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(
|
||||
|
||||
// 3. 機台管理
|
||||
Route::prefix('machines')->name('machines.')->group(function () {
|
||||
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('/utilization', [App\Http\Controllers\Admin\MachineController::class, 'utilization'])->name('utilization');
|
||||
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::resource('machines', App\Http\Controllers\Admin\MachineController::class);
|
||||
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('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization');
|
||||
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::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');
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
// 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::post('/theme', [App\Http\Controllers\Admin\ThemeController::class , 'update'])->name('theme.update');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::post("/profile/avatar", [ProfileController::class, "updateAvatar"])->name("profile.avatar");
|
||||
Route::get('/profile', [ProfileController::class , 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class , 'update'])->name('profile.update');
|
||||
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::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('/social-login', [App\Http\Controllers\SocialLoginTestController::class , 'index'])->name('social-login');
|
||||
Route::get('/line/callback', [App\Http\Controllers\SocialLoginTestController::class , 'lineCallback'])->name('line.callback');
|
||||
});
|
||||
68
tests/Feature/TenantIsolationTest.php
Normal file
68
tests/Feature/TenantIsolationTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user