Files
ladill-queue/app/Services/Qms/ReportService.php
T
isaaccladandCursor cca98eefd2
Deploy Ladill Queue / deploy (push) Successful in 56s
Initial Ladill Queue release — enterprise QMS standalone app.
Phases 1–6: tickets, counters, displays, appointments, workflows, rules, analytics, reports, feedback, admin, device API, and Gitea deploy workflow for queue.ladill.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 20:19:52 +00:00

167 lines
6.5 KiB
PHP

<?php
namespace App\Services\Qms;
use App\Models\Branch;
use App\Models\Counter;
use App\Models\CustomerFeedback;
use App\Models\QueueAppointment;
use App\Models\ServiceQueue;
use App\Models\ServiceSession;
use App\Models\Ticket;
use App\Support\DatabaseTime;
use Illuminate\Support\Carbon;
class ReportService
{
/**
* @return array<string, mixed>
*/
public function ticketsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$base = Ticket::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('issued_at', [$from, $to]);
$waitDiff = DatabaseTime::diffSeconds('issued_at', 'called_at');
$serviceDiff = DatabaseTime::diffSeconds('serving_started_at', 'completed_at');
return [
'total_issued' => (clone $base)->count(),
'completed' => (clone $base)->where('status', 'completed')->count(),
'no_show' => (clone $base)->where('status', 'no_show')->count(),
'cancelled' => (clone $base)->where('status', 'cancelled')->count(),
'avg_wait_seconds' => (int) round((float) (clone $base)
->where('status', 'completed')
->whereNotNull('called_at')
->selectRaw("AVG({$waitDiff}) as v")
->value('v')),
'avg_service_seconds' => (int) round((float) (clone $base)
->where('status', 'completed')
->whereNotNull('serving_started_at')
->whereNotNull('completed_at')
->selectRaw("AVG({$serviceDiff}) as v")
->value('v')),
];
}
/**
* @return array<string, mixed>
*/
public function waitTimesReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$hourExpr = DatabaseTime::hourOf('issued_at');
$waitDiff = DatabaseTime::diffSeconds('issued_at', 'called_at');
$rows = Ticket::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('issued_at', [$from, $to])
->where('status', 'completed')
->whereNotNull('called_at')
->selectRaw("{$hourExpr} as hour, AVG({$waitDiff}) as avg_wait")
->groupBy('hour')
->orderBy('hour')
->get();
$peak = $rows->sortByDesc('avg_wait')->first();
return [
'hourly' => $rows->map(fn ($r) => ['hour' => (int) $r->hour, 'avg_wait_seconds' => (int) round((float) $r->avg_wait)])->values()->all(),
'peak_hour' => $peak ? (int) $peak->hour : null,
'peak_avg_wait_seconds' => $peak ? (int) round((float) $peak->avg_wait) : 0,
];
}
/**
* @return array<string, mixed>
*/
public function countersReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$counters = Counter::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->withCount(['tickets as served_count' => function ($q) use ($from, $to) {
$q->where('status', 'completed')
->whereBetween('completed_at', [$from, $to]);
}])
->orderByDesc('served_count')
->get();
return [
'counters' => $counters->map(fn ($c) => [
'name' => $c->name,
'served' => $c->served_count,
])->all(),
];
}
/**
* @return array<string, mixed>
*/
public function appointmentsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$base = QueueAppointment::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('scheduled_at', [$from, $to]);
return [
'total' => (clone $base)->count(),
'checked_in' => (clone $base)->where('status', 'checked_in')->count(),
'completed' => (clone $base)->where('status', 'completed')->count(),
'cancelled' => (clone $base)->where('status', 'cancelled')->count(),
'no_show' => (clone $base)->where('status', 'no_show')->count(),
];
}
/**
* @return array<string, mixed>
*/
public function feedbackReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$base = CustomerFeedback::owned($ownerRef)
->where('organization_id', $organizationId)
->whereBetween('created_at', [$from, $to])
->when($branchId, function ($q) use ($branchId) {
$q->whereHas('ticket', fn ($t) => $t->where('branch_id', $branchId));
});
$count = (clone $base)->count();
$avgRating = (clone $base)->avg('rating');
return [
'responses' => $count,
'avg_rating' => $count ? round((float) $avgRating, 1) : 0,
'avg_service_quality' => round((float) (clone $base)->avg('service_quality'), 1),
'avg_wait_experience' => round((float) (clone $base)->avg('wait_experience'), 1),
'avg_staff_professionalism' => round((float) (clone $base)->avg('staff_professionalism'), 1),
];
}
/**
* @return array<string, mixed>
*/
public function serviceSessionsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$base = ServiceSession::owned($ownerRef)
->whereBetween('started_at', [$from, $to])
->whereHas('ticket', function ($q) use ($organizationId, $branchId) {
$q->where('organization_id', $organizationId);
if ($branchId) {
$q->where('branch_id', $branchId);
}
});
$completed = (clone $base)->whereNotNull('ended_at');
return [
'sessions' => (clone $base)->count(),
'completed_sessions' => (clone $completed)->count(),
'avg_duration_seconds' => (int) round((float) (clone $completed)->avg('duration_seconds')),
'total_service_seconds' => (int) (clone $completed)->sum('duration_seconds'),
];
}
}