with('establishment:id,name,uuid'); $this->scopeEstablishmentQuery($query); $this->applyEstablishmentFilter($request, $query); if ($request->filled('status')) { $query->where('status', $request->string('status')); } if ($request->filled('type')) { $query->where('type', $request->string('type')); } if ($request->filled('search')) { $search = $request->string('search'); $query->where(function ($query) use ($search) { $query->where('name', 'like', "%{$search}%") ->orWhere('qr_code', 'like', "%{$search}%"); }); } $machines = $query ->tap(fn ($builder) => $this->applySort($request, $builder, [ 'name' => 'machines.name', 'establishment' => fn ($builder, string $direction) => $builder ->leftJoin('establishments', 'machines.establishment_id', '=', 'establishments.id') ->orderBy('establishments.name', $direction) ->select('machines.*'), 'type' => 'machines.type', 'status' => 'machines.status', 'last_heartbeat_at' => 'machines.last_heartbeat_at', ], 'name', 'asc')) ->paginate($this->supervisorListPerPage($request)) ->withQueryString() ->through(fn (Machine $machine) => $this->formatMachineListItem($machine)); return Inertia::render('Supervisor/Machines/Index', [ 'machines' => $machines, 'establishments' => $this->establishmentOptions(), 'canManageMachines' => $this->canManageMachines(), 'filters' => [ 'establishment_id' => $request->filled('establishment_id') ? (int) $request->input('establishment_id') : null, 'status' => $request->string('status')->toString() ?: null, 'type' => $request->string('type')->toString() ?: null, 'search' => $request->string('search')->toString() ?: null, 'sort' => $request->string('sort')->toString() ?: 'name', 'direction' => $request->string('direction')->toString() === 'desc' ? 'desc' : 'asc', 'per_page' => $this->supervisorListPerPage($request), ], 'statusOptions' => $this->statusOptions(), 'typeOptions' => $this->typeOptions(), ]); } public function store(Request $request): RedirectResponse { abort_unless($this->canManageMachines(), 403); $validated = $this->validateMachine($request); $this->assertEstablishmentAccessible((int) $validated['establishment_id']); $validated['qr_code'] = $this->resolveQrCode($validated['qr_code'] ?? null); $validated['status'] = $validated['status'] ?? 'available'; $machine = Machine::query()->create($validated); $this->createSimulatedIntegration($machine); return redirect()->route('supervisor.machines.index') ->with('success', 'Machine créée.'); } public function update(Request $request, string $uuid): RedirectResponse { abort_unless($this->canManageMachines(), 403); $machine = $this->findAccessibleMachine($uuid); $validated = $this->validateMachine($request, $machine); $this->assertEstablishmentAccessible((int) $validated['establishment_id']); if (array_key_exists('qr_code', $validated) && blank($validated['qr_code'])) { unset($validated['qr_code']); } $machine->update($validated); return redirect()->route('supervisor.machines.index') ->with('success', 'Machine mise à jour.'); } public function destroy(string $uuid): RedirectResponse { abort_unless($this->canManageMachines(), 403); $machine = $this->findAccessibleMachine($uuid); if (in_array($machine->status, ['running', 'reserved'], true)) { return redirect()->route('supervisor.machines.index') ->withErrors(['delete' => 'Impossible de supprimer une machine en cours d\'utilisation.']); } if ($machine->bookings()->exists() || $machine->washes()->exists()) { return redirect()->route('supervisor.machines.index') ->withErrors(['delete' => 'Impossible de supprimer une machine avec un historique de réservations ou de lavages.']); } PricingRule::query()->where('machine_id', $machine->id)->update(['machine_id' => null]); MachineCommand::query()->where('machine_id', $machine->id)->delete(); MachineEvent::query()->where('machine_id', $machine->id)->delete(); MachineStatusHistory::query()->where('machine_id', $machine->id)->delete(); $machine->machineIntegration()?->delete(); $machine->delete(); return redirect()->route('supervisor.machines.index') ->with('success', 'Machine supprimée.'); } public function show(string $uuid): Response { $query = Machine::query()->where('uuid', $uuid); $this->scopeEstablishmentQuery($query); $machine = $query ->with(['establishment:id,name,uuid,address,city', 'machineIntegration', 'currentUser:id,first_name,last_name,email']) ->firstOrFail(); $recentEvents = MachineEvent::query() ->where('machine_id', $machine->id) ->orderByDesc('occurred_at') ->limit(20) ->get() ->map(fn (MachineEvent $event) => [ 'event_type' => $event->event_type, 'event_type_label' => $this->eventTypeLabel($event->event_type), 'occurred_at' => $event->occurred_at?->toIso8601String(), 'processing_status' => $event->processing_status, ]); return Inertia::render('Supervisor/Machines/Show', [ 'machine' => [ 'uuid' => $machine->uuid, 'name' => $machine->name, 'type' => $machine->type, 'type_label' => $this->typeLabel($machine->type), 'qr_code' => $machine->qr_code, 'status' => $machine->status, 'status_label' => $this->statusLabel($machine->status), 'cycle_started_at' => $machine->cycle_started_at?->toIso8601String(), 'cycle_ends_at' => $machine->cycle_ends_at?->toIso8601String(), 'last_heartbeat_at' => $machine->last_heartbeat_at?->toIso8601String(), 'establishment' => $machine->establishment ? [ 'uuid' => $machine->establishment->uuid, 'name' => $machine->establishment->name, 'address' => $machine->establishment->address, 'city' => $machine->establishment->city, ] : null, 'current_user' => $machine->currentUser ? [ 'name' => trim($machine->currentUser->first_name.' '.$machine->currentUser->last_name), 'email' => $machine->currentUser->email, ] : null, 'integration' => $machine->machineIntegration ? [ 'provider' => $machine->machineIntegration->provider, 'mode' => $machine->machineIntegration->mode, 'is_active' => $machine->machineIntegration->is_active, ] : null, ], 'recentEvents' => $recentEvents, ]); } /** * @return array */ private function formatMachineListItem(Machine $machine): array { return [ 'uuid' => $machine->uuid, 'establishment_id' => $machine->establishment_id, 'name' => $machine->name, 'type' => $machine->type, 'type_label' => $this->typeLabel($machine->type), 'qr_code' => $machine->qr_code, 'status' => $machine->status, 'status_label' => $this->statusLabel($machine->status), 'establishment_name' => $machine->establishment?->name, 'last_heartbeat_at' => $machine->last_heartbeat_at?->toIso8601String(), ]; } /** * @return array */ private function validateMachine(Request $request, ?Machine $machine = null): array { return $request->validate([ 'establishment_id' => ['required', 'integer', 'exists:establishments,id'], 'name' => ['required', 'string', 'max:100'], 'type' => ['required', 'in:washer_small,washer_large,dryer_small,dryer_large'], 'qr_code' => [ 'nullable', 'string', 'max:255', Rule::unique('machines', 'qr_code')->ignore($machine?->id), ], 'status' => ['required', 'in:available,reserved,running,maintenance,offline,error'], ]); } private function findAccessibleMachine(string $uuid): Machine { $query = Machine::query()->where('uuid', $uuid); $this->scopeEstablishmentQuery($query); return $query->firstOrFail(); } private function resolveQrCode(?string $qrCode): string { if (filled($qrCode)) { return $qrCode; } do { $candidate = 'LAVERIE-'.strtoupper(Str::random(8)); } while (Machine::query()->where('qr_code', $candidate)->exists()); return $candidate; } private function createSimulatedIntegration(Machine $machine): void { $machine->load('establishment'); MachineIntegration::query()->create([ 'machine_id' => $machine->id, 'provider' => config('laverie.simulation.provider_name', 'simulated'), 'external_machine_id' => 'SIM-'.$machine->uuid, 'external_site_id' => 'SITE-'.$machine->establishment?->uuid, 'mode' => 'simulated', 'config' => [ 'cycle_duration_seconds' => config('laverie.simulation.cycle_duration_seconds', 30), ], 'is_active' => true, ]); } private function canManageMachines(): bool { return $this->supervisor()->role !== 'viewer'; } /** * @return array */ private function statusOptions(): array { return [ 'available' => 'Disponible', 'reserved' => 'Réservée', 'running' => 'En cours', 'maintenance' => 'Maintenance', 'offline' => 'Hors ligne', 'error' => 'Erreur', ]; } /** * @return array */ private function typeOptions(): array { return [ 'washer_small' => 'Lave-linge petit', 'washer_large' => 'Lave-linge grand', 'dryer_small' => 'Sèche-linge petit', 'dryer_large' => 'Sèche-linge grand', ]; } private function statusLabel(string $status): string { return $this->statusOptions()[$status] ?? $status; } private function typeLabel(string $type): string { return $this->typeOptions()[$type] ?? $type; } private function eventTypeLabel(string $type): string { return match ($type) { 'heartbeat' => 'Signal de vie', 'machine_online' => 'En ligne', 'machine_offline' => 'Hors ligne', 'cycle_started' => 'Cycle démarré', 'cycle_completed' => 'Cycle terminé', 'cycle_failed' => 'Cycle échoué', 'error_reported' => 'Erreur signalée', 'status_changed' => 'Changement de statut', default => $type, }; } }