From fc79148879fe6ad0c282562354aa3a672f09883d Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 17 Mar 2026 16:53:28 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E5=AF=A6=E4=BD=9C=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=AC=8A=E9=99=90=E5=88=86=E9=A1=9E=E3=80=81=E7=A7=9F=E6=88=B6?= =?UTF-8?q?=E8=A7=92=E6=8E=A7=E7=AE=A1=E7=90=86=E8=88=87=E4=BB=8B=E9=9D=A2?= =?UTF-8?q?=E5=A4=9A=E8=AA=9E=E7=B3=BB=E5=84=AA=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. [FEAT] 權限劃分為「系統層級」與「客戶層級」,並在後端強制過濾跨權限分配。 2. [FEAT] 整合選單權限至主選單層級 (基本設定、權限設定),簡化角色管理 UI。 3. [STYLE] 側邊欄優化:補齊多語系翻譯,並為基本設定子選單增加視覺圖示。 4. [REFACTOR] 更新 RoleSeeder,將 tenant-admin 重新分類為客戶層級角色。 --- .agents/rules/framework.md | 4 +- .agents/skills/ui-minimal-luxury/SKILL.md | 96 ++++-- .../MachineSettingController.php | 199 +++++++++++ .../BasicSettings/PaymentConfigController.php | 100 ++++++ .../Controllers/Admin/CompanyController.php | 2 +- .../Controllers/Admin/MachineController.php | 13 +- .../Admin/PermissionController.php | 133 ++++++-- app/Listeners/LogSuccessfulLogin.php | 2 +- app/Models/Machine/Machine.php | 63 ++++ app/Models/Machine/MachineModel.php | 35 ++ app/Models/System/PaymentConfig.php | 40 +++ app/Models/System/Role.php | 34 ++ app/Models/System/User.php | 2 +- app/Models/{ => System}/UserLoginLog.php | 4 +- config/permission.php | 2 +- ..._17_105051_create_machine_models_table.php | 31 ++ ...17_105100_create_payment_configs_table.php | 32 ++ ..._105101_add_settings_to_machines_table.php | 76 +++++ ...17_131857_add_images_to_machines_table.php | 28 ++ database/seeders/RoleSeeder.php | 23 +- docs/architecture_plan.md | 61 +++- lang/en.json | 18 +- lang/ja.json | 15 +- lang/zh_TW.json | 91 ++++- resources/css/app.css | 16 +- .../basic-settings/machines/edit.blade.php | 251 ++++++++++++++ .../basic-settings/machines/index.blade.php | 317 ++++++++++++++++++ .../payment-configs/create.blade.php | 187 +++++++++++ .../payment-configs/edit.blade.php | 191 +++++++++++ .../payment-configs/index.blade.php | 82 +++++ .../views/admin/companies/index.blade.php | 12 +- .../admin/data-config/accounts.blade.php | 80 +++-- .../views/admin/permission/roles.blade.php | 84 +++-- .../views/components/breadcrumbs.blade.php | 7 +- .../components/luxury-time-input.blade.php | 49 +++ resources/views/layouts/admin.blade.php | 4 +- .../layouts/partials/sidebar-menu.blade.php | 291 ++++++++-------- routes/web.php | 26 +- 38 files changed, 2398 insertions(+), 303 deletions(-) create mode 100644 app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php create mode 100644 app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php create mode 100644 app/Models/Machine/MachineModel.php create mode 100644 app/Models/System/PaymentConfig.php create mode 100644 app/Models/System/Role.php rename app/Models/{ => System}/UserLoginLog.php (82%) create mode 100644 database/migrations/2026_03_17_105051_create_machine_models_table.php create mode 100644 database/migrations/2026_03_17_105100_create_payment_configs_table.php create mode 100644 database/migrations/2026_03_17_105101_add_settings_to_machines_table.php create mode 100644 database/migrations/2026_03_17_131857_add_images_to_machines_table.php create mode 100644 resources/views/admin/basic-settings/machines/edit.blade.php create mode 100644 resources/views/admin/basic-settings/machines/index.blade.php create mode 100644 resources/views/admin/basic-settings/payment-configs/create.blade.php create mode 100644 resources/views/admin/basic-settings/payment-configs/edit.blade.php create mode 100644 resources/views/admin/basic-settings/payment-configs/index.blade.php create mode 100644 resources/views/components/luxury-time-input.blade.php diff --git a/.agents/rules/framework.md b/.agents/rules/framework.md index cd34bf9..98a6412 100644 --- a/.agents/rules/framework.md +++ b/.agents/rules/framework.md @@ -14,7 +14,8 @@ trigger: always_on * **核心組件**:Redis (用於高併發 IoT 隊列與快取,為系統穩定之必要條件) * **前端視圖 (View)**:Laravel Blade * **前端互動 (JS)**:Alpine.js (專注於行為,不負責渲染) -* **介面與樣式 (CSS)**:Tailwind CSS + Preline UI (直接寫作於 Blade 模板中) +* **介面與樣式 (CSS)**:Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)。 + * **重要規範**:Preline UI 僅作為「原子組件」與「JS 互動邏輯」的參考庫。整體的「佈局」與「美學」必須嚴格遵守「極簡奢華風 UI 實作規範 (SKILL.md)」。 * **前端建置工具**:Vite * **資料庫**:MySQL 8.0 * **開發環境**:Laravel Sail (Docker / WSL2) @@ -64,6 +65,7 @@ trigger: always_on * **角色設定**:你是一位專業的全端開發工程師助手。 * **代碼生成指令**: * 所有的解釋說明請使用 **繁體中文**。 + * **【警告:Preline 冗餘】** Preline UI 的官方範例常包含多餘的控制項(如頂部筆數切換)。**嚴禁**照抄其佈局,必須確保頂部工具列(Header/Toolbar)維持極簡,重複功能一律收納至底部。 * **【警告】** 此專案前端禁用 React / Vue / Inertia.js。所有的前端頁面生成必須使用 **Blade 模板** 結合 **Tailwind CSS** 與 **Alpine.js**。 * **【多語系強制要求】** 任何新增的 Blade UI 區塊,禁止硬編碼 (Hard-coded) 中文或英文。必須使用 `__('...')` 並同步在 `lang/*.json` 補上翻譯。 * 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。 diff --git a/.agents/skills/ui-minimal-luxury/SKILL.md b/.agents/skills/ui-minimal-luxury/SKILL.md index f588fc9..be418ad 100644 --- a/.agents/skills/ui-minimal-luxury/SKILL.md +++ b/.agents/skills/ui-minimal-luxury/SKILL.md @@ -58,7 +58,11 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範 ## 3. 動畫與互動 ### 進場動畫 -- **`.animate-luxury-in`**: 所有的主內容區域或卡片在頁面載入時,應具備由下而上的淡入效果。 +- **`.animate-luxury-in`**: 所有的主內容區域或卡片在頁面載入時,應具備由下而轉的淡入效果。 + +### 互動過渡 (Transitions) +- **標準時間**: 所有的懸停、色彩變換等過渡效果,統一建議使用 **`duration-300`** (300ms)。 +- **例外**: 極其細微的透明度變化可縮短至 `150ms`,但涉及背景色與位移的互動一律以 `300ms` 為準。 ### Alpine.js 互動模式 (以時間選擇器為例) - **互動原則**: 點擊觸發下拉選單時,必須使用 `x-transition` 且帶有 `scale` 偏移。 @@ -67,6 +71,7 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範 - [ ] **列表佈局**: 是否採用「整合式卡片」結構且內距設為 `p-8`? - [ ] **分頁與總數**: 列表底部是否正確召喚 `vendor.pagination.luxury`? - [ ] **文字色階**: 符合標題 `slate-900/white` 與標籤 `slate-500` 的對比度。 +- [ ] **可讀性檢查**: 二級資訊是否達到 `text-xs` (12px) 且權重不超過 `font-bold`? ## 5. 開發注意事項 (Important Notes) @@ -152,34 +157,54 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範 ``` +## 8. 編輯與詳情頁規範 (Detail & Edit Views) + +為了讓分層資訊更具視覺引導,各個區塊 (Section) 的圖示應採用不同的顏色意象。 + +### 區塊圖示色彩意象 (Section Icon Palette) +- **基本資訊 (Basic Info)**: **翠綠色 (`Emerald`)**。代表核心、穩定與起點。 + - 樣式: `bg-emerald-500/10 text-emerald-500` +- **硬體/插槽設定**: **琥珀色 (`Amber/Orange`)**。代表動作、物理連接與硬體警告。 + - 樣式: `bg-amber-500/10 text-amber-500` +- **系統/進階設定**: **靛藍色 (`Indigo`)**。代表邏輯、權限與深層配置。 + - 樣式: `bg-indigo-500/10 text-indigo-500` +- **危險/移除動作**: **玫瑰紅 (`Rose`)**。代表破壞性操作。 + - 樣式: `bg-rose-500/10 text-rose-500` +``` + ## 8. 資料表格規範 (Data Tables) 為了確保管理後台資料的可讀性與精密感,表格內的所有文字級別必須對齊以下規範: ### 文字大小與權重 (Typography Hierarchy) - **表頭 (Table Header)**: - - 類別: `text-[11px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]` - - 作用: 提供清晰的欄位定義而不奪取資料視覺焦點。 + - 類別: `text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em]` + - 作用: 提供清晰的欄位定義而不奪取資料視覺焦點。具備足夠對比度。 - **主標題 (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.1em]` + - 類別: `text-xs font-bold text-slate-500 dark:text-slate-400 tracking-wide` - 範例: 使用者帳號、備註、權限名稱。 - **狀態標籤 (Status Badge)**: - 範例: 啟用 (`emerald`)、禁用 (`rose`) / 角色名稱 (`sky`/`indigo`)。 - - 特性: `px-2.5 py-1 rounded-lg text-[11px] font-black border tracking-wider` + - 特性: `px-2.5 py-1 rounded-lg text-xs font-bold border tracking-wider` ### 空間與反應 (Spacing & Interaction) - **單元格內距**: 統一使用 `px-6 py-6`。 - **懸停反應**: 必須在 `tr` 套用 `group` 且子元素套用 `group-hover:bg-slate-50/80` (深色: `dark:group-hover:bg-slate-800/40`) 以提供高級的互動感知。 +- **圖示容器懸停 (Icon Hover Palette)**: + - 列表左側的主圖示容器在 `group-hover` 時,應由淡色背景轉為 **實體主題色**。 + - 類別: `group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300`。 +- **文字同步變色**: + - 主標題文字在 `group-hover` 時應同步變色,以強化點擊引導。 + - 類別: `group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors`。 ### 分頁與列表控制項 (Pagination & Controls) 為了維持操作一致性,所有列表的分頁與切換組件必須遵循以下「Luxury Jump」模式: - **統一高度**: 所有控制項(按鈕、下拉選單)固定為 `h-9` (36px)。 - **筆數切換 (Limit Selector)**: - - 樣式: 使用 `bg-slate-50` (深色: `dark:bg-slate-800`) 配合 `text-[11px] font-black`。 - - 位置: 位於表格右上方。 + - 規範: **禁止**在表格上方(Header/Toolbar)重複放置筆數切換選單。統一收納於底部分頁欄位。 - **分頁導航 (Luxury Jump)**: - 模式: 捨棄傳統頁碼按鈕,全端統一使用「跳轉選單」。 - 寬度: 下拉選單內部 Padding 為 `pl-4 pr-10`。 @@ -189,30 +214,38 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範 - 數字顏色對齊 `text-slate-600` (深色: `text-slate-300`)。 ### 底部清單控制項 (Bottom List Controls) -為了確保長列表的操作便利,清單底部必須具備以下元素: -- **位置**: 卡片底部,內距 `px-8 py-6`,並帶有 `border-t border-slate-100/50 dark:border-slate-800/50`。 -- **左側:每頁筆數 (Per Page Selector)**: - - 樣式: `luxury-select` (緊湊型),高度固定為 `h-9`。 - - 規範: 提供 `20, 50, 100` 等選項,並在變更時立即提交。 -- **中央:資料指示 (Info)**: - - 樣式: `text-[11px] font-bold tracking-widest uppercase text-slate-400`。 - - 內容: `Showing X to Y of Z results`。 -- **右側:分頁導航 (Pagination)**: - - 模式: 優先使用 `Luxury Jump` (跳轉下拉選單) 以節省空間並提升效率。 +為了確保長列表的操作便利,清單底### 標準操作按鈕 (Standard Action Icons) +表格內的操作欄位(如「編輯」、「刪除」、「詳情」)必須使用以下定義之 **「黃金標準 (Gold Standard)」**: -### 標準清單萬用模板 (Standard List View Bible) -建立新列表頁面時,**必須**以此結構為基底: +- **共同樣式**: + - 容器: `p-2 rounded-lg bg-slate-50 dark:bg-slate-800` + - 主色: `text-slate-400` + - 邊框: `border border-transparent` (防閃爍處理) + - 過渡: `transition-all` (使用預設速度以確保俐落感) + - 圖示粗細: `stroke-width="2.5"` + - 尺寸: `w-4 h-4` -```html -
- -
-
-

