[FEAT] 實作維修管理模組與 RBAC 權限整合、多語系支援及 UI 優化
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m3s

This commit is contained in:
2026-03-25 14:25:42 +08:00
parent 3d24ddff5a
commit 37ef6f1c10
23 changed files with 1446 additions and 460 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin\BasicSettings;
use App\Http\Controllers\Controller;
use App\Models\Machine\Machine;
use App\Traits\ImageHandler;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@@ -11,6 +12,8 @@ use Illuminate\Support\Facades\Storage;
class MachinePhotoController extends Controller
{
use ImageHandler;
/**
* 更新機台照片
*/
@@ -64,50 +67,4 @@ class MachinePhotoController extends Controller
return back()->with('error', __('Failed to update machine images: ') . $e->getMessage());
}
}
/**
* 將圖片轉換為 WebP 並儲存
*/
protected function storeAsWebp($file, $directory): string
{
$extension = $file->getClientOriginalExtension();
$filename = uniqid() . '.webp';
$path = "{$directory}/{$filename}";
// 讀取原始圖片
$imageType = exif_imagetype($file->getRealPath());
switch ($imageType) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($file->getRealPath());
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($file->getRealPath());
break;
case IMAGETYPE_WEBP:
$source = imagecreatefromwebp($file->getRealPath());
break;
default:
// 如果格式不支援,直接存
return $file->storeAs($directory, $file->hashName(), 'public');
}
if (!$source) {
return $file->storeAs($directory, $file->hashName(), 'public');
}
// 確保支援真彩色(解決 palette image 問題)
if (!imageistruecolor($source)) {
imagepalettetotruecolor($source);
}
// 捕捉輸出
ob_start();
imagewebp($source, null, 80);
$content = ob_get_clean();
imagedestroy($source);
Storage::disk('public')->put($path, $content);
return $path;
}
}

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Admin\AdminController;
use App\Models\Machine\Machine;
use App\Models\Machine\MachineModel;
use App\Models\System\PaymentConfig;
use App\Traits\ImageHandler;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
@@ -16,6 +17,8 @@ use Illuminate\Support\Facades\Log;
class MachineSettingController extends AdminController
{
use ImageHandler;
/**
* 顯示機台與型號設定列表 (採用標籤頁整合)
*/
@@ -75,7 +78,7 @@ class MachineSettingController extends AdminController
$imagePaths = [];
if ($request->hasFile('images')) {
foreach (array_slice($request->file('images'), 0, 3) as $image) {
$imagePaths[] = $this->processAndStoreImage($image);
$imagePaths[] = $this->storeAsWebp($image, 'machines');
}
}
@@ -171,7 +174,7 @@ class MachineSettingController extends AdminController
}
// 處理並儲存新圖
$currentImages[$index] = $this->processAndStoreImage($file);
$currentImages[$index] = $this->storeAsWebp($file, 'machines');
$updated = true;
}
@@ -184,49 +187,4 @@ class MachineSettingController extends AdminController
return redirect()->route('admin.basic-settings.machines.index')
->with('success', __('Machine settings updated successfully.'));
}
/**
* 處理並儲存圖片 (轉換為 WebP 並調整大小)
*/
protected function processAndStoreImage($file)
{
$path = 'machines/' . \Illuminate\Support\Str::random(40) . '.webp';
// 載入原圖
$imageInfo = getimagesize($file->getRealPath());
$mime = $imageInfo['mime'];
switch ($mime) {
case 'image/jpeg':
$image = imagecreatefromjpeg($file->getRealPath());
break;
case 'image/png':
$image = imagecreatefrompng($file->getRealPath());
break;
case 'image/gif':
$image = imagecreatefromgif($file->getRealPath());
break;
default:
return $file->store('machines', 'public');
}
if ($image) {
// [修正] imagewebp(): Palette image not supported by webp
// 若為 Palette 圖片 (例如 GIF),轉換為 Truecolor
if (!imageistruecolor($image)) {
imagepalettetotruecolor($image);
}
\Illuminate\Support\Facades\Storage::disk('public')->makeDirectory('machines');
$fullPath = \Illuminate\Support\Facades\Storage::disk('public')->path($path);
// 轉換並儲存 (品質 80)
imagewebp($image, $fullPath, 80);
imagedestroy($image);
return $path;
}
return $file->store('machines', 'public');
}
}

