*/ public function getKpis(Supervisor $supervisor, ?int $establishmentFilter = null): array { $offlineThresholdMinutes = (int) config('laverie.machine.offline_threshold_minutes', 10); $today = Carbon::today(); $washQuery = $this->washQuery($supervisor, $establishmentFilter)->whereDate('started_at', $today); $bookingQuery = $this->bookingQuery($supervisor, $establishmentFilter)->whereDate('slot_start', $today); $machineQuery = $this->machineQuery($supervisor, $establishmentFilter); $offlineThreshold = now()->subMinutes($offlineThresholdMinutes); $offlineCount = (clone $machineQuery) ->where(function ($query) use ($offlineThreshold) { $query->where('status', 'offline') ->orWhere(function ($query) use ($offlineThreshold) { $query->where('last_heartbeat_at', '<', $offlineThreshold) ->orWhereNull('last_heartbeat_at'); }); }) ->count(); $machinesTotal = (clone $machineQuery)->count(); return [ 'revenue_today' => (float) (clone $washQuery) ->where('status', 'completed') ->sum('cost'), 'washes_today' => (clone $washQuery)->count(), 'bookings_today' => (clone $bookingQuery)->count(), 'offline_machines' => $offlineCount, 'machines_total' => $machinesTotal, 'running_machines' => (clone $machineQuery)->where('status', 'running')->count(), 'available_machines' => (clone $machineQuery)->where('status', 'available')->count(), 'active_bookings' => $this->bookingQuery($supervisor, $establishmentFilter) ->whereIn('status', ['confirmed', 'active']) ->where('slot_end', '>=', now()) ->count(), ]; } /** * @return array */ public function getMachineStatusBreakdown(Supervisor $supervisor, ?int $establishmentFilter = null): array { $statuses = ['available', 'reserved', 'running', 'maintenance', 'offline', 'error']; $counts = $this->machineQuery($supervisor, $establishmentFilter) ->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->pluck('count', 'status'); $breakdown = []; foreach ($statuses as $status) { $breakdown[$status] = (int) ($counts[$status] ?? 0); } return $breakdown; } /** * @return Collection> */ public function getRecentWashes(Supervisor $supervisor, ?int $establishmentFilter = null, int $limit = 5): Collection { return $this->washQuery($supervisor, $establishmentFilter) ->with(['machine:id,name,uuid', 'machine.establishment:id,name']) ->orderByDesc('started_at') ->limit($limit) ->get() ->map(fn (Wash $wash) => [ 'uuid' => $wash->uuid, 'machine_name' => $wash->machine?->name ?? '—', 'establishment_name' => $wash->machine?->establishment?->name, 'status' => $wash->status, 'status_label' => $this->washStatusLabel($wash->status), 'cost' => (float) $wash->cost, 'started_at' => $wash->started_at?->toIso8601String(), ]); } /** * @return array */ public function getContext(Supervisor $supervisor, ?int $establishmentFilter = null): array { $supervisor->loadMissing(['organization:id,name', 'establishment:id,name']); $establishmentName = $supervisor->establishment?->name; if ($establishmentName === null && $establishmentFilter !== null) { $establishmentName = Establishment::query() ->where('id', $establishmentFilter) ->where('organization_id', $supervisor->organization_id) ->value('name'); } return [ 'organization_name' => $supervisor->organization?->name, 'establishment_name' => $establishmentName, 'is_all_establishments' => $supervisor->establishment_id === null && $establishmentFilter === null, ]; } /** * @return Collection> */ public function getAlerts(Supervisor $supervisor, ?int $establishmentFilter = null, int $limit = 10): Collection { $machineIds = $this->machineQuery($supervisor, $establishmentFilter)->pluck('id'); if ($machineIds->isEmpty()) { return collect(); } $errorMachines = Machine::query() ->whereIn('id', $machineIds) ->whereIn('status', ['error', 'maintenance', 'offline']) ->with('establishment:id,name') ->limit($limit) ->get() ->map(fn (Machine $machine) => [ 'type' => 'machine_status', 'severity' => $machine->status === 'error' ? 'high' : 'medium', 'message' => sprintf( '%s (%s) — %s', $machine->name, $machine->establishment?->name ?? '—', $this->machineStatusLabel($machine->status), ), 'occurred_at' => $machine->updated_at?->toIso8601String(), ]); $recentEvents = MachineEvent::query() ->whereIn('machine_id', $machineIds) ->whereIn('event_type', ['error_reported', 'machine_offline', 'cycle_failed']) ->with('machine:id,name,establishment_id', 'machine.establishment:id,name') ->orderByDesc('occurred_at') ->limit($limit) ->get() ->map(fn (MachineEvent $event) => [ 'type' => 'machine_event', 'severity' => $event->event_type === 'error_reported' ? 'high' : 'medium', 'message' => sprintf( '%s — %s', $event->machine?->name ?? 'Machine', $this->eventTypeLabel($event->event_type), ), 'occurred_at' => $event->occurred_at?->toIso8601String(), ]); return $errorMachines ->concat($recentEvents) ->sortByDesc('occurred_at') ->take($limit) ->values(); } /** * @return Collection> */ public function getMachinesOverview(Supervisor $supervisor, ?int $establishmentFilter = null): Collection { return $this->machineQuery($supervisor, $establishmentFilter) ->with('establishment:id,name') ->orderBy('name') ->limit(12) ->get() ->map(fn (Machine $machine) => [ 'uuid' => $machine->uuid, 'name' => $machine->name, 'type' => $machine->type, 'type_label' => $this->machineTypeLabel($machine->type), 'status' => $machine->status, 'status_label' => $this->machineStatusLabel($machine->status), 'establishment_name' => $machine->establishment?->name, 'last_heartbeat_at' => $machine->last_heartbeat_at?->toIso8601String(), ]); } private function machineQuery(Supervisor $supervisor, ?int $establishmentFilter = null) { $establishmentId = $supervisor->establishment_id ?? $establishmentFilter; $query = Machine::query()->whereHas('establishment', function ($query) use ($supervisor, $establishmentId) { $query->where('organization_id', $supervisor->organization_id); if ($establishmentId !== null) { $query->where('id', $establishmentId); } }); if ($establishmentId !== null) { $query->where('establishment_id', $establishmentId); } return $query; } private function washQuery(Supervisor $supervisor, ?int $establishmentFilter = null) { $establishmentId = $supervisor->establishment_id ?? $establishmentFilter; return Wash::query()->whereHas('machine.establishment', function ($query) use ($supervisor, $establishmentId) { $query->where('organization_id', $supervisor->organization_id); if ($establishmentId !== null) { $query->where('id', $establishmentId); } }); } private function bookingQuery(Supervisor $supervisor, ?int $establishmentFilter = null) { $establishmentId = $supervisor->establishment_id ?? $establishmentFilter; return Booking::query()->whereHas('machine.establishment', function ($query) use ($supervisor, $establishmentId) { $query->where('organization_id', $supervisor->organization_id); if ($establishmentId !== null) { $query->where('id', $establishmentId); } }); } private function machineStatusLabel(string $status): string { return match ($status) { 'available' => 'Disponible', 'reserved' => 'Réservée', 'running' => 'En cours', 'maintenance' => 'Maintenance', 'offline' => 'Hors ligne', 'error' => 'Erreur', default => $status, }; } private function machineTypeLabel(string $type): string { return match ($type) { 'washer_small' => 'Lave-linge petit', 'washer_large' => 'Lave-linge grand', 'dryer_small' => 'Sèche-linge petit', 'dryer_large' => 'Sèche-linge grand', default => $type, }; } private function eventTypeLabel(string $type): string { return match ($type) { 'error_reported' => 'Erreur signalée', 'machine_offline' => 'Machine hors ligne', 'cycle_failed' => 'Cycle échoué', default => $type, }; } private function washStatusLabel(string $status): string { return match ($status) { 'pending_start' => 'En attente', 'running' => 'En cours', 'completed' => 'Terminé', 'cancelled' => 'Annulé', 'failed' => 'Échoué', default => $status, }; } }