{{ __('Title') }}

-

{{ __('Subtitle') }}

-
-
- +- **編輯按鈕 (Edit)**: + - 懸停特效: `hover:text-cyan-500 hover:bg-cyan-500/5 hover:border-cyan-500/20` + - SVG 路徑: + ```html + + ``` + +- **查看詳情 (View/Detail)**: + - 懸停特效: `hover:text-indigo-500 hover:bg-indigo-500/5 hover:border-indigo-500/20` + - SVG 路徑: + ```html + + ``` + +- **刪除按鈕 (Delete)**: + - 懸停特效: `hover:text-rose-500 hover:bg-rose-500/5 hover:border-rose-500/20` + - SVG 路徑: + ```html + + ``` +y items-center gap-2">...
@@ -220,8 +253,9 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範
-
- + + +
diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php new file mode 100644 index 0000000..c0d8552 --- /dev/null +++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php @@ -0,0 +1,199 @@ +input('per_page', 20); + $query = Machine::query()->with(['machineModel', 'paymentConfig', 'company']); + + // 搜尋:名稱或序號 + if ($search = $request->input('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('serial_no', 'like', "%{$search}%"); + }); + } + + $machines = $query->latest() + ->paginate($per_page) + ->withQueryString(); + + $models = MachineModel::select('id', 'name')->get(); + $paymentConfigs = PaymentConfig::select('id', 'name')->get(); + // 這裡應根據租戶 (Company) 決定可用的選項,暫採簡單模擬或從 Auth 取得 + $companies = \App\Models\System\Company::select('id', 'name')->get(); + + return view('admin.basic-settings.machines.index', compact('machines', 'models', 'paymentConfigs', 'companies')); + } + + /** + * 儲存新機台 (僅核心欄位) + */ + public function store(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'serial_no' => 'required|string|unique:machines,serial_no', + 'company_id' => 'required|exists:companies,id', + 'machine_model_id' => 'required|exists:machine_models,id', + 'payment_config_id' => 'nullable|exists:payment_configs,id', + 'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048', + ]); + + $imagePaths = []; + if ($request->hasFile('images')) { + foreach (array_slice($request->file('images'), 0, 3) as $image) { + $imagePaths[] = $this->processAndStoreImage($image); + } + } + + $machine = Machine::create(array_merge($validated, [ + 'status' => 'offline', + 'creator_id' => auth()->id(), + 'updater_id' => auth()->id(), + 'card_reader_seconds' => 30, // 預設值 + 'card_reader_checkout_time_1' => '22:30:00', + 'card_reader_checkout_time_2' => '23:45:00', + 'payment_buffer_seconds' => 5, + 'images' => $imagePaths, + ])); + + return redirect()->route('admin.basic-settings.machines.index') + ->with('success', __('Machine created successfully.')); + } + + /** + * 顯示詳細編輯頁面 + */ + public function edit(Machine $machine): View + { + $models = MachineModel::select('id', 'name')->get(); + $paymentConfigs = PaymentConfig::select('id', 'name')->get(); + $companies = \App\Models\System\Company::select('id', 'name')->get(); + + return view('admin.basic-settings.machines.edit', compact('machine', 'models', 'paymentConfigs', 'companies')); + } + + /** + * 更新機台詳細參數 + */ + public function update(Request $request, Machine $machine): RedirectResponse + { + Log::info('Machine Update Request', ['machine_id' => $machine->id, 'data' => $request->all()]); + + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'card_reader_seconds' => 'required|integer|min:0', + 'payment_buffer_seconds' => 'required|integer|min:0', + 'card_reader_checkout_time_1' => 'nullable|string', + 'card_reader_checkout_time_2' => 'nullable|string', + 'heating_start_time' => 'nullable|string', + 'heating_end_time' => 'nullable|string', + 'card_reader_no' => 'nullable|string|max:255', + 'key_no' => 'nullable|string|max:255', + 'invoice_status' => 'required|integer|in:0,1,2', + 'welcome_gift_enabled' => 'boolean', + 'is_spring_slot_1_10' => 'boolean', + 'is_spring_slot_11_20' => 'boolean', + 'is_spring_slot_21_30' => 'boolean', + 'is_spring_slot_31_40' => 'boolean', + 'is_spring_slot_41_50' => 'boolean', + 'is_spring_slot_51_60' => 'boolean', + 'member_system_enabled' => 'boolean', + 'machine_model_id' => 'required|exists:machine_models,id', + 'payment_config_id' => 'nullable|exists:payment_configs,id', + ]); + + Log::info('Machine Update Validated Data', ['data' => $validated]); + } catch (\Illuminate\Validation\ValidationException $e) { + Log::error('Machine Update Validation Failed', ['errors' => $e->errors()]); + throw $e; + } + + $machine->update(array_merge($validated, [ + 'updater_id' => auth()->id(), + ])); + + // 處理圖片更新 (若有上傳新圖片,則替換或附加,這裡採簡單邏輯:若有傳 images 則全換) + if ($request->hasFile('images')) { + // 刪除舊圖 + if (!empty($machine->images)) { + foreach ($machine->images as $oldPath) { + Storage::disk('public')->delete($oldPath); + } + } + + $imagePaths = []; + foreach (array_slice($request->file('images'), 0, 3) as $image) { + $imagePaths[] = $this->processAndStoreImage($image); + } + $machine->update(['images' => $imagePaths]); + } + + return redirect()->route('admin.basic-settings.machines.index') + ->with('success', __('Machine settings updated successfully.')); + } + + /** + * 處理圖片並轉換為 WebP + */ + private function processAndStoreImage($file): string + { + $filename = Str::random(40) . '.webp'; + $path = 'machines/' . $filename; + + // 建立圖資源 + $image = null; + $extension = strtolower($file->getClientOriginalExtension()); + + switch ($extension) { + case 'jpeg': + case 'jpg': + $image = imagecreatefromjpeg($file->getRealPath()); + break; + case 'png': + $image = imagecreatefrompng($file->getRealPath()); + break; + case 'gif': + $image = imagecreatefromgif($file->getRealPath()); + break; + case 'webp': + $image = imagecreatefromwebp($file->getRealPath()); + break; + } + + if ($image) { + // 確保目錄存在 + Storage::disk('public')->makeDirectory('machines'); + $fullPath = Storage::disk('public')->path($path); + + // 轉換並儲存 + imagewebp($image, $fullPath, 80); // 品質 80 + imagedestroy($image); + + return $path; + } + + // Fallback to standard store if GD fails + return $file->store('machines', 'public'); + } +} diff --git a/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php b/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php new file mode 100644 index 0000000..a7936d4 --- /dev/null +++ b/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php @@ -0,0 +1,100 @@ +input('per_page', 20); + $configs = PaymentConfig::query() + ->with(['company', 'creator']) + ->latest() + ->paginate($per_page) + ->withQueryString(); + + return view('admin.basic-settings.payment-configs.index', [ + 'paymentConfigs' => $configs + ]); + } + + /** + * 顯示新增頁面 + */ + public function create(): View + { + $companies = \App\Models\System\Company::select('id', 'name')->get(); + return view('admin.basic-settings.payment-configs.create', compact('companies')); + } + + /** + * 儲存金流配置 + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'name' => 'required|string|max:255', + 'company_id' => 'required|exists:companies,id', + 'settings' => 'required|array', + ]); + + PaymentConfig::create([ + 'name' => $request->name, + 'company_id' => $request->company_id, + 'settings' => $request->settings, + 'creator_id' => auth()->id(), + 'updater_id' => auth()->id(), + ]); + + return redirect()->route('admin.basic-settings.payment-configs.index') + ->with('success', __('Payment Configuration created successfully.')); + } + + /** + * 顯示編輯頁面 + */ + public function edit(PaymentConfig $paymentConfig): View + { + $companies = \App\Models\System\Company::select('id', 'name')->get(); + return view('admin.basic-settings.payment-configs.edit', compact('paymentConfig', 'companies')); + } + + /** + * 更新金流配置 + */ + public function update(Request $request, PaymentConfig $paymentConfig): RedirectResponse + { + $request->validate([ + 'name' => 'required|string|max:255', + 'settings' => 'required|array', + ]); + + $paymentConfig->update([ + 'name' => $request->name, + 'settings' => $request->settings, + 'updater_id' => auth()->id(), + ]); + + return redirect()->route('admin.basic-settings.payment-configs.index') + ->with('success', __('Payment Configuration updated successfully.')); + } + + /** + * 刪除金流配置 + */ + public function destroy(PaymentConfig $paymentConfig): RedirectResponse + { + $paymentConfig->delete(); + return redirect()->route('admin.basic-settings.payment-configs.index') + ->with('success', __('Payment Configuration deleted successfully.')); + } +} diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index b7d03b1..e2ee1a7 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -96,7 +96,7 @@ class CompanyController extends Controller '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_name' => 'nullable|string|max:255', 'contact_phone' => 'nullable|string|max:50', 'contact_email' => 'nullable|email|max:255', 'valid_until' => 'nullable|date', diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index e1c89d5..6d4a8b5 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -14,8 +14,17 @@ class MachineController extends AdminController public function index(Request $request): View { $per_page = $request->input('per_page', 10); - $machines = Machine::query() - ->when($request->status, function ($query, $status) { + $query = Machine::query(); + + // 搜尋:名稱或序號 + if ($search = $request->input('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('serial_no', 'like', "%{$search}%"); + }); + } + + $machines = $query->when($request->status, function ($query, $status) { return $query->where('status', $status); }) ->latest() diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php index 3b452fe..d4688a3 100644 --- a/app/Http/Controllers/Admin/PermissionController.php +++ b/app/Http/Controllers/Admin/PermissionController.php @@ -11,15 +11,46 @@ class PermissionController extends Controller public function roles() { $per_page = request()->input('per_page', 10); - $roles = \Spatie\Permission\Models\Role::with(['permissions', 'users'])->latest()->paginate($per_page)->withQueryString(); - $all_permissions = \Spatie\Permission\Models\Permission::all()->groupBy(function($perm) { - if (str_starts_with($perm->name, 'menu.')) { - return 'menu'; - } - return 'other'; - }); + $user = auth()->user(); + $query = \App\Models\System\Role::query()->with(['permissions', 'users']); + + // 租戶隔離:租戶只能看到自己公司的角色 + 系統角色 (company_id is null) + if (!$user->isSystemAdmin()) { + $query->where(function($q) use ($user) { + $q->where('company_id', $user->company_id) + ->orWhereNull('company_id'); + }); + } + + // 搜尋:角色名稱 + if ($search = request()->input('search')) { + $query->where('name', 'like', "%{$search}%"); + } + + $roles = $query->latest()->paginate($per_page)->withQueryString(); + $all_permissions = \Spatie\Permission\Models\Permission::all() + ->filter(function($perm) { + // 排除子項目的權限,只顯示主選單權限 + $excluded = [ + 'menu.basic.machines', + 'menu.basic.payment-configs', + 'menu.companies', + 'menu.accounts', + 'menu.roles', + ]; + return !in_array($perm->name, $excluded); + }) + ->groupBy(function($perm) { + if (str_starts_with($perm->name, 'menu.')) { + return 'menu'; + } + return 'other'; + }); - return view('admin.permission.roles', compact('roles', 'all_permissions')); + // 根據路由決定標題 + $title = request()->routeIs('*.sub-account-roles') ? __('Sub Account Roles') : __('Role Settings'); + + return view('admin.permission.roles', compact('roles', 'all_permissions', 'title')); } /** @@ -33,14 +64,22 @@ class PermissionController extends Controller 'permissions.*' => 'string|exists:permissions,name', ]); - $role = \Spatie\Permission\Models\Role::create([ + $is_system = auth()->user()->isSystemAdmin() && $request->boolean('is_system'); + + $role = \App\Models\System\Role::create([ 'name' => $validated['name'], 'guard_name' => 'web', - 'is_system' => false, + 'company_id' => $is_system ? null : auth()->user()->company_id, + 'is_system' => $is_system, ]); if (!empty($validated['permissions'])) { - $role->syncPermissions($validated['permissions']); + $perms = $validated['permissions']; + // 如果不是系統角色,排除主選單的系統權限 + if (!$is_system) { + $perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']); + } + $role->syncPermissions($perms); } return redirect()->back()->with('success', __('Role created successfully.')); @@ -51,7 +90,7 @@ class PermissionController extends Controller */ public function updateRole(Request $request, $id) { - $role = \Spatie\Permission\Models\Role::findOrFail($id); + $role = \App\Models\System\Role::findOrFail($id); $validated = $request->validate([ 'name' => 'required|string|max:255|unique:roles,name,' . $id, @@ -59,11 +98,28 @@ class PermissionController extends Controller 'permissions.*' => 'string|exists:permissions,name', ]); - if (!$role->is_system) { - $role->update(['name' => $validated['name']]); + if ($role->name === 'super-admin') { + return redirect()->back()->with('error', __('The Super Admin role is immutable.')); } - $role->syncPermissions($validated['permissions'] ?? []); + if (!auth()->user()->isSystemAdmin() && $role->is_system) { + return redirect()->back()->with('error', __('System roles cannot be modified by tenant administrators.')); + } + + $is_system = auth()->user()->isSystemAdmin() ? $request->boolean('is_system') : $role->is_system; + + $role->update([ + 'name' => $validated['name'], + 'is_system' => $is_system, + 'company_id' => $is_system ? null : $role->company_id, + ]); + + $perms = $validated['permissions'] ?? []; + // 如果不是系統角色,排除主選單的系統權限 + if (!$is_system) { + $perms = array_diff($perms, ['menu.basic-settings', 'menu.permissions']); + } + $role->syncPermissions($perms); return redirect()->back()->with('success', __('Role updated successfully.')); } @@ -73,10 +129,14 @@ class PermissionController extends Controller */ public function destroyRole($id) { - $role = \Spatie\Permission\Models\Role::findOrFail($id); + $role = \App\Models\System\Role::findOrFail($id); - if ($role->is_system) { - return redirect()->back()->with('error', __('System roles cannot be deleted.')); + if ($role->name === 'super-admin') { + return redirect()->back()->with('error', __('The Super Admin role cannot be deleted.')); + } + + if (!auth()->user()->isSystemAdmin() && $role->is_system) { + return redirect()->back()->with('error', __('System roles cannot be deleted by tenant administrators.')); } if ($role->users()->count() > 0) { @@ -115,9 +175,19 @@ class PermissionController extends Controller $per_page = $request->input('per_page', 10); $users = $query->latest()->paginate($per_page)->withQueryString(); $companies = auth()->user()->isSystemAdmin() ? \App\Models\System\Company::all() : collect(); - $roles = \Spatie\Permission\Models\Role::all(); + $roles_query = \App\Models\System\Role::where('name', '!=', 'super-admin'); + if (!auth()->user()->isSystemAdmin()) { + $roles_query->where(function($q) { + $q->where('company_id', auth()->user()->company_id) + ->orWhereNull('company_id'); + }); + } + $roles = $roles_query->get(); - return view('admin.data-config.accounts', compact('users', 'companies', 'roles')); + // 根據路由決定標題 + $title = request()->routeIs('*.sub-accounts') ? __('Sub Account Management') : __('Account Management'); + + return view('admin.data-config.accounts', compact('users', 'companies', 'roles', 'title')); } /** @@ -158,6 +228,10 @@ class PermissionController extends Controller { $user = \App\Models\System\User::findOrFail($id); + if ($user->hasRole('super-admin')) { + return redirect()->back()->with('error', __('System super admin accounts cannot be modified via this interface.')); + } + $validated = $request->validate([ 'name' => 'required|string|max:255', 'username' => 'required|string|max:255|unique:users,username,' . $id, @@ -178,7 +252,13 @@ class PermissionController extends Controller ]; if (auth()->user()->isSystemAdmin()) { - $updateData['company_id'] = $validated['company_id']; + // 防止超級管理員不小心把自己綁定到租客公司或降級 + if ($user->id === auth()->id()) { + $updateData['company_id'] = null; + $validated['role'] = 'super-admin'; + } else { + $updateData['company_id'] = $validated['company_id']; + } } if (!empty($validated['password'])) { @@ -187,7 +267,12 @@ class PermissionController extends Controller $user->update($updateData); - $user->syncRoles([$validated['role']]); + // 如果是編輯自己且原本是超級管理員,強制保留 super-admin 角色 + if ($user->id === auth()->id() && auth()->user()->isSystemAdmin()) { + $user->syncRoles(['super-admin']); + } else { + $user->syncRoles([$validated['role']]); + } return redirect()->back()->with('success', __('Account updated successfully.')); } @@ -199,6 +284,10 @@ class PermissionController extends Controller { $user = \App\Models\System\User::findOrFail($id); + if ($user->hasRole('super-admin')) { + return redirect()->back()->with('error', __('System super admin accounts cannot be deleted.')); + } + if ($user->id === auth()->id()) { return redirect()->back()->with('error', __('You cannot delete your own account.')); } diff --git a/app/Listeners/LogSuccessfulLogin.php b/app/Listeners/LogSuccessfulLogin.php index f63a186..4d37776 100644 --- a/app/Listeners/LogSuccessfulLogin.php +++ b/app/Listeners/LogSuccessfulLogin.php @@ -2,7 +2,7 @@ namespace App\Listeners; -use App\Models\UserLoginLog; +use App\Models\System\UserLoginLog; use Illuminate\Auth\Events\Login; use Illuminate\Http\Request; diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index 6a29848..e06a6af 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -25,15 +25,78 @@ class Machine extends Model 'firmware_version', 'api_token', 'last_heartbeat_at', + 'card_reader_seconds', + 'card_reader_checkout_time_1', + 'card_reader_checkout_time_2', + 'heating_start_time', + 'heating_end_time', + 'payment_buffer_seconds', + 'card_reader_no', + 'key_no', + 'invoice_status', + 'welcome_gift_enabled', + 'is_spring_slot_1_10', + 'is_spring_slot_11_20', + 'is_spring_slot_21_30', + 'is_spring_slot_31_40', + 'is_spring_slot_41_50', + 'is_spring_slot_51_60', + 'member_system_enabled', + 'payment_config_id', + 'machine_model_id', + 'images', + 'creator_id', + 'updater_id', ]; protected $casts = [ 'last_heartbeat_at' => 'datetime', + 'welcome_gift_enabled' => 'boolean', + 'is_spring_slot_1_10' => 'boolean', + 'is_spring_slot_11_20' => 'boolean', + 'is_spring_slot_21_30' => 'boolean', + 'is_spring_slot_31_40' => 'boolean', + 'is_spring_slot_41_50' => 'boolean', + 'is_spring_slot_51_60' => 'boolean', + 'member_system_enabled' => 'boolean', + 'images' => 'array', ]; + /** + * Get machine images absolute URLs + */ + public function getImageUrlsAttribute(): array + { + if (empty($this->images)) { + return []; + } + + return array_map(fn($path) => \Illuminate\Support\Facades\Storage::disk('public')->url($path), $this->images); + } + public function logs() { return $this->hasMany(MachineLog::class); } + public function machineModel() + { + return $this->belongsTo(MachineModel::class); + } + + public function paymentConfig() + { + return $this->belongsTo(\App\Models\System\PaymentConfig::class); + } + + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'creator_id'); + } + + public function updater() + { + return $this->belongsTo(\App\Models\System\User::class, 'updater_id'); + } + } diff --git a/app/Models/Machine/MachineModel.php b/app/Models/Machine/MachineModel.php new file mode 100644 index 0000000..f2724de --- /dev/null +++ b/app/Models/Machine/MachineModel.php @@ -0,0 +1,35 @@ +hasMany(Machine::class); + } + + public function company() + { + return $this->belongsTo(\App\Models\System\Company::class); + } + + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'creator_id'); + } + + public function updater() + { + return $this->belongsTo(\App\Models\System\User::class, 'updater_id'); + } +} diff --git a/app/Models/System/PaymentConfig.php b/app/Models/System/PaymentConfig.php new file mode 100644 index 0000000..8f0b126 --- /dev/null +++ b/app/Models/System/PaymentConfig.php @@ -0,0 +1,40 @@ + 'array', + ]; + + public function machines() + { + return $this->hasMany(\App\Models\Machine\Machine::class); + } + + public function company() + { + return $this->belongsTo(\App\Models\System\Company::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'creator_id'); + } + + public function updater() + { + return $this->belongsTo(User::class, 'updater_id'); + } +} diff --git a/app/Models/System/Role.php b/app/Models/System/Role.php new file mode 100644 index 0000000..cc6dc32 --- /dev/null +++ b/app/Models/System/Role.php @@ -0,0 +1,34 @@ +belongsTo(Company::class); + } + + /** + * Scope a query to only include roles for a specific company or system roles. + */ + public function scopeForCompany($query, $company_id) + { + return $query->where(function($q) use ($company_id) { + $q->where('company_id', $company_id) + ->orWhereNull('company_id'); + }); + } +} diff --git a/app/Models/System/User.php b/app/Models/System/User.php index 066b705..2d2f571 100644 --- a/app/Models/System/User.php +++ b/app/Models/System/User.php @@ -58,7 +58,7 @@ class User extends Authenticatable */ public function loginLogs() { - return $this->hasMany(\App\Models\UserLoginLog::class); + return $this->hasMany(UserLoginLog::class); } /** diff --git a/app/Models/UserLoginLog.php b/app/Models/System/UserLoginLog.php similarity index 82% rename from app/Models/UserLoginLog.php rename to app/Models/System/UserLoginLog.php index dc70a54..fc4993b 100644 --- a/app/Models/UserLoginLog.php +++ b/app/Models/System/UserLoginLog.php @@ -1,9 +1,9 @@ Spatie\Permission\Models\Role::class, + 'role' => \App\Models\System\Role::class, ], diff --git a/database/migrations/2026_03_17_105051_create_machine_models_table.php b/database/migrations/2026_03_17_105051_create_machine_models_table.php new file mode 100644 index 0000000..8efd6ae --- /dev/null +++ b/database/migrations/2026_03_17_105051_create_machine_models_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name')->comment('型號名稱'); + $table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade')->comment('關聯公司'); + $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者'); + $table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('machine_models'); + } +}; diff --git a/database/migrations/2026_03_17_105100_create_payment_configs_table.php b/database/migrations/2026_03_17_105100_create_payment_configs_table.php new file mode 100644 index 0000000..93cc8f2 --- /dev/null +++ b/database/migrations/2026_03_17_105100_create_payment_configs_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('company_id')->constrained()->onDelete('cascade')->comment('關聯公司'); + $table->string('name')->comment('組合名稱'); + $table->json('settings')->nullable()->comment('金流參數 (JSON)'); + $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者'); + $table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('payment_configs'); + } +}; diff --git a/database/migrations/2026_03_17_105101_add_settings_to_machines_table.php b/database/migrations/2026_03_17_105101_add_settings_to_machines_table.php new file mode 100644 index 0000000..0a5b23c --- /dev/null +++ b/database/migrations/2026_03_17_105101_add_settings_to_machines_table.php @@ -0,0 +1,76 @@ +integer('card_reader_seconds')->nullable()->default(30)->comment('刷卡機秒數'); + $table->time('card_reader_checkout_time_1')->nullable()->default('22:30:00')->comment('卡機結帳時間1'); + $table->time('card_reader_checkout_time_2')->nullable()->default('23:45:00')->comment('卡機結帳時間2'); + $table->time('heating_start_time')->default('00:00:00')->comment('開啟-加熱時間'); + $table->time('heating_end_time')->default('00:00:00')->comment('關閉-加熱時間'); + $table->integer('payment_buffer_seconds')->nullable()->default(5)->comment('金流緩衝時間(s)'); + $table->string('card_reader_no')->nullable()->comment('刷卡機編號'); + $table->string('key_no')->nullable()->comment('鑰匙編號'); + $table->tinyInteger('invoice_status')->default(0)->comment('發票狀態碼: 0=不開發票, 1=預設捐, 2=預設不捐'); + $table->boolean('welcome_gift_enabled')->default(0)->comment('來店禮開關'); + $table->boolean('is_spring_slot_1_10')->default(0)->comment('貨道類型(1~10): 0=履帶, 1=彈簧'); + $table->boolean('is_spring_slot_11_20')->default(0)->comment('貨道類型(11~20): 0=履帶, 1=彈簧'); + $table->boolean('is_spring_slot_21_30')->default(0)->comment('貨道類型(21~30): 0=履帶, 1=彈簧'); + $table->boolean('is_spring_slot_31_40')->default(0)->comment('貨道類型(31~40): 0=履帶, 1=彈簧'); + $table->boolean('is_spring_slot_41_50')->default(0)->comment('貨道類型(41~50): 0=履帶, 1=彈簧'); + $table->boolean('is_spring_slot_51_60')->default(0)->comment('貨道類型(51~60): 0=履帶, 1=彈簧'); + $table->boolean('member_system_enabled')->default(0)->comment('會員系統開關'); + + $table->foreignId('payment_config_id')->nullable()->constrained('payment_configs')->nullOnDelete()->comment('關聯金流參數組合'); + $table->foreignId('machine_model_id')->nullable()->constrained('machine_models')->nullOnDelete()->comment('關類型號組合'); + $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete()->comment('建立者'); + $table->foreignId('updater_id')->nullable()->constrained('users')->nullOnDelete()->comment('修改者'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('machines', function (Blueprint $table) { + $table->dropForeign(['payment_config_id']); + $table->dropForeign(['machine_model_id']); + $table->dropForeign(['creator_id']); + $table->dropForeign(['updater_id']); + + $table->dropColumn([ + 'card_reader_seconds', + 'card_reader_checkout_time_1', + 'card_reader_checkout_time_2', + 'heating_start_time', + 'heating_end_time', + 'payment_buffer_seconds', + 'card_reader_no', + 'key_no', + 'invoice_status', + 'welcome_gift_enabled', + 'is_spring_slot_1_10', + 'is_spring_slot_11_20', + 'is_spring_slot_21_30', + 'is_spring_slot_31_40', + 'is_spring_slot_41_50', + 'is_spring_slot_51_60', + 'member_system_enabled', + 'payment_config_id', + 'machine_model_id', + 'creator_id', + 'updater_id', + ]); + }); + } +}; diff --git a/database/migrations/2026_03_17_131857_add_images_to_machines_table.php b/database/migrations/2026_03_17_131857_add_images_to_machines_table.php new file mode 100644 index 0000000..5d6247c --- /dev/null +++ b/database/migrations/2026_03_17_131857_add_images_to_machines_table.php @@ -0,0 +1,28 @@ +json('images')->after('firmware_version')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('machines', function (Blueprint $table) { + $table->dropColumn('images'); + }); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index 1f466fe..b9d7fc3 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -31,9 +31,8 @@ class RoleSeeder extends Seeder 'menu.line', 'menu.reservation', 'menu.special-permission', - 'menu.companies', - 'menu.accounts', - 'menu.roles', + 'menu.basic-settings', + 'menu.permissions', ]; foreach ($permissions as $permission) { @@ -47,9 +46,23 @@ class RoleSeeder extends Seeder ); $superAdmin->syncPermissions(Permission::all()); - Role::updateOrCreate( + $tenantAdmin = Role::updateOrCreate( ['name' => 'tenant-admin'], - ['is_system' => true] + ['is_system' => false] ); + $tenantAdmin->syncPermissions([ + 'menu.members', + 'menu.machines', + 'menu.app', + 'menu.warehouses', + 'menu.sales', + 'menu.analysis', + 'menu.audit', + 'menu.data-config', + 'menu.remote', + 'menu.line', + 'menu.reservation', + 'menu.special-permission', + ]); } } diff --git a/docs/architecture_plan.md b/docs/architecture_plan.md index 7c02008..0f46126 100644 --- a/docs/architecture_plan.md +++ b/docs/architecture_plan.md @@ -187,18 +187,59 @@ Spatie 預設的 roles 表必須加上多租戶設計,確保留戶只能管理 --- -#### 🟡 `machines` 表 — 需擴充欄位 +### 機台設定與參數模型 (Configuration & Parameters) -現有 `machines` 表僅有基礎欄位,需為 B010 API 補充: +為了提升機台的可維護性與金流設定的靈活性,系統引入了以下結構: -| 新增欄位 | 類型 | 說明 | 來源 API | -|----------|------|------|----------| -| `serial_no` | `VARCHAR UNIQUE` | 機台序號(API 用此識別) | B010 `machine` | -| `model` | `VARCHAR` | 機台型號 | B010 `M_Stus` | -| `current_page` | `TINYINT` | 當前頁面狀態碼 | B010 `M_Stus2` | -| `door_status` | `VARCHAR` | 門禁狀態 | B010 `door` | -| `is_online` | `BOOLEAN` | 是否在線(心跳超時判斷) | 計算欄位 | -| `api_token` | `VARCHAR` | 機台專屬 API Token | 取代硬編碼 key | +#### 1. 機台型號設定表 (machine_models) +記錄系統支援的機台型號,供機台綁定基礎參數。 + +| 欄位 | 類型 | 說明 | +|------|------|------| +| `id` | BIGINT PK | — | +| `name` | VARCHAR(255) | 型號名稱 (例如: "B010-STD") | +| `company_id` | BIGINT FK | 所屬公司 (支援多租戶隔離) | +| `creator_id / updater_id` | BIGINT FK | 審計記錄 (建立者/修改者) | +| `timestamps` | — | — | + +--- + +#### 2. 金流參數組合表 (payment_configs) +**範本化設計**:由公司建立支付參數組合範本,機台端選擇套用。 + +| 欄位 | 類型 | 說明 | +|------|------|------| +| `id` | BIGINT PK | — | +| `company_id` | BIGINT FK | 所屬公司 | +| `name` | VARCHAR(255) | 組合名稱 (例如: "玉山+綠界組合A") | +| `settings` | **JSON** | 儲存各支付平台 API Key (Ecpay, Esun, Tappay, LinePay 等) | +| `creator_id / updater_id` | BIGINT FK | 審計記錄 | +| `timestamps` | — | — | + +--- + +#### 3. 機台主表擴充 (machines) +針對 `machines` 表擴充以下欄位以支援 IoT 通訊與進階營運設定: + +| 類別 | 欄位 | 類型 | 說明 | +|------|------|------|------| +| **基礎識別** | `serial_no` | `VARCHAR` | 機台序號 (API 唯一識別碼) | +| **狀態監控** | `current_page` | `VARCHAR` | 機台當前畫面停留頁面 | +| | `door_status` | `VARCHAR` | 門禁狀態 (open/closed) | +| | `temperature` | `DECIMAL` | 機器溫度 (來自硬體回傳) | +| **金流設定** | `payment_config_id` | `BIGINT FK` | 關聯金流參數樣本 (`payment_configs`) | +| | `card_reader_seconds` | `INT` | 刷卡機秒數 (預設 30) | +| | `card_reader_checkout_time_1 / 2` | `TIME` | 卡機結帳清機時間 | +| | `payment_buffer_seconds` | `INT` | 金流回傳緩衝時間 (預設 5) | +| **硬體與營運**| `machine_model_id` | `BIGINT FK` | 關聯機台型號 (`machine_models`) | +| | `heating_start / end_time` | `TIME` | 加熱自動排程開啟與關閉時間 | +| | `card_reader_no / key_no` | `VARCHAR` | 刷卡機編號與鑰匙編號 | +| | `invoice_status` | `TINYINT` | 發票狀態 (0:不開, 1:預設捐, 2:預設不捐) | +| | `welcome_gift_enabled` | `BOOLEAN` | 來店禮開關 | +| | `member_system_enabled` | `BOOLEAN` | 會員系統開關 | +| | `is_spring_slot_1_10` ~ `60` | `BOOLEAN` | 貨道類型標記 (0=履帶, 1=彈簧) | +| **審計權限** | `company_id` | `BIGINT FK` | 關聯所屬公司 (租戶隔離) | +| | `creator_id / updater_id` | `BIGINT FK` | 建立者與最後修改者 ID | --- diff --git a/lang/en.json b/lang/en.json index e756e9c..441b27d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -140,13 +140,15 @@ "Clear Stock": "Clear Stock", "APK Versions": "APK Versions", "Discord Notifications": "Discord Notifications", + "Basic Settings": "Basic Settings", + "Machine Settings": "Machine Settings", "Permission Settings": "Permission Settings", "APP Features": "APP Features", "Sales": "Sales", "Others": "Others", "AI Prediction": "AI Prediction", - "Roles": "Roles", - "Role Management": "Role Management", + "Roles": "Role Permissions", + "Role Management": "Role Permission Management", "Define and manage security roles and permissions.": "Define and manage security roles and permissions.", "Search roles...": "Search roles...", "No permissions": "No permissions", @@ -162,6 +164,11 @@ "Permissions": "Permissions", "Users": "Users", "System role name cannot be modified.": "System role name cannot be modified.", + "The Super Admin role name cannot be modified.": "The Super Admin role name cannot be modified.", + "System Level": "System Level", + "Company Level": "Company Level", + "Global roles accessible by all administrators.": "Global roles accessible by all administrators.", + "Roles scoped to specific customer companies.": "Roles scoped to specific customer companies.", "members": "Member Management", "machines": "Machine Management", "app": "APP Management", @@ -176,8 +183,9 @@ "special-permission": "Special Permission", "companies": "Customer Management", "accounts": "Account Management", - "roles": "Role Settings", - "Role Settings": "Role Settings", + "roles": "Role Permissions", + "Role Permissions": "Role Permissions", + "Role Settings": "Role Permissions", "No login history yet": "No login history yet", "Signed in as": "Signed in as", "Logout": "Logout", @@ -268,5 +276,7 @@ "Unknown": "Unknown", "Info": "Info", "Warning": "Warning", + "basic-settings": "Basic Settings", + "permissions": "Permission Settings", "Error": "Error" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 1687864..b5be74c 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -140,13 +140,15 @@ "Clear Stock": "在庫クリア", "APK Versions": "APKバージョン", "Discord Notifications": "Discord通知", + "Basic Settings": "基本設定", + "Machine Settings": "機台設定", "Permission Settings": "権限設定", "APP Features": "APP機能", "Sales": "販売", "Others": "その他", "AI Prediction": "AI予測", - "Roles": "ロール", - "Role Management": "ロール管理", + "Roles": "ロール権限", + "Role Management": "ロール権限管理", "Define and manage security roles and permissions.": "システムのセキュリティロールと権限を定義および管理します。", "Search roles...": "ロールを検索...", "No permissions": "権限項目なし", @@ -162,6 +164,8 @@ "Permissions": "権限", "Users": "ユーザー数", "System role name cannot be modified.": "システムロール名は変更できません。", + "System Level": "システムレベル", + "Company Level": "顧客レベル", "Menu Permissions": "メニュー権限", "Save Changes": "変更を保存", "members": "会員管理", @@ -178,8 +182,9 @@ "special-permission": "特別権限", "companies": "顧客管理", "accounts": "アカウント管理", - "roles": "ロール設定", - "Role Settings": "ロール設定", + "roles": "ロール権限", + "Role Permissions": "ロール権限", + "Role Settings": "ロール權限", "No login history yet": "ログイン履歴はまだありません", "Signed in as": "ログイン中", "Logout": "ログアウト", @@ -269,5 +274,7 @@ "Unknown": "不明", "Info": "情報", "Warning": "警告", + "basic-settings": "基本設定", + "permissions": "權限設定", "Error": "エラー" } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 261bbe6..9bb7c71 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -140,13 +140,15 @@ "Clear Stock": "庫存清空", "APK Versions": "APK版本", "Discord Notifications": "Discord通知", + "Basic Settings": "基本設定", + "Machine Settings": "機台設定", "Permission Settings": "權限設定", "APP Features": "APP功能", "Sales": "銷售管理", "Others": "其他功能", "AI Prediction": "AI智能預測", - "Roles": "角色設定", - "Role Management": "角色管理", + "Roles": "角色權限", + "Role Management": "角色權限管理", "Define and manage security roles and permissions.": "定義並管理系統安全角色與權限。", "Search roles...": "搜尋角色...", "No permissions": "無權限項目", @@ -162,6 +164,11 @@ "Permissions": "權限", "Users": "帳號數", "System role name cannot be modified.": "內建系統角色的名稱無法修改。", + "The Super Admin role name cannot be modified.": "超級管理員角色的名稱無法修改。", + "System Level": "系統層級", + "Company Level": "客戶層級", + "Global roles accessible by all administrators.": "適用於所有管理者的全域角色。", + "Roles scoped to specific customer companies.": "適用於各個客戶單位的特定角色。", "members": "會員管理", "machines": "機台管理", "app": "APP 管理", @@ -176,8 +183,9 @@ "special-permission": "特殊權限", "companies": "客戶管理", "accounts": "帳號管理", - "roles": "角色設定", - "Role Settings": "角色設定", + "roles": "角色權限", + "Role Permissions": "角色權限", + "Role Settings": "角色權限", "No login history yet": "尚無登入紀錄", "Signed in as": "登入身份", "Logout": "登出", @@ -273,5 +281,78 @@ "Unknown": "未知", "Info": "一般", "Warning": "警告", - "Error": "錯誤" + "Error": "錯誤", + "Management of operational parameters": "機台運作參數管理", + "Add Machine": "新增機台", + "Search machines...": "搜尋機台...", + "Items": "項", + "Machine Name": "機台名稱", + "Serial No": "機台序號", + "Owner": "所屬客戶", + "Model": "機台型號", + "Action": "操作", + "No location set": "尚未設定位置", + "Edit Settings": "編輯設定", + "Enter machine name": "請輸入機台名稱", + "Enter serial number": "請輸入機台序號", + "Select Owner": "請選擇所屬客戶", + "Select Model": "請選擇機台型號", + "Customer Payment Config": "客戶金流設定", + "Not Used": "不使用", + "Edit Machine Settings": "編輯機台設定", + "Operational Parameters": "運作參數", + "Card Reader Seconds": "刷卡機秒數", + "Payment Buffer Seconds": "金流緩衝時間(s)", + "Checkout Time 1": "卡機結帳時間1", + "Checkout Time 2": "卡機結帳時間2", + "Heating Start Time": "開啟-加熱時間", + "Heating End Time": "關閉-加熱時間", + "Hardware & Slots": "硬體與貨道設定", + "Card Reader No": "刷卡機編號", + "Key No": "鑰匙編號", + "Slot Mechanism (default: Conveyor, check for Spring)": "貨道類型 (預設履帶,勾選為彈簧)", + "Payment & Invoice": "金流與發票", + "Invoice Status": "發票狀態碼", + "No Invoice": "不開發票", + "Default Donate": "開發票預設捐", + "Default Not Donate": "開發票預設不捐", + "Member & External": "會員與外部系統", + "Welcome Gift": "來店禮開關", + "Enabled/Disabled": "啟用/禁用", + "Member System": "會員系統", + "Payment Configuration": "客戶金流設定", + "Merchant payment gateway settings management": "特約商店支付網關參數管理", + "Create Config": "建立配置", + "Config Name": "配置名稱", + "Last Updated": "最後更新日期", + "Are you sure you want to delete this configuration?": "您確定要刪除此金流配置嗎?", + "Create Payment Config": "建立金流配置", + "Define new third-party payment parameters": "定義新的第三方支付介接參數", + "Save Config": "儲存配置", + "Configuration Name": "金流組合名稱", + "Belongs To Company": "所屬客戶公司", + "ECPay Invoice": "綠界發票", + "Store ID": "特約商店代號 (MerchantID)", + "HashKey": "HashKey", + "HashIV": "HashIV", + "E.SUN QR Scan": "玉山掃碼", + "StoreID": "商店代號 (StoreID)", + "TermID": "終端代號 (TermID)", + "Key": "金鑰 (Key)", + "LINE Pay Direct": "Line官方支付", + "ChannelId": "ChannelId", + "ChannelSecret": "ChannelSecret", + "TapPay Integration": "TapPay 整合支付", + "PARTNER_KEY": "PARTNER_KEY", + "APP_ID": "APP_ID", + "APP_KEY": "APP_KEY", + "Merchant IDs": "特約商店代號 (Merchant IDs)", + "LINE_MERCHANT_ID": "LINE Pay 商店代號", + "JKO_MERCHANT_ID": "街口支付 商店代號", + "PI_MERCHANT_ID": "Pi 拍錢包 商店代號", + "PS_MERCHANT_ID": "全盈+Pay 商店代號", + "EASY_MERCHANT_ID": "悠遊付 商店代號", + "basic-settings": "基本設定", + "permissions": "權限設定", + "Edit Payment Config": "編輯金流配置" } \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index 3ab8179..9d39296 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -99,9 +99,9 @@ @layer components { .luxury-nav-item { - @apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-medium rounded-xl transition-all duration-200; - @apply text-slate-500 hover:text-slate-900 hover:bg-slate-100; - @apply dark:text-slate-400 dark:hover:text-white dark:hover:bg-white/5; + @apply flex items-center gap-x-3.5 py-2.5 px-4 text-sm font-semibold rounded-xl transition-all duration-200; + @apply text-slate-600 hover:text-slate-900 hover:bg-slate-100; + @apply dark:text-slate-300 dark:hover:text-white dark:hover:bg-white/5; } .luxury-nav-item.active { @@ -184,13 +184,17 @@ @apply dark:ring-cyan-500/20 dark:border-cyan-400/50; } - /* Date Input Calendar Icon Optimization */ - .luxury-input[type="date"]::-webkit-calendar-picker-indicator { + /* Input Icon Optimization (Date/Time) */ + .luxury-input[type="date"]::-webkit-calendar-picker-indicator, + .luxury-input[type="time"]::-webkit-calendar-picker-indicator, + .luxury-input[type="datetime-local"]::-webkit-calendar-picker-indicator { @apply cursor-pointer transition-opacity hover:opacity-100; opacity: 0.6; } - .dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator { + .dark .luxury-input[type="date"]::-webkit-calendar-picker-indicator, + .dark .luxury-input[type="time"]::-webkit-calendar-picker-indicator, + .dark .luxury-input[type="datetime-local"]::-webkit-calendar-picker-indicator { filter: invert(1); } diff --git a/resources/views/admin/basic-settings/machines/edit.blade.php b/resources/views/admin/basic-settings/machines/edit.blade.php new file mode 100644 index 0000000..ced0f3a --- /dev/null +++ b/resources/views/admin/basic-settings/machines/edit.blade.php @@ -0,0 +1,251 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+ + + +
+

