# CR-16 — Ajax delete with popup across seven listings

Status: **specified 2026-09-08** — implementation assigned to an Opus agent; BA review to
follow. Generalizes CR-15.3's document-delete pattern (see
`docs/customer-documents-requirements.md` §8) to the seven module listings the client
named.

## §0 Client-mandated overrides — do NOT "fix" back

1. **No new test files, no policies.** Existing pinned tests may be minimally and
   honestly updated where they pin the old markup (§4 lists them); nothing else.
2. **No lang file for the new success/error messages** (client verbatim: "Please don't
   use language file for success or error messsage"). The JSON arm's success message and
   the shared partial's strings are inline. Existing lang keys already in the codebase
   (confirm sentences, guard refusal sentences, the redirect arms' flashes) are NOT
   migrated or duplicated into lang — see J6/J7 for exactly what is reused vs written
   inline.

## §1 Client instruction (verbatim, 2026-09-08)

> When delete Projects & Units, Inquiry, User Management, Role Management, Broker
> Management, Booking Types and Customer Document Types show popup instead of alert for
> confirmation and confirm then delete the entry from database using ajax. pass delete
> route url in Delete button data-url and use that url when click on confirm button. if
> you will use this logic then you can create common ajax request and popup for all
> modules. once delete just draw datatable so same filter and pagination will not
> change. once delete show success message as a toaster message getting error then show
> error toaster.

## §2 Decisions (J-list)

- **J1 — Scope = the seven listings' delete controls, nothing else.** Recon (2026-09-08)
  confirmed each of the seven has exactly one delete control, on the index page only:
  projects / inquiries / users / roles / brokers / booking_types / customer_doc_types
  `partials/actions.blade.php`. Units, wings, floor plans, unit types have NO delete
  controls anywhere (CR-5 §15.1 removed them) — nothing to convert for "& Units". The
  users STATUS-toggle confirm() (3 sites) is NOT a delete and stays as-is. The document
  surfaces (CR-15.3) already work and are untouched.
- **J2 — One shared trigger shape.** Each actions partial replaces its
  `<x-form.open method="DELETE" onsubmit="confirm(...)">` + submit button with a plain
  `<button type="button">` carrying: `data-role="ajax-delete"`, `data-url` =
  `route('<module>.destroy', $model)` (server-rendered, verbatim — client instruction),
  `data-id` = the model key, and `data-confirm` = the module's EXISTING confirm sentence
  (`__('<module>.delete_confirm', [...])` — reusing the lang sentence the old confirm()
  showed; these are confirmation texts, not success/error messages, so §0.2 does not
  forbid them, and they carry module-specific warnings worth keeping: "Its wings, floor
  plans and units are removed with it", "Its whole follow up history is removed with
  it"). Same icon/classes as today (`.btn-icon text-danger`, or the masters'
  `.btn-icon-sm text-danger` — each partial keeps its own wrapper classes). All existing
  GATES are preserved exactly (`@can('delete', $model)`, `@can('users.delete')` +
  `$destructible`, the roles `$locked/$showDelete` expressions, `@can('brokers.delete')`).
- **J3 — One shared popup + request module.** New
  `public/assets/js/nrems-ajax-delete.js`, modeled on `nrems-document-delete.js`'s
  contracts (delegated document-level click on `[data-role="ajax-delete"]`, refuse to
  open without url/modal/CSRF meta, single-flight `pending`, fetch DELETE with
  `X-CSRF-TOKEN` + `X-Requested-With: XMLHttpRequest` + `Accept: application/json`,
  `credentials: 'same-origin'`). The modal BODY renders the clicked button's
  `data-confirm` sentence via `textContent` (one `<span data-role="...">`, the CR-15.3
  isolation pattern); title verbatim **"Are you sure you want to delete?"**; buttons
  Cancel / Confirm. It does NOT touch rows or payloads: post-success behavior is J4.
  `nrems-document-delete.js` stays separate (its post-success behavior is DOM surgery,
  not a redraw) — do not merge them; state the boundary in both headers.
- **J4 — Post-success = redraw the DataTable, filters and page kept.** On a 2xx JSON
  answer: success toaster (server `message` wins, fallback inline string from the
  partial), then `window.LaravelDataTables[tableId].ajax.reload(null, false)` — the
  second argument `false` is what keeps the pagination position; the filter values live
  in the page's own selects and ride the existing preXhr wiring, so they persist by
  construction. `tableId` comes from `button.closest('table').id` (every one of the
  seven tables carries a `kt_*_table` id and yajra's `scripts()` registers each instance
  in that registry — recon §8 confirmed this holds on the two master pages that have no
  inline script of their own). If the registry or table is missing, fall back to a full
  `window.location.reload()` — worse UX but never a silently-stale table; say so in a
  comment.
- **J5 — Failure = error toaster, nothing removed, no redraw.** Non-OK answers: prefer
  the JSON body's `message` when the response carries one (a 422 guard refusal — J7),
  else the partial's inline generic failure string. Network throw → same generic string.
  Reuse `NREMS.Toaster.show` (CR-15.3) exactly as-is.
- **J6 — Server: same destroy() methods, new JSON arm; success messages INLINE.** Each
  of the seven `destroy()` methods gains a `Request` param and the house
  `ajax() && wantsJson()` arm (both halves — CR-15.3 records why) answering
  `{"deleted": <id>, "message": <inline string>}` on success. The inline success
  messages (per §0.2, NOT lang): "Project deleted." / "Inquiry deleted." /
  "User deleted." / "Role deleted." / "Broker deleted." / "Booking type deleted." /
  "Document type deleted." The redirect arms stay byte-identical (existing non-ajax
  behavior, existing lang flashes — they are dead to this UI but honest to any other
  caller, and several CRUD tests pin them; §4).
- **J7 — Server refusals surface their own sentence.** Where a guard refuses with a
  message today, the ajax arm answers **422 JSON** `{"message": <the same sentence>}`:
  users (last-active-Super-Admin) and roles (Super Admin locked / system role / in use)
  catch their ValidationException and, in the ajax arm, return the exception's first
  message as 422 instead of `back()->with('error', ...)`; booking_types' in-use flash
  path likewise returns its `trans_choice` sentence as 422 JSON in the ajax arm. These
  sentences already exist in lang and are resolved at runtime through the guard — NO new
  lang keys are added and none is duplicated inline, which is the honest reading of
  §0.2 (the client forbade new lang usage for these messages, not the guards' existing
  wording). Plain 403s (policy/permission/`abort_unless`) need no arm — the JS turns any
  non-OK, message-less answer into the generic failure toaster.
- **J8 — One shared modal partial**, e.g.
  `resources/views/partials/ajax-delete-modal.blade.php`: root div with `data-modal`,
  `data-strings` (inline: generic failure sentence, fallback success sentence,
  "Dismiss"), the `<x-modal>` (title verbatim, Cancel/Confirm footer), and the two
  `@push('scripts')` lines (toaster `@once`, then `nrems-ajax-delete.js`, both defer).
  Included at the END of each of the seven index pages' content sections, never inside a
  `.card` (Bootstrap fixed-position clipping — the CR-15.3 rule). The CR-15.3 partial's
  structure is the template; strings are its own (this partial serves seven modules, so
  wording is generic: the per-module confirm sentence arrives per-row via
  `data-confirm`).
- **J9 — Users' `$destructible` and roles' `$showDelete` server truth is unchanged**:
  the ajax route hits the same `Gate::authorize` + `abort_unless` + service guards, so a
  hand-rolled DELETE gets exactly the answers it gets today (403/422), now also as JSON
  when it asks for JSON. No behavioral loosening anywhere.
- **J10 — Empty-page edge accepted**: deleting the last row of page N with
  `reload(null,false)` may land on an empty page (DataTables' own behavior). Accepted —
  the client asked for pagination to be preserved; the user pages back one step. Not
  worked around.

## §3 Per-module notes (recon 2026-09-08, verify in code)

| Module | destroy route | Guards beyond permission | Refusal shape today |
|---|---|---|---|
| Projects | `projects.destroy` | none (cascades units in tx) | 403 only |
| Inquiries | `inquiries.destroy` | none (cascades follow-ups in tx) | 403 only |
| Users | `users.destroy` | `isDestructibleBy` (403), last-active-Super-Admin (ValidationException→error flash) | 403 / error flash |
| Roles | `roles.destroy` | Super Admin locked / is_system / in-use (all ValidationException→error flash) | error flash |
| Brokers | `brokers.destroy` | none | 403 only |
| Booking Types | `booking_types.destroy` | in-use count under lockForUpdate (error flash, `trans_choice`) | error flash |
| Customer Doc Types | `customer_doc_types.destroy` | none (detaches pivot in tx) | 403 only |

## §4 Pinned tests — minimal HONEST updates only (quote before/after)

- `ProjectsDataTableTest` :475-511 — pins `value="DELETE"` (positive + negative twins).
  Re-pin on the new trigger (`data-url` + destroy route presence/absence); keep the XSS
  pin meaningful against the new markup.
- `InquiriesDataTableTest` :466-488 — pins the escaped confirm string and destroy route.
  Re-pin `data-confirm`'s escaped sentence (same lang key) + `data-url`.
- `BookingTypesDataTableTest` :294-357 — confirm-string, destroy-route, wrapper/button
  class pins (positive + negative). Re-pin equivalents.
- `CustomerDocTypesDataTableTest` :367-398 — destroy-route pins, both directions.
- CRUD tests pinning destroy REDIRECT+flash (`BookingTypeCrudTest` :343-412,
  `CustomerDocTypeCrudTest` :386-401, `InquiryCrudTest` :1060-1094) keep passing
  untouched — the redirect arm is byte-identical; do not edit them. `FormOpenTest`
  :181-214 tests the component, not a module — untouched.

## §5 Gates

Full `artisan test` (baseline 1112 / 1108 / 4 skips / 4313 — every delta explained by a
§4 edit), phpstan level 6 = 0, pint on the footprint, `node --check` on the new JS.
Probes on the REAL running app (the CR-15.3 lesson — real asset stack, not a fixture):
modal opens with the module's own sentence; cancel sends nothing; confirmed delete →
row gone after redraw with page + filters preserved (measure a page-2, filtered draw);
success toaster; roles in-use / users last-Super-Admin / booking-types in-use each
refuse with their own sentence in the error toaster and the row survives; non-ajax
DELETE still redirects with the old flash; XSS probe (a name like `');alert(1)//`)
inert in `data-confirm` and in the modal body. PHP tooling via
`C:\wamp64\bin\php\php8.3.28\php.exe`.
