{{-- Multi-step LDAP settings wizard. Four steps: 1. Connection (server URL, TLS, cert) 2. Authenticate & Scope (bind creds + base DN + filters) 3. Attribute Mapping (LDAP-attr → Snipe-IT-field) 4. Sync & Defaults (enable toggle, default group, forgot-pass URL) Layout order inside the box: box-header (title) box-body: step title + help text wizard progress indicator alerts (session flash + inline test result) step-specific field content box-footer: step-specific action buttons (Save & Continue, Back) A11y treatments: - Complete steps carry a checkmark icon in addition to color state so color-blind users have a non-color affordance (WCAG SC 1.4.1). - Every step button carries an sr-only "Step N of 5, {title}, {state}" so screen readers announce full progress context (WCAG SC 1.3.1 / 2.4.6). - Locked buttons reference #wizard-locked-note via aria-describedby so SR users hear WHY the button is disabled. - wire:confirm arms on step buttons only when $dirty is true so users don't lose in-progress edits by clicking around the header. - On step change we dispatch a browser event; the Alpine listener on the wizard panel focuses the new panel's fieldset so keyboard + SR users move with the visual context. --}}
{{-- Scoped dark-mode fix: for reasons I couldn't fully diagnose, .form-control inputs render white on dark inside this component while other forms elsewhere in the app read the theme correctly. Rather than reach for a global override that might collide with whatever mechanism the rest of the app uses, target only fields inside this wizard. Uses the theme's --box-bg + --color-fg so it matches whatever dark-mode palette the site is running. --}}

{{ $currentStep === 5 ? trans('admin/settings/general.ldap_wizard.done.title') : ($this->stepTitles[$currentStep] ?? '') }}

{{-- One-click disable when LDAP is currently on. Renders on every step (via the shared header) so admins never have to walk to step 4 to turn it off. Fires an AdminLTE .modal-danger red-header confirm rather than a browser native prompt so it matches the rest of the app's destructive-action UX. --}} @if ($snipeSettings->ldap_enabled == '1')
@endif
{{-- Step title + help text, always the current step's copy. --}} @php $stepHelpKey = match ($currentStep) { 1 => 'admin/settings/general.ldap_wizard.step_connection_help', 2 => 'admin/settings/general.ldap_wizard.step_authscope_help', 3 => 'admin/settings/general.ldap_wizard.step_mapping_help', 4 => 'admin/settings/general.ldap_wizard.step_sync_help', default => null, }; @endphp @if ($stepHelpKey) @endif @if ($isReadOnly) This is a demo. Every LDAP config field is read-only, but you can still enter a sample username on step 3 and use the Test Find User button to see the wizard search against the pre-seeded readonly directory. (You can search on tesla, einstein, or curie.) The wizard will not actually save any LDAP settings in this demo. @endif {{-- Wizard progress indicator. Same .bs-wizard class the quickstart setup layout + importer modal use. Flex + flex:1 on children rather than bootstrap col-md-*, so the layout stays uniform regardless of step count. --}}
@foreach ($this->stepTitles as $stepNum => $stepTitle) @php $state = $stepNum < $currentStep ? 'complete' : ($stepNum === $currentStep ? 'active' : 'disabled'); $reachable = $stepNum <= $highestStepReached; $srState = match ($state) { 'complete' => trans('admin/settings/general.ldap_wizard.state_complete'), 'active' => trans('admin/settings/general.ldap_wizard.state_current'), default => trans('admin/settings/general.ldap_wizard.state_locked'), }; @endphp
{{-- The circular indicator on the progress line is now itself a click target (matching the text-based click target above) --}}
@endforeach
{{-- Step-specific field content. Focusable wrapper so the Alpine listener can move focus here after a step change. Inline test-status alert renders INSIDE this panel (right above the fields it relates to) so a failed network test sits next to what the user needs to fix. Session flashes are intentionally NOT rendered here. Steps 1-3 advance in place (the wizard checkmark IS the success signal), and the step 4 / disableLdap paths redirect to a page that renders session flashes via the app layout. --}}
{{-- Panel-top alert is for INFRASTRUCTURE feedback (connect failures, TLS handshake, bind rejection). Step 3's lookup-success alert renders inside the well next to the search box instead, since it's scoped to what the user just searched. The check below suppresses it here for that case. --}} @if ($testStatus && ! ($currentStep === 3 && $testStatus === 'success')) {!! $testMessage !!} @endif @if ($currentStep === 1) @if ($is_ad) @endif @elseif ($currentStep === 2) {{-- Merged Authenticate + Scope step. Bind credentials appear next to the base DN so users composing a full admin DN can see the base DN portion right alongside. That was the friction that pushed us to combine what used to be two separate steps. --}} {{-- Placeholder swaps based on the step-1 AD flag: UPN form for AD, full DN form otherwise. --}} @elseif ($currentStep === 3) {{-- Single-column x-form.row layout. Only ldap_username_field + ldap_fname_field are required. Everything else is optional. The directory just skips syncing whatever attribute isn't mapped. Trans labels shortened to drop the "LDAP " prefix (the step title already provides that context). --}} {{-- Field descriptors come from $this->mappingFields (computed on the component). Placeholders swap between AD (samaccountname, streetaddress, etc.) and non-AD (uid, street, etc.) based on the is_ad flag from step 1. Fifth tuple element is an optional help-text trans key, only set on fields with per-field operational notes carried over from the legacy form. --}} @foreach ($this->mappingFields as [$propName, $labelKey, $placeholderExample, $required, $helpKey]) @endforeach {{-- Invert active flag lives in its own row below the grid. It's a checkbox, not a text input, and belongs conceptually with ldap_active_flag but doesn't visually fit the two-column layout. --}} {{-- Sample-lookup section, boxed in an x-well so it reads as a "try it" tool distinct from the field list above. Fires wire:click directly (not the Save & Continue flow) so users can iterate on the preview without triggering step advance. --}}

{{ trans('admin/settings/general.ldap_wizard.mapping.sample_username_label') }}

{!! trans('admin/settings/general.ldap_wizard.mapping.sample_username_help') !!}

{{-- Inline lookup errors (not_found, multiple_found, empty sample) render here, right below the search box where results would appear. --}} {{-- Lookup-success alert lives inside the well so it sits with the search box and the preview table it introduces. Infrastructure errors (connect/bind failures) still surface at the top of the panel because they suggest going back to earlier steps. --}} @if ($testStatus === 'success') {!! $testMessage !!} @endif {{-- Preview table populated by a successful lookup. Muted rows for unmapped fields or mapped attributes the entry doesn't carry. Makes it obvious where the mapping produces no data. --}} @if (! empty($step3TestAttributes))

{{ $step3TestDn }}

@foreach ($step3TestAttributes as $snipeField => $preview) @endforeach
{{ trans('admin/settings/general.ldap_wizard.mapping.preview_snipe_field') }} {{ trans('admin/settings/general.ldap_wizard.mapping.preview_ldap_attribute') }} {{ trans('admin/settings/general.ldap_wizard.mapping.preview_ldap_value') }}
{{ $preview['label'] ?? $snipeField }} @if ($preview['attr']) {{ $preview['attr'] }} @else {{ trans('admin/settings/general.ldap_wizard.mapping.field_not_mapped') }} @endif @if ($preview['value'] !== null) {{ $preview['value'] }} @elseif ($preview['attr']) {{ trans('admin/settings/general.ldap_wizard.mapping.value_missing') }} @endif
@endif
@elseif ($currentStep === 4) {{-- ldap_enabled is intentionally not rendered here. Reaching step 4 means the earlier steps verified the connection, bind, and mapping. The save action flips ldap_enabled=1 on the user's behalf. Users who want to disable LDAP later toggle it from the LDAP settings page directly. --}} @elseif ($currentStep === 5) {{-- Completion summary. Reached only via a successful save on step 4. Points admins at sync-scheduling options. Login is live but recurring user sync has to be scheduled separately since the app ships no default schedule for snipeit:ldap-sync. --}}

{{ trans('admin/settings/general.ldap_wizard.done.subtitle') }}

{{ trans('admin/settings/general.ldap_wizard.done.intro') }}

{{ trans('admin/settings/general.ldap_wizard.done.sync_intro') }}

{{-- Persisted-config summary, grouped by wizard step with an "Edit" button that jumps back to that step. Sensitive fields (bind password, TLS client key) are shown as a set/not-set indicator rather than the actual value. Empty fields are hidden. --}} @php // Group field keys by the wizard step they // live on. Kept as an inline map so the // summary stays self-contained; if steps // move fields around, only this array needs // to update (plus the corresponding form // sections above, of course). $summaryGroups = [ 1 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_connection'), 'fields' => [ 'ldap_server' => trans('admin/settings/general.ldap_server'), 'ldap_tls' => trans('admin/settings/general.ldap_tls'), 'ldap_server_cert_ignore' => trans('admin/settings/general.ldap_server_cert_ignore'), 'is_ad' => trans('admin/settings/general.is_ad'), 'ad_domain' => trans('admin/settings/general.ad_domain'), ], ], 2 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_authscope'), 'fields' => [ 'ldap_uname' => trans('admin/settings/general.ldap_uname'), 'ldap_pword' => trans('admin/settings/general.ldap_pword'), 'ldap_basedn' => trans('admin/settings/general.ldap_basedn'), 'ldap_filter' => trans('admin/settings/general.ldap_filter'), 'ldap_auth_filter_query' => trans('admin/settings/general.ldap_auth_filter_query'), ], ], 3 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_mapping'), 'fields' => [ 'ldap_username_field' => trans('admin/settings/general.ldap_username_field'), 'ldap_fname_field' => trans('admin/settings/general.ldap_fname_field'), 'ldap_lname_field' => trans('admin/settings/general.ldap_lname_field'), 'ldap_display_name' => trans('admin/settings/general.ldap_display_name'), 'ldap_email' => trans('admin/settings/general.ldap_email'), 'ldap_emp_num' => trans('admin/settings/general.ldap_emp_num'), 'ldap_phone_field' => trans('admin/settings/general.ldap_phone'), 'ldap_mobile' => trans('admin/settings/general.ldap_mobile'), 'ldap_jobtitle' => trans('admin/settings/general.ldap_jobtitle'), 'ldap_manager' => trans('admin/settings/general.ldap_manager'), 'ldap_dept' => trans('admin/settings/general.ldap_dept'), 'ldap_location' => trans('admin/settings/general.ldap_location'), 'ldap_active_flag' => trans('admin/settings/general.ldap_active_flag'), 'ldap_invert_active_flag' => trans('admin/settings/general.ldap_invert_active_flag'), ], ], 4 => [ 'title' => trans('admin/settings/general.ldap_wizard.step_sync'), 'fields' => [ 'ldap_pw_sync' => trans('admin/settings/general.ldap_pw_sync'), 'ldap_default_group' => trans('admin/settings/general.ldap_default_group'), 'custom_forgot_pass_url' => trans('admin/settings/general.custom_forgot_pass_url'), ], ], ]; // Bind password and TLS client key/cert are // never rendered as their raw persisted // value in the summary - show only whether // they are set. Keeps sensitive material off // any incidental screenshot/screen-share. $summarySecretFields = ['ldap_pword', 'ldap_client_tls_key', 'ldap_client_tls_cert']; @endphp

{{ trans('admin/settings/general.ldap_wizard.done.summary_heading') }}

@foreach ($summaryGroups as $stepNum => $group)

{{ $stepNum }}. {{ $group['title'] }}

@foreach ($group['fields'] as $field => $label) @php $value = $this->{$field} ?? null; // Booleans always render (yes/no // is meaningful); everything else // (secrets included) hides when // unset so the summary stays // focused on what the admin // actually configured. $isBool = is_bool($value); $isSecret = in_array($field, $summarySecretFields, true); // ldap_pword deliberately stays '' on // the component (never round-tripped // to the browser). Check the persisted // row via the computed helper so a // stored password renders as masked // asterisks instead of being hidden. $secretIsSet = $isSecret ? ($field === 'ldap_pword' ? $this->hasPersistedLdapPword : ($value !== null && $value !== '')) : false; if ($isSecret && ! $secretIsSet) { continue; } if (! $isBool && ! $isSecret && ($value === null || $value === '')) { continue; } if ($field === 'ldap_default_group' && $value !== null && $value !== '') { // Resolve the id to the group name for readability. $group_name = \App\Models\Group::find($value)?->name; $displayValue = $group_name ?? trans('general.unknown'); } elseif ($isBool) { $displayValue = $value ? trans('general.yes') : trans('general.no'); } elseif ($isSecret) { // Show masked asterisks when a // value is stored so operators // can see the credential IS set, // without ever surfacing the // actual value. $displayValue = '************'; } else { $displayValue = $value; } @endphp {{-- Boolean rows render icon + label for scan-ability; secret rows skip copy_what so the masked asterisks aren't offered as clipboard content. Everything else uses the standard copy-to-clipboard treatment so admins can grab the value the same way the hardware view lets them copy asset details. --}} @if ($isBool) {{ $displayValue }} @elseif ($isSecret) {{ $displayValue }} @else {{ $displayValue }} @endif @endforeach
@endforeach
@endif
{{-- Verification-in-progress hint. Only visible while saveAndAdvance is running so users don't wonder why the button "hung". The actual delay is the LDAP handshake + bind, which is out of our control. .delay.longer waits 500ms before showing so fast requests (no-op advances, cached lookups) don't briefly flash the hint at the user, which read as "you missed something" on quick steps. Only slow LDAP round-trips actually paint it. --}}

{{ trans('admin/settings/general.ldap_wizard.verifying_help') }}

Having trouble with the new wizard? You can find the legacy form here, but please do let us know what trouble you're having so we can fix it. {{-- Disable-LDAP confirm modal. Only useful when LDAP is currently on; skipped otherwise so we don't emit dead markup. wire:ignore so Livewire's morphdom leaves Bootstrap's `.in`/`body.modal-open` state alone between wizard re-renders. The confirm button still fires wire:click because Livewire delegates click events at the document level. --}} @if ($snipeSettings->ldap_enabled == '1') @endif