Initial Ladill Queue release — enterprise QMS standalone app.
Deploy Ladill Queue / deploy (push) Successful in 56s
Deploy Ladill Queue / deploy (push) Successful in 56s
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>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user