diff --git a/app/Http/Controllers/Admin/BasicSettings/MachinePhotoController.php b/app/Http/Controllers/Admin/BasicSettings/MachinePhotoController.php index 620ed77..9f03d23 100644 --- a/app/Http/Controllers/Admin/BasicSettings/MachinePhotoController.php +++ b/app/Http/Controllers/Admin/BasicSettings/MachinePhotoController.php @@ -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; - } } diff --git a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php index d3620ab..954e064 100644 --- a/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php +++ b/app/Http/Controllers/Admin/BasicSettings/MachineSettingController.php @@ -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'); - } } diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index c28aa75..fc6ff52 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -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, diff --git a/app/Http/Controllers/Admin/MaintenanceController.php b/app/Http/Controllers/Admin/MaintenanceController.php new file mode 100644 index 0000000..761d726 --- /dev/null +++ b/app/Http/Controllers/Admin/MaintenanceController.php @@ -0,0 +1,105 @@ +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')); + } +} diff --git a/app/Models/Machine/MaintenanceRecord.php b/app/Models/Machine/MaintenanceRecord.php new file mode 100644 index 0000000..3373d7a --- /dev/null +++ b/app/Models/Machine/MaintenanceRecord.php @@ -0,0 +1,45 @@ + 'array', + 'maintenance_at' => 'datetime', + ]; + + public function machine() + { + return $this->belongsTo(Machine::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function company() + { + return $this->belongsTo(\App\Models\System\Company::class); + } +} diff --git a/app/Policies/Machine/MaintenanceRecordPolicy.php b/app/Policies/Machine/MaintenanceRecordPolicy.php new file mode 100644 index 0000000..e46c8ad --- /dev/null +++ b/app/Policies/Machine/MaintenanceRecordPolicy.php @@ -0,0 +1,66 @@ +can('menu.machines.maintenance'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, MaintenanceRecord $maintenanceRecord): bool + { + return $user->can('menu.machines.maintenance'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->can('menu.machines.maintenance'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, MaintenanceRecord $maintenanceRecord): bool + { + return $user->can('menu.machines.maintenance'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, MaintenanceRecord $maintenanceRecord): bool + { + return $user->isSystemAdmin() && $user->can('menu.machines.maintenance'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, MaintenanceRecord $maintenanceRecord): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, MaintenanceRecord $maintenanceRecord): bool + { + return false; + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 54756cd..ff82975 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -13,7 +13,7 @@ class AuthServiceProvider extends ServiceProvider * @var array */ protected $policies = [ - // + \App\Models\Machine\MaintenanceRecord::class => \App\Policies\Machine\MaintenanceRecordPolicy::class, ]; /** diff --git a/app/Services/Machine/MachineService.php b/app/Services/Machine/MachineService.php index dd41ecd..f9b266c 100644 --- a/app/Services/Machine/MachineService.php +++ b/app/Services/Machine/MachineService.php @@ -106,6 +106,51 @@ class MachineService ]); } + /** + * Get machine utilization and OEE statistics for entire fleet. + */ + public function getFleetStats(string $date): array + { + $start = Carbon::parse($date)->startOfDay(); + $end = Carbon::parse($date)->endOfDay(); + + // 1. Online Count (Base on current status) + $machines = Machine::all(); // This is filtered by TenantScoped + $totalMachines = $machines->count(); + $onlineCount = $machines->where('status', 'online')->count(); + + $machineIds = $machines->pluck('id')->toArray(); + + // 2. Total Daily Sales (Sum of B600 logs across all authorized machines) + $totalSales = MachineLog::whereIn('machine_id', $machineIds) + ->where('message', 'like', '%B600%') + ->whereBetween('created_at', [$start, $end]) + ->count(); + + // 3. Average OEE (Simulated based on individual machine stats for performance) + $totalOee = 0; + $count = 0; + + foreach ($machines as $machine) { + $stats = $this->getUtilizationStats($machine, $date); + $totalOee += $stats['overview']['oee']; + $count++; + } + + $avgOee = ($count > 0) ? ($totalOee / $count) : 0; + + return [ + 'avgOee' => round($avgOee, 2), + 'onlineCount' => $onlineCount, + 'totalMachines' => $totalMachines, + 'totalSales' => $totalSales, + 'alertCount' => MachineLog::whereIn('machine_id', $machineIds) + ->where('level', 'error') + ->whereBetween('created_at', [$start, $end]) + ->count() + ]; + } + /** * Get machine utilization and OEE statistics. */ diff --git a/app/Traits/ImageHandler.php b/app/Traits/ImageHandler.php new file mode 100644 index 0000000..6a644da --- /dev/null +++ b/app/Traits/ImageHandler.php @@ -0,0 +1,70 @@ +getRealPath()); + if (!$imageInfo) { + return $file->store($directory, 'public'); + } + + $mime = $imageInfo['mime']; + $source = null; + + switch ($mime) { + case 'image/jpeg': + $source = imagecreatefromjpeg($file->getRealPath()); + break; + case 'image/png': + $source = imagecreatefrompng($file->getRealPath()); + break; + case 'image/gif': + $source = imagecreatefromgif($file->getRealPath()); + break; + case 'image/webp': + $source = imagecreatefromwebp($file->getRealPath()); + break; + default: + // 不支援的格式直接存 + return $file->store($directory, 'public'); + } + + if (!$source) { + return $file->store($directory, 'public'); + } + + // 確保支援真彩色 (解決 palette image 問題) + if (!imageistruecolor($source)) { + imagepalettetotruecolor($source); + } + + // 確保目錄存在 + Storage::disk('public')->makeDirectory($directory); + $fullPath = Storage::disk('public')->path($path); + + // 轉換並儲存 + imagewebp($source, $fullPath, $quality); + imagedestroy($source); + + return $path; + } +} diff --git a/database/migrations/2026_03_25_112212_create_maintenance_records_table.php b/database/migrations/2026_03_25_112212_create_maintenance_records_table.php new file mode 100644 index 0000000..79f672d --- /dev/null +++ b/database/migrations/2026_03_25_112212_create_maintenance_records_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('company_id')->constrained()->onDelete('cascade'); + $table->foreignId('machine_id')->constrained()->onDelete('cascade'); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->string('category')->comment('維修、裝機、撤機、保養'); + $table->text('content')->nullable(); + $table->json('photos')->nullable(); + $table->timestamp('maintenance_at')->useCurrent(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('maintenance_records'); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index 72e95f8..6d90a5c 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -22,6 +22,8 @@ class RoleSeeder extends Seeder 'menu.members', 'menu.machines', 'menu.machines.list', + 'menu.machines.utilization', + 'menu.machines.maintenance', 'menu.app', 'menu.warehouses', 'menu.sales', @@ -62,6 +64,8 @@ class RoleSeeder extends Seeder 'menu.members', 'menu.machines', 'menu.machines.list', + 'menu.machines.utilization', + 'menu.machines.maintenance', 'menu.app', 'menu.warehouses', 'menu.sales', diff --git a/lang/en.json b/lang/en.json index 0db242d..90e4f87 100644 --- a/lang/en.json +++ b/lang/en.json @@ -25,6 +25,7 @@ "Add Customer": "Add Customer", "Add Machine": "Add Machine", "Add Machine Model": "Add Machine Model", + "Add Maintenance Record": "Add Maintenance Record", "Add Role": "Add Role", "Admin": "Admin", "Admin Name": "Admin Name", @@ -40,6 +41,7 @@ "Alerts Pending": "Alerts Pending", "All": "All", "All Affiliations": "All Affiliations", + "All Categories": "All Categories", "All Companies": "All Companies", "All Levels": "All Levels", "All Machines": "All Machines", @@ -83,6 +85,7 @@ "Card Reader No": "Card Reader No", "Card Reader Restart": "Card Reader Restart", "Card Reader Seconds": "Card Reader Seconds", + "Category": "Category", "Change": "Change", "Change Stock": "Change Stock", "ChannelId": "ChannelId", @@ -141,8 +144,10 @@ "Delete Account": "Delete Account", "Delete Permanently": "Delete Permanently", "Deposit Bonus": "Deposit Bonus", + "Describe the repair or maintenance status...": "Describe the repair or maintenance status...", "Deselect All": "取消全選", "Detail": "Detail", + "Device Information": "Device Information", "Device Status Logs": "Device Status Logs", "Disabled": "Disabled", "Discord Notifications": "Discord Notifications", @@ -168,6 +173,7 @@ "Edit Sub Account Role": "編輯子帳號角色", "Email": "Email", "Enabled/Disabled": "Enabled/Disabled", + "Engineer": "Engineer", "Ensure your account is using a long, random password to stay secure.": "Ensure your account is using a long, random password to stay secure.", "Enter login ID": "Enter login ID", "Enter machine location": "Enter machine location", @@ -178,6 +184,7 @@ "Enter your password to confirm": "Enter your password to confirm", "Equipment efficiency and OEE metrics": "設備效能與 OEE 綜合指標", "Error": "Error", + "Execution Time": "Execution Time", "Expired": "Expired", "Expired / Disabled": "Expired / Disabled", "Expiry Date": "Expiry Date", @@ -185,6 +192,7 @@ "Failed to fetch machine data.": "Failed to fetch machine data.", "Failed to save permissions.": "Failed to save permissions.", "Failed to update machine images: ": "Failed to update machine images: ", + "Fill in the device repair or maintenance details": "Fill in the device repair or maintenance details", "Firmware Version": "Firmware Version", "Fleet Avg OEE": "全機隊平均 OEE", "Fleet Performance": "全機隊效能", @@ -209,6 +217,7 @@ "Info": "Info", "Initial Admin Account": "Initial Admin Account", "Initial Role": "Initial Role", + "Installation": "Installation", "Invoice Status": "Invoice Status", "Items": "Items", "JKO_MERCHANT_ID": "JKO_MERCHANT_ID", @@ -269,7 +278,16 @@ "Machine settings updated successfully.": "Machine settings updated successfully.", "Machines": "Machines", "Machines Online": "在線機台數", + "Maintenance": "Maintenance", + "Maintenance Content": "Maintenance Content", + "Maintenance Date": "Maintenance Date", + "Maintenance Details": "Maintenance Details", + "Maintenance Photos": "Maintenance Photos", + "Maintenance QR": "Maintenance QR", + "Maintenance QR Code": "Maintenance QR Code", "Maintenance Records": "Maintenance Records", + "Maintenance record created successfully": "Maintenance record created successfully", + "Scan this code to quickly access the maintenance form for this device.": "Scan this code to quickly access the maintenance form for this device.", "Manage Account Access": "管理帳號存取", "Manage Expiry": "Manage Expiry", "Manage administrative and tenant accounts": "Manage administrative and tenant accounts", @@ -302,12 +320,14 @@ "Never Connected": "Never Connected", "New Password": "New Password", "New Password (leave blank to keep current)": "New Password (leave blank to keep current)", + "New Record": "New Record", "New Sub Account Role": "新增子帳號角色", "Next": "Next", "No Invoice": "No Invoice", "No accounts found": "No accounts found", "No alert summary": "No alert summary", "No configurations found": "No configurations found", + "No content provided": "No content provided", "No customers found": "No customers found", "No data available": "No data available", "No file uploaded.": "No file uploaded.", @@ -318,6 +338,7 @@ "No machines assigned": "未分配機台", "No machines available": "No machines available", "No machines available in this company.": "此客戶目前沒有可供分配的機台。", + "No maintenance records found": "No maintenance records found", "No matching logs found": "No matching logs found", "No permissions": "No permissions", "No roles found.": "No roles found.", @@ -414,6 +435,8 @@ "Remote Lock": "Remote Lock", "Remote Management": "Remote Management", "Remote Permissions": "Remote Permissions", + "Removal": "Removal", + "Repair": "Repair", "Replenishment Audit": "Replenishment Audit", "Replenishment Page": "Replenishment Page", "Replenishment Records": "Replenishment Records", @@ -457,6 +480,8 @@ "Search machines...": "Search machines...", "Search models...": "Search models...", "Search roles...": "Search roles...", + "Search serial no or name...": "Search serial no or name...", + "Search serial or machine...": "Search serial or machine...", "Search users...": "Search users...", "Select All": "全選", "Select Company": "Select Company", @@ -494,6 +519,7 @@ "Sub Accounts": "Sub Accounts", "Sub-actions": "子項目", "Sub-machine Status Request": "Sub-machine Status", + "Submit Record": "Submit Record", "Success": "Success", "Super Admin": "Super Admin", "Super-admin role cannot be assigned to tenant accounts.": "Super-admin role cannot be assigned to tenant accounts.", @@ -533,6 +559,7 @@ "Total Selected": "已選擇總數", "Total Slots": "Total Slots", "Total items": "Total items: :count", + "Track device health and maintenance history": "Track device health and maintenance history", "Transfer Audit": "Transfer Audit", "Transfers": "Transfers", "Tutorial Page": "Tutorial Page", @@ -569,6 +596,7 @@ "Warning: You are editing your own role!": "Warning: You are editing your own role!", "Welcome Gift": "Welcome Gift", "Welcome Gift Status": "Welcome Gift Status", + "Work Content": "Work Content", "Yesterday": "Yesterday", "You cannot assign permissions you do not possess.": "You cannot assign permissions you do not possess.", "You cannot delete your own account.": "You cannot delete your own account.", @@ -611,6 +639,8 @@ "menu.line": "Line Management", "menu.machines": "Machine Management", "menu.machines.list": "Machine List", + "menu.machines.maintenance": "Maintenance Records", + "menu.machines.utilization": "Utilization Rate", "menu.members": "Member Management", "menu.permission": "Permission Settings", "menu.permissions": "權限管理", @@ -638,4 +668,4 @@ "vs Yesterday": "vs Yesterday", "warehouses": "Warehouse Management", "待填寫": "Pending" -} +} \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index c308317..57c42cf 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -25,6 +25,7 @@ "Add Customer": "顧客を追加", "Add Machine": "機台を追加", "Add Machine Model": "機台型號を追加", + "Add Maintenance Record": "メンテナンス記録を追加", "Add Role": "ロールを追加", "Admin": "管理者", "Admin Name": "管理者名", @@ -40,6 +41,7 @@ "Alerts Pending": "アラート待機中", "All": "すべて", "All Affiliations": "全ての所属", + "All Categories": "すべてのカテゴリ", "All Companies": "すべての会社", "All Levels": "すべてのレベル", "All Machines": "すべての機体", @@ -83,6 +85,7 @@ "Card Reader No": "カードリーダー番号", "Card Reader Restart": "カードリーダー再起動", "Card Reader Seconds": "カードリーダー秒数", + "Category": "カテゴリ", "Change": "変更", "Change Stock": "小銭在庫", "ChannelId": "チャンネルID", @@ -141,8 +144,10 @@ "Delete Account": "アカウントの削除", "Delete Permanently": "完全に削除", "Deposit Bonus": "入金ボーナス", + "Describe the repair or maintenance status...": "修理またはメンテナンスの状況を説明してください...", "Deselect All": "取消全選", "Detail": "詳細", + "Device Information": "デバイス情報", "Device Status Logs": "デバイス状態ログ", "Disabled": "停止中", "Discord Notifications": "Discord通知", @@ -168,6 +173,7 @@ "Edit Sub Account Role": "編輯子帳號角色", "Email": "メールアドレス", "Enabled/Disabled": "有効/無効", + "Engineer": "メンテナンス担当者", "Ensure your account is using a long, random password to stay secure.": "セキュリティを維持するため、アカウントには長くランダムなパスワードを使用してください。", "Enter login ID": "ログインIDを入力してください", "Enter machine location": "機台の場所を入力してください", @@ -178,6 +184,7 @@ "Enter your password to confirm": "確認のためパスワードを入力してください", "Equipment efficiency and OEE metrics": "設備效能與 OEE 綜合指標", "Error": "エラー", + "Execution Time": "実行時間", "Expired": "期限切れ", "Expired / Disabled": "期限切れ / 停止中", "Expiry Date": "有效日期", @@ -185,6 +192,7 @@ "Failed to fetch machine data.": "無法取得機台資料。", "Failed to save permissions.": "無法儲存權限設定。", "Failed to update machine images: ": "機台画像の更新に失敗しました:", + "Fill in the device repair or maintenance details": "デバイスの修理またはメンテナンスの詳細を入力してください", "Firmware Version": "ファームウェアバージョン", "Fleet Avg OEE": "全機隊平均 OEE", "Fleet Performance": "全機隊效能", @@ -209,6 +217,7 @@ "Info": "情報", "Initial Admin Account": "初期管理者アカウント", "Initial Role": "初期ロール", + "Installation": "設置", "Invoice Status": "発票発行状態", "Items": "個の項目", "JKO_MERCHANT_ID": "街口支付 加盟店ID", @@ -269,7 +278,16 @@ "Machine settings updated successfully.": "機台設定が正常に更新されました。", "Machines": "機台リスト", "Machines Online": "在線機台數", + "Maintenance": "保守", + "Maintenance Content": "メンテナンス内容", + "Maintenance Date": "メンテナンス日", + "Maintenance Details": "メンテナンス詳細", + "Maintenance Photos": "メンテナンス写真", + "Maintenance QR": "メンテナンス QR", + "Maintenance QR Code": "メンテナンス QR コード", "Maintenance Records": "メンテナンス記録", + "Maintenance record created successfully": "メンテナンス記録が正常に作成されました", + "Scan this code to quickly access the maintenance form for this device.": "このコードをスキャンして、このデバイスのメンテナンスフォームに素早くアクセスしてください。", "Manage Account Access": "管理帳號存取", "Manage Expiry": "進入效期管理", "Manage administrative and tenant accounts": "管理者およびテナントアカウントを管理します", @@ -302,12 +320,14 @@ "Never Connected": "未接続", "New Password": "新しいパスワード", "New Password (leave blank to keep current)": "新しいパスワード (変更しない場合は空欄)", + "New Record": "新規記録", "New Sub Account Role": "新增子帳號角色", "Next": "次へ", "No Invoice": "発票を発行しない", "No accounts found": "アカウントが見つかりません", "No alert summary": "アラートなし", "No configurations found": "設定が見つかりません", + "No content provided": "内容がありません", "No customers found": "顧客が見つかりません", "No data available": "データなし", "No file uploaded.": "ファイルがアップロードされていません。", @@ -318,6 +338,7 @@ "No machines assigned": "未分配機台", "No machines available": "目前沒有可供分配的機台", "No machines available in this company.": "此客戶目前沒有可供分配的機台。", + "No maintenance records found": "メンテナンス記録が見つかりません", "No matching logs found": "一致するログが見つかりません", "No permissions": "権限項目なし", "No roles found.": "ロールが見つかりませんでした。", @@ -414,6 +435,8 @@ "Remote Lock": "リモートロック", "Remote Management": "リモート管理", "Remote Permissions": "リモート管理權限", + "Removal": "撤去", + "Repair": "修理", "Replenishment Audit": "補充監査", "Replenishment Page": "補充画面", "Replenishment Records": "補充記録", @@ -457,6 +480,8 @@ "Search machines...": "機台を検索...", "Search models...": "型番を検索...", "Search roles...": "ロールを検索...", + "Search serial no or name...": "シリアル番号または名前を検索...", + "Search serial or machine...": "シリアルまたはマシンを検索...", "Search users...": "ユーザーを検索...", "Select All": "全選", "Select Company": "会社を選択", @@ -475,7 +500,7 @@ "Showing :from to :to of :total items": ":total 件中 :from から :to 件を表示", "Sign in to your account": "アカウントにサインイン", "Signed in as": "ログイン中", - "Slot": "貨道", + "Slot": "スロット", "Slot Mechanism (default: Conveyor, check for Spring)": "貨道メカニズム (デフォルト:コンベア、チェックでスプリング)", "Slot Status": "貨道效期", "Slot Test": "テスト中", @@ -494,6 +519,7 @@ "Sub Accounts": "サブアカウント", "Sub-actions": "子項目", "Sub-machine Status Request": "下位機状態リクエスト", + "Submit Record": "記録を送信", "Success": "成功", "Super Admin": "スーパー管理者", "Super-admin role cannot be assigned to tenant accounts.": "スーパー管理者ロールはテナントアカウントに割り当てることはできません。", @@ -533,6 +559,7 @@ "Total Selected": "已選擇總數", "Total Slots": "合計スロット数", "Total items": "合計 :count 件", + "Track device health and maintenance history": "デバイスの健全性とメンテナンス履歴を追跡します", "Transfer Audit": "転送監査", "Transfers": "転送", "Tutorial Page": "チュートリアル画面", @@ -569,6 +596,7 @@ "Warning: You are editing your own role!": "警告:現在使用中のロールを編集しています!", "Welcome Gift": "会員登録特典", "Welcome Gift Status": "来店特典", + "Work Content": "作業内容", "Yesterday": "昨日", "You cannot assign permissions you do not possess.": "ご自身が所有していない権限を割り當てることはできません。", "You cannot delete your own account.": "ご自身のアカウントは削除できません。", @@ -611,6 +639,8 @@ "menu.line": "LINE 設定", "menu.machines": "機台管理", "menu.machines.list": "機台リスト", + "menu.machines.maintenance": "メンテナンス記録", + "menu.machines.utilization": "稼働率", "menu.members": "会員管理", "menu.permission": "權限設定", "menu.permissions": "權限管理", @@ -638,4 +668,4 @@ "vs Yesterday": "前日比", "warehouses": "倉庫管理", "待填寫": "待填寫" -} +} \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 3c97c36..f575074 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -25,6 +25,7 @@ "Add Customer": "新增客戶", "Add Machine": "新增機台", "Add Machine Model": "新增機台型號", + "Add Maintenance Record": "新增維修管理單", "Add Role": "新增角色", "Admin": "管理員", "Admin Name": "管理員姓名", @@ -40,6 +41,7 @@ "Alerts Pending": "待處理告警", "All": "全部", "All Affiliations": "全部單位", + "All Categories": "所有類別", "All Companies": "所有公司", "All Levels": "所有層級", "All Machines": "所有機台", @@ -83,6 +85,7 @@ "Card Reader No": "刷卡機編號", "Card Reader Restart": "卡機重啟", "Card Reader Seconds": "刷卡機秒數", + "Category": "類別", "Change": "更換", "Change Stock": "零錢庫存", "ChannelId": "ChannelId", @@ -93,7 +96,7 @@ "Clear Stock": "庫存清空", "Click here to re-send the verification email.": "點擊此處重新發送驗證郵件。", "Click to upload": "點擊上傳", - "Close Panel": "關閉面板", + "Close Panel": "關閉控制面板", "Company": "所屬客戶", "Company Code": "公司代碼", "Company Information": "公司資訊", @@ -141,8 +144,10 @@ "Delete Account": "刪除帳號", "Delete Permanently": "確認永久刪除資料", "Deposit Bonus": "儲值回饋", + "Describe the repair or maintenance status...": "請描述維修或保養狀況...", "Deselect All": "取消全選", "Detail": "詳細", + "Device Information": "設備資訊", "Device Status Logs": "設備狀態紀錄", "Disabled": "已停用", "Discord Notifications": "Discord通知", @@ -168,6 +173,7 @@ "Edit Sub Account Role": "編輯子帳號角色", "Email": "電子郵件", "Enabled/Disabled": "啟用/停用", + "Engineer": "維修人員", "Ensure your account is using a long, random password to stay secure.": "確保您的帳號使用了足夠強度的隨機密碼以維持安全。", "Enter login ID": "請輸入登入帳號", "Enter machine location": "請輸入機台地點", @@ -178,6 +184,7 @@ "Enter your password to confirm": "請輸入您的密碼以確認", "Equipment efficiency and OEE metrics": "設備效能與 OEE 綜合指標", "Error": "異常", + "Execution Time": "執行時間", "Expired": "已過期", "Expired / Disabled": "已過期 / 停用", "Expiry Date": "有效日期", @@ -185,9 +192,10 @@ "Failed to fetch machine data.": "無法取得機台資料。", "Failed to save permissions.": "無法儲存權限設定。", "Failed to update machine images: ": "更新機台圖片失敗:", + "Fill in the device repair or maintenance details": "填寫設備維修或保養詳情", "Firmware Version": "韌體版本", - "Fleet Avg OEE": "全機隊平均 OEE", - "Fleet Performance": "全機隊效能", + "Fleet Avg OEE": "全機台平均 OEE", + "Fleet Performance": "全機台效能", "From": "從", "Full Access": "全機台授權", "Full Name": "全名", @@ -209,6 +217,7 @@ "Info": "一般", "Initial Admin Account": "初始管理帳號", "Initial Role": "初始角色", + "Installation": "裝機", "Invoice Status": "發票開立狀態", "Items": "個項目", "JKO_MERCHANT_ID": "街口支付 商店代號", @@ -269,7 +278,16 @@ "Machine settings updated successfully.": "機台設定已成功更新。", "Machines": "機台列表", "Machines Online": "在線機台數", + "Maintenance": "保養", + "Maintenance Content": "維修內容", + "Maintenance Date": "維修日期", + "Maintenance Details": "維修詳情", + "Maintenance Photos": "維修照片", + "Maintenance QR": "維修掃描碼", + "Maintenance QR Code": "維修掃描碼", "Maintenance Records": "維修管理單", + "Maintenance record created successfully": "維修紀錄已成功建立", + "Scan this code to quickly access the maintenance form for this device.": "掃描此 QR Code 即可快速進入此設備的維修單填寫頁面。", "Manage Account Access": "管理帳號存取", "Manage Expiry": "進入效期管理", "Manage administrative and tenant accounts": "管理系統管理者與租戶帳號", @@ -302,12 +320,14 @@ "Never Connected": "從未連線", "New Password": "新密碼", "New Password (leave blank to keep current)": "新密碼 (若不修改請留空)", + "New Record": "新增單據", "New Sub Account Role": "新增子帳號角色", "Next": "下一頁", "No Invoice": "不開立發票", "No accounts found": "找不到帳號資料", "No alert summary": "暫無告警記錄", "No configurations found": "暫無相關配置", + "No content provided": "未提供內容", "No customers found": "找不到客戶資料", "No data available": "暫無資料", "No file uploaded.": "未上傳任何檔案。", @@ -318,6 +338,7 @@ "No machines assigned": "未分配機台", "No machines available": "目前沒有可供分配的機台", "No machines available in this company.": "此客戶目前沒有可供分配的機台。", + "No maintenance records found": "找不到維修紀錄", "No matching logs found": "找不到符合條件的日誌", "No permissions": "無權限項目", "No roles found.": "找不到角色資料。", @@ -414,6 +435,8 @@ "Remote Lock": "遠端鎖定", "Remote Management": "遠端管理", "Remote Permissions": "遠端管理權限", + "Removal": "撤機", + "Repair": "維修", "Replenishment Audit": "補貨單", "Replenishment Page": "補貨頁", "Replenishment Records": "機台補貨紀錄", @@ -457,6 +480,8 @@ "Search machines...": "搜尋機台...", "Search models...": "搜尋型號...", "Search roles...": "搜尋角色...", + "Search serial no or name...": "搜尋序號或機台名稱...", + "Search serial or machine...": "搜尋序號或機台名稱...", "Search users...": "搜尋用戶...", "Select All": "全選", "Select Company": "選擇所屬公司", @@ -475,7 +500,7 @@ "Showing :from to :to of :total items": "顯示第 :from 到 :to 項,共 :total 項", "Sign in to your account": "隨時隨地掌控您的業務。", "Signed in as": "登入身份", - "Slot": "貨道", + "Slot": "照片格", "Slot Mechanism (default: Conveyor, check for Spring)": "貨道機制 (預設履帶,勾選為彈簧)", "Slot Status": "貨道效期", "Slot Test": "貨道測試", @@ -494,6 +519,7 @@ "Sub Accounts": "子帳號", "Sub-actions": "子項目", "Sub-machine Status Request": "下位機狀態回傳", + "Submit Record": "提交紀錄", "Success": "成功", "Super Admin": "超級管理員", "Super-admin role cannot be assigned to tenant accounts.": "超級管理員角色無法指派給租戶帳號。", @@ -533,6 +559,7 @@ "Total Selected": "已選擇總數", "Total Slots": "總貨道數", "Total items": "總計 :count 項", + "Track device health and maintenance history": "追蹤設備健康與維修歷史", "Transfer Audit": "調撥單", "Transfers": "調撥單", "Tutorial Page": "教學頁", @@ -569,6 +596,7 @@ "Warning: You are editing your own role!": "警告:您正在編輯目前使用的角色!", "Welcome Gift": "註冊成效禮", "Welcome Gift Status": "來店禮", + "Work Content": "工作內容", "Yesterday": "昨日", "You cannot assign permissions you do not possess.": "您無法指派您自身不具備的權限。", "You cannot delete your own account.": "您無法刪除自己的帳號。", @@ -611,6 +639,8 @@ "menu.line": "LINE 配置", "menu.machines": "機台管理", "menu.machines.list": "機台列表", + "menu.machines.maintenance": "維修管理單", + "menu.machines.utilization": "機台嫁動率", "menu.members": "會員管理", "menu.permission": "權限設定", "menu.permissions": "權限管理", @@ -638,4 +668,4 @@ "vs Yesterday": "較昨日", "warehouses": "倉庫管理", "待填寫": "待填寫" -} +} \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index cbbee40..36f90d5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -309,4 +309,13 @@ .hs-select-search-input { @apply luxury-input py-2 px-3 text-xs border-slate-100 dark:border-slate-800; } + + /* Small variants for filter bars */ + .luxury-input-sm { + @apply py-2.5 text-sm !important; + } + + .luxury-select-sm .hs-select-toggle { + @apply py-2.5 text-sm !important; + } } \ No newline at end of file diff --git a/resources/views/admin/basic-settings/machines/index.blade.php b/resources/views/admin/basic-settings/machines/index.blade.php index ab11c30..fe809ec 100644 --- a/resources/views/admin/basic-settings/machines/index.blade.php +++ b/resources/views/admin/basic-settings/machines/index.blade.php @@ -15,6 +15,15 @@ deletedPhotos: [false, false, false], showImageLightbox: false, lightboxImageUrl: '', + showMaintenanceQrModal: false, + maintenanceQrMachineName: '', + maintenanceQrUrl: '', + openMaintenanceQr(machine) { + this.maintenanceQrMachineName = machine.name; + const baseUrl = '{{ route('admin.maintenance.create', ['serial_no' => 'SERIAL_NO']) }}'; + this.maintenanceQrUrl = baseUrl.replace('SERIAL_NO', machine.serial_no); + this.showMaintenanceQrModal = true; + }, openDetail(machine) { this.currentMachine = machine; this.showDetailDrawer = true; @@ -207,6 +216,14 @@ + - - + + @@ -755,16 +834,12 @@
+ + +
+@endsection diff --git a/resources/views/components/breadcrumbs.blade.php b/resources/views/components/breadcrumbs.blade.php index 15ad795..e2227bb 100644 --- a/resources/views/components/breadcrumbs.blade.php +++ b/resources/views/components/breadcrumbs.blade.php @@ -34,6 +34,7 @@ 'admin.special-permission' => __('Special Permission'), 'admin.permission' => __('Permission Settings'), 'admin.basic-settings' => __('Basic Settings'), + 'admin.maintenance' => __('Machine Management'), ]; // 1. 找出所屬大模組 @@ -67,18 +68,32 @@ 'payment-configs' => __('Customer Payment Config'), 'warehouses' => __('Warehouse List'), 'sales' => __('Sales Records'), + 'maintenance' => __('Maintenance Records'), default => null, }; if ($midLabel) { $links[] = [ 'label' => $midLabel, - 'url' => '#', // 如果有需要可以導向 index 路由 + 'url' => match($midSegment) { + 'maintenance' => route('admin.maintenance.index'), + 'machines' => str_contains($routeName, 'basic-settings') ? route('admin.basic-settings.machines.index') : '#', + default => '#', + }, 'active' => $lastSegment === 'index' ]; } } + // 特殊處理:當只有三段且第二段是 maintenance 時,增加中間層級 + if (count($segments) === 3 && $segments[1] === 'maintenance' && $lastSegment !== 'index') { + $links[] = [ + 'label' => __('Maintenance Records'), + 'url' => route('admin.maintenance.index'), + 'active' => false + ]; + } + // 3. 處理最後一個動作/頁面 $pageLabel = match($lastSegment) { 'index' => match($segments[1] ?? '') { @@ -88,6 +103,7 @@ 'sales' => __('Sales Records'), 'analysis' => __('Analysis Management'), 'audit' => __('Audit Management'), + 'maintenance' => __('Maintenance Records'), default => null, }, 'edit' => str_starts_with($routeName, 'profile') ? null : __('Edit'), diff --git a/resources/views/components/loading-screen.blade.php b/resources/views/components/loading-screen.blade.php index 058f9c4..233b0a3 100644 --- a/resources/views/components/loading-screen.blade.php +++ b/resources/views/components/loading-screen.blade.php @@ -1,6 +1,8 @@
routeIs('admin.machines.*') ? 'true' : 'false' }} }"> +
  • @endcan + @can('menu.machines.utilization')
  • {{ __('Utilization Rate') }}
  • -
  • + @endcan + @can('menu.machines.maintenance') +
  • {{ __('Maintenance Records') }}
  • + @endcan
    diff --git a/routes/web.php b/routes/web.php index 239f2b4..a131f23 100644 --- a/routes/web.php +++ b/routes/web.php @@ -41,14 +41,20 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name( Route::get('/permissions/accounts/{user}', [App\Http\Controllers\Admin\MachineController::class, 'getAccountMachines'])->name('permissions.accounts.get'); Route::post('/permissions/accounts/{user}', [App\Http\Controllers\Admin\MachineController::class, 'syncAccountMachines'])->name('permissions.accounts.sync'); Route::get('/utilization', [App\Http\Controllers\Admin\MachineController::class , 'utilization'])->name('utilization'); - Route::get('/{id}/utilization-ajax', [App\Http\Controllers\Admin\MachineController::class, 'utilizationData'])->name('utilization-ajax'); + Route::get('/utilization-ajax/{id?}', [App\Http\Controllers\Admin\MachineController::class, 'utilizationData'])->name('utilization-ajax'); Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax'); Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update'); Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax'); - Route::get('/maintenance', [App\Http\Controllers\Admin\MachineController::class , 'maintenance'])->name('maintenance'); }); Route::resource('machines', App\Http\Controllers\Admin\MachineController::class); + // 維修管理 + Route::prefix('maintenance')->name('maintenance.')->middleware('can:menu.machines.maintenance')->group(function () { + Route::get('/', [App\Http\Controllers\Admin\MaintenanceController::class, 'index'])->name('index'); + Route::get('/create/{serial_no?}', [App\Http\Controllers\Admin\MaintenanceController::class, 'create'])->name('create'); + Route::post('/', [App\Http\Controllers\Admin\MaintenanceController::class, 'store'])->name('store'); + }); + // 4. APP管理 Route::prefix('app')->name('app.')->group(function () { Route::get('/ui-elements', [App\Http\Controllers\Admin\AppConfigController::class , 'uiElements'])->name('ui-elements');