{{-- Edit Role — docs/wireframes-v12/edit-role.html (the info box at lines 112-116 and the card at 118-354), minus everything CR-14 §0 removes. ## Shape An `.info-box` above ONE `.card`. Inside the card, a `.form-section` per heading — each with its `.form-section-title` and the wireframe's own icon — and a `.card-footer` at the bottom. Fields are laid out by v12's `.form-row.form-row-2` grids, not by Bootstrap columns, which is why no field carries a `group-class`: the row owns the columns and `.form-group` owns its own margin. ## The twin rule (recipe §9) This file and roles/create.blade.php are twins. Every field is written out in both, and neither includes a shared form partial. **A field added, removed or renamed here must be mirrored in create.blade.php in the same commit**, and the permission matrix is one field for that purpose even though it draws thirty-eight boxes — it is generated from App\Support\RolePermissionMatrix on both screens, so a module added there appears on both without either file changing. Both twins are in tests/Feature/TwinViewParityTest.php ('roles' dataset row), which fetches the two screens over real authenticated GETs and compares their rendered `name` sets and required-attribute sets. What that test does NOT cover, and what therefore stays this docblock's job: label wording, field order and validation-rule drift. ### What this screen has that its twin does not * the action (`roles.update`) and the method (PUT, not POST); * the submit label — "Update Role", where the twin says "Save Role"; * the rule source — UpdateRoleRequest, whose unique rule ignores the row being edited and whose after() refuses the Super Admin role; * the header wording and the breadcrumb's last crumb; * **the info box** (edit-role.html:112-116), carrying the REAL number of people the save will affect where the wireframe hardcodes "4 users"; * **prefills** — the role's name, its system flag and its DIRECT permissions, where the twin is handed a fresh model and an empty list. ## This screen can never show the Super Admin role (decision D1) RoleController::edit() answers 403 for it before anything here renders, the listing draws no pencil on its row, UpdateRoleRequest refuses a post naming it, and RoleService refuses it again against a fresh locked row. Four walls, because the role behind them passes every permission check in the application through Gate::before. ## Deliberate differences from the wireframe Identical to the twin's, which is the point of a twin — see roles/create.blade.php's header for the full list with its wireframe line numbers: Role Code, Description, the Status toggle and the whole Additional Permissions section are removed (§0.3), the matrix draws the client's ten modules rather than the wireframe's seventeen and has no Export column (§0.3, §0.5), Booking and Cancellation render an empty Delete cell (D7), and there are no select-all controls because v12 draws none. --}} @extends('layouts.app') @section('title', __('roles.edit_title')) @section('breadcrumbs') @endsection @section('content') @php /* * The checkbox states, resolved the same way on both twins. * * form_old_array() and not form_old(): a matrix with nothing ticked * posts no `permissions` key at all, and resolving that absence through * the ordinary fallback would re-tick everything the user just cleared * on a failed submit. Once any old input exists, a missing key means * empty — which on THIS screen is the difference between "I revoked * everything" and "put the role's stored grants back". * * `is_system` is a scalar and takes the ordinary fallback: the * FormRequest's prepareForValidation() merges it as a real boolean * before validation, so a rejected submit flashes `false` rather than * nothing, and the box comes back the way the user left it. */ $checked = form_selected_values(form_old_array('permissions', $granted)); $isSystem = (bool) form_old('is_system', $role->is_system); @endphp {{-- edit-role.html:112-116. The count is the role's REAL `users_count` (loaded by the controller), pluralised with Laravel's | separator so one holder does not read as "1 users". It counts live accounts only — the users relation runs through App\Models\User's soft-delete scope — which is the same number the listing draws and the same number the delete guard refuses on. --}}

{{ trans_choice('roles.edit_notice', $role->users_count, ['count' => $role->users_count]) }}

{{ __('roles.sections.information') }}
{{-- `name` is excluded from the client rule set — its `unique` would render as a REMOTE check — so its `required` is re-armed from the attributes below, read by nrems-client-rules.js. The message is the SERVER's own, from the lang layer with the same displayable attribute the FormRequest uses, so the two sides say one sentence. Renaming is allowed for every role that reaches this screen (decision D11): people hold a role by id, so the holders are unaffected, and UpdateRoleRequest's unique rule ignores this row. --}} {{-- The Role Code slot, kept empty so the row stays two tracks wide and Role Name keeps its half. --}}
{{-- v12 draws this as a bare checkbox and a label side by side with the helper line under the pair, and no `.form-label` above it (edit-role.html:143-149). with no label is exactly that shape, and it brings the hint line and the inline error with it. The control is hand-written rather than an because the library ships no single-checkbox component: checkbox-group renders a grid of boxes posting one array name, which is a different control. It stays editable on this screen. `is_system` blocks DELETION and nothing else (decision D1's weaker half), so an administrator may promote or demote a role without the flag ever becoming a one-way door. --}}
{{-- The Status slot, kept empty for the same reason as the Role Code one above. --}}
{{ __('roles.sections.permissions') }}
{{-- `table-wrapper` is v12's own scroll box; `table-responsive` rides along because it is Bootstrap's name for the same thing and the one the rest of this application uses. The grid is ten rows and five columns, so it scrolls sideways on a phone rather than compressing the module names — which is why `.rl-permission-table tbody td:first-child` is `nowrap`. The table is written out here and again in the twin rather than pulled into a shared partial: recipe §9's twin rule is that both form files spell out every field, with no shared form partial, and this grid IS the second field on the screen. What keeps the duplication honest is that neither copy contains a list — both iterate $modules, which the controller reads from App\Support\RolePermissionMatrix, so a module added there appears on both screens with neither file changing. --}}
@foreach (\App\Support\RolePermissionMatrix::ACTIONS as $action) @endforeach @foreach ($modules as $module) @foreach ($module['cells'] as $action => $permission) {{-- A null cell renders EMPTY and keeps its @endforeach @endforeach
{{ __('roles.matrix.columns.module') }}{{ __('roles.matrix.columns.'.$action) }}
{{ __('roles.matrix.modules.'.$module['key']) }} (decision D7): Booking and Cancellation have no Delete checkbox, and dropping the cell would shift every column after it out of line. --}} @if ($permission !== null) {{-- The two data- attributes are how nrems-role-form.js finds a box. It never parses the label beside it, which would have made the auto-check rules one translation away from silently not firing. aria-label because the visible label for these boxes is the column header plus the row heading, and a screen reader announcing "checkbox" thirty- eight times is not a form. --}} @endif
{{-- v12's card footer: Cancel as the outline button, then the primary action, both right-aligned inside the card's bottom rule. `.card-footer` is already the flex row, so no wrapper of its own. --}}
{{-- Every key of this request is excluded, so the generated rule set is empty and what this component still does is BIND a validator to the form — which is what nrems-client-rules.js arms `required` onto, and what NREMS.Forms.bindValidationErrorPlacement needs to exist. See StoreRoleRequest::clientRuleExclusions() (inherited) for why each of the four cannot be rendered honestly in a browser. --}} @push('scripts') {{-- Re-arms the safe client rules the exclusion list dropped, from a closed list of rule names that can never include `remote`. Pushed AFTER above, which is what makes it work: jsvalidation's generated script registers its `.validate()` call on jQuery's ready queue first, so the callback this file registers runs after it and finds a validator to add to. --}} {{-- The matrix's two auto-check invariants (decision D5, D6). A convenience only: RolePermissionMatrix::normalize() applies the same rules to the posted set on every save. --}} @endpush @endsection