View File

@@ -172,9 +172,20 @@ class MachineController extends AdminController
{
// 取得當前使用者有權限的所有機台 (已透過 Global Scope 過濾)
$machines = Machine::all();
$date = $request->get('date', now()->toDateString());
$service = app(\App\Services\Machine\MachineService::class);
$fleetStats = $service->getFleetStats($date);
return view('admin.machines.utilization', [
'machines' => $machines
'machines' => $machines,
'fleetStats' => $fleetStats,
'compactMachines' => $machines->map(fn($m) => [
'id' => $m->id,
'name' => $m->name,
'serial_no' => $m->serial_no,
'status' => $m->status
])->values()
]);
}
@@ -225,13 +236,17 @@ class MachineController extends AdminController
/**
* 取得機台統計數據 (AJAX)
*/
public function utilizationData(int $id, Request $request)
public function utilizationData(Request $request, $id = null)
{
$machine = Machine::findOrFail($id);
$date = $request->get('date', now()->toDateString());
$service = app(\App\Services\Machine\MachineService::class);
$stats = $service->getUtilizationStats($machine, $date);
if ($id) {
$machine = Machine::findOrFail($id);
$stats = $service->getUtilizationStats($machine, $date);
} else {
$stats = $service->getFleetStats($date);
}
return response()->json([
'success' => true,

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Machine\Machine;
use App\Models\Machine\MaintenanceRecord;
use App\Traits\ImageHandler;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class MaintenanceController extends Controller
{
use ImageHandler;
/**
* 維修紀錄列表
*/
public function index(Request $request)
{
$this->authorize('viewAny', MaintenanceRecord::class);
$query = MaintenanceRecord::with(['machine', 'user', 'company'])
->latest('maintenance_at');
// 搜尋邏輯
if ($request->filled('search')) {
$search = $request->search;
$query->whereHas('machine', function($q) use ($search) {
$q->where('serial_no', 'like', "%{$search}%")
->orWhere('name', 'like', "%{$search}%");
});
}
if ($request->filled('category')) {
$query->where('category', $request->category);
}
$records = $query->paginate(15)->withQueryString();
return view('admin.maintenance.index', compact('records'));
}
/**
* 顯示新增維修單頁面
*/
public function create(Request $request, $serial_no = null)
{
$this->authorize('create', MaintenanceRecord::class);
$machine = null;
if ($serial_no) {
$machine = Machine::where('serial_no', $serial_no)->firstOrFail();
}
// 供手動新增時選擇的機台清單 (僅限有權限存取的)
$machines = Machine::all();
return view('admin.maintenance.create', compact('machine', 'machines'));
}
/**
* 儲存維修單
*/
public function store(Request $request)
{
$this->authorize('create', MaintenanceRecord::class);
$validated = $request->validate([
'machine_id' => 'required|exists:machines,id',
'category' => 'required|in:Repair,Installation,Removal,Maintenance',
'content' => 'nullable|string',
'maintenance_at' => 'required|date',
'photos.*' => 'nullable|image|max:5120', // 每張上限 5MB
]);
$machine = Machine::findOrFail($validated['machine_id']);
$photoPaths = [];
if ($request->hasFile('photos')) {
foreach ($request->file('photos') as $photo) {
if (!$photo) continue;
if (count($photoPaths) >= 3) break;
// 轉為 WebP 格式與保存
$path = $this->storeAsWebp($photo, "maintenance/{$machine->id}");
$photoPaths[] = $path;
}
}
$record = MaintenanceRecord::create([
'company_id' => $machine->company_id, // 從機台帶入歸屬客戶
'machine_id' => $machine->id,
'user_id' => Auth::id(),
'category' => $validated['category'],
'content' => $validated['content'],
'photos' => $photoPaths,
'maintenance_at' => $validated['maintenance_at'],
]);
return redirect()->route('admin.maintenance.index')
->with('success', __('Maintenance record created successfully'));
}
}