Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,158 @@
|
||||
{{-- Permission grid partial. Expects: $model (string), $permissionGroups (array), $allVerbs (array), $isSystem (bool) --}}
|
||||
@php $isSystem = $isSystem ?? false; @endphp
|
||||
<div class="-mx-6 overflow-x-auto px-6">
|
||||
<div style="min-width: calc(140px + {{ count($allVerbs) }} * 72px)">
|
||||
@if ($isSystem)
|
||||
<div class="mb-2 flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<x-icon name="lock-closed" class="h-3.5 w-3.5" />
|
||||
{{ __('System role permissions are locked and cannot be modified.') }}
|
||||
</div>
|
||||
@endif
|
||||
@foreach ($permissionGroups as $group)
|
||||
@if (!empty($group['section']))
|
||||
<div class="mt-3 mb-1 border-t border-gray-200 pt-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-400">{{ __($group['section']) }}</span>
|
||||
</div>
|
||||
@elseif (!empty($group['inline']))
|
||||
@php $inlinePerms = array_values($group['permissions']); $inlineKeys = array_keys($group['permissions']); @endphp
|
||||
<div class="grid gap-x-1 rounded hover:bg-gray-50 py-2"
|
||||
style="grid-template-columns: 140px repeat({{ count($allVerbs) }}, 72px); align-items: start;">
|
||||
<div class="text-xs font-medium text-gray-700 pl-1">{{ __($group['inline']) }}</div>
|
||||
@foreach ($allVerbs as $vIndex => $verb)
|
||||
<div class="flex justify-center">
|
||||
@if (isset($inlineKeys[$vIndex]))
|
||||
<label class="flex flex-col items-center gap-0.5 {{ $isSystem ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $inlineKeys[$vIndex] }}"
|
||||
wire:model="{{ $model }}.permissions"
|
||||
{{ $isSystem ? 'disabled' : '' }}
|
||||
class="rounded border-gray-300 text-theme-brand focus:ring-theme-brand h-3.5 w-3.5 {{ $isSystem ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' }}">
|
||||
<span class="text-xs text-gray-500">{{ __($inlinePerms[$vIndex]) }}</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif (!empty($group['fixed']))
|
||||
@foreach ($group['fixed'] as $fixedPermission)
|
||||
<div class="grid gap-x-1 rounded hover:bg-gray-50 py-2" style="grid-template-columns: 140px repeat({{ count($allVerbs) }}, 72px); align-items: start;">
|
||||
<div class="text-xs font-medium text-gray-700 pl-1 truncate">
|
||||
{{ __($fixedPermission) }}
|
||||
</div>
|
||||
@foreach ($allVerbs as $verb)
|
||||
<div class="flex justify-center">
|
||||
@if ($loop->last)
|
||||
<label class="flex flex-col items-center gap-0.5 {{ $isSystem ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $fixedPermission }}"
|
||||
wire:model="{{ $model }}.permissions"
|
||||
{{ $isSystem ? 'disabled' : '' }}
|
||||
class="rounded border-gray-300 text-theme-brand focus:ring-theme-brand h-3.5 w-3.5 {{ $isSystem ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' }}">
|
||||
<span class="text-xs text-gray-500">{{ __($verb) }}</span>
|
||||
</label>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
@php
|
||||
$resource = $group['resource'];
|
||||
$verbs = $group['verbs'];
|
||||
$allPerms = array_map(fn($v) => $v . ' ' . $resource, $verbs);
|
||||
$allPermsJs = json_encode($allPerms);
|
||||
$manageKey = 'manage ' . $resource;
|
||||
$rowLocked = $isSystem && empty($group['unlocked']);
|
||||
@endphp
|
||||
<div
|
||||
class="grid gap-x-1 rounded hover:bg-gray-50 py-2"
|
||||
style="grid-template-columns: 140px repeat({{ count($allVerbs) }}, 72px); align-items: start;"
|
||||
@if (!$rowLocked)
|
||||
x-data="{
|
||||
toggleManage(checked, allPerms) {
|
||||
let current = [...($wire.get('{{ $model }}.permissions') || [])];
|
||||
allPerms.forEach(perm => {
|
||||
if (checked) {
|
||||
if (!current.includes(perm)) current.push(perm);
|
||||
} else {
|
||||
current = current.filter(p => p !== perm);
|
||||
}
|
||||
});
|
||||
$wire.set('{{ $model }}.permissions', current);
|
||||
},
|
||||
uncheckManage(manageKey) {
|
||||
let current = [...($wire.get('{{ $model }}.permissions') || [])];
|
||||
current = current.filter(p => p !== manageKey);
|
||||
$wire.set('{{ $model }}.permissions', current);
|
||||
},
|
||||
checkView(viewKey) {
|
||||
let current = [...($wire.get('{{ $model }}.permissions') || [])];
|
||||
if (!current.includes(viewKey)) {
|
||||
current.push(viewKey);
|
||||
$wire.set('{{ $model }}.permissions', current);
|
||||
}
|
||||
},
|
||||
uncheckAll(allPerms) {
|
||||
let current = [...($wire.get('{{ $model }}.permissions') || [])];
|
||||
current = current.filter(p => !allPerms.includes(p));
|
||||
$wire.set('{{ $model }}.permissions', current);
|
||||
}
|
||||
}"
|
||||
@endif
|
||||
>
|
||||
<div class="text-xs font-medium text-gray-700 pl-1 truncate">
|
||||
{{ __($group['label']) }}
|
||||
</div>
|
||||
@foreach ($allVerbs as $verb)
|
||||
<div class="flex justify-center">
|
||||
@if (in_array($verb, $verbs))
|
||||
@if ($verb === 'manage')
|
||||
<label class="flex flex-col items-center gap-0.5 {{ $rowLocked ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $manageKey }}"
|
||||
wire:model="{{ $model }}.permissions"
|
||||
{{ $rowLocked ? 'disabled' : '' }}
|
||||
@if (!$rowLocked)
|
||||
x-on:change="toggleManage($event.target.checked, {{ $allPermsJs }}); {{ in_array('view', $verbs) ? "if (\$event.target.checked) checkView('view {$resource}')" : '' }}"
|
||||
@endif
|
||||
class="rounded border-gray-300 text-theme-brand focus:ring-theme-brand h-3.5 w-3.5 {{ $rowLocked ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' }}">
|
||||
<span class="text-xs text-gray-500">{{ __($verb) }}</span>
|
||||
</label>
|
||||
@elseif ($verb === 'view')
|
||||
<label class="flex flex-col items-center gap-0.5 {{ $rowLocked ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="view {{ $resource }}"
|
||||
wire:model="{{ $model }}.permissions"
|
||||
{{ $rowLocked ? 'disabled' : '' }}
|
||||
@if (!$rowLocked)
|
||||
x-on:change="if (!$event.target.checked) uncheckAll({{ $allPermsJs }})"
|
||||
@endif
|
||||
class="rounded border-gray-300 text-theme-brand focus:ring-theme-brand h-3.5 w-3.5 {{ $rowLocked ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' }}">
|
||||
<span class="text-xs text-gray-500">{{ __($verb) }}</span>
|
||||
</label>
|
||||
@else
|
||||
<label class="flex flex-col items-center gap-0.5 {{ $rowLocked ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $verb }} {{ $resource }}"
|
||||
wire:model="{{ $model }}.permissions"
|
||||
{{ $rowLocked ? 'disabled' : '' }}
|
||||
@if (!$rowLocked)
|
||||
x-on:change="if (!$event.target.checked) uncheckManage('{{ $manageKey }}'); if ($event.target.checked) checkView('view {{ $resource }}')"
|
||||
@endif
|
||||
class="rounded border-gray-300 text-theme-brand focus:ring-theme-brand h-3.5 w-3.5 {{ $rowLocked ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' }}">
|
||||
<span class="text-xs text-gray-500">{{ __($verb) }}</span>
|
||||
</label>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Care about people's approval and you will be their prisoner. --}}
|
||||
</div>
|
||||
@@ -0,0 +1,617 @@
|
||||
<div class="mt-12">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-semibold text-theme-primary">{{ __('Admin Roles') }}</h2>
|
||||
<p class="mt-1 text-sm text-theme-secondary">{{ __('Manage roles that can be assigned to admin profiles. System roles cannot be modified.') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Create button -->
|
||||
@can('create roles')
|
||||
<div class="mb-6">
|
||||
<x-jetstream.button wire:click="openCreateModal" class="bg-theme-brand hover:bg-opacity-80">
|
||||
<x-icon name="plus" class="mr-2 h-4 w-4" />
|
||||
{{ __('Create Role') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<div class="relative w-2/3">
|
||||
<input
|
||||
class="w-full rounded-md border border-theme-primary px-3 py-1 pr-10 text-theme-primary shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
||||
placeholder="{{ __('Search roles') }}..."
|
||||
type="text"
|
||||
wire:model.live.debounce.300ms="search">
|
||||
@if ($search)
|
||||
<button
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-theme-secondary hover:text-theme-primary focus:outline-none"
|
||||
wire:click.prevent="resetSearch">
|
||||
<x-icon mini name="backspace" solid />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 cursor-pointer"
|
||||
wire:click="sortBy('label')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Label') }}</span>
|
||||
@if ($sortField === 'label')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 cursor-pointer"
|
||||
wire:click="sortBy('name')">
|
||||
<div class="flex items-center space-x-1">
|
||||
<span>{{ __('Name') }}</span>
|
||||
@if ($sortField === 'name')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
<th class="hidden xl:table-cell px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
{{ __('Permissions') }}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
{{ __('Type') }}
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">
|
||||
{{ __('Actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@forelse ($roles as $role)
|
||||
<tr class="hover:bg-gray-50 align-top" wire:key="role-{{ $role->id }}">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">
|
||||
{{ $role->label }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">
|
||||
{{ $role->name }}
|
||||
</td>
|
||||
<td class="hidden xl:table-cell px-4 py-3 text-xs text-gray-500 leading-relaxed">
|
||||
@php
|
||||
$perms = $role->permissions ?? [];
|
||||
$managedResources = collect($perms)
|
||||
->filter(fn($p) => str_starts_with($p, 'manage '))
|
||||
->map(fn($p) => substr($p, 7))
|
||||
->all();
|
||||
$displayed = collect($perms)->filter(function ($p) use ($managedResources) {
|
||||
if (!str_contains($p, ' ')) return true;
|
||||
[$verb, $resource] = explode(' ', $p, 2);
|
||||
return $verb === 'manage' || !in_array($resource, $managedResources);
|
||||
})->values()->all();
|
||||
@endphp
|
||||
{{ implode(', ', $displayed) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
@if ($role->is_system)
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
||||
{{ __('System') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
||||
{{ __('Custom') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex justify-end space-x-2">
|
||||
@can('update roles')
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Edit') }}"
|
||||
wire:click="openEditModal({{ $role->id }})">
|
||||
<x-icon name="pencil-square" class="h-5 w-5" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
@can('delete roles')
|
||||
@if (!$role->is_system)
|
||||
<x-jetstream.danger-button
|
||||
title="{{ __('Delete') }}"
|
||||
wire:click="openDeleteModal({{ $role->id }})">
|
||||
<x-icon name="trash" class="h-5 w-5" />
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">
|
||||
{{ __('No roles found.') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($roles->hasPages())
|
||||
<div class="border-t border-gray-200 px-4 py-3">
|
||||
{{ $roles->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@php
|
||||
$roleTableHeaders = ['Label', 'Name', 'Permissions', 'Type', 'Actions'];
|
||||
@endphp
|
||||
|
||||
<!-- Organization Roles -->
|
||||
<div class="mt-12 mb-6">
|
||||
<h2 class="text-lg font-semibold text-theme-primary">{{ __('Organization Roles') }}</h2>
|
||||
<p class="mt-1 text-sm text-theme-secondary">{{ __('Roles assigned to users linked to organization profiles. System roles cannot be deleted.') }}</p>
|
||||
</div>
|
||||
|
||||
@can('create roles')
|
||||
<div class="mb-6">
|
||||
<x-jetstream.button wire:click="openCreateOrgRoleModal('organization')" class="bg-theme-brand hover:bg-opacity-80">
|
||||
<x-icon name="plus" class="mr-2 h-4 w-4" />
|
||||
{{ __('Create Organization Role') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Label') }}</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Name') }}</th>
|
||||
<th class="hidden xl:table-cell px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Permissions') }}</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Type') }}</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@forelse ($orgRoles as $orgRole)
|
||||
<tr class="hover:bg-gray-50 align-top" wire:key="org-role-{{ $orgRole->id }}">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">{{ $orgRole->label }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ $orgRole->name }}</td>
|
||||
<td class="hidden xl:table-cell px-4 py-3 text-xs text-gray-500 leading-relaxed">{{ implode(', ', $orgRole->permissions ?? []) }}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
@if ($orgRole->is_system)
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">{{ __('System') }}</span>
|
||||
@else
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">{{ __('Custom') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex justify-end space-x-2">
|
||||
@can('update roles')
|
||||
<x-jetstream.secondary-button title="{{ __('Edit') }}"
|
||||
wire:click="openEditOrgRoleModal({{ $orgRole->id }})">
|
||||
<x-icon name="pencil-square" class="h-5 w-5" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
@can('delete roles')
|
||||
@if (!$orgRole->is_system)
|
||||
<x-jetstream.danger-button title="{{ __('Delete') }}"
|
||||
wire:click="openDeleteOrgRoleModal({{ $orgRole->id }})">
|
||||
<x-icon name="trash" class="h-5 w-5" />
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">{{ __('No roles found.') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bank Roles -->
|
||||
<div class="mt-12 mb-6">
|
||||
<h2 class="text-lg font-semibold text-theme-primary">{{ __('Bank Roles') }}</h2>
|
||||
<p class="mt-1 text-sm text-theme-secondary">{{ __('Roles assigned to users linked to bank profiles. System roles cannot be deleted.') }}</p>
|
||||
</div>
|
||||
|
||||
@can('create roles')
|
||||
<div class="mb-6">
|
||||
<x-jetstream.button wire:click="openCreateOrgRoleModal('bank')" class="bg-theme-brand hover:bg-opacity-80">
|
||||
<x-icon name="plus" class="mr-2 h-4 w-4" />
|
||||
{{ __('Create Bank Role') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
<div class="overflow-hidden rounded-lg bg-white shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Label') }}</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Name') }}</th>
|
||||
<th class="hidden xl:table-cell px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Permissions') }}</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Type') }}</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@forelse ($bankRoles as $orgRole)
|
||||
<tr class="hover:bg-gray-50 align-top" wire:key="bank-role-{{ $orgRole->id }}">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">{{ $orgRole->label }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ $orgRole->name }}</td>
|
||||
<td class="hidden xl:table-cell px-4 py-3 text-xs text-gray-500 leading-relaxed">{{ implode(', ', $orgRole->permissions ?? []) }}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
@if ($orgRole->is_system)
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">{{ __('System') }}</span>
|
||||
@else
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">{{ __('Custom') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex justify-end space-x-2">
|
||||
@can('update roles')
|
||||
<x-jetstream.secondary-button title="{{ __('Edit') }}"
|
||||
wire:click="openEditOrgRoleModal({{ $orgRole->id }})">
|
||||
<x-icon name="pencil-square" class="h-5 w-5" />
|
||||
</x-jetstream.secondary-button>
|
||||
@endcan
|
||||
@can('delete roles')
|
||||
@if (!$orgRole->is_system)
|
||||
<x-jetstream.danger-button title="{{ __('Delete') }}"
|
||||
wire:click="openDeleteOrgRoleModal({{ $orgRole->id }})">
|
||||
<x-icon name="trash" class="h-5 w-5" />
|
||||
</x-jetstream.danger-button>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">{{ __('No roles found.') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Rows = resources, columns = verbs.
|
||||
// Each row lists only the verbs that apply to that resource.
|
||||
$permissionGroups = [
|
||||
['label' => 'Posts', 'resource' => 'posts', 'verbs' => ['view','create','update','publish','unpublish','delete','manage']],
|
||||
['label' => 'Polls', 'resource' => 'polls', 'verbs' => ['view','create','update','export','delete','manage']],
|
||||
['label' => 'Calls', 'resource' => 'calls', 'verbs' => ['view','create','update','publish','unpublish','delete','manage']],
|
||||
['label' => 'Mailings', 'resource' => 'mailings', 'verbs' => ['view','create','update','send','delete','manage']],
|
||||
['label' => 'Tags', 'resource' => 'tags', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Categories', 'resource' => 'categories', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Users', 'resource' => 'users', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Organizations', 'resource' => 'organizations', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Banks', 'resource' => 'banks', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Admins', 'resource' => 'admins', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Accounts', 'resource' => 'accounts', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Profiles', 'resource' => 'profiles', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Roles', 'resource' => 'roles', 'verbs' => ['view','create','update','delete','manage']],
|
||||
['label' => 'Statistics', 'resource' => 'stats', 'verbs' => ['view'], 'unlocked' => true],
|
||||
['label' => 'Maintenance', 'resource' => 'maintenance', 'verbs' => ['manage']],
|
||||
['section' => 'Logs'],
|
||||
['label' => 'Search', 'resource' => 'search-metrics', 'verbs' => ['view'], 'unlocked' => true],
|
||||
['label' => 'Search Reindex', 'resource' => 'search-reindex-log', 'verbs' => ['view'], 'unlocked' => true],
|
||||
['label' => 'Application', 'resource' => 'application-log', 'verbs' => ['view'], 'unlocked' => true],
|
||||
['label' => 'Inactive Profiles', 'resource' => 'inactive-profiles-log', 'verbs' => ['view'], 'unlocked' => true],
|
||||
];
|
||||
|
||||
// All verb columns in display order
|
||||
$allVerbs = ['view','create','update','publish','unpublish','send','export','delete','manage'];
|
||||
|
||||
// Admin profile capability permissions (separate inline grid)
|
||||
$adminCapGroups = [
|
||||
['inline' => 'Capabilities', 'permissions' => [
|
||||
'admin reports' => 'Reports',
|
||||
'admin contacts' => 'Contacts',
|
||||
'admin messenger' => 'Messenger',
|
||||
'admin settings' => 'Settings',
|
||||
'admin change-password' => 'Change Password',
|
||||
]],
|
||||
];
|
||||
$adminCapVerbs = ['reports', 'contacts', 'messenger', 'settings', 'change-password'];
|
||||
@endphp
|
||||
|
||||
<!-- Create Role Modal -->
|
||||
@if ($modalCreateRole)
|
||||
<x-jetstream.dialog-modal wire:key="modalCreateRole" wire:model.live="modalCreateRole" maxWidth="4xl">
|
||||
<x-slot name="title">{{ __('Create New Role') }}</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-5">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<x-input
|
||||
label="{{ __('Name') }}"
|
||||
placeholder="{{ __('e.g. site-editor') }}"
|
||||
hint="{{ __('Lowercase letters, numbers and hyphens only. Cannot be changed later.') }}"
|
||||
wire:model.defer="createRole.name" />
|
||||
<x-input
|
||||
label="{{ __('Label') }}"
|
||||
placeholder="{{ __('e.g. Site Editor') }}"
|
||||
hint="{{ __('Human-readable display name.') }}"
|
||||
wire:model.defer="createRole.label" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">{{ __('Permissions') }}</label>
|
||||
@include('livewire.roles._permission-grid', ['model' => 'createRole', 'isSystem' => false])
|
||||
@error('createRole.permissions')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700">{{ __('Admin Profile Capabilities') }}</label>
|
||||
<p class="mb-2 text-xs text-gray-500">{{ __('Permissions that apply when the user is acting as an admin profile.') }}</p>
|
||||
@include('livewire.roles._permission-grid', ['model' => 'createRole', 'isSystem' => false, 'permissionGroups' => $adminCapGroups, 'allVerbs' => $adminCapVerbs])
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalCreateRole', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3 bg-theme-brand" wire:click.prevent="saveCreate" wire:loading.attr="disabled">
|
||||
{{ __('Create Role') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Edit Role Modal -->
|
||||
@if ($modalEditRole)
|
||||
<x-jetstream.dialog-modal wire:key="modalEditRole" wire:model.live="modalEditRole" maxWidth="4xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Edit Role') }}: <span class="font-mono text-base">{{ $editRole['name'] }}</span>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-5">
|
||||
<x-input
|
||||
label="{{ __('Label') }}"
|
||||
placeholder="{{ __('e.g. Site Editor') }}"
|
||||
wire:model.defer="editRole.label" />
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">{{ __('Permissions') }}</label>
|
||||
@include('livewire.roles._permission-grid', ['model' => 'editRole', 'isSystem' => $editRoleIsSystem])
|
||||
@error('editRole.permissions')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700">{{ __('Admin Profile Capabilities') }}</label>
|
||||
<p class="mb-2 text-xs text-gray-500">{{ __('Permissions that apply when the user is acting as an admin profile.') }}</p>
|
||||
@include('livewire.roles._permission-grid', ['model' => 'editRole', 'isSystem' => $editRoleIsSystem, 'permissionGroups' => $adminCapGroups, 'allVerbs' => $adminCapVerbs])
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-gray-300 bg-gray-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-800">
|
||||
<strong>{{ __('Important') }}:</strong>
|
||||
{{ __('Changing permissions will immediately update all admin profiles that have this role assigned.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalEditRole', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3 bg-theme-brand" wire:click.prevent="saveEdit" wire:loading.attr="disabled">
|
||||
{{ __('Save Changes') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Delete Role Modal -->
|
||||
@if ($modalDeleteRole)
|
||||
<x-jetstream.dialog-modal wire:key="modalDeleteRole" wire:model.live="modalDeleteRole" maxWidth="lg">
|
||||
<x-slot name="title">{{ __('Delete Role') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-700">
|
||||
{{ __('Are you sure you want to delete the role') }}
|
||||
<strong>{{ $deleteRoleName }}</strong>?
|
||||
</p>
|
||||
@if (!empty($deleteAffectedUsers))
|
||||
<div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
||||
<p class="mb-2 font-medium flex items-center gap-1">
|
||||
<x-icon name="exclamation-triangle" class="h-4 w-4 shrink-0" />
|
||||
{{ __('The following users will be unlinked from their admin profile:') }}
|
||||
</p>
|
||||
<ul class="ml-5 list-disc space-y-0.5">
|
||||
@foreach ($deleteAffectedUsers as $entry)
|
||||
<li>{{ $entry['user'] }} — <span class="font-medium">{{ $entry['profile'] }}</span></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-3 text-sm text-gray-600">
|
||||
<x-icon name="information-circle" class="mr-1 inline h-4 w-4" />
|
||||
{{ __('No users are currently assigned this role.') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalDeleteRole', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click.prevent="deleteRole" wire:loading.attr="disabled">
|
||||
{{ __('Delete Role') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$orgPermissionGroups = [
|
||||
['inline' => 'Capabilities', 'permissions' => [
|
||||
'org pay' => 'Pay',
|
||||
'org transactions' => 'Transactions',
|
||||
'org reports' => 'Reports',
|
||||
'org stats' => 'Statistics',
|
||||
'org contacts' => 'Contacts',
|
||||
'org calls' => 'Calls',
|
||||
'org messenger' => 'Messenger',
|
||||
'org edit-profile' => 'Edit Profile',
|
||||
'org settings' => 'Settings',
|
||||
'org change-password' => 'Change Password',
|
||||
]],
|
||||
];
|
||||
$orgAllVerbs = ['pay','transactions','reports','stats','contacts','calls','messenger','edit-profile','settings','change-password'];
|
||||
@endphp
|
||||
|
||||
<!-- Create Org/Bank Role Modal -->
|
||||
@if ($modalCreateOrgRole)
|
||||
<x-jetstream.dialog-modal wire:key="modalCreateOrgRole" wire:model.live="modalCreateOrgRole" maxWidth="4xl">
|
||||
<x-slot name="title">{{ $createOrgRoleType === 'bank' ? __('Create New Bank Role') : __('Create New Organization Role') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-5">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<x-input
|
||||
label="{{ __('Name') }}"
|
||||
placeholder="{{ $createOrgRoleType === 'bank' ? __('e.g. bank-editor') : __('e.g. organization-editor') }}"
|
||||
hint="{{ __('Must start with') }} "{{ $createOrgRoleType }}-". {{ __('Lowercase letters, numbers and hyphens only. Cannot be changed later.') }}"
|
||||
wire:model.defer="createOrgRole.name" />
|
||||
<x-input
|
||||
label="{{ __('Label') }}"
|
||||
placeholder="{{ __('e.g. Org Editor') }}"
|
||||
hint="{{ __('Human-readable display name.') }}"
|
||||
wire:model.defer="createOrgRole.label" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">{{ __('Permissions') }}</label>
|
||||
@include('livewire.roles._permission-grid', ['model' => 'createOrgRole', 'isSystem' => false, 'permissionGroups' => $orgPermissionGroups, 'allVerbs' => $orgAllVerbs])
|
||||
@error('createOrgRole.permissions')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalCreateOrgRole', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3 bg-theme-brand" wire:click.prevent="saveCreateOrgRole" wire:loading.attr="disabled">
|
||||
{{ __('Create Role') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Edit Org Role Modal -->
|
||||
@if ($modalEditOrgRole)
|
||||
<x-jetstream.dialog-modal wire:key="modalEditOrgRole" wire:model.live="modalEditOrgRole" maxWidth="4xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Edit Role') }}: <span class="font-mono text-base">{{ $editOrgRole['name'] }}</span>
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-5">
|
||||
<x-input
|
||||
label="{{ __('Label') }}"
|
||||
placeholder="{{ __('e.g. Org Editor') }}"
|
||||
wire:model.defer="editOrgRole.label" />
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">{{ __('Permissions') }}</label>
|
||||
@include('livewire.roles._permission-grid', ['model' => 'editOrgRole', 'isSystem' => $editOrgRoleIsSystem, 'permissionGroups' => $orgPermissionGroups, 'allVerbs' => $orgAllVerbs])
|
||||
@error('editOrgRole.permissions')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<div class="rounded-md border border-gray-300 bg-gray-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-800">
|
||||
<strong>{{ __('Important') }}:</strong>
|
||||
{{ str_starts_with($editOrgRole['name'], 'bank-') ? __('Changing permissions will immediately update all bank profiles that have this role assigned.') : __('Changing permissions will immediately update all organization profiles that have this role assigned.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalEditOrgRole', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3 bg-theme-brand" wire:click.prevent="saveEditOrgRole" wire:loading.attr="disabled">
|
||||
{{ __('Save Changes') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Delete Org Role Modal -->
|
||||
@if ($modalDeleteOrgRole)
|
||||
<x-jetstream.dialog-modal wire:key="modalDeleteOrgRole" wire:model.live="modalDeleteOrgRole" maxWidth="lg">
|
||||
<x-slot name="title">{{ __('Delete Role') }}</x-slot>
|
||||
<x-slot name="content">
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-700">
|
||||
{{ __('Are you sure you want to delete the role') }}
|
||||
<strong>{{ $deleteOrgRoleName }}</strong>?
|
||||
</p>
|
||||
@if (!empty($deleteOrgAffectedUsers))
|
||||
<div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
||||
<p class="mb-2 font-medium flex items-center gap-1">
|
||||
<x-icon name="exclamation-triangle" class="h-4 w-4 shrink-0" />
|
||||
{{ $deleteOrgRoleType === 'bank'
|
||||
? __('The following users will be unlinked from their bank profile:')
|
||||
: __('The following users will be unlinked from their organization profile:') }}
|
||||
</p>
|
||||
<ul class="ml-5 list-disc space-y-0.5">
|
||||
@foreach ($deleteOrgAffectedUsers as $entry)
|
||||
<li>{{ $entry['user'] }} — <span class="font-medium">{{ $entry['profile'] }}</span></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-3 text-sm text-gray-600">
|
||||
<x-icon name="information-circle" class="mr-1 inline h-4 w-4" />
|
||||
{{ __('No users are currently assigned this role.') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalDeleteOrgRole', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click.prevent="deleteOrgRole" wire:loading.attr="disabled">
|
||||
{{ __('Delete Role') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
Reference in new issue
Block a user