From 56daf8940bce9bded27cf1f679e6cff7586d7a43 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 13 Mar 2026 17:35:22 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E5=84=AA=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=9A=E5=8A=A0=E5=85=A5=20RoleSeeder=20?= =?UTF-8?q?=E8=88=87=20AdminUserSeeder=EF=BC=8C=E4=B8=A6=E5=AF=A6=E4=BD=9C?= =?UTF-8?q?=E6=AC=8A=E9=99=90=E7=B3=BB=E7=B5=B1=E5=9F=BA=E7=A4=8E=E6=9E=B6?= =?UTF-8?q?=E6=A7=8B=E8=88=87=E5=A4=9A=E7=A7=9F=E6=88=B6=E9=9A=94=E9=9B=A2?= =?UTF-8?q?=E6=A9=9F=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/ui-minimal-luxury/SKILL.md | 101 ++++- .gitea/workflows/deploy-demo.yaml | 4 +- .../Controllers/Admin/CompanyController.php | 125 ++++++ .../Controllers/Admin/DashboardController.php | 20 +- .../Admin/DataConfigController.php | 8 - .../Controllers/Admin/MachineController.php | 7 +- .../Admin/PermissionController.php | 182 +++++++- app/Http/Kernel.php | 4 + app/Http/Middleware/EnsureTenantAccess.php | 37 ++ app/Models/Machine/Machine.php | 5 +- app/Models/System/Company.php | 48 +++ app/Models/System/User.php | 30 +- app/Traits/TenantScoped.php | 41 ++ composer.json | 3 +- composer.lock | 150 ++++++- config/permission.php | 202 +++++++++ .../factories/{ => System}/UserFactory.php | 2 +- ..._03_13_111434_create_permission_tables.php | 137 ++++++ ...26_03_13_111435_create_companies_table.php | 37 ++ ...11437_add_company_id_to_machines_table.php | 29 ++ ...3_111437_add_company_id_to_users_table.php | 31 ++ ...13_134237_add_is_system_to_roles_table.php | 28 ++ ...532_make_email_nullable_on_users_table.php | 28 ++ database/seeders/AdminUserSeeder.php | 7 +- database/seeders/DatabaseSeeder.php | 1 + database/seeders/RoleSeeder.php | 31 ++ lang/en.json | 248 +++++++++++ lang/ja.json | 120 +++++- lang/zh_TW.json | 132 +++++- resources/css/app.css | 30 ++ .../views/admin/companies/index.blade.php | 397 ++++++++++++++++++ resources/views/admin/dashboard.blade.php | 77 ++-- .../admin/data-config/accounts.blade.php | 259 ++++++++++++ .../views/admin/machines/index.blade.php | 22 +- resources/views/admin/machines/logs.blade.php | 10 +- .../views/admin/permission/roles.blade.php | 163 +++++++ .../views/components/breadcrumbs.blade.php | 183 ++++---- .../layouts/partials/sidebar-menu.blade.php | 27 +- .../views/vendor/pagination/luxury.blade.php | 59 +++ routes/web.php | 317 +++++++------- tests/Feature/TenantIsolationTest.php | 68 +++ 41 files changed, 3052 insertions(+), 358 deletions(-) create mode 100644 app/Http/Controllers/Admin/CompanyController.php create mode 100644 app/Http/Middleware/EnsureTenantAccess.php create mode 100644 app/Models/System/Company.php create mode 100644 app/Traits/TenantScoped.php create mode 100644 config/permission.php rename database/factories/{ => System}/UserFactory.php (96%) create mode 100644 database/migrations/2026_03_13_111434_create_permission_tables.php create mode 100644 database/migrations/2026_03_13_111435_create_companies_table.php create mode 100644 database/migrations/2026_03_13_111437_add_company_id_to_machines_table.php create mode 100644 database/migrations/2026_03_13_111437_add_company_id_to_users_table.php create mode 100644 database/migrations/2026_03_13_134237_add_is_system_to_roles_table.php create mode 100644 database/migrations/2026_03_13_135532_make_email_nullable_on_users_table.php create mode 100644 database/seeders/RoleSeeder.php create mode 100644 lang/en.json create mode 100644 resources/views/admin/companies/index.blade.php create mode 100644 resources/views/admin/data-config/accounts.blade.php create mode 100644 resources/views/admin/permission/roles.blade.php create mode 100644 resources/views/vendor/pagination/luxury.blade.php create mode 100644 tests/Feature/TenantIsolationTest.php diff --git a/.agents/skills/ui-minimal-luxury/SKILL.md b/.agents/skills/ui-minimal-luxury/SKILL.md index 16983d2..967dc39 100644 --- a/.agents/skills/ui-minimal-luxury/SKILL.md +++ b/.agents/skills/ui-minimal-luxury/SKILL.md @@ -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') +
+ +
+
+

