Fix ticket issue 500 on assigned-only queues.
Deploy Ladill Queue / deploy (push) Successful in 51s

Reception create form omitted service point; catch that as validation and require a desk/room when the queue uses assigned routing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 19:41:22 +00:00
co-authored by Cursor
parent 8062cce8d1
commit 9c92a04a74
3 changed files with 142 additions and 13 deletions
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Qms;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Controllers\Qms\Concerns\ScopesToAccount; use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
use App\Models\Counter;
use App\Models\ServiceQueue; use App\Models\ServiceQueue;
use App\Models\Ticket; use App\Models\Ticket;
use App\Services\Qms\QueueEngine; use App\Services\Qms\QueueEngine;
@@ -57,6 +58,10 @@ class TicketController extends Controller
$queues = ServiceQueue::owned($owner) $queues = ServiceQueue::owned($owner)
->where('organization_id', $organization->id) ->where('organization_id', $organization->id)
->where('is_active', true) ->where('is_active', true)
->with([
'branch',
'counters' => fn ($q) => $q->where('is_active', true)->orderBy('name'),
])
->orderBy('name') ->orderBy('name')
->get(); ->get();
@@ -70,6 +75,7 @@ class TicketController extends Controller
$validated = $request->validate([ $validated = $request->validate([
'service_queue_id' => ['required', 'exists:queue_service_queues,id'], 'service_queue_id' => ['required', 'exists:queue_service_queues,id'],
'assigned_counter_id' => ['nullable', 'exists:queue_counters,id'],
'customer_name' => ['nullable', 'string', 'max:255'], 'customer_name' => ['nullable', 'string', 'max:255'],
'customer_phone' => ['nullable', 'string', 'max:50'], 'customer_phone' => ['nullable', 'string', 'max:50'],
'priority' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('qms.ticket_priorities')))], 'priority' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('qms.ticket_priorities')))],
@@ -78,11 +84,31 @@ class TicketController extends Controller
$queue = ServiceQueue::findOrFail($validated['service_queue_id']); $queue = ServiceQueue::findOrFail($validated['service_queue_id']);
$this->authorizeOwner($request, $queue); $this->authorizeOwner($request, $queue);
if ($queue->usesAssignedOnlyRouting() && empty($validated['assigned_counter_id'])) {
return back()
->withInput()
->withErrors(['assigned_counter_id' => 'Select a service point for this queue.']);
}
if (! empty($validated['assigned_counter_id'])) {
$counter = Counter::query()->findOrFail($validated['assigned_counter_id']);
$this->authorizeOwner($request, $counter);
if (! $queue->counters()->where('queue_counters.id', $counter->id)->exists()) {
return back()
->withInput()
->withErrors(['assigned_counter_id' => 'That service point is not linked to this queue.']);
}
}
try {
$ticket = $this->engine->issueTicket($queue, $owner, [ $ticket = $this->engine->issueTicket($queue, $owner, [
...$validated, ...$validated,
'source' => 'reception', 'source' => 'reception',
'actor_ref' => $owner, 'actor_ref' => $owner,
]); ]);
} catch (\RuntimeException $e) {
return back()->withInput()->withErrors(['service_queue_id' => $e->getMessage()]);
}
return redirect()->route('qms.tickets.show', $ticket)->with('success', 'Ticket issued.'); return redirect()->route('qms.tickets.show', $ticket)->with('success', 'Ticket issued.');
} }
+69 -8
View File
@@ -1,29 +1,90 @@
<x-app-layout title="Issue ticket"> <x-app-layout title="Issue ticket">
<div class="mx-auto max-w-lg"> @php
$queuePayload = $queues->map(fn ($queue) => [
'id' => $queue->id,
'label' => $queue->name.($queue->branch ? ' ('.$queue->branch->name.')' : ''),
'assigned_only' => $queue->usesAssignedOnlyRouting(),
'counters' => $queue->counters->map(fn ($c) => [
'id' => $c->id,
'label' => $c->name.($c->code ? ' · '.$c->code : ''),
])->values(),
])->values();
@endphp
<div
class="mx-auto max-w-lg"
x-data="{
queues: @json($queuePayload),
queueId: @json(old('service_queue_id', $queues->first()?->id)),
counterId: @json(old('assigned_counter_id')),
get selected() {
return this.queues.find(q => String(q.id) === String(this.queueId)) || null
},
get needsPoint() {
return Boolean(this.selected?.assigned_only)
},
get counters() {
return this.selected?.counters || []
},
onQueueChange() {
if (! this.counters.some(c => String(c.id) === String(this.counterId))) {
this.counterId = this.counters[0]?.id || ''
}
}
}"
x-init="onQueueChange()"
>
<h1 class="text-2xl font-semibold">Issue ticket</h1> <h1 class="text-2xl font-semibold">Issue ticket</h1>
<form method="POST" action="{{ route('qms.tickets.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900"> <form method="POST" action="{{ route('qms.tickets.store') }}" class="mt-6 space-y-4 rounded-2xl border bg-white p-6 dark:border-slate-700 dark:bg-slate-900">
@csrf @csrf
<div> <div>
<label class="block text-sm font-medium">Queue</label> <label class="block text-sm font-medium">Queue</label>
<select name="service_queue_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"> <select
@foreach ($queues as $queue) name="service_queue_id"
<option value="{{ $queue->id }}">{{ $queue->name }} ({{ $queue->branch?->name }})</option> required
@endforeach class="mt-1 w-full rounded-lg border-slate-300 text-sm"
x-model="queueId"
@change="onQueueChange()"
>
<template x-for="queue in queues" :key="queue.id">
<option :value="queue.id" x-text="queue.label"></option>
</template>
</select> </select>
@error('service_queue_id')
<p class="mt-1 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<div x-show="needsPoint" x-cloak>
<label class="block text-sm font-medium">Service point</label>
<select
name="assigned_counter_id"
class="mt-1 w-full rounded-lg border-slate-300 text-sm"
x-model="counterId"
:required="needsPoint"
:disabled="!needsPoint"
>
<option value="">Select service point</option>
<template x-for="counter in counters" :key="counter.id">
<option :value="counter.id" x-text="counter.label"></option>
</template>
</select>
<p class="mt-1 text-xs text-slate-500">This queue only serves tickets assigned to a desk or room.</p>
@error('assigned_counter_id')
<p class="mt-1 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div> </div>
<div> <div>
<label class="block text-sm font-medium">Customer name</label> <label class="block text-sm font-medium">Customer name</label>
<input name="customer_name" class="mt-1 w-full rounded-lg border-slate-300 text-sm"> <input name="customer_name" value="{{ old('customer_name') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div> </div>
<div> <div>
<label class="block text-sm font-medium">Phone</label> <label class="block text-sm font-medium">Phone</label>
<input name="customer_phone" type="tel" class="mt-1 w-full rounded-lg border-slate-300 text-sm"> <input name="customer_phone" type="tel" value="{{ old('customer_phone') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div> </div>
<div> <div>
<label class="block text-sm font-medium">Priority</label> <label class="block text-sm font-medium">Priority</label>
<select name="priority" class="mt-1 w-full rounded-lg border-slate-300 text-sm"> <select name="priority" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach (config('qms.ticket_priorities') as $value => $label) @foreach (config('qms.ticket_priorities') as $value => $label)
<option value="{{ $value }}">{{ $label }}</option> <option value="{{ $value }}" @selected(old('priority', 'walk_in') === $value)>{{ $label }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
+42
View File
@@ -2,6 +2,7 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Branch; use App\Models\Branch;
use App\Models\Counter; use App\Models\Counter;
use App\Models\DisplayScreen; use App\Models\DisplayScreen;
@@ -19,6 +20,12 @@ class ServicePointRoutingTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
}
/** /**
* @return array{0: User, 1: Organization, 2: Branch, 3: ServiceQueue, 4: Counter, 5: Counter} * @return array{0: User, 1: Organization, 2: Branch, 3: ServiceQueue, 4: Counter, 5: Counter}
*/ */
@@ -110,6 +117,41 @@ class ServicePointRoutingTest extends TestCase
]); ]);
} }
public function test_web_issue_without_service_point_returns_validation_error_not_500(): void
{
[$user, , , $queue] = $this->setUpAssignedConsultation();
$this->actingAs($user)
->from(route('qms.tickets.create'))
->post(route('qms.tickets.store'), [
'service_queue_id' => $queue->id,
'customer_name' => 'Walk-in',
'priority' => 'walk_in',
])
->assertRedirect(route('qms.tickets.create'))
->assertSessionHasErrors('assigned_counter_id');
$this->assertSame(0, Ticket::query()->where('service_queue_id', $queue->id)->count());
}
public function test_web_issue_with_service_point_succeeds(): void
{
[$user, , , $queue, $room4] = $this->setUpAssignedConsultation();
$this->actingAs($user)
->post(route('qms.tickets.store'), [
'service_queue_id' => $queue->id,
'assigned_counter_id' => $room4->id,
'customer_name' => 'Walk-in',
'priority' => 'walk_in',
])
->assertRedirect();
$ticket = Ticket::query()->where('service_queue_id', $queue->id)->first();
$this->assertNotNull($ticket);
$this->assertSame($room4->id, $ticket->assigned_counter_id);
}
public function test_display_shows_latest_per_service_point_with_destination_and_staff(): void public function test_display_shows_latest_per_service_point_with_destination_and_staff(): void
{ {
[$user, , $branch, $queue, $room4, $room5] = $this->setUpAssignedConsultation(); [$user, , $branch, $queue, $room4, $room5] = $this->setUpAssignedConsultation();