{{ __('Edit Machine Settings') }}

+

{{ $machine->name }} / {{ $machine->serial_no }}

+
+
+
+ +
+
+ +
+ @csrf + @method('PUT') + + + @if ($errors->any()) +
+
+ +

{{ __('Some fields need attention') }}

+
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + +
+ +
+ +
+
+
+ +
+

{{ __('Basic Information') }}

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

{{ __('Operational Parameters') }}

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

{{ __('Hardware & Slots') }}

+
+ +
+
+
+ + +
+
+ + +
+
+ +
+ +
+ @foreach([['1-10', 'is_spring_slot_1_10'], ['11-20', 'is_spring_slot_11_20'], ['21-30', 'is_spring_slot_21_30'], ['31-40', 'is_spring_slot_31_40'], ['41-50', 'is_spring_slot_41_50'], ['51-60', 'is_spring_slot_51_60']] as $slot) + + @endforeach +
+
+
+
+
+ + +
+
+
+
+ +
+

{{ __('Payment & Invoice') }}

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

{{ __('Member & External') }}

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

{{ __('Machine Images') }}

+
+ +
+ @if(!empty($machine->image_urls)) +
+ @foreach($machine->image_urls as $url) +
+ +
+ @endforeach +
+ @else +
+

{{ __('No images uploaded') }}

