{{-- Row actions for the Customer Documents listing (CR-15 decision D8) — docs/wireframes-v12/upload-document.html's `.action-group` of `.btn-icon`s, reduced to the three this module has. v12 draws four: View, Verify, Reject and Download. **Verify and Reject go with the Status column** the client removed (decision D6) — they are a verification workflow this application does not have and `customer_docs` has no column for — and **View goes with `view-customer-document.html`**, a page the wireframe pack links to and the client's own column list gives nothing to show: the row already names the type, the date and the uploader, and a fifth screen nobody asked for is not a substitute for the file itself. What is left is Download, plus **Edit and Delete**, which the client asked for by name ("user can download document, edit and delete in action column"). ## The cell is rebuilt client-side, and this is its opening state The two dropdowns above choose which document the row is about, so the actions change with them. public/assets/js/nrems-customer-document-list.js rebuilds this group from the row payload with createElement/textContent — never innerHTML — and takes every url from the payload verbatim rather than assembling one from an id. What is drawn here is the state for the document the row opens on, server-rendered for the reason partials/doc-type.blade.php records: no `change` event fires on a freshly drawn page. ## Where each action goes * **Download** streams the file back as an attachment through the EXISTING `bookings.documents.download` route (CR-15 §4.1). A link and not a form, because it reads and nothing else; the route binding is scoped to the booking, so a document id from another booking is a 404 from the router. * **Edit** is this module's own screen. * **Delete** is a plain button that opens the shared confirmation dialog (customer-documents/partials/delete-modal.blade.php) and, on Confirm, sends an ajax DELETE to the EXISTING `bookings.documents.destroy` route — never a link, because a GET that destroys data would be fired by any crawler or prefetch. It was a `
` with a `confirm()` guard until CR-15.3 item H2. The route, the gate and the controller method are unchanged; only the answer shape is new (`ajax() && wantsJson()`), so the listing never reloads and the deleted document leaves the row payload instead. Both reused routes now carry this module's gates (decision D3), so there is one streaming path and one delete path in the application. ## Permission Each control is drawn only when its url is in the payload, and a url is in the payload only when the viewer holds the matching ability (CustomerDocumentsDataTable::payloadFor()). Gating on the payload rather than with a second `@can` here is what makes the client-rendered cell and the server-rendered one obey the same rule — the module cannot draw an icon it has no address for, and there is no address to recover from the DOM. Rendered through rawColumns(['actions']), so this output is injected unescaped. The only user-controlled string on it is the document's original filename, and since CR-15.3 it no longer reaches a Javascript context at all: it rides in the delete button's `data-name` ATTRIBUTE, html-escaped by `{{ }}` like everything else here, and the dialog writes it to the screen with `textContent`. That retires the `Js::from()` this partial used to need — `{{ }}` escapes for HTML and not for Javascript, and commit 3492ec1 is the stored XSS that proved the difference — because there is no longer a script literal for a filename to be parsed inside of. Params: document one payload `docs` entry, or null for the "No Document" state --}} @php $urls = is_array($document['urls'] ?? null) ? $document['urls'] : []; $name = is_string($document['name'] ?? null) ? $document['name'] : ''; @endphp @if ($document === null || $urls === []) {{-- Decision D5: no document selected, or nothing this viewer may do to it. The em-dash is this application's blank (recipe §9). --}} — @else
@isset($urls['download']) @endisset @isset($urls['edit']) @endisset @isset($urls['delete']) {{-- A plain button since CR-15.3 item H2: the shared dialog (customer-documents/partials/delete-modal.blade.php) confirms and nrems-document-delete.js sends the DELETE by ajax, so the listing never reloads. nrems-customer-document-list.js builds the identical element when it re-renders this cell for another document, which is why the three data attributes are the whole contract between the two. --}} @endisset
@endif