{{--
Row actions for the Booking Types DataTable —
docs/wireframes-v12/booking-type.html (~line 144):
`.action-btns` and `.btn-icon-sm` are this page's own names, not the
projects listing's `.action-group` / `.btn-icon`; both pairs are declared in
the wireframe stylesheet and the wireframe uses this one here.
Rendered by BookingTypesDataTable through rawColumns(['actions']), **so this
output is injected into the page unescaped** and the rule
projects/partials/actions.blade.php states applies: nothing user-controlled
may reach it except through an escaped attribute or Js::from(). There are
exactly two such places and both are covered —
* `data-row`, one JSON attribute written with `{{ }}`, which escapes the
JSON for an HTML attribute. nrems-master-modals.js reads it with
JSON.parse and writes the name into the input through `.value`, so a
booking type called `` is a string on the way
in, a string in the attribute and a string in the field.
* the delete confirmation, through Js::from() — `{{ }}` escapes for HTML
and not for Javascript, and commit 3492ec1 is the stored XSS that
proved the difference.
What the payload holds, and why:
id which row a rejected edit belongs to (the hidden reopen_id).
name the Edit modal's prefill.
url where the Edit form should PUT. **Generated server-side** by
route(), so the client never builds a path out of an id — the
module's JS has no route table and cannot acquire one.
## Delete is a POST form, never a link
A GET that destroys data is triggered by any crawler or prefetch. The
confirm() guard is the wireframe-free option — no modal, no new JS — and is
the same affordance the projects listing uses, class for class apart from
the icon-button name this page draws.
The delete may still be refused on the server: a booking type in use by a
live customer doc type cannot be deleted (CR-7 §3.1). The button does not
try to know that — a listing cell cannot be trusted with an integrity rule,
and the controller answers with an error flash either way.
Every control is gated on its own ability, so a view-only holder of
`booking_types.view` gets an empty cell rather than two buttons that 403.
yajra hands the row's model in as $model.
--}}
@php
/** @var \App\Models\BookingType $model */
$row = [
'id' => $model->getKey(),
'name' => $model->name,
'url' => route('booking_types.update', $model),
];
// JSON_INVALID_UTF8_SUBSTITUTE, and JSON_THROW_ON_ERROR behind it.
//
// A name is bytes somebody typed. MySQL will hold an invalid UTF-8 sequence
// in a utf8mb4 column if it arrives as one, and json_encode's DEFAULT answer
// to that is to return `false` — which Blade renders as `data-row=""`, which
// JSON.parse rejects, which leaves the pencil silently dead with nothing on
// screen or in a log to say why. Silent is the worst of the three outcomes.
//
// SUBSTITUTE replaces the bad bytes with U+FFFD, so the dialog opens and the
// user sees a replacement character in a name that already had one. THROW is
// kept underneath it for every OTHER encoding failure (recursion, INF/NAN —
// none reachable from these three keys today), where a 500 naming the real
// fault beats a dead button. Chosen over a bare THROW because this partial
// renders once per row of a listing: one hostile byte in one name would
// otherwise take the whole page down rather than one row's edit button.
$rowJson = json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR);
@endphp
@can('update', $model)
@endcan
@can('delete', $model)
{{-- CR-16 item J2: a plain button, not a form with a confirm().
`data-url` is the destroy route rendered by route() — the client's
own instruction — and `data-confirm` is the SAME lang sentence the
confirm() showed, which the shared dialog writes with textContent.
The masters' own `.btn-icon-sm` is kept: each partial keeps its own
wrapper and icon classes. A type that is in use still refuses, now
as 422 JSON carrying its own `trans_choice` sentence (item J7). --}}
@endcan