{{ __('Title') }}

+

{{ __('Subtitle') }}

+
+
+ +
+
+ + +
+
+ + +
+
+
+
+@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 + + + +``` + +## 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-*` 系列組件是否滿足需求。** diff --git a/.gitea/workflows/deploy-demo.yaml b/.gitea/workflows/deploy-demo.yaml index e61ca0c..30a05ce 100644 --- a/.gitea/workflows/deploy-demo.yaml +++ b/.gitea/workflows/deploy-demo.yaml @@ -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 diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php new file mode 100644 index 0000000..a3ebdb3 --- /dev/null +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -0,0 +1,125 @@ +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.')); + } +} diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 0cca2e4..6000d50 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -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' )); } } diff --git a/app/Http/Controllers/Admin/DataConfigController.php b/app/Http/Controllers/Admin/DataConfigController.php index 8441e0d..61793f8 100644 --- a/app/Http/Controllers/Admin/DataConfigController.php +++ b/app/Http/Controllers/Admin/DataConfigController.php @@ -34,14 +34,6 @@ class DataConfigController extends Controller ]); } - // 帳號管理 - public function accounts() - { - return view('admin.placeholder', [ - 'title' => '帳號管理', - 'description' => '主帳號管理', - ]); - } // 子帳號管理 public function subAccounts() diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index d21d2eb..5a622e0 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -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(); diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php index cdc79f7..0703d71 100644 --- a/app/Http/Controllers/Admin/PermissionController.php +++ b/app/Http/Controllers/Admin/PermissionController.php @@ -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() { diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 48774c9..4127281 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, ]; } diff --git a/app/Http/Middleware/EnsureTenantAccess.php b/app/Http/Middleware/EnsureTenantAccess.php new file mode 100644 index 0000000..008bd17 --- /dev/null +++ b/app/Http/Middleware/EnsureTenantAccess.php @@ -0,0 +1,37 @@ +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); + } +} diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index cb7b456..d4c4083 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -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', diff --git a/app/Models/System/Company.php b/app/Models/System/Company.php new file mode 100644 index 0000000..ba50c2b --- /dev/null +++ b/app/Models/System/Company.php @@ -0,0 +1,48 @@ + '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); + } +} diff --git a/app/Models/System/User.php b/app/Models/System/User.php index 5270eb0..7213a50 100644 --- a/app/Models/System/User.php +++ b/app/Models/System/User.php @@ -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 */ 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); } } diff --git a/app/Traits/TenantScoped.php b/app/Traits/TenantScoped.php new file mode 100644 index 0000000..3b2c307 --- /dev/null +++ b/app/Traits/TenantScoped.php @@ -0,0 +1,41 @@ +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); + } +} diff --git a/composer.json b/composer.json index fc678cd..0b68ad9 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index a5051d5..d3219e0 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..082ca30 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,202 @@ + [ + + /* + * 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', + ], +]; diff --git a/database/factories/UserFactory.php b/database/factories/System/UserFactory.php similarity index 96% rename from database/factories/UserFactory.php rename to database/factories/System/UserFactory.php index 9319fc1..90effd1 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/System/UserFactory.php @@ -1,6 +1,6 @@ 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']); + } +}; diff --git a/database/migrations/2026_03_13_111435_create_companies_table.php b/database/migrations/2026_03_13_111435_create_companies_table.php new file mode 100644 index 0000000..6eab1d4 --- /dev/null +++ b/database/migrations/2026_03_13_111435_create_companies_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/database/migrations/2026_03_13_111437_add_company_id_to_machines_table.php b/database/migrations/2026_03_13_111437_add_company_id_to_machines_table.php new file mode 100644 index 0000000..aaa708f --- /dev/null +++ b/database/migrations/2026_03_13_111437_add_company_id_to_machines_table.php @@ -0,0 +1,29 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_03_13_111437_add_company_id_to_users_table.php b/database/migrations/2026_03_13_111437_add_company_id_to_users_table.php new file mode 100644 index 0000000..c9199ef --- /dev/null +++ b/database/migrations/2026_03_13_111437_add_company_id_to_users_table.php @@ -0,0 +1,31 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_03_13_134237_add_is_system_to_roles_table.php b/database/migrations/2026_03_13_134237_add_is_system_to_roles_table.php new file mode 100644 index 0000000..b2d5cc5 --- /dev/null +++ b/database/migrations/2026_03_13_134237_add_is_system_to_roles_table.php @@ -0,0 +1,28 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_03_13_135532_make_email_nullable_on_users_table.php b/database/migrations/2026_03_13_135532_make_email_nullable_on_users_table.php new file mode 100644 index 0000000..c7bdb4f --- /dev/null +++ b/database/migrations/2026_03_13_135532_make_email_nullable_on_users_table.php @@ -0,0 +1,28 @@ +string('email')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('email')->nullable(false)->change(); + }); + } +}; diff --git a/database/seeders/AdminUserSeeder.php b/database/seeders/AdminUserSeeder.php index 7a8d274..27c1dda 100644 --- a/database/seeders/AdminUserSeeder.php +++ b/database/seeders/AdminUserSeeder.php @@ -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 帳號建立成功!'); } } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 591f74b..498fbce 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->call([ + RoleSeeder::class, AdminUserSeeder::class, MachineSeeder::class, MemberSeeder::class, diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php new file mode 100644 index 0000000..7fd7233 --- /dev/null +++ b/database/seeders/RoleSeeder.php @@ -0,0 +1,31 @@ +forgetCachedPermissions(); + + // 建立角色 + Role::updateOrCreate( + ['name' => 'super-admin'], + ['is_system' => true] + ); + + Role::updateOrCreate( + ['name' => 'tenant-admin'], + ['is_system' => true] + ); + } +} diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..4906b7a --- /dev/null +++ b/lang/en.json @@ -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" +} diff --git a/lang/ja.json b/lang/ja.json index 4df6e03..c8a3657 100644 --- a/lang/ja.json +++ b/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": "表示中" } diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 8bd8741..a867888 100644 --- a/lang/zh_TW.json +++ b/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": "顯示第" } diff --git a/resources/css/app.css b/resources/css/app.css index cf477a8..3ab8179 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; + } } \ No newline at end of file diff --git a/resources/views/admin/companies/index.blade.php b/resources/views/admin/companies/index.blade.php new file mode 100644 index 0000000..80dfcfe --- /dev/null +++ b/resources/views/admin/companies/index.blade.php @@ -0,0 +1,397 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+

