{{-- Add New Role — docs/wireframes-v12/add-role.html (the card at lines 114-350), minus everything CR-14 §0 removes. ## Shape ONE `.card`. Inside it, 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/edit.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 edit.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.store`) and the method (POST, not PUT); * the submit label — "Save Role", where the twin says "Update Role"; * the rule source — StoreRoleRequest, where the twin names UpdateRoleRequest (whose unique rule ignores the row being edited and which refuses the Super Admin role in after()); * the header wording and the breadcrumb's last crumb; * **no info box above the card** — the twin draws the wireframe's "This role is assigned to N users" strip, and a role that does not exist yet is assigned to nobody. Everything else is identical, prefills included: `$role`, `$modules` and `$granted` are the same three keys on both screens, and this one is handed a fresh model and an empty grant list rather than a different shape. ## Deliberate differences from the wireframe * **The Role Code field is gone** (client instruction, CR-14 §0.3; add-role.html:123-127). There is no code column on `roles` and no sequence to generate one from; the wireframe's own helper text calls it auto-generated-and-editable, which is two behaviours this module does not have. Its slot in the row is kept EMPTY so Role Name stays half a row wide rather than stretching to full, exactly as the users twins keep the removed Username slot. * **The Description textarea is gone** (§0.3; :129-132). No column. * **The Status toggle is gone** (§0.3; :141-149), and so are the three `.toggle-*` rules that styled it — see the CR-14 block in nrems-wireframe.css. A role has no status: it is held or it is not. * **The whole Additional Permissions section is gone** (§0.3; :310-343). Its seven checkboxes — Approve Bookings, Generate Reports, Bulk Import/Export, System Configuration, Audit Log Access … — name features this application does not have, and a checkbox that grants nothing is furniture. * **The matrix draws the client's TEN modules, not the wireframe's seventeen** (§0.5), and **its Export column is gone** (§0.3). See App\Support\RolePermissionMatrix, which owns the list. * **Booking and Cancellation have no Delete checkbox** (decision D7). The cells still render, empty, so the grid stays aligned. * **There are no "select all" controls**, on a row or on a column — the wireframe draws none, and none is invented. --}} @extends('layouts.app') @section('title', __('roles.create_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. * * `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
{{ __('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. --}} {{-- 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 (add-role.html:134-140). 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. --}}
{{-- 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() 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