{{-- Edit User — docs/wireframes-v12/edit-user.html (the card at lines 76-318), minus everything CR-13 §0 removes. ## The twin rule (recipe §9) This file and users/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.** These twins are deliberately NOT in TwinViewParityTest — create.blade.php's header explains why at length, and the short version is that the client's own password rule (§0.6) makes the two screens' required sets differ by design, which is exactly what that test fails on. The docblock rule above is the guardrail this pair gets instead. ### What this screen has that its twin does not * the action (`users.update`) and the method (PUT, not POST); * the submit label — "Update User", where the twin says "Save User"; * the rule source — UpdateUserRequest, whose email rule ignores this row and whose password rules are `nullable`; * the header wording (it names the user) and the breadcrumb's last crumb; * **optional Password and Confirm Password**, with the BA's own "Leave blank to keep current password" placeholder in place of the twin's "Enter password"; * the read-only **Account Activity** section, which has nothing to show on a user who does not exist yet; * the footer's **Deactivate / Activate** button, for the same reason; * a Role select that may hold ONE option — see the note on that field. ## Deliberate differences from the wireframe * **The Username field is gone** (client, §0.3; edit-user.html:119-122). v12 draws it readonly with a grey ground; there is no username column to show, and login is by email. * **The "Reset Password" button is gone** (decision D2; edit-user.html:125). The client's own rule — blank keeps the current password, filled changes it — makes the two inline fields the mechanism, and a separate reset flow would have been a second writer of the same column. The self-service forgot-password flow is untouched and is still how a user resets their own. * **The Permissions section is gone** (client, §0.4; lines 159-250) and **Additional Settings** with it (client, §0.5; lines 252-280). * **Account Activity loses its Failed Login Attempts row** (decision D4; line 304-307): nothing counts them. The row is not replaced — the second row of that section runs one detail item and two empty cells, exactly as v12's own second row does with its third cell. * **Total Logins is counted from the activity log**, not from a counter column (decision D4). It therefore counts logins since the audit trail began rather than since the account was created; the wireframe's "482" is a mock-up either way. * **The Role select lists the REAL spatie roles** (decision D3), narrowed by the two guards a screen can answer for free — see its own note below. ## Why the status button is a form of its own, declared outside the card `
` inside `` is not markup a browser keeps: the inner one is dropped at parse time, and the Deactivate button would have submitted the EDIT form instead — saving the profile while claiming to change a status. So the status form is declared at page level, below the edit form, and the footer's button reaches it with HTML5's `form="…"` attribute. That is the CR-10 pattern (resources/views/inquiries/partials/task-action.blade.php), the feature exists for exactly this case, and it is in every browser this application supports. --}} @extends('layouts.app') @section('title', __('users.edit_title', ['name' => $user->name])) @section('breadcrumbs') @endsection @section('content') @php $dateFormat = (string) config('nrems.date_format'); $actor = auth()->user(); $actor = $actor instanceof \App\Models\User ? $actor : null; // Guard §4.6.1 + §4.6.2b, the same predicate the listing's row actions // and UserController::status() use. Hiding the button is only the // visible half; the controller answers 403 on the same condition. $canToggleStatus = $user->isDestructibleBy($actor); $activating = $user->status === \App\Enums\UserStatus::Inactive; $statusFormId = 'nrems_user_status_form'; @endphp
{{ __('users.sections.personal') }}
{{-- The excluded-field markers, identical to the twin's — see its note. `unique` (here, `unique` minus this row) is a REMOTE rule, so the whole field is off the client set and `required` + `email` come back from these attributes (CR-13.1 B1). --}}
{{ __('users.sections.account') }}
{{-- The client's edit-password rule, made visible: no asterisk on either label, and a placeholder that says what leaving them alone does. UpdateUserRequest's `nullable` is the server half, and UserController::update() drops a blank value before the service can see it — so the stored hash and `password_changed_at` both stay put. Never carries a value, and cannot: Laravel's FormRequest never flashes `password` or `password_confirmation` into old input. **Password carries no `data-validate-rules` here**, and the absence is the point: it is optional on this screen, so there is no client rule to re-arm for it. The confirmation carries `equalTo` and nothing else — an empty pair is equal and passes, a filled password with a blank or different confirm fails ON THIS FIELD, which is exactly the pair of rules UpdateUserRequest::passwordRules() applies server-side (CR-13.1 B1/B2). --}}
{{-- The Username slot, kept empty so the row stays three tracks wide — v12 draws an empty `.form-group` in the third cell of this very row (edit-user.html:127) for the same reason. --}}
{{-- $roleOptions is narrowed by UserController::roleOptions(): a user editing THEMSELVES is offered only the role they already hold (guard §4.6.1, so the select cannot promote or demote them), and an actor who is not a Super Admin is never offered that role (guard §4.6.2a). Both are re-checked server-side, because an option list is client input. --}}
{{-- v12's read-only Account Activity block: `.detail-item` pairs laid out on the same `.form-row.form-row-3` grid the fields above use, with the second row nudged down 16px exactly as the wireframe does it (`.usr-activity-row`, app-additions tail). Every value is real and every blank is the em-dash. --}}
{{ __('users.sections.activity') }}
{{ __('users.activity.created_at') }} {{ $user->created_at?->format($dateFormat) ?? '—' }}
{{ __('users.activity.last_login') }} {{ $user->last_login_at?->format($dateFormat.', g:i A') ?? '—' }}
{{ __('users.activity.total_logins') }} {{ $totalLogins }}
{{ __('users.activity.password_changed_at') }} {{ $user->password_changed_at?->format($dateFormat) ?? '—' }}
{{-- v12 puts Failed Login Attempts in the second cell and leaves the third empty. Nothing counts failed attempts per account (decision D4), so the cell is empty too rather than filled with a zero this application cannot stand behind. --}}
@can('users.edit') @if ($canToggleStatus) {{-- Page level, so no can contain it. It carries no fields at all — the action is a toggle and the server reads the current status — so `:novalidate="false"` is the honest description of a form with nothing to validate. The confirmation goes through Js::from(), which emits the user's name as a JSON string literal: `{{ }}` escapes for HTML and not for Javascript. --}} @endif @endcan {{-- The same three exclusions the Add twin passes, by the same name: it is a static on StoreUserRequest and is inherited here. See StoreUserRequest::clientRuleExclusions(), and the `data-validate-*` attributes above for what CR-13.1 B1 re-arms in their place — a shorter list than the twin's, because password is optional on this screen. --}} @push('scripts') {{-- Pushed after for the ordering reason the twin states: the validator has to exist before anything can add to it. --}} @endpush @endsection