+
+ @endif + +
+ + +

* {{ __('Uploading new images will replace all existing images.') }}

+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php new file mode 100644 index 0000000..ba254af --- /dev/null +++ b/resources/views/admin/basic-settings/machines/index.blade.php @@ -0,0 +1,317 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+

{{ __('Machine Settings') }}

+

{{ __('Management of operational parameters') }}

+
+
+ +
+
+ + +
+ +
+
+ + + + + + + +
+ +
+ + +
+ + + + + + + + + + + + @foreach($machines as $machine) + + + + + + + + @endforeach + +
{{ __('Machine Info') }}{{ __('Status') }}{{ __('Card Reader') }}{{ __('Owner') }}{{ __('Action') }}
+
+
+ +
+
+
{{ $machine->name }}
+
+ {{ $machine->serial_no }} + + {{ $machine->machineModel->name ?? '--' }} +
+
+
+
+ @php + $isOnline = $machine->last_heartbeat_at && $machine->last_heartbeat_at->diffInMinutes() < 5; + @endphp +
+
+ @if($isOnline) + + + @else + + @endif +
+ + {{ $isOnline ? __('Online') : __('Offline') }} + +
+
+
+ {{ $machine->card_reader_seconds ?? 0 }}s / No.{{ $machine->card_reader_no ?? '--' }} +
+
+ + {{ $machine->company->name ?? __('None') }} + + + + + + +
+
+ + +
+ {{ $machines->links('vendor.pagination.luxury') }} +
+
+ + + + + + +
+@endsection diff --git a/resources/views/admin/basic-settings/payment-configs/create.blade.php b/resources/views/admin/basic-settings/payment-configs/create.blade.php new file mode 100644 index 0000000..0aa4cee --- /dev/null +++ b/resources/views/admin/basic-settings/payment-configs/create.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+ + + +
+