{{ __('Customer Management') }}

+

{{ __('Manage all tenant accounts and validity') }}

+
+ +
+ +
+
+

{{ __('Total Customers') }}

+

{{ $companies->total() }}

+
+
+

{{ __('Active') }}

+

{{ $companies->where('status', + 1)->count() }}

+
+
+

{{ __('Expired / Disabled') }}

+

{{ $companies->where('status', + 0)->count() }}

+
+
+ + +
+
+
+ + + + + + + +
+ +
+
+ @if(request('search'))@endif + @if(request('status'))@endif + +
+ + +
+
+ +
+ + + + + + + + + + + + @forelse($companies as $company) + + + + + + + + @empty + + + + @endforelse + +
+ {{ __('Customer Info') }} + {{ __('Status') }} + {{ __('Accounts / Machines') }} + {{ __('Valid Until') }} + {{ __('Actions') }}
+
+
+ + + +
+
+ {{ + $company->name }} + {{ + $company->code }} +
+
+
+ @if($company->status) + + {{ __('Active') }} + + @else + + {{ __('Disabled') }} + + @endif + +
+
+ {{ $company->users_count }} + {{ __('Users') }} +
+
+
+ {{ $company->machines_count }} + {{ __('Machines') }} +
+
+
+ + {{ $company->valid_until ? $company->valid_until->format('Y/m/d') : __('Permanent') }} + + +
+ +
+ @csrf @method('DELETE') + +
+
+
+
+
+ + + +
+

