c492becbdbc6f480fc0f494800a3444d681f0e30
The operator, 11:46 PDT 2026-09-10, having just had the device picker
converted: "are there other places we can search users where we're not
using the chips? please go through and check." The lead's audit found ten
person searches on the platform — four drawing chips, six not — and this
is central-ticketing's.
`ticketing.users.search` answered `DB::table('users')->get(['id', 'name',
'email'])`, and `show()` handed the Participants card the same three
columns for the submitter and for everybody already on the ticket. So the
one card on this page that is entirely about WHO SOMEBODY IS drew three
text rows in three different shapes: a name over an address in the
dropdown, a name-or-address string per participant, and a third spelling
for the submitter. An agent adding a participant to a ticket about a
child chooses between the child and the parent of the same surname with
no role dot to tell them apart.
And it was worse than untidy. Name and email were the SHIPPED DEFAULT for
a chip, so a school that configured the student chip to grade + homeroom
kept being shown an address here — on the one people surface the
operator's own setting could not reach. That is the defect #871 exists to
remove, and this endpoint was still carrying it.
Both halves now go through `PersonChip::rows()`, so what a row SAYS is
the setting rather than a select list in this file, and the person PICKED
is byte-for-byte the person picked FROM — one serializer, one answer. The
search's payload is now the same, key for key, as
`admin.settings.users.search`; the test compares the two endpoints against
each other rather than against a literal list, because a literal passes on
the day it is written and then lets this surface fall behind the next key
the shared row grows.
`PersonChip::columns()` is the select list, not `['id', 'name', 'email']`:
a chip line may name any user column, and a constrained select hands the
helper a model whose attribute is simply ABSENT — which it cannot tell
from a person who has no value, so the line goes blank for everybody with
nothing failing.
`rows()` and NOT the model, in `show()`. That map also feeds the message
bylines and the assignee, so handing the page a `User` would put every
column of `users` — `preferred_pin`, `preferred_password`,
`date_of_birth` — in front of any agent who opens a ticket. `rows()` is
`{id, name, email, person}` and `person` is the curated allow-list. There
is a test that a PIN cannot reach the page.
WHO IS SEARCHABLE DOES NOT CHANGE, per the card. Same two-character
floor, same name-or-email `like`, same limit of 10, same exclusion of the
people already on the ticket, same agent gate; `User` carries no global
scope or soft delete `DB::table('users')` did not. The name-or-email
clause stays inside its own closure on purpose — flattened beside the
exclusion it is `(name) OR (email AND NOT IN)` by SQL's own precedence,
so a name match jumps the exclusion, which is the hole the same conversion
opened on [central-inventory] #1594. `orderBy('name')` is the one visible
difference and it is new: the old query had no order at all, so the same
two letters could list the same ten people differently twice running.
The Vue half, the source guard and the feature tests are on
dashboard-shell's branch of the same name.
dashboard-ticketing
Description
Languages
PHP
100%