{{ __('Create Payment Config') }}

+

{{ __('Define new third-party payment parameters') }}

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

{{ __('ECPay Invoice') }}

+

綠界科技電子發票設定

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

{{ __('E.SUN QR Scan') }}

+

玉山銀行掃碼支付設定

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

{{ __('LINE Pay Direct') }}

+

LINE Pay 官方直連設定

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

{{ __('TapPay Integration') }}

+

喬睿科技支付串接設定

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

{{ __('Merchant IDs (商店代號群)') }}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/basic-settings/payment-configs/edit.blade.php b/resources/views/admin/basic-settings/payment-configs/edit.blade.php new file mode 100644 index 0000000..f586d6b --- /dev/null +++ b/resources/views/admin/basic-settings/payment-configs/edit.blade.php @@ -0,0 +1,191 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+ + + +
+

{{ __('Edit Payment Config') }}

+

{{ $paymentConfig->name }}

+
+
+
+ +
+
+ +
+ @csrf + @method('PUT') + +
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + + @php + $settings = $paymentConfig->settings ?? []; + @endphp +
+ +
+
+
+ +
+
+

{{ __('ECPay Invoice') }}

+

綠界科技電子發票設定

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

{{ __('E.SUN QR Scan') }}

+

玉山銀行掃碼支付設定

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

