diff --git a/app/Services/Qms/DisplayService.php b/app/Services/Qms/DisplayService.php index 0fe80dc..8167ba3 100644 --- a/app/Services/Qms/DisplayService.php +++ b/app/Services/Qms/DisplayService.php @@ -34,19 +34,25 @@ class DisplayService ->orderBy('name') ->get(); + // One active ticket per destination (counter). Older called/serving + // tickets at the same window must not crowd the public board. $nowServing = Ticket::query() ->whereIn('service_queue_id', $queueIds) ->whereIn('status', ['called', 'serving']) ->with(['counter', 'serviceQueue']) ->orderByDesc('called_at') - ->limit(12) + ->limit(48) ->get() + ->unique(fn (Ticket $t) => $t->counter_id + ? 'counter:'.$t->counter_id + : 'queue:'.$t->service_queue_id) + ->take(12) + ->values() ->map(fn (Ticket $t) => [ 'ticket_number' => $t->ticket_number, 'queue_name' => $t->serviceQueue?->name, 'counter' => $t->counter?->name, ]) - ->values() ->all(); $waiting = Ticket::query() diff --git a/resources/css/app.css b/resources/css/app.css index 8888e39..3575b55 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -481,6 +481,7 @@ html:has(.qms-display) body { } .qms-display__ticket { + container-type: size; display: flex; min-height: 0; flex-direction: column; @@ -508,63 +509,67 @@ html:has(.qms-display) body { } .qms-display__ticket-body { - display: grid; + display: flex; min-height: 0; flex: 1; - grid-template-rows: auto minmax(0, 1fr) auto; - align-items: stretch; - padding: clamp(0.9rem, 2.2vh, 1.75rem) clamp(1rem, 2.2vw, 2.25rem); + flex-direction: column; + gap: clamp(0.35rem, 1vh, 0.75rem); + padding: clamp(0.85rem, 2vh, 1.5rem) clamp(1rem, 2vw, 1.75rem); } .qms-display__ticket-meta { display: flex; + flex-shrink: 0; min-width: 0; align-items: center; justify-content: space-between; gap: 0.75rem; } -.qms-display__badge, -.qms-display__active-label { +.qms-display__badge { flex-shrink: 0; border-radius: 9999px; - padding: 0.35rem 0.65rem; + padding: 0.35rem 0.7rem; font-size: clamp(0.62rem, 0.85vw, 0.78rem); font-weight: 700; letter-spacing: 0.08em; line-height: 1; text-transform: uppercase; -} - -.qms-display__badge { background: rgb(67 56 202); color: #fff; } -.qms-display__active-label { +.qms-display__ticket:not(.qms-display__ticket--newest) .qms-display__badge { background: rgb(241 245 249); - color: rgb(100 116 139); + color: rgb(71 85 105); } .qms-display__queue-name { min-width: 0; - overflow-wrap: anywhere; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; text-align: right; - font-size: clamp(0.75rem, 1.2vw, 1.05rem); + font-size: clamp(0.75rem, 1.1vw, 1rem); font-weight: 700; line-height: 1.15; color: rgb(51 65 85); } .qms-display__ticket-number { - align-self: center; - padding: clamp(0.45rem, 1.5vh, 1.25rem) 0; + display: flex; + min-height: 0; + flex: 1 1 auto; + align-items: center; + justify-content: center; + margin: 0; + overflow: hidden; text-align: center; - font-size: clamp(3rem, 8vw, 8.5rem); + font-size: clamp(2.5rem, min(12cqh, 9cqw), 5.5rem); font-weight: 800; font-variant-numeric: tabular-nums; - letter-spacing: -0.055em; - line-height: 0.9; + letter-spacing: -0.045em; + line-height: 1; color: rgb(30 41 59); } @@ -573,51 +578,53 @@ html:has(.qms-display) body { } .qms-display__destination { + flex-shrink: 0; min-width: 0; border-top: 1px solid rgb(226 232 240); - padding-top: clamp(0.6rem, 1.4vh, 1rem); + padding-top: clamp(0.55rem, 1.2vh, 0.9rem); text-align: center; } .qms-display__destination > span { display: block; - margin-bottom: 0.2rem; - font-size: clamp(0.65rem, 0.9vw, 0.82rem); + margin-bottom: 0.25rem; + font-size: clamp(0.65rem, 0.85vw, 0.8rem); font-weight: 700; letter-spacing: 0.1em; - line-height: 1.15; + line-height: 1.2; text-transform: uppercase; color: rgb(100 116 139); } .qms-display__counter-name { + margin: 0; max-width: 100%; overflow-wrap: anywhere; - font-size: clamp(1.25rem, 2.5vw, 2.75rem); + font-size: clamp(1.1rem, min(4.5cqh, 3.2cqw), 2rem); font-weight: 800; - line-height: 1.05; + line-height: 1.15; letter-spacing: -0.02em; color: rgb(15 23 42); } .qms-display__tickets[data-ticket-count="1"] .qms-display__ticket-body { - padding-inline: clamp(2rem, 8vw, 10rem); + padding-inline: clamp(1.5rem, 6vw, 6rem); } .qms-display__tickets[data-ticket-count="1"] .qms-display__ticket-number { - font-size: clamp(5rem, 18vw, 15rem); + font-size: clamp(4rem, min(28cqh, 18cqw), 12rem); } .qms-display__tickets[data-ticket-count="1"] .qms-display__counter-name { - font-size: clamp(2rem, 5vw, 5rem); + font-size: clamp(1.5rem, min(7cqh, 5cqw), 3.5rem); } .qms-display__tickets[data-ticket-count="2"] .qms-display__ticket-number { - font-size: clamp(4rem, 12vw, 11rem); + font-size: clamp(3.25rem, min(18cqh, 12cqw), 8rem); } .qms-display__tickets[data-ticket-count="2"] .qms-display__counter-name { - font-size: clamp(1.5rem, 3.5vw, 3.75rem); + font-size: clamp(1.25rem, min(5.5cqh, 3.8cqw), 2.5rem); } @media (max-width: 767px) { @@ -638,14 +645,27 @@ html:has(.qms-display) body { .qms-display__tickets, .qms-display__tickets[data-ticket-count] { grid-template-columns: minmax(0, 1fr); - grid-auto-rows: minmax(14rem, auto); + grid-auto-rows: minmax(13rem, auto); overflow: visible; } + .qms-display__ticket { + container-type: inline-size; + min-height: 13rem; + } + .qms-display__ticket-number, .qms-display__tickets[data-ticket-count="1"] .qms-display__ticket-number, .qms-display__tickets[data-ticket-count="2"] .qms-display__ticket-number { - font-size: clamp(4.5rem, 26vw, 8rem); + flex: 0 0 auto; + padding-block: 0.75rem; + font-size: clamp(3.25rem, 18vw, 5.5rem); + } + + .qms-display__counter-name, + .qms-display__tickets[data-ticket-count="1"] .qms-display__counter-name, + .qms-display__tickets[data-ticket-count="2"] .qms-display__counter-name { + font-size: clamp(1.15rem, 5vw, 1.75rem); } } @@ -677,15 +697,16 @@ html:has(.qms-display) body { } .qms-display__ticket-body { - padding-block: 0.65rem; + padding-block: 0.55rem; + gap: 0.35rem; } .qms-display__ticket-number { - font-size: clamp(2.75rem, 12vh, 5rem); + font-size: clamp(2.25rem, min(14cqh, 8cqw), 4rem); } .qms-display__counter-name { - font-size: clamp(1.1rem, 4vh, 1.75rem); + font-size: clamp(1rem, min(5cqh, 3cqw), 1.5rem); } } diff --git a/resources/views/qms/display/public.blade.php b/resources/views/qms/display/public.blade.php index e464c73..ca47d10 100644 --- a/resources/views/qms/display/public.blade.php +++ b/resources/views/qms/display/public.blade.php @@ -90,8 +90,7 @@
- Now serving - Currently serving + Serving

