Files
backend/app/Http/Controllers/Supervisor/MachinePageController.php
T

331 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers\Supervisor;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Supervisor\Concerns\ScopesSupervisorResources;
use App\Models\Establishment;
use App\Models\Machine;
use App\Models\MachineCommand;
use App\Models\MachineEvent;
use App\Models\MachineIntegration;
use App\Models\MachineStatusHistory;
use App\Models\PricingRule;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
class MachinePageController extends Controller
{
use ScopesSupervisorResources;
public function index(Request $request): Response
{
$query = Machine::query()
->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<string, mixed>
*/
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<string, mixed>
*/
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<string, string>
*/
private function statusOptions(): array
{
return [
'available' => 'Disponible',
'reserved' => 'Réservée',
'running' => 'En cours',
'maintenance' => 'Maintenance',
'offline' => 'Hors ligne',
'error' => 'Erreur',
];
}
/**
* @return array<string, string>
*/
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,
};
}
}