{{ __('LINE Pay Direct') }}

+

LINE Pay 官方直連設定

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

{{ __('TapPay Integration') }}

+

喬睿科技支付串接設定

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

{{ __('Merchant IDs (商店代號群)') }}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/admin/basic-settings/payment-configs/index.blade.php b/resources/views/admin/basic-settings/payment-configs/index.blade.php new file mode 100644 index 0000000..bfb7e84 --- /dev/null +++ b/resources/views/admin/basic-settings/payment-configs/index.blade.php @@ -0,0 +1,82 @@ +@extends('layouts.admin') + +@section('content') +
+ +
+
+

{{ __('Payment Configuration') }}

+

{{ __('Merchant payment gateway settings management') }}

+
+ +
+ + +
+
+ + + + + + + + + + + @foreach($paymentConfigs as $config) + + + + + + + @endforeach + +
{{ __('Config Name') }}{{ __('Belongs To') }}{{ __('Last Updated') }}{{ __('Action') }}
+
+
+ +
+
+
{{ $config->name }}
+
ID: {{ str_pad($config->id, 5, '0', STR_PAD_LEFT) }}
+
+
+
+ + {{ $config->company->name ?? __('None') }} + + +
+ {{ $config->updated_at->format('Y/m/d H:i') }} + {{ $config->updated_at->diffForHumans() }} +
+
+ + + +
+ @csrf + @method('DELETE') + +
+
+
+ +
+ {{ $paymentConfigs->links('vendor.pagination.luxury') }} +
+
+
+@endsection diff --git a/resources/views/admin/companies/index.blade.php b/resources/views/admin/companies/index.blade.php index 5321a16..786c87f 100644 --- a/resources/views/admin/companies/index.blade.php +++ b/resources/views/admin/companies/index.blade.php @@ -62,16 +62,16 @@
- + @@ -254,12 +254,10 @@
@csrf - +
diff --git a/resources/views/admin/data-config/accounts.blade.php b/resources/views/admin/data-config/accounts.blade.php index d4175d5..5bf2f44 100644 --- a/resources/views/admin/data-config/accounts.blade.php +++ b/resources/views/admin/data-config/accounts.blade.php @@ -1,5 +1,10 @@ @extends('layouts.admin') +@php + $routeName = request()->route()->getName(); + $baseRoute = str_contains($routeName, 'sub-accounts') ? 'admin.data-config.sub-accounts' : 'admin.permission.accounts'; +@endphp + @section('content')
-

{{ __('Account Management') }}

-

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

+

{{ $title }}

+

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