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,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Qms\AnalyticsService;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AnalyticsController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected AnalyticsService $analytics,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'reports.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$overview = $this->analytics->overview($owner, $organization->id, $branchScope);
|
||||
$trend = $this->analytics->dailyTrend($owner, $organization->id, 14, $branchScope);
|
||||
|
||||
$branches = Branch::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.analytics.index', compact('overview', 'trend', 'organization', 'branches', 'branchScope'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\QueueAppointment;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\AppointmentService;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppointmentController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected AppointmentService $appointments,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$appointments = QueueAppointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->when($request->query('status'), fn ($q, $s) => $q->where('status', $s))
|
||||
->with(['serviceQueue', 'branch', 'ticket'])
|
||||
->orderBy('scheduled_at')
|
||||
->paginate(30);
|
||||
|
||||
return view('qms.appointments.index', compact('appointments', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($owner)->where('organization_id', $organization->id)->where('is_active', true)->orderBy('name')->get();
|
||||
$queues = ServiceQueue::owned($owner)->where('organization_id', $organization->id)->where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('qms.appointments.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'queues' => $queues,
|
||||
'modes' => config('qms.appointment_modes'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'branch_id' => ['required', 'exists:queue_branches,id'],
|
||||
'service_queue_id' => ['nullable', 'exists:queue_service_queues,id'],
|
||||
'customer_name' => ['required', 'string', 'max:255'],
|
||||
'customer_phone' => ['nullable', 'string', 'max:50'],
|
||||
'customer_email' => ['nullable', 'email', 'max:255'],
|
||||
'scheduled_at' => ['required', 'date'],
|
||||
'mode' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))],
|
||||
]);
|
||||
|
||||
$appointment = QueueAppointment::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
...$validated,
|
||||
'mode' => $validated['mode'] ?? $organization->settings['appointment_mode'] ?? 'hybrid',
|
||||
'status' => 'scheduled',
|
||||
'reference' => 'APT-'.Str::upper(Str::random(6)),
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'appointment.created', $organization->id, $owner, QueueAppointment::class, $appointment->id);
|
||||
|
||||
return redirect()->route('qms.appointments.index')->with('success', 'Appointment scheduled.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, QueueAppointment $appointment): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return view('qms.appointments.edit', [
|
||||
'appointment' => $appointment,
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'modes' => config('qms.appointment_modes'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, QueueAppointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
abort_unless($appointment->status === 'scheduled', 422, 'Only scheduled appointments can be edited.');
|
||||
|
||||
$validated = $request->validate([
|
||||
'branch_id' => ['required', 'exists:queue_branches,id'],
|
||||
'service_queue_id' => ['nullable', 'exists:queue_service_queues,id'],
|
||||
'customer_name' => ['required', 'string', 'max:255'],
|
||||
'customer_phone' => ['nullable', 'string', 'max:50'],
|
||||
'customer_email' => ['nullable', 'email', 'max:255'],
|
||||
'scheduled_at' => ['required', 'date'],
|
||||
'mode' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))],
|
||||
]);
|
||||
|
||||
$appointment->update($validated);
|
||||
AuditLogger::record($this->ownerRef($request), 'appointment.created', $appointment->organization_id, $this->ownerRef($request), QueueAppointment::class, $appointment->id, ['updated' => true]);
|
||||
|
||||
return redirect()->route('qms.appointments.index')->with('success', 'Appointment updated.');
|
||||
}
|
||||
|
||||
public function checkIn(Request $request, QueueAppointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
|
||||
$ticket = $this->appointments->checkIn($appointment, $this->ownerRef($request));
|
||||
|
||||
return redirect()->route('qms.tickets.show', $ticket)->with('success', 'Appointment checked in. Ticket issued.');
|
||||
}
|
||||
|
||||
public function cancel(Request $request, QueueAppointment $appointment): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'appointments.manage');
|
||||
$this->authorizeOwner($request, $appointment);
|
||||
|
||||
$appointment->update(['status' => 'cancelled']);
|
||||
AuditLogger::record($this->ownerRef($request), 'appointment.cancelled', $appointment->organization_id, $this->ownerRef($request), QueueAppointment::class, $appointment->id);
|
||||
|
||||
return back()->with('success', 'Appointment cancelled.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\AuditLog;
|
||||
use App\Services\Qms\QmsPermissions;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AuditLogController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'audit.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$logs = $this->query($request, $organization)->paginate(50)->withQueryString();
|
||||
|
||||
return view('qms.audit.index', [
|
||||
'logs' => $logs,
|
||||
'organization' => $organization,
|
||||
'actions' => config('qms.audit_actions'),
|
||||
'canExport' => app(QmsPermissions::class)->can($this->member($request), 'audit.export'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'audit.export');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$filename = 'queue-audit-'.now()->format('Y-m-d-His').'.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($request, $organization) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
fputcsv($handle, ['Time', 'Action', 'Actor', 'Subject', 'Metadata', 'IP']);
|
||||
|
||||
$this->query($request, $organization)->chunk(200, function ($logs) use ($handle) {
|
||||
foreach ($logs as $log) {
|
||||
fputcsv($handle, [
|
||||
$log->created_at?->toDateTimeString(),
|
||||
$log->action,
|
||||
$log->actor_ref ?? '—',
|
||||
$log->subject_type ? class_basename($log->subject_type).' #'.$log->subject_id : '—',
|
||||
json_encode($log->metadata ?? []),
|
||||
$log->ip_address ?? '—',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
fclose($handle);
|
||||
}, $filename, ['Content-Type' => 'text/csv']);
|
||||
}
|
||||
|
||||
protected function query(Request $request, $organization)
|
||||
{
|
||||
return AuditLog::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->when($request->action, fn ($q, $action) => $q->where('action', $action))
|
||||
->when($request->from, fn ($q, $from) => $q->where('created_at', '>=', Carbon::parse($from)->startOfDay()))
|
||||
->when($request->to, fn ($q, $to) => $q->where('created_at', '<=', Carbon::parse($to)->endOfDay()))
|
||||
->when($request->q, function ($q, $search) {
|
||||
$q->where(function ($inner) use ($search) {
|
||||
$inner->where('action', 'like', "%{$search}%")
|
||||
->orWhere('metadata', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->orderByDesc('created_at');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use App\Services\Qms\PlanService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BranchController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->withCount('departments')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.admin.branches.index', compact('branches', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
|
||||
return view('qms.admin.branches.create', ['organization' => $this->organization($request)]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$currentCount = Branch::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->count();
|
||||
|
||||
if (! app(PlanService::class)->canAddBranch($organization, $currentCount)) {
|
||||
return back()->withInput()->with('error', 'Your plan allows one branch. Upgrade to Pro for unlimited branches.');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'code' => ['nullable', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
$branch = Branch::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
...$validated,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'branch.created', $organization->id, $owner, Branch::class, $branch->id);
|
||||
|
||||
return redirect()->route('qms.branches.index')->with('success', 'Branch created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Branch $branch): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
$this->authorizeOwner($request, $branch);
|
||||
|
||||
return view('qms.admin.branches.edit', compact('branch'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Branch $branch): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||
$this->authorizeOwner($request, $branch);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'code' => ['nullable', 'string', 'max:50'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'phone' => ['nullable', 'string', 'max:50'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$branch->update([
|
||||
...$validated,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'branch.updated', $branch->organization_id, $this->ownerRef($request), Branch::class, $branch->id);
|
||||
|
||||
return redirect()->route('qms.branches.index')->with('success', 'Branch updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms\Concerns;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use App\Services\Qms\QmsPermissions;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ScopesToAccount
|
||||
{
|
||||
protected function ownerRef(Request $request): string
|
||||
{
|
||||
return (string) $request->user()->public_id;
|
||||
}
|
||||
|
||||
protected function organization(Request $request): Organization
|
||||
{
|
||||
$organization = $request->attributes->get('qms.organization')
|
||||
?? app(OrganizationResolver::class)->resolveForUser($request->user());
|
||||
|
||||
abort_unless($organization, 404);
|
||||
|
||||
return $organization;
|
||||
}
|
||||
|
||||
protected function member(Request $request): ?Member
|
||||
{
|
||||
return $request->attributes->get('qms.member')
|
||||
?? app(OrganizationResolver::class)->memberFor($request->user(), $this->organization($request));
|
||||
}
|
||||
|
||||
protected function authorizeAbility(Request $request, string $ability): void
|
||||
{
|
||||
abort_unless(
|
||||
app(QmsPermissions::class)->can($this->member($request), $ability),
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
protected function authorizeOwner(Request $request, Model $model): void
|
||||
{
|
||||
abort_unless($model->getAttribute('owner_ref') === $this->ownerRef($request), 404);
|
||||
}
|
||||
|
||||
protected function scopeToBranch(Request $request, Builder $query, string $column = 'branch_id'): Builder
|
||||
{
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
if ($branchId !== null) {
|
||||
$query->where($column, $branchId);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Counter;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use App\Services\Qms\QueueEngine;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConsoleController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected QueueEngine $engine,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, Counter $counter): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'console.use');
|
||||
$this->authorizeOwner($request, $counter);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$counter->load(['serviceQueues', 'branch']);
|
||||
$queues = $counter->serviceQueues;
|
||||
|
||||
$currentTicket = Ticket::owned($owner)
|
||||
->where('counter_id', $counter->id)
|
||||
->whereIn('status', ['called', 'serving', 'on_hold'])
|
||||
->with('serviceQueue')
|
||||
->latest('called_at')
|
||||
->first();
|
||||
|
||||
$waitingByQueue = collect($queues)->mapWithKeys(function (ServiceQueue $queue) {
|
||||
return [$queue->id => $this->engine->waitingTickets($queue, 10)];
|
||||
});
|
||||
|
||||
$allQueues = ServiceQueue::owned($owner)
|
||||
->where('organization_id', $counter->organization_id)
|
||||
->where('branch_id', $counter->branch_id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.console.show', compact('counter', 'queues', 'currentTicket', 'waitingByQueue', 'allQueues'));
|
||||
}
|
||||
|
||||
public function action(Request $request, Counter $counter): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'console.use');
|
||||
$this->authorizeOwner($request, $counter);
|
||||
$actor = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'action' => ['required', 'string'],
|
||||
'queue_id' => ['nullable', 'integer'],
|
||||
'ticket_id' => ['nullable', 'integer'],
|
||||
'to_queue_id' => ['nullable', 'integer'],
|
||||
'reason' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
|
||||
match ($validated['action']) {
|
||||
'call_next' => $this->handleCallNext($counter, (int) $validated['queue_id'], $actor),
|
||||
'recall' => $this->handleTicketAction($validated['ticket_id'], 'recall', $actor),
|
||||
'start' => $this->handleTicketAction($validated['ticket_id'], 'start', $actor),
|
||||
'hold' => $this->handleTicketAction($validated['ticket_id'], 'hold', $actor),
|
||||
'skip' => $this->handleTicketAction($validated['ticket_id'], 'skip', $actor),
|
||||
'complete' => $this->handleTicketAction($validated['ticket_id'], 'complete', $actor),
|
||||
'no_show' => $this->handleTicketAction($validated['ticket_id'], 'no_show', $actor),
|
||||
'transfer' => $this->handleTransfer($validated['ticket_id'], (int) $validated['to_queue_id'], $counter, $validated['reason'] ?? null, $actor),
|
||||
'available' => $counter->update(['status' => 'available']),
|
||||
'offline' => $counter->update(['status' => 'offline']),
|
||||
default => null,
|
||||
};
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
protected function handleCallNext(Counter $counter, int $queueId, string $actor): void
|
||||
{
|
||||
$queue = ServiceQueue::findOrFail($queueId);
|
||||
$this->engine->callNext($queue, $counter, $actor);
|
||||
}
|
||||
|
||||
protected function handleTicketAction(?int $ticketId, string $action, string $actor): void
|
||||
{
|
||||
if (! $ticketId) {
|
||||
return;
|
||||
}
|
||||
$ticket = Ticket::findOrFail($ticketId);
|
||||
match ($action) {
|
||||
'recall' => $this->engine->recall($ticket, $actor),
|
||||
'start' => $this->engine->startServing($ticket, $actor),
|
||||
'hold' => $this->engine->hold($ticket, $actor),
|
||||
'skip' => $this->engine->skip($ticket, $actor),
|
||||
'complete' => $this->engine->completeTicket($ticket, $actor),
|
||||
'no_show' => $this->engine->markNoShow($ticket, $actor),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
protected function handleTransfer(?int $ticketId, int $toQueueId, Counter $counter, ?string $reason, string $actor): void
|
||||
{
|
||||
if (! $ticketId || ! $toQueueId) {
|
||||
return;
|
||||
}
|
||||
$ticket = Ticket::findOrFail($ticketId);
|
||||
$toQueue = ServiceQueue::findOrFail($toQueueId);
|
||||
$this->engine->transfer($ticket, $toQueue, $counter, $reason, $actor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Counter;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CounterController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'counters.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$counters = Counter::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->with(['branch', 'serviceQueues'])
|
||||
->when(true, fn ($q) => $this->scopeToBranch($request, $q))
|
||||
->orderBy('name')
|
||||
->paginate(20);
|
||||
|
||||
return view('qms.counters.index', compact('counters', 'organization'));
|
||||
}
|
||||
|
||||
public function show(Request $request, Counter $counter): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'counters.view');
|
||||
$this->authorizeOwner($request, $counter);
|
||||
$counter->load(['branch', 'serviceQueues']);
|
||||
|
||||
return view('qms.counters.show', compact('counter'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'counters.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($owner)->where('organization_id', $organization->id)->where('is_active', true)->orderBy('name')->get();
|
||||
$queues = ServiceQueue::owned($owner)->where('organization_id', $organization->id)->where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('qms.counters.create', compact('organization', 'branches', 'queues'));
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'counters.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $this->validatedCounter($request);
|
||||
|
||||
$counter = Counter::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
'branch_id' => $validated['branch_id'],
|
||||
'name' => $validated['name'],
|
||||
'code' => $validated['code'] ?? null,
|
||||
'status' => 'available',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
if (! empty($validated['queue_ids'])) {
|
||||
$counter->serviceQueues()->sync($validated['queue_ids']);
|
||||
}
|
||||
|
||||
AuditLogger::record($owner, 'counter.created', $organization->id, $owner, Counter::class, $counter->id);
|
||||
|
||||
return redirect()->route('qms.counters.show', $counter)->with('success', 'Counter created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Counter $counter): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'counters.manage');
|
||||
$this->authorizeOwner($request, $counter);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
return view('qms.counters.edit', [
|
||||
'counter' => $counter->load('serviceQueues'),
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $counter->organization_id)->orderBy('name')->get(),
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $counter->organization_id)->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Counter $counter): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'counters.manage');
|
||||
$this->authorizeOwner($request, $counter);
|
||||
|
||||
$validated = $this->validatedCounter($request);
|
||||
$counter->update([
|
||||
'name' => $validated['name'],
|
||||
'code' => $validated['code'] ?? null,
|
||||
'branch_id' => $validated['branch_id'],
|
||||
'is_active' => $request->boolean('is_active', $counter->is_active),
|
||||
]);
|
||||
$counter->serviceQueues()->sync($validated['queue_ids'] ?? []);
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'counter.updated', $counter->organization_id, $this->ownerRef($request), Counter::class, $counter->id);
|
||||
|
||||
return redirect()->route('qms.counters.show', $counter)->with('success', 'Counter updated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedCounter(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'code' => ['nullable', 'string', 'max:50'],
|
||||
'branch_id' => ['required', 'exists:queue_branches,id'],
|
||||
'queue_ids' => ['nullable', 'array'],
|
||||
'queue_ids.*' => ['integer', 'exists:queue_service_queues,id'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\DashboardStats;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected DashboardStats $stats,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'dashboard.view');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$branchQuery = Branch::owned($owner)->where('organization_id', $organization->id);
|
||||
$this->scopeToBranch($request, $branchQuery);
|
||||
|
||||
$orgStats = [
|
||||
'branches' => (clone $branchQuery)->where('is_active', true)->count(),
|
||||
'team_members' => Member::owned($owner)->where('organization_id', $organization->id)->count(),
|
||||
'queues' => ServiceQueue::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->where('is_active', true)
|
||||
->count(),
|
||||
];
|
||||
|
||||
$operational = $this->stats->forOrganization($owner, $organization->id, $branchScope);
|
||||
$branches = (clone $branchQuery)->withCount('serviceQueues')->orderBy('name')->get();
|
||||
|
||||
return view('qms.dashboard', compact('organization', 'orgStats', 'branches', 'operational'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Department;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DepartmentController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.view');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$departments = Department::owned($owner)
|
||||
->whereHas('branch', fn ($q) => $q->where('organization_id', $organization->id))
|
||||
->with('branch')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.admin.departments.index', [
|
||||
'departments' => $departments,
|
||||
'organization' => $organization,
|
||||
'types' => config('qms.department_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.admin.departments.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'types' => config('qms.department_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'branch_id' => ['required', 'integer', 'exists:queue_branches,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.department_types')))],
|
||||
]);
|
||||
|
||||
$branch = Branch::owned($owner)->findOrFail($validated['branch_id']);
|
||||
abort_unless($branch->organization_id === $organization->id, 404);
|
||||
|
||||
$department = Department::create([
|
||||
'owner_ref' => $owner,
|
||||
'branch_id' => $branch->id,
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'department.created', $organization->id, $owner, Department::class, $department->id);
|
||||
|
||||
return redirect()->route('qms.departments.index')->with('success', 'Department created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Department $department): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$this->authorizeOwner($request, $department);
|
||||
|
||||
return view('qms.admin.departments.edit', [
|
||||
'department' => $department,
|
||||
'types' => config('qms.department_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Department $department): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$this->authorizeOwner($request, $department);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.department_types')))],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$department->update([
|
||||
...$validated,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
AuditLogger::record(
|
||||
$this->ownerRef($request),
|
||||
'department.updated',
|
||||
$department->branch->organization_id,
|
||||
$this->ownerRef($request),
|
||||
Department::class,
|
||||
$department->id,
|
||||
);
|
||||
|
||||
return redirect()->route('qms.departments.index')->with('success', 'Department updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Department $department): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.departments.manage');
|
||||
$this->authorizeOwner($request, $department);
|
||||
|
||||
$department->load('branch');
|
||||
$organizationId = $department->branch->organization_id;
|
||||
$departmentId = $department->id;
|
||||
$department->delete();
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'department.deleted', $organizationId, $this->ownerRef($request), Department::class, $departmentId);
|
||||
|
||||
return redirect()->route('qms.departments.index')->with('success', 'Department removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Device;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use App\Services\Qms\DeviceService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DeviceController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected DeviceService $devices,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$devices = Device::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('name')
|
||||
->paginate(20);
|
||||
|
||||
return view('qms.devices.index', compact('devices', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
$branches = Branch::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get();
|
||||
|
||||
return view('qms.devices.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'deviceTypes' => config('qms.device_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.device_types')))],
|
||||
'branch_id' => ['nullable', 'exists:queue_branches,id'],
|
||||
]);
|
||||
|
||||
$device = Device::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
'branch_id' => $validated['branch_id'] ?? null,
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'status' => 'offline',
|
||||
]);
|
||||
|
||||
$token = $this->devices->generateToken($device);
|
||||
AuditLogger::record($owner, 'device.created', $organization->id, $owner, Device::class, $device->id);
|
||||
|
||||
return redirect()->route('qms.devices.index')
|
||||
->with('success', 'Device registered. Token: '.$token);
|
||||
}
|
||||
|
||||
public function regenerateToken(Request $request, Device $device): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$this->authorizeOwner($request, $device);
|
||||
|
||||
$token = $this->devices->generateToken($device);
|
||||
|
||||
return back()->with('success', 'New device token: '.$token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Qms\DisplayService;
|
||||
use App\Services\Qms\VoiceAnnouncementService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DisplayPublicController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected DisplayService $displays,
|
||||
protected VoiceAnnouncementService $voice,
|
||||
) {}
|
||||
|
||||
public function show(string $token): View
|
||||
{
|
||||
$screen = $this->displays->findByToken($token) ?? abort(404);
|
||||
$this->displays->touch($screen);
|
||||
|
||||
return view('qms.display.public', [
|
||||
'screen' => $screen,
|
||||
'dataUrl' => route('qms.display.data', $token),
|
||||
]);
|
||||
}
|
||||
|
||||
public function data(string $token): JsonResponse
|
||||
{
|
||||
$screen = $this->displays->findByToken($token) ?? abort(404);
|
||||
$this->displays->touch($screen);
|
||||
|
||||
$payload = $this->displays->payload($screen);
|
||||
|
||||
foreach ($payload['announcements'] as $announcement) {
|
||||
if (isset($announcement['id'])) {
|
||||
$model = \App\Models\VoiceAnnouncement::find($announcement['id']);
|
||||
if ($model) {
|
||||
$this->voice->markPlayed($model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\DisplayScreen;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DisplayScreenController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$screens = DisplayScreen::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('name')
|
||||
->paginate(20);
|
||||
|
||||
return view('qms.displays.index', compact('screens', 'organization'));
|
||||
}
|
||||
|
||||
public function show(Request $request, DisplayScreen $display): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.view');
|
||||
$this->authorizeOwner($request, $display);
|
||||
$display->load('branch');
|
||||
|
||||
return view('qms.displays.show', compact('display'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return view('qms.displays.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'layouts' => config('qms.display_layouts'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $this->validatedDisplay($request);
|
||||
|
||||
$screen = DisplayScreen::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
'branch_id' => $validated['branch_id'],
|
||||
'name' => $validated['name'],
|
||||
'layout' => $validated['layout'],
|
||||
'service_queue_ids' => $validated['queue_ids'] ?? [],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'display.created', $organization->id, $owner, DisplayScreen::class, $screen->id);
|
||||
|
||||
return redirect()->route('qms.displays.show', $screen)
|
||||
->with('success', 'Display created. URL: '.route('qms.display.public', $screen->access_token));
|
||||
}
|
||||
|
||||
public function edit(Request $request, DisplayScreen $display): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$this->authorizeOwner($request, $display);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
return view('qms.displays.edit', [
|
||||
'display' => $display,
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $display->organization_id)->orderBy('name')->get(),
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $display->organization_id)->orderBy('name')->get(),
|
||||
'layouts' => config('qms.display_layouts'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, DisplayScreen $display): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'displays.manage');
|
||||
$this->authorizeOwner($request, $display);
|
||||
|
||||
$validated = $this->validatedDisplay($request);
|
||||
$display->update([
|
||||
'name' => $validated['name'],
|
||||
'branch_id' => $validated['branch_id'],
|
||||
'layout' => $validated['layout'],
|
||||
'service_queue_ids' => $validated['queue_ids'] ?? [],
|
||||
'is_active' => $request->boolean('is_active', $display->is_active),
|
||||
]);
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'display.updated', $display->organization_id, $this->ownerRef($request), DisplayScreen::class, $display->id);
|
||||
|
||||
return redirect()->route('qms.displays.show', $display)->with('success', 'Display updated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedDisplay(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'branch_id' => ['required', 'exists:queue_branches,id'],
|
||||
'layout' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.display_layouts')))],
|
||||
'queue_ids' => ['nullable', 'array'],
|
||||
'queue_ids.*' => ['integer', 'exists:queue_service_queues,id'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\CustomerFeedback;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class FeedbackAdminController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'feedback.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$feedback = CustomerFeedback::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->when($branchScope, fn ($q) => $q->whereHas('ticket', fn ($t) => $t->where('branch_id', $branchScope)))
|
||||
->with(['ticket', 'counter'])
|
||||
->latest()
|
||||
->paginate(30);
|
||||
|
||||
$avgRating = CustomerFeedback::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->when($branchScope, fn ($q) => $q->whereHas('ticket', fn ($t) => $t->where('branch_id', $branchScope)))
|
||||
->avg('rating');
|
||||
|
||||
return view('qms.feedback.index', [
|
||||
'feedback' => $feedback,
|
||||
'organization' => $organization,
|
||||
'avgRating' => round((float) $avgRating, 1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomerFeedback;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class FeedbackController extends Controller
|
||||
{
|
||||
public function show(string $qrToken): View
|
||||
{
|
||||
$ticket = Ticket::where('qr_token', $qrToken)->firstOrFail();
|
||||
abort_unless($ticket->status === 'completed', 404);
|
||||
|
||||
$existing = CustomerFeedback::where('ticket_id', $ticket->id)->first();
|
||||
|
||||
return view('qms.feedback.show', compact('ticket', 'existing'));
|
||||
}
|
||||
|
||||
public function store(Request $request, string $qrToken): RedirectResponse
|
||||
{
|
||||
$ticket = Ticket::where('qr_token', $qrToken)->firstOrFail();
|
||||
abort_unless($ticket->status === 'completed', 404);
|
||||
abort_if(CustomerFeedback::where('ticket_id', $ticket->id)->exists(), 422, 'Feedback already submitted.');
|
||||
|
||||
$validated = $request->validate([
|
||||
'rating' => ['required', 'integer', 'min:1', 'max:5'],
|
||||
'service_quality' => ['nullable', 'integer', 'min:1', 'max:5'],
|
||||
'wait_experience' => ['nullable', 'integer', 'min:1', 'max:5'],
|
||||
'staff_professionalism' => ['nullable', 'integer', 'min:1', 'max:5'],
|
||||
'comments' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$feedback = CustomerFeedback::create([
|
||||
'owner_ref' => $ticket->owner_ref,
|
||||
'organization_id' => $ticket->organization_id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'counter_id' => $ticket->counter_id,
|
||||
...$validated,
|
||||
]);
|
||||
|
||||
AuditLogger::record($ticket->owner_ref, 'feedback.submitted', $ticket->organization_id, null, CustomerFeedback::class, $feedback->id);
|
||||
|
||||
return back()->with('success', 'Thank you for your feedback.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\DeviceService;
|
||||
use App\Services\Qms\QueueEngine;
|
||||
use App\Services\Qms\TicketPresenter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class KioskDeviceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected QueueEngine $engine,
|
||||
protected DeviceService $devices,
|
||||
) {}
|
||||
|
||||
public function show(Request $request): View
|
||||
{
|
||||
$device = $request->attributes->get('qms.device');
|
||||
$queues = ServiceQueue::query()
|
||||
->where('organization_id', $device->organization_id)
|
||||
->when($device->branch_id, fn ($q) => $q->where('branch_id', $device->branch_id))
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$this->devices->recordHeartbeat($device);
|
||||
|
||||
return view('qms.kiosk.device', compact('device', 'queues'));
|
||||
}
|
||||
|
||||
public function issue(Request $request): JsonResponse
|
||||
{
|
||||
$device = $request->attributes->get('qms.device');
|
||||
$this->devices->recordHeartbeat($device);
|
||||
|
||||
$validated = $request->validate([
|
||||
'queue_id' => ['required', 'integer', 'exists:queue_service_queues,id'],
|
||||
'customer_name' => ['nullable', 'string', 'max:255'],
|
||||
'customer_phone' => ['nullable', 'string', 'max:50'],
|
||||
'priority' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$queue = ServiceQueue::findOrFail($validated['queue_id']);
|
||||
abort_unless($queue->organization_id === $device->organization_id, 403);
|
||||
|
||||
$ticket = $this->engine->issueTicket($queue, $device->owner_ref, [
|
||||
'customer_name' => $validated['customer_name'] ?? null,
|
||||
'customer_phone' => $validated['customer_phone'] ?? null,
|
||||
'priority' => $validated['priority'] ?? 'walk_in',
|
||||
'source' => 'kiosk',
|
||||
]);
|
||||
|
||||
return response()->json(['data' => TicketPresenter::toArray($ticket)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Member;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MemberController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$members = Member::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->with('branch')
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
return view('qms.admin.members.index', [
|
||||
'members' => $members,
|
||||
'organization' => $organization,
|
||||
'roles' => config('qms.roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.admin.members.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'roles' => config('qms.roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'user_ref' => ['required', 'string', 'max:255'],
|
||||
'role' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.roles')))],
|
||||
'branch_id' => ['nullable', 'integer', 'exists:queue_branches,id'],
|
||||
]);
|
||||
|
||||
$member = Member::updateOrCreate(
|
||||
[
|
||||
'organization_id' => $organization->id,
|
||||
'user_ref' => $validated['user_ref'],
|
||||
],
|
||||
[
|
||||
'owner_ref' => $owner,
|
||||
'role' => $validated['role'],
|
||||
'branch_id' => $validated['branch_id'] ?? null,
|
||||
],
|
||||
);
|
||||
|
||||
AuditLogger::record($owner, 'member.created', $organization->id, $owner, Member::class, $member->id);
|
||||
|
||||
return redirect()->route('qms.members.index')->with('success', 'Member saved.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Member $member): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'admin.members.manage');
|
||||
$this->authorizeOwner($request, $member);
|
||||
|
||||
abort_if($member->user_ref === $this->ownerRef($request), 422, 'You cannot remove yourself.');
|
||||
|
||||
$memberId = $member->id;
|
||||
$organizationId = $member->organization_id;
|
||||
$member->delete();
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'member.deleted', $organizationId, $this->ownerRef($request), Member::class, $memberId);
|
||||
|
||||
return redirect()->route('qms.members.index')->with('success', 'Member removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Qms\QueueEngine;
|
||||
use App\Services\Qms\TicketPresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MobileQueueController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected QueueEngine $engine,
|
||||
) {}
|
||||
|
||||
public function joinForm(): View
|
||||
{
|
||||
return view('qms.mobile.join');
|
||||
}
|
||||
|
||||
public function join(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'queue_uuid' => ['required', 'uuid'],
|
||||
'customer_name' => ['nullable', 'string', 'max:255'],
|
||||
'customer_phone' => ['required', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
$queue = ServiceQueue::where('uuid', $validated['queue_uuid'])->where('is_active', true)->firstOrFail();
|
||||
|
||||
$ticket = $this->engine->issueTicket($queue, $queue->owner_ref, [
|
||||
'customer_name' => $validated['customer_name'] ?? null,
|
||||
'customer_phone' => $validated['customer_phone'],
|
||||
'source' => 'mobile',
|
||||
]);
|
||||
|
||||
return redirect()->route('qms.mobile.show', $ticket->qr_token);
|
||||
}
|
||||
|
||||
public function show(string $qrToken): View
|
||||
{
|
||||
$ticket = Ticket::where('qr_token', $qrToken)->with(['serviceQueue', 'counter'])->firstOrFail();
|
||||
|
||||
$ahead = Ticket::query()
|
||||
->where('service_queue_id', $ticket->service_queue_id)
|
||||
->where('status', 'waiting')
|
||||
->where('issued_at', '<', $ticket->issued_at)
|
||||
->count();
|
||||
|
||||
return view('qms.mobile.show', [
|
||||
'ticket' => $ticket,
|
||||
'presented' => TicketPresenter::toArray($ticket, false),
|
||||
'customersAhead' => $ahead,
|
||||
'pollUrl' => route('qms.mobile.data', $qrToken),
|
||||
]);
|
||||
}
|
||||
|
||||
public function data(string $qrToken): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$ticket = Ticket::where('qr_token', $qrToken)->with(['serviceQueue', 'counter'])->firstOrFail();
|
||||
$ahead = Ticket::query()
|
||||
->where('service_queue_id', $ticket->service_queue_id)
|
||||
->where('status', 'waiting')
|
||||
->where('issued_at', '<', $ticket->issued_at)
|
||||
->count();
|
||||
|
||||
return response()->json([
|
||||
'data' => TicketPresenter::toArray($ticket, false),
|
||||
'customers_ahead' => $ahead,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delay(Request $request, string $qrToken): RedirectResponse
|
||||
{
|
||||
$ticket = Ticket::where('qr_token', $qrToken)->whereIn('status', ['waiting', 'on_hold'])->firstOrFail();
|
||||
$validated = $request->validate(['minutes' => ['required', 'integer', 'min:5', 'max:120']]);
|
||||
$this->engine->delayArrival($ticket, $validated['minutes']);
|
||||
|
||||
return back()->with('success', 'Arrival delayed.');
|
||||
}
|
||||
|
||||
public function cancel(string $qrToken): RedirectResponse
|
||||
{
|
||||
$ticket = Ticket::where('qr_token', $qrToken)->whereIn('status', ['waiting', 'on_hold', 'called'])->firstOrFail();
|
||||
$this->engine->cancel($ticket);
|
||||
|
||||
return redirect()->route('qms.mobile.join')->with('success', 'Ticket cancelled.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use App\Support\OrganizationBranding;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class OnboardingController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected OrganizationResolver $organizations,
|
||||
) {}
|
||||
|
||||
public function show(Request $request): View|RedirectResponse
|
||||
{
|
||||
if ($this->organizations->isOnboarded($request->user())) {
|
||||
return redirect()->route('qms.dashboard');
|
||||
}
|
||||
|
||||
return view('qms.onboarding.show', [
|
||||
'user' => $request->user(),
|
||||
'timezones' => timezone_identifiers_list(),
|
||||
'industries' => config('qms.industry_templates'),
|
||||
'appointmentModes' => config('qms.appointment_modes'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($this->organizations->isOnboarded($request->user())) {
|
||||
return redirect()->route('qms.dashboard');
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'organization_name' => ['required', 'string', 'max:255'],
|
||||
'industry' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.industry_templates')))],
|
||||
'appointment_mode' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))],
|
||||
'branch_name' => ['required', 'string', 'max:255'],
|
||||
'branch_address' => ['nullable', 'string', 'max:500'],
|
||||
'branch_phone' => ['nullable', 'string', 'max:50'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'],
|
||||
]);
|
||||
|
||||
$organization = $this->organizations->completeOnboarding($request->user(), $validated);
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
$organization->update([
|
||||
'logo_path' => OrganizationBranding::storeLogo($organization, $request->file('logo')),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('qms.dashboard')->with('success', 'Welcome to Ladill Queue!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class QueueBoardController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
|
||||
$queues = ServiceQueue::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->where('is_active', true)
|
||||
->with('branch')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$waiting = Ticket::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('status', 'waiting')
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->with('serviceQueue')
|
||||
->orderBy('issued_at')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
$serving = Ticket::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereIn('status', ['called', 'serving'])
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->with(['serviceQueue', 'counter'])
|
||||
->orderByDesc('called_at')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
return view('qms.board.index', compact('organization', 'queues', 'waiting', 'serving'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\QueueRule;
|
||||
use App\Models\ServiceQueue;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class QueueRuleController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request, ServiceQueue $serviceQueue): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'rules.view');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
|
||||
$rules = QueueRule::owned($this->ownerRef($request))
|
||||
->where('service_queue_id', $serviceQueue->id)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
return view('qms.rules.index', [
|
||||
'queue' => $serviceQueue,
|
||||
'rules' => $rules,
|
||||
'ruleTypes' => config('qms.rule_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, ServiceQueue $serviceQueue): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'rules.manage');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
|
||||
$validated = $request->validate([
|
||||
'rule_type' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.rule_types')))],
|
||||
'waiting_threshold' => ['nullable', 'integer', 'min:1'],
|
||||
'overflow_queue_id' => ['nullable', 'exists:queue_service_queues,id'],
|
||||
'priority' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('qms.ticket_priorities')))],
|
||||
'source' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$conditions = [];
|
||||
$actions = [];
|
||||
|
||||
if ($validated['rule_type'] === 'overflow') {
|
||||
$conditions['waiting_threshold'] = (int) ($validated['waiting_threshold'] ?? 10);
|
||||
$actions['overflow_queue_id'] = $validated['overflow_queue_id'] ?? null;
|
||||
} elseif ($validated['rule_type'] === 'priority_boost') {
|
||||
$conditions['source'] = $validated['source'] ?? null;
|
||||
$actions['priority'] = $validated['priority'] ?? 'vip';
|
||||
}
|
||||
|
||||
QueueRule::create([
|
||||
'owner_ref' => $this->ownerRef($request),
|
||||
'service_queue_id' => $serviceQueue->id,
|
||||
'rule_type' => $validated['rule_type'],
|
||||
'conditions' => $conditions,
|
||||
'actions' => $actions,
|
||||
'sort_order' => QueueRule::where('service_queue_id', $serviceQueue->id)->max('sort_order') + 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Rule added.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, ServiceQueue $serviceQueue, QueueRule $rule): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'rules.manage');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
abort_unless($rule->service_queue_id === $serviceQueue->id, 404);
|
||||
|
||||
$rule->delete();
|
||||
|
||||
return back()->with('success', 'Rule removed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Qms\OrganizationResolver;
|
||||
use App\Services\Qms\QmsPermissions;
|
||||
use App\Services\Qms\ReportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected ReportService $reports,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'reports.view');
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.reports.index', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'reports' => config('qms.report_types'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $type): View
|
||||
{
|
||||
$this->authorizeReport($request, $type);
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
$branchId = $request->input('branch_id') ? (int) $request->input('branch_id') : $branchScope;
|
||||
[$from, $to] = $this->dateRange($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$data = match ($type) {
|
||||
'tickets' => $this->reports->ticketsReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'wait_times' => $this->reports->waitTimesReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'counters' => $this->reports->countersReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'appointments' => $this->reports->appointmentsReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'feedback' => $this->reports->feedbackReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'service_sessions' => $this->reports->serviceSessionsReport($owner, $organization->id, $from, $to, $branchId),
|
||||
default => abort(404),
|
||||
};
|
||||
|
||||
$branches = Branch::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.reports.show', [
|
||||
'type' => $type,
|
||||
'label' => config('qms.report_types')[$type] ?? $type,
|
||||
'data' => $data,
|
||||
'from' => $from->toDateString(),
|
||||
'to' => $to->toDateString(),
|
||||
'branchId' => $branchId,
|
||||
'branches' => $branches,
|
||||
'canExport' => app(QmsPermissions::class)->can($this->member($request), 'reports.export'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request, string $type): StreamedResponse
|
||||
{
|
||||
$this->authorizeReport($request, $type);
|
||||
abort_unless(app(QmsPermissions::class)->can($this->member($request), 'reports.export'), 403);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
|
||||
$branchId = $request->input('branch_id') ? (int) $request->input('branch_id') : $branchScope;
|
||||
[$from, $to] = $this->dateRange($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$data = match ($type) {
|
||||
'tickets' => $this->reports->ticketsReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'wait_times' => $this->reports->waitTimesReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'counters' => $this->reports->countersReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'appointments' => $this->reports->appointmentsReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'feedback' => $this->reports->feedbackReport($owner, $organization->id, $from, $to, $branchId),
|
||||
'service_sessions' => $this->reports->serviceSessionsReport($owner, $organization->id, $from, $to, $branchId),
|
||||
default => abort(404),
|
||||
};
|
||||
|
||||
$filename = "queue-report-{$type}-".now()->format('Y-m-d').'.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($data, $type) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
if ($type === 'wait_times' && isset($data['hourly'])) {
|
||||
fputcsv($handle, ['Hour', 'Avg wait (seconds)']);
|
||||
foreach ($data['hourly'] as $row) {
|
||||
fputcsv($handle, [$row['hour'], $row['avg_wait_seconds']]);
|
||||
}
|
||||
} elseif ($type === 'counters' && isset($data['counters'])) {
|
||||
fputcsv($handle, ['Counter', 'Served']);
|
||||
foreach ($data['counters'] as $row) {
|
||||
fputcsv($handle, [$row['name'], $row['served']]);
|
||||
}
|
||||
} else {
|
||||
fputcsv($handle, ['Metric', 'Value']);
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
fputcsv($handle, [$key, $value]);
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
}, $filename, ['Content-Type' => 'text/csv']);
|
||||
}
|
||||
|
||||
protected function authorizeReport(Request $request, string $type): void
|
||||
{
|
||||
abort_unless(array_key_exists($type, config('qms.report_types')), 404);
|
||||
$this->authorizeAbility($request, 'reports.view');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Carbon, 1: Carbon}
|
||||
*/
|
||||
protected function dateRange(Request $request): array
|
||||
{
|
||||
$from = Carbon::parse($request->input('from', now()->subDays(30)->toDateString()))->startOfDay();
|
||||
$to = Carbon::parse($request->input('to', now()->toDateString()))->endOfDay();
|
||||
|
||||
return [$from, $to];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use App\Services\Qms\PlanService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ServiceQueueController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$queues = ServiceQueue::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->with(['branch', 'department'])
|
||||
->when(true, fn ($q) => $this->scopeToBranch($request, $q))
|
||||
->orderBy('name')
|
||||
->paginate(20);
|
||||
|
||||
return view('qms.queues.index', compact('queues', 'organization'));
|
||||
}
|
||||
|
||||
public function show(Request $request, ServiceQueue $serviceQueue): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.view');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
$serviceQueue->load(['branch', 'department']);
|
||||
|
||||
$waiting = Ticket::owned($this->ownerRef($request))
|
||||
->where('service_queue_id', $serviceQueue->id)
|
||||
->where('status', 'waiting')
|
||||
->count();
|
||||
|
||||
return view('qms.queues.show', [
|
||||
'queue' => $serviceQueue,
|
||||
'waiting' => $waiting,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$branches = Branch::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.queues.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'strategies' => config('qms.queue_strategies'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$currentCount = ServiceQueue::owned($owner)->where('organization_id', $organization->id)->count();
|
||||
if (! app(PlanService::class)->canAddQueue($organization, $currentCount)) {
|
||||
return back()->withInput()->with('error', 'Your plan allows '.$currentCount.' queues. Upgrade to Pro for unlimited queues.');
|
||||
}
|
||||
|
||||
$validated = $this->validatedQueue($request);
|
||||
|
||||
abort_unless(
|
||||
Branch::owned($owner)->where('organization_id', $organization->id)->where('id', $validated['branch_id'])->exists(),
|
||||
404,
|
||||
);
|
||||
|
||||
$queue = ServiceQueue::create([
|
||||
...$validated,
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
'color' => $validated['color'] ?? '#4f46e5',
|
||||
'avg_service_seconds' => $validated['avg_service_seconds'] ?? 300,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'service_queue.created', $organization->id, $owner, ServiceQueue::class, $queue->id);
|
||||
|
||||
return redirect()->route('qms.queues.show', $queue)->with('success', 'Queue created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, ServiceQueue $serviceQueue): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.manage');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
return view('qms.queues.edit', [
|
||||
'queue' => $serviceQueue,
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $serviceQueue->organization_id)->orderBy('name')->get(),
|
||||
'strategies' => config('qms.queue_strategies'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, ServiceQueue $serviceQueue): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.manage');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
|
||||
$validated = $this->validatedQueue($request, updating: true);
|
||||
$serviceQueue->update([
|
||||
...$validated,
|
||||
'color' => $validated['color'] ?? $serviceQueue->color,
|
||||
'is_active' => $request->boolean('is_active', $serviceQueue->is_active),
|
||||
]);
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'service_queue.updated', $serviceQueue->organization_id, $this->ownerRef($request), ServiceQueue::class, $serviceQueue->id);
|
||||
|
||||
return redirect()->route('qms.queues.show', $serviceQueue)->with('success', 'Queue updated.');
|
||||
}
|
||||
|
||||
public function pause(Request $request, ServiceQueue $serviceQueue): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.manage');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
app(\App\Services\Qms\QueueEngine::class)->pauseQueue($serviceQueue, $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Queue paused.');
|
||||
}
|
||||
|
||||
public function resume(Request $request, ServiceQueue $serviceQueue): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'queues.manage');
|
||||
$this->authorizeOwner($request, $serviceQueue);
|
||||
app(\App\Services\Qms\QueueEngine::class)->resumeQueue($serviceQueue, $this->ownerRef($request));
|
||||
|
||||
return back()->with('success', 'Queue resumed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function validatedQueue(Request $request, bool $updating = false): array
|
||||
{
|
||||
return $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'branch_id' => ['required', 'exists:queue_branches,id'],
|
||||
'department_id' => ['nullable', 'exists:queue_departments,id'],
|
||||
'color' => ['nullable', 'string', 'max:20'],
|
||||
'prefix' => [$updating ? 'sometimes' : 'required', 'string', 'max:10'],
|
||||
'strategy' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.queue_strategies')))],
|
||||
'avg_service_seconds' => ['nullable', 'integer', 'min:60', 'max:7200'],
|
||||
'max_capacity' => ['nullable', 'integer', 'min:1'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use App\Services\Qms\QmsPermissions;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.view');
|
||||
$organization = $this->organization($request);
|
||||
$canManage = app(QmsPermissions::class)->can($this->member($request), 'settings.manage');
|
||||
|
||||
$branchCount = Branch::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->count();
|
||||
|
||||
return view('qms.settings.edit', [
|
||||
'organization' => $organization,
|
||||
'canManage' => $canManage,
|
||||
'branchCount' => $branchCount,
|
||||
'appointmentModes' => config('qms.appointment_modes'),
|
||||
'industries' => config('qms.industry_templates'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'settings.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'appointment_mode' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.appointment_modes')))],
|
||||
'industry' => ['nullable', 'string'],
|
||||
'notifications_enabled' => ['boolean'],
|
||||
]);
|
||||
|
||||
$settings = $organization->settings ?? [];
|
||||
$settings['appointment_mode'] = $validated['appointment_mode'];
|
||||
$settings['industry'] = $validated['industry'] ?? $settings['industry'] ?? null;
|
||||
$settings['notifications_enabled'] = $request->boolean('notifications_enabled', true);
|
||||
|
||||
$organization->update([
|
||||
'name' => $validated['name'],
|
||||
'timezone' => $validated['timezone'],
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, \App\Models\Organization::class, $organization->id);
|
||||
|
||||
return back()->with('success', 'Settings saved.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Models\Ticket;
|
||||
use App\Services\Qms\QueueEngine;
|
||||
use App\Services\Qms\TicketPresenter;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected QueueEngine $engine,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'tickets.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$tickets = Ticket::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->with(['serviceQueue', 'counter'])
|
||||
->when($request->query('status'), fn ($q, $s) => $q->where('status', $s))
|
||||
->latest('issued_at')
|
||||
->paginate(30);
|
||||
|
||||
$queues = ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get();
|
||||
|
||||
return view('qms.tickets.index', compact('tickets', 'queues', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'tickets.issue');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$queues = ServiceQueue::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.tickets.create', compact('queues', 'organization'));
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'tickets.issue');
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'service_queue_id' => ['required', 'exists:queue_service_queues,id'],
|
||||
'customer_name' => ['nullable', 'string', 'max:255'],
|
||||
'customer_phone' => ['nullable', 'string', 'max:50'],
|
||||
'priority' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('qms.ticket_priorities')))],
|
||||
]);
|
||||
|
||||
$queue = ServiceQueue::findOrFail($validated['service_queue_id']);
|
||||
$this->authorizeOwner($request, $queue);
|
||||
|
||||
$ticket = $this->engine->issueTicket($queue, $owner, [
|
||||
...$validated,
|
||||
'source' => 'reception',
|
||||
'actor_ref' => $owner,
|
||||
]);
|
||||
|
||||
return redirect()->route('qms.tickets.show', $ticket)->with('success', 'Ticket issued.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Ticket $ticket): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'tickets.view');
|
||||
$this->authorizeOwner($request, $ticket);
|
||||
|
||||
$ticket->load(['serviceQueue', 'counter', 'events']);
|
||||
|
||||
return view('qms.tickets.show', [
|
||||
'ticket' => $ticket,
|
||||
'presented' => TicketPresenter::toArray($ticket),
|
||||
]);
|
||||
}
|
||||
|
||||
public function print(Request $request, Ticket $ticket): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'tickets.view');
|
||||
$this->authorizeOwner($request, $ticket);
|
||||
$ticket->load(['serviceQueue', 'branch']);
|
||||
|
||||
return view('qms.tickets.print', compact('ticket'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Qms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
|
||||
use App\Models\Branch;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Models\Workflow;
|
||||
use App\Models\WorkflowStep;
|
||||
use App\Services\Qms\AuditLogger;
|
||||
use App\Services\Qms\WorkflowService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class WorkflowController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected WorkflowService $workflows,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.view');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$workflows = Workflow::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->withCount('steps')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('qms.workflows.index', compact('workflows', 'organization'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
return view('qms.workflows.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'templates' => config('qms.industry_templates'),
|
||||
'workflowTemplates' => config('qms.workflow_templates'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.manage');
|
||||
$owner = $this->ownerRef($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'branch_id' => ['nullable', 'exists:queue_branches,id'],
|
||||
'industry_template' => ['nullable', 'string'],
|
||||
'steps' => ['required', 'array', 'min:1'],
|
||||
'steps.*.name' => ['required', 'string', 'max:255'],
|
||||
'steps.*.service_queue_id' => ['nullable', 'exists:queue_service_queues,id'],
|
||||
]);
|
||||
|
||||
$workflow = Workflow::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'branch_id' => $validated['branch_id'] ?? null,
|
||||
'industry_template' => $validated['industry_template'] ?? null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
foreach ($validated['steps'] as $i => $step) {
|
||||
WorkflowStep::create([
|
||||
'workflow_id' => $workflow->id,
|
||||
'name' => $step['name'],
|
||||
'service_queue_id' => $step['service_queue_id'] ?? null,
|
||||
'sort_order' => $i + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
AuditLogger::record($owner, 'workflow.created', $organization->id, $owner, Workflow::class, $workflow->id);
|
||||
|
||||
return redirect()->route('qms.workflows.index')->with('success', 'Workflow created.');
|
||||
}
|
||||
|
||||
public function show(Request $request, Workflow $workflow): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.view');
|
||||
$this->authorizeOwner($request, $workflow);
|
||||
$workflow->load(['steps.serviceQueue', 'branch']);
|
||||
|
||||
return view('qms.workflows.show', compact('workflow'));
|
||||
}
|
||||
|
||||
public function edit(Request $request, Workflow $workflow): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.manage');
|
||||
$this->authorizeOwner($request, $workflow);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
return view('qms.workflows.edit', [
|
||||
'workflow' => $workflow->load('steps'),
|
||||
'branches' => Branch::owned($owner)->where('organization_id', $workflow->organization_id)->orderBy('name')->get(),
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $workflow->organization_id)->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Workflow $workflow): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.manage');
|
||||
$this->authorizeOwner($request, $workflow);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'branch_id' => ['nullable', 'exists:queue_branches,id'],
|
||||
'is_active' => ['boolean'],
|
||||
'steps' => ['required', 'array', 'min:1'],
|
||||
'steps.*.name' => ['required', 'string', 'max:255'],
|
||||
'steps.*.service_queue_id' => ['nullable', 'exists:queue_service_queues,id'],
|
||||
]);
|
||||
|
||||
$workflow->update([
|
||||
'name' => $validated['name'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'branch_id' => $validated['branch_id'] ?? null,
|
||||
'is_active' => $request->boolean('is_active', $workflow->is_active),
|
||||
]);
|
||||
|
||||
$workflow->steps()->delete();
|
||||
foreach ($validated['steps'] as $i => $step) {
|
||||
WorkflowStep::create([
|
||||
'workflow_id' => $workflow->id,
|
||||
'name' => $step['name'],
|
||||
'service_queue_id' => $step['service_queue_id'] ?? null,
|
||||
'sort_order' => $i + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'workflow.updated', $workflow->organization_id, $this->ownerRef($request), Workflow::class, $workflow->id);
|
||||
|
||||
return redirect()->route('qms.workflows.show', $workflow)->with('success', 'Workflow updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Workflow $workflow): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'workflows.manage');
|
||||
$this->authorizeOwner($request, $workflow);
|
||||
$workflow->delete();
|
||||
|
||||
return redirect()->route('qms.workflows.index')->with('success', 'Workflow removed.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user