diff --git a/tests/Feature/DisplayVoiceTest.php b/tests/Feature/DisplayVoiceTest.php index 94283d1..4dd3c1c 100644 --- a/tests/Feature/DisplayVoiceTest.php +++ b/tests/Feature/DisplayVoiceTest.php @@ -99,11 +99,11 @@ class DisplayVoiceTest extends TestCase ->assertSee('Counter 1'); } - public function test_display_returns_enough_active_tickets_for_a_busy_grid(): void + public function test_display_shows_only_latest_ticket_per_counter(): void { [$user, , $branch, $queue] = $this->setUpOrg(); - $counter = Counter::create([ + $room12 = Counter::create([ 'owner_ref' => $user->public_id, 'organization_id' => $queue->organization_id, 'branch_id' => $queue->branch_id, @@ -111,6 +111,14 @@ class DisplayVoiceTest extends TestCase 'status' => 'available', 'is_active' => true, ]); + $room14 = Counter::create([ + 'owner_ref' => $user->public_id, + 'organization_id' => $queue->organization_id, + 'branch_id' => $queue->branch_id, + 'name' => 'Room 14', + 'status' => 'available', + 'is_active' => true, + ]); $screen = DisplayScreen::create([ 'owner_ref' => $user->public_id, @@ -122,27 +130,33 @@ class DisplayVoiceTest extends TestCase 'is_active' => true, ]); - foreach (range(1, 7) as $number) { + foreach ([ + ['A001', $room12->id, 6], + ['A002', $room12->id, 5], + ['A003', $room12->id, 2], + ['A004', $room14->id, 4], + ['A005', $room14->id, 1], + ] as [$number, $counterId, $minutesAgo]) { Ticket::create([ 'owner_ref' => $user->public_id, 'organization_id' => $queue->organization_id, 'branch_id' => $branch->id, 'service_queue_id' => $queue->id, - 'counter_id' => $counter->id, - 'ticket_number' => sprintf('A%03d', $number), + 'counter_id' => $counterId, + 'ticket_number' => $number, 'status' => 'called', - 'issued_at' => now()->subMinutes(20 - $number), - 'called_at' => now()->subMinutes(7 - $number), + 'issued_at' => now()->subMinutes(20), + 'called_at' => now()->subMinutes($minutesAgo), ]); } $this->getJson(route('qms.display.data', $screen->access_token)) ->assertOk() - ->assertJsonCount(7, 'now_serving') - ->assertJsonPath('now_serving.0.ticket_number', 'A007') - ->assertJsonPath('now_serving.0.queue_name', 'Reception') - ->assertJsonPath('now_serving.0.counter', 'Room 12') - ->assertJsonPath('now_serving.6.ticket_number', 'A001'); + ->assertJsonCount(2, 'now_serving') + ->assertJsonPath('now_serving.0.ticket_number', 'A005') + ->assertJsonPath('now_serving.0.counter', 'Room 14') + ->assertJsonPath('now_serving.1.ticket_number', 'A003') + ->assertJsonPath('now_serving.1.counter', 'Room 12'); } public function test_display_falls_back_to_branch_queues_when_none_assigned(): void