289 lines
10 KiB
PHP
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,
|
|
};
|
|
}
|
|
}
|