[FEAT] 實作角色權限分類、租戶角控管理與介面多語系優化

1. [FEAT] 權限劃分為「系統層級」與「客戶層級」,並在後端強制過濾跨權限分配。
2. [FEAT] 整合選單權限至主選單層級 (基本設定、權限設定),簡化角色管理 UI。
3. [STYLE] 側邊欄優化:補齊多語系翻譯,並為基本設定子選單增加視覺圖示。
4. [REFACTOR] 更新 RoleSeeder,將 tenant-admin 重新分類為客戶層級角色。
This commit is contained in:
2026-03-17 16:53:28 +08:00
parent 3ce88ed342
commit fc79148879
38 changed files with 2398 additions and 303 deletions

View File

@@ -0,0 +1,199 @@
<?php
namespace App\Http\Controllers\Admin\BasicSettings;
use App\Http\Controllers\Admin\AdminController;
use App\Models\Machine\Machine;
use App\Models\Machine\MachineModel;
use App\Models\System\PaymentConfig;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
class MachineSettingController extends AdminController
{
/**
* 顯示機台設定列表
*/
public function index(Request $request): View
{
$per_page = $request->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');
}
}