feat: add resources and view components
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
{{--
|
||||
Two-panel permission picker — single source of truth.
|
||||
ALL items rendered once in Available. Pre-selected ones moved to Assigned by JS on init.
|
||||
Multi-select: click = single, Ctrl+click = toggle, Shift+click = range.
|
||||
--}}
|
||||
@php
|
||||
$preSelected = collect($rolePermIds ?? []);
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
.dp-panel {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(72vh - 120px);
|
||||
min-height: 400px;
|
||||
}
|
||||
.dp-panel-head {
|
||||
background: #f8fafc;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dp-panel-search {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dp-hint-row {
|
||||
padding: 2px 10px 4px;
|
||||
font-size: 0.6rem;
|
||||
color: #b0b9c8;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dp-panel-body {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.dp-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
transition: background .1s;
|
||||
user-select: none;
|
||||
}
|
||||
.dp-item:hover { background: #f0f9ff; }
|
||||
.dp-item.selected {
|
||||
background: #dbeafe;
|
||||
outline: 1px solid #93c5fd;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
.dp-item-icon { flex-shrink: 0; width: 18px; text-align: center; }
|
||||
.dp-item-name { font-size: 0.78rem; line-height: 1.3; flex: 1; min-width: 0; }
|
||||
.dp-item-cat { font-size: 0.6rem; color: #94a3b8; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.dp-item-tab { display: inline-block; font-size: 0.58rem; background: #e2e8f0; color: #475569; padding: 0 4px; border-radius: 3px; font-weight: 600; margin-right: 3px; }
|
||||
.dp-item-type-manage { color: #7c3aed; }
|
||||
.dp-item-type-view { color: #0369a1; }
|
||||
.dp-btn-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dp-btn {
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: all .15s;
|
||||
font-size: 0.8rem;
|
||||
color: #475569;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dp-btn:hover { background: #111827; color: white; border-color: #111827; }
|
||||
.dp-group-header {
|
||||
padding: 4px 10px 2px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.dp-empty-hint { padding: 40px 16px; text-align: center; color: #cbd5e1; font-size: 0.78rem; }
|
||||
</style>
|
||||
|
||||
<div class="d-flex align-items-stretch" id="dp-{{ $panelId }}" style="min-width:0;gap:0;">
|
||||
|
||||
{{-- ── LEFT: Available (ALL items live here initially) ──────── --}}
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="dp-panel">
|
||||
<div class="dp-panel-head d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold small text-dark">{{ __('Available') }}</span>
|
||||
<span class="badge text-bg-secondary rounded-pill dp-available-count" style="font-size:0.6rem;">0</span>
|
||||
</div>
|
||||
<div class="dp-panel-search">
|
||||
<input type="text" class="form-control form-control-sm dp-search-available border-0 p-0 bg-transparent"
|
||||
placeholder="🔍 {{ __('Filter...') }}" style="font-size:0.78rem;box-shadow:none;">
|
||||
</div>
|
||||
<div class="dp-hint-row">
|
||||
<i class="bi bi-info-circle me-1"></i>Click = select · Ctrl+click = multi · Shift+click = range · Dbl-click = move
|
||||
</div>
|
||||
<div class="dp-panel-body dp-available-list">
|
||||
@foreach ($groupedPermissions as $category => $catPerms)
|
||||
@php
|
||||
$catItems = collect();
|
||||
foreach ($catPerms as $menuName => $menuData) {
|
||||
if ($menuData['manage']) $catItems->push(['perm' => $menuData['manage'], 'menu' => $menuName, 'tab' => null, 'type' => 'manage']);
|
||||
if ($menuData['view']) $catItems->push(['perm' => $menuData['view'], 'menu' => $menuName, 'tab' => null, 'type' => 'view']);
|
||||
foreach ($menuData['tabs'] as $tabSlug => $tabPerms) {
|
||||
if ($tabPerms['manage']) $catItems->push(['perm' => $tabPerms['manage'], 'menu' => $menuName, 'tab' => $tabSlug, 'type' => 'manage']);
|
||||
if ($tabPerms['view']) $catItems->push(['perm' => $tabPerms['view'], 'menu' => $menuName, 'tab' => $tabSlug, 'type' => 'view']);
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
@if($catItems->isNotEmpty())
|
||||
<div class="dp-group-header dp-cat-header" data-cat="{{ Str::slug($category) }}">{{ $category }}</div>
|
||||
@foreach ($catItems as $entry)
|
||||
<div class="dp-item dp-avail-item"
|
||||
data-id="{{ $entry['perm']->id }}"
|
||||
data-name="{{ strtolower($entry['perm']->name) }}"
|
||||
data-cat="{{ Str::slug($category) }}"
|
||||
data-preselected="{{ $preSelected->contains($entry['perm']->id) ? '1' : '0' }}">
|
||||
<span class="dp-item-icon">
|
||||
@if($entry['type'] === 'manage')
|
||||
<i class="bi bi-pencil-square dp-item-type-manage" style="font-size:0.7rem;"></i>
|
||||
@else
|
||||
<i class="bi bi-eye dp-item-type-view" style="font-size:0.7rem;"></i>
|
||||
@endif
|
||||
</span>
|
||||
<span class="dp-item-name">
|
||||
@if($entry['tab'])
|
||||
<span class="dp-item-tab">{{ $entry['tab'] }}</span>
|
||||
@endif
|
||||
{{ $entry['perm']->name }}
|
||||
<span class="dp-item-cat">{{ $category }}</span>
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── CENTER: Buttons ─────────────────────────────────────── --}}
|
||||
<div class="dp-btn-col">
|
||||
<button type="button" class="dp-btn dp-btn-add-selected" title="{{ __('Move selected →') }}"><i class="bi bi-chevron-right"></i></button>
|
||||
<button type="button" class="dp-btn dp-btn-add-all" title="{{ __('Move all →→') }}"><i class="bi bi-chevron-double-right"></i></button>
|
||||
<button type="button" class="dp-btn dp-btn-remove-selected" title="{{ __('← Remove selected') }}"><i class="bi bi-chevron-left"></i></button>
|
||||
<button type="button" class="dp-btn dp-btn-remove-all" title="{{ __('←← Remove all') }}"><i class="bi bi-chevron-double-left"></i></button>
|
||||
</div>
|
||||
|
||||
{{-- ── RIGHT: Assigned (empty on load, filled by JS) ──────── --}}
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div class="dp-panel">
|
||||
<div class="dp-panel-head d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold small text-dark">{{ __('Assigned') }}</span>
|
||||
<span class="badge rounded-pill dp-assigned-count text-bg-secondary" style="font-size:0.6rem;">0</span>
|
||||
</div>
|
||||
<div class="dp-panel-search">
|
||||
<input type="text" class="form-control form-control-sm dp-search-assigned border-0 p-0 bg-transparent"
|
||||
placeholder="🔍 {{ __('Filter...') }}" style="font-size:0.78rem;box-shadow:none;">
|
||||
</div>
|
||||
<div class="dp-hint-row">
|
||||
<i class="bi bi-info-circle me-1"></i>Dbl-click or select + ◀ to remove
|
||||
</div>
|
||||
<div class="dp-panel-body dp-assigned-list">
|
||||
<div class="dp-empty-hint">
|
||||
<i class="bi bi-arrow-left-circle d-block mb-2" style="font-size:1.8rem;opacity:0.2;"></i>
|
||||
{{ __('No permissions assigned yet') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,206 @@
|
||||
{{--
|
||||
Permission matrix partial — tree view with collapsible tabs.
|
||||
Variables:
|
||||
$groupedPermissions — tree from RoleManagementController::groupPermissions()
|
||||
$idPrefix — 'add' | 'edit'
|
||||
$rolePermIds — array of pre-selected permission IDs (empty for add)
|
||||
--}}
|
||||
@foreach ($groupedPermissions as $category => $perms)
|
||||
@php
|
||||
$catSlug = Str::slug($category);
|
||||
$allInCat = collect($perms)->flatMap(function ($m) {
|
||||
$ids = [];
|
||||
if ($m['manage']) $ids[] = $m['manage']->id;
|
||||
if ($m['view']) $ids[] = $m['view']->id;
|
||||
foreach ($m['tabs'] as $t) {
|
||||
if ($t['manage']) $ids[] = $t['manage']->id;
|
||||
if ($t['view']) $ids[] = $t['view']->id;
|
||||
}
|
||||
return $ids;
|
||||
})->values();
|
||||
$totalInCat = $allInCat->count();
|
||||
@endphp
|
||||
|
||||
<div class="perm-category-group mb-2"
|
||||
data-total="{{ $totalInCat }}"
|
||||
data-cat="{{ $catSlug }}">
|
||||
|
||||
{{-- ── Category header ── --}}
|
||||
<div class="d-flex justify-content-between align-items-center px-2 py-1 rounded-2 mb-1"
|
||||
style="background:#f1f5f9; border-left:3px solid #64748b;">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-bold small text-dark">{{ $category }}</span>
|
||||
<span class="perm-badge badge rounded-pill text-bg-secondary" style="font-size:0.6rem;">
|
||||
0 / {{ $totalInCat }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input select-all-category" type="checkbox"
|
||||
id="{{ $idPrefix }}-all-{{ $catSlug }}"
|
||||
title="{{ __('Select all in this category') }}">
|
||||
<label class="form-check-label small text-muted cursor-pointer"
|
||||
for="{{ $idPrefix }}-all-{{ $catSlug }}">
|
||||
{{ __('All') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Column headers ── --}}
|
||||
<div class="row g-0 px-1 mb-1 text-uppercase text-secondary"
|
||||
style="font-size:0.6rem;font-weight:800;letter-spacing:0.4px;">
|
||||
<div class="col-6 ps-1">{{ __('Manage') }}</div>
|
||||
<div class="col-6 ps-3">{{ __('View') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-1">
|
||||
@foreach ($perms as $menuName => $menuData)
|
||||
@php
|
||||
$hasTabs = ! empty($menuData['tabs']);
|
||||
$menuSlug = Str::slug($menuName);
|
||||
$collapseId = $idPrefix . '-tabs-' . $catSlug . '-' . $menuSlug;
|
||||
$tabCount = count($menuData['tabs']);
|
||||
@endphp
|
||||
|
||||
<div class="perm-menu-row mb-1" data-base="{{ strtolower($menuName) }}">
|
||||
|
||||
{{-- ── Menu-level row ── --}}
|
||||
<div class="row g-0 align-items-center permission-pair-row"
|
||||
data-base="{{ strtolower($menuName) }}">
|
||||
|
||||
{{-- Manage column --}}
|
||||
<div class="col-6 text-break pe-2">
|
||||
@if($menuData['manage'])
|
||||
@php $p = $menuData['manage']; @endphp
|
||||
<div class="permission-item d-flex align-items-center gap-1"
|
||||
data-name="{{ strtolower($p->name) }}">
|
||||
@if($hasTabs)
|
||||
<button type="button"
|
||||
class="btn btn-link btn-sm p-0 lh-1 text-secondary tab-collapse-toggle flex-shrink-0"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ $collapseId }}"
|
||||
aria-expanded="false"
|
||||
title="{{ $tabCount }} tab permissions">
|
||||
<i class="bi bi-chevron-right perm-chevron" style="font-size:0.65rem;transition:transform .2s;"></i>
|
||||
</button>
|
||||
@else
|
||||
<span style="width:14px;display:inline-block;"></span>
|
||||
@endif
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input perm-checkbox perm-manage"
|
||||
type="checkbox" name="permissions[]"
|
||||
value="{{ $p->id }}"
|
||||
id="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
data-manage-for="{{ $idPrefix }}-perm-{{ $menuData['view']?->id }}"
|
||||
@if(in_array($p->id, $rolePermIds ?? [])) checked @endif>
|
||||
<label class="form-check-label small cursor-pointer fw-semibold lh-sm"
|
||||
for="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
title="{{ $p->name }}">
|
||||
{{ $p->name }}
|
||||
@if($hasTabs)
|
||||
<span class="text-muted fw-normal" style="font-size:0.6rem;">(+{{ $tabCount }} tabs)</span>
|
||||
@endif
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- View column --}}
|
||||
<div class="col-6 ps-3 text-break">
|
||||
@if($menuData['view'])
|
||||
@php $p = $menuData['view']; @endphp
|
||||
<div class="permission-item" data-name="{{ strtolower($p->name) }}">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input perm-checkbox perm-view"
|
||||
type="checkbox" name="permissions[]"
|
||||
value="{{ $p->id }}"
|
||||
id="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
@if(in_array($p->id, $rolePermIds ?? [])) checked @endif>
|
||||
<label class="form-check-label small cursor-pointer fw-semibold lh-sm"
|
||||
for="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
title="{{ $p->name }}">
|
||||
{{ $p->name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Collapsible tab rows ── --}}
|
||||
@if($hasTabs)
|
||||
<div class="collapse" id="{{ $collapseId }}">
|
||||
<div class="ms-4 mt-1 ps-2 border-start border-2"
|
||||
style="border-color:#cbd5e1 !important;">
|
||||
<div class="row g-0 mb-1 text-uppercase text-secondary"
|
||||
style="font-size:0.58rem;font-weight:800;letter-spacing:0.3px;">
|
||||
<div class="col-6">{{ __('Manage Tab') }}</div>
|
||||
<div class="col-6 ps-3">{{ __('View Tab') }}</div>
|
||||
</div>
|
||||
@foreach ($menuData['tabs'] as $tabSlug => $tabPerms)
|
||||
<div class="row g-0 align-items-center mb-1 permission-pair-row"
|
||||
data-base="{{ strtolower($menuName . ':' . $tabSlug) }}">
|
||||
|
||||
{{-- Manage tab --}}
|
||||
<div class="col-6 text-break pe-2">
|
||||
@if($tabPerms['manage'])
|
||||
@php $p = $tabPerms['manage']; @endphp
|
||||
<div class="permission-item" data-name="{{ strtolower($p->name) }}">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input perm-checkbox perm-manage"
|
||||
type="checkbox" name="permissions[]"
|
||||
value="{{ $p->id }}"
|
||||
id="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
data-manage-for="{{ $idPrefix }}-perm-{{ $tabPerms['view']?->id }}"
|
||||
@if(in_array($p->id, $rolePermIds ?? [])) checked @endif>
|
||||
<label class="form-check-label small cursor-pointer lh-sm"
|
||||
for="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
title="{{ $p->name }}">
|
||||
<span class="badge me-1"
|
||||
style="font-size:0.55rem;background:#e2e8f0;color:#475569;font-weight:600;">
|
||||
{{ $tabSlug }}
|
||||
</span>
|
||||
manage
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- View tab --}}
|
||||
<div class="col-6 ps-3 text-break">
|
||||
@if($tabPerms['view'])
|
||||
@php $p = $tabPerms['view']; @endphp
|
||||
<div class="permission-item" data-name="{{ strtolower($p->name) }}">
|
||||
<div class="form-check mb-0">
|
||||
<input class="form-check-input perm-checkbox perm-view"
|
||||
type="checkbox" name="permissions[]"
|
||||
value="{{ $p->id }}"
|
||||
id="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
@if(in_array($p->id, $rolePermIds ?? [])) checked @endif>
|
||||
<label class="form-check-label small cursor-pointer lh-sm"
|
||||
for="{{ $idPrefix }}-perm-{{ $p->id }}"
|
||||
title="{{ $p->name }}">
|
||||
<span class="badge me-1"
|
||||
style="font-size:0.55rem;background:#e2e8f0;color:#475569;font-weight:600;">
|
||||
{{ $tabSlug }}
|
||||
</span>
|
||||
view
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
Reference in New Issue
Block a user