{{ __('No customers found') }}

+
+
+
+ +
+ {{ $companies->links('vendor.pagination.luxury') }} +
+
+ + +
+
+
+
+ + + +
+ +
+

+ +
+ +
+ @csrf + + + +
+
+
+

{{ + __('Company Information') }}

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+

+ {{ __('Initial Admin Account') }} + ({{ __('Optional') }}) +

+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+

{{ + __('Contact & Details') }}

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ +@endsection \ No newline at end of file diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 26c27c3..0c53730 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -122,13 +122,23 @@

{{ __('Machine Status List') }}

- {{ __('Total items', ['count' => count($latestActivities)]) }} + {{ __('Total items', ['count' => $machines->total()]) }}

{{ __('Real-time monitoring across all machines') }}

-
+
+
+ + +
+
@@ -136,25 +146,25 @@ - +
-
+
- - - - - - + + + + + + - @forelse($latestActivities as $activity) + @forelse($machines as $machine)
{{ __('Machine Info') }}{{ __('Running Status') }}{{ __('Today Cumulative Sales') }}{{ __('Current Stock') }}{{ __('Last Communication') }}{{ __('Alert Summary') }}{{ __('Machine Info') }}{{ __('Running Status') }}{{ __('Today Cumulative Sales') }}{{ __('Current Stock') }}{{ __('Last Signal') }}{{ __('Alert Summary') }}
@@ -162,18 +172,24 @@
- {{ $activity->machine->name ?? __('Machine Info') }} - (SN: {{ $activity->machine->serial_no ?? 'N/A' }}) + {{ $machine->name }} + (SN: {{ $machine->serial_no }})
- - {{ $activity->machine->status === 'online' ? __('Online') : __('Offline') }} - + @if($machine->status === 'online') + + {{ __('Online') }} + + @else + + {{ __('Offline') }} + + @endif - $ 0 + $ 0
@@ -184,7 +200,9 @@
- {{ $activity->created_at->format('Y/m/d H:i') }} + + {{ $machine->last_heartbeat_at ? $machine->last_heartbeat_at->format('Y/m/d H:i') : '---' }} + {{ __('No alert summary') }} @@ -199,27 +217,8 @@
- -
-

- {{ __('Showing :from to :to of :total items', ['from' => 1, 'to' => count($latestActivities), 'total' => count($latestActivities)]) }} -

- -
- -
- - - -
- -
+
+ {{ $machines->appends(request()->query())->links('vendor.pagination.luxury') }}
diff --git a/resources/views/admin/data-config/accounts.blade.php b/resources/views/admin/data-config/accounts.blade.php new file mode 100644 index 0000000..76e11db --- /dev/null +++ b/resources/views/admin/data-config/accounts.blade.php @@ -0,0 +1,259 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+

{{ __('Account Management') }}

+

{{ __('Manage administrative and tenant accounts') }}

