Files
backend/app/Services/DashboardService.php
T

289 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Models\Booking;
use App\Models\Establishment;
use App\Models\Machine;
use App\Models\MachineEvent;
use App\Models\Supervisor;
use App\Models\Wash;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class DashboardService
{
/**
* @return array<string, mixed>
*/
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<string, int>
*/
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<int, array<string, mixed>>
*/
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<string, string|null>
*/
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<int, array<string, mixed>>
*/
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<int, array<string, mixed>>
*/
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,
};
}
}