+
+ +
+ + +
+
+
+
+ + + + + + + +
+ + @if(auth()->user()->isSystemAdmin()) + + @endif + + +
+ +
+ +
+
+ +
+ + + + +@if(auth()->user()->isSystemAdmin()) + +@endif + + + + + + + @forelse($users as $user) + + + @if(auth()->user()->isSystemAdmin()) + + @endif + + + + + @empty + + + + @endforelse + +
{{ __('User Info') }}{{ __('Belongs To') }}{{ __('Role') }}{{ __('Status') }}{{ __('Actions') }}
+
+
+ @if($user->avatar) + + @else + {{ substr($user->name, 0, 1) }} + @endif +
+
+ {{ $user->name }} + {{ $user->username }} @if($user->email) • {{ $user->email }} @endif +
+
+
+ @if($user->company) + {{ $user->company->name }} + @else + {{ __('SYSTEM') }} + @endif + + @foreach($user->roles as $role) + + {{ $role->name }} + + @endforeach + + @if($user->status) + + {{ __('Active') }} + + @else + + {{ __('Disabled') }} + + @endif + +
+ +
+ @csrf + @method('DELETE') + +
+
+
+

{{ __('No users found') }}

+
+
+ +
+ {{ $users->links('vendor.pagination.luxury') }} +
+
+ + +
+
+
+ + + +
+ +
+

+ +
+ +
+ @csrf + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + @if(auth()->user()->isSystemAdmin()) +
+ + +
+ @endif + +
+ + +
+ +
+ + +
+
+
+
+
+
+ +@endsection diff --git a/resources/views/admin/machines/index.blade.php b/resources/views/admin/machines/index.blade.php index 8c5ef2e..ba42b8e 100644 --- a/resources/views/admin/machines/index.blade.php +++ b/resources/views/admin/machines/index.blade.php @@ -13,10 +13,22 @@

機台列表

-
- 全部 - 線上 - 異常 +
+
+ @if(request('status')) + + @endif + +
+
+ 全部 + 線上 + 異常 +
@@ -71,7 +83,7 @@
- {{ $machines->links() }} + {{ $machines->links('vendor.pagination.luxury') }}
diff --git a/resources/views/admin/machines/logs.blade.php b/resources/views/admin/machines/logs.blade.php index 7a2fe46..3dbdc48 100644 --- a/resources/views/admin/machines/logs.blade.php +++ b/resources/views/admin/machines/logs.blade.php @@ -40,6 +40,14 @@ +
+ + +
diff --git a/resources/views/admin/permission/roles.blade.php b/resources/views/admin/permission/roles.blade.php new file mode 100644 index 0000000..e6c78f6 --- /dev/null +++ b/resources/views/admin/permission/roles.blade.php @@ -0,0 +1,163 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+

{{ __('Roles') }}

+

{{ __('Define and manage security roles for the system.') }}

+
+ +
+ + +
+
+
+ + + + + + + +
+ +
+ @if(request('search'))@endif + + +
+
+
+ + +
+
+ + + + + + + + + + + @forelse($roles as $role) + + + + + + + @empty + + + + @endforelse + +
{{ __('Role Name') }}{{ __('Type') }}{{ __('Users') }}{{ __('Actions') }}
+
+ {{ $role->name }} + @if($role->is_system) + + + + @endif +
+
+ @if($role->is_system) + + {{ __('System') }} + + @else + + {{ __('Custom') }} + + @endif + + {{ $role->users_count }} + +
+ @if(!$role->is_system) + +
+ @csrf + @method('DELETE') + +
+ @else + {{ __('Protected') }} + @endif +
+
+
+ +

{{ __('No roles found.') }}

+
+
+
+ +
+ {{ $roles->links('vendor.pagination.luxury') }} +
+
+ + + +
+@endsection diff --git a/resources/views/components/breadcrumbs.blade.php b/resources/views/components/breadcrumbs.blade.php index a9493cd..f242f1f 100644 --- a/resources/views/components/breadcrumbs.blade.php +++ b/resources/views/components/breadcrumbs.blade.php @@ -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