feature: add user assignments component

- add title, placeholder and subtitle in avatar component
- add role collection in users dto
- make show empty text on table by default
This commit is contained in:
= 2026-05-11 09:44:59 +00:00
parent 9443ac181c
commit c8e950bb79
8 changed files with 186 additions and 15 deletions

View File

@ -4,9 +4,19 @@
namespace App\Data\Users;
use Spatie\LaravelData\Data;
use App\Data\Ui\ManageRoles\RoleData;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\{Data, DataCollection};
final class UserData extends Data
{
public function __construct(public string $name, public string $email) {}
public function __construct(
public string $name,
public string $email,
public bool $active = false,
/** @var DataCollection<int, RoleData>|null */
#[DataCollectionOf(class: RoleData::class)]
public ?DataCollection $roles = null,
) {}
}

View File

@ -7,7 +7,7 @@
use Illuminate\Support\{Arr, ServiceProvider};
use Illuminate\Support\Facades\Blade;
final class ScopeServiceProvider extends ServiceProvider
final class DirectiveServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
@ -15,9 +15,19 @@ final class ScopeServiceProvider extends ServiceProvider
public function boot(): void
{
$this->registerScopeDirective();
$this->registerInitialsDirective();
}
public function registerScopeDirective(): void
private function registerInitialsDirective(): void
{
Blade::directive('initials', fn ($expression) => "<?php
echo implode('', array_map(function(\$w) {
return strtoupper(\$w[0] ?? '');
}, explode(' ', $expression)));
?>");
}
private function registerScopeDirective(): void
{
/**
* All credits from this blade directive goes to Konrad Kalemba.

View File

@ -23,7 +23,7 @@ public function __construct(
public bool $withPagination = false,
public array $rowDecoration = [],
public array $cellDecoration = [],
public bool $showEmptyText = false,
public bool $showEmptyText = true,
public string $emptyText = 'No records found.',
public string $containerClass = 'relative overflow-x-auto rounded-2xl -pb-2',
public bool $noHover = false,
@ -173,6 +173,7 @@ public function render(): View|Closure|string
</tr>
</thead>
@isset($rows)
<tbody>
@foreach($rows as $k => $row)
<tr
@ -230,7 +231,7 @@ public function render(): View|Closure|string
</tr>
@endforeach
</tbody>
@endisset
@isset ($footer)
<tfoot @class([
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
@ -241,7 +242,7 @@ public function render(): View|Closure|string
@endisset
</table>
@if(count($rows) === 0)
@if(!$rows || count($rows) === 0)
@if($showEmptyText)
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ $emptyText }}

View File

@ -5,5 +5,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\ScopeServiceProvider::class,
App\Providers\DirectiveServiceProvider::class,
];

View File

@ -85,11 +85,7 @@ public function mount(): void
@php /** @var RoleData $role */ @endphp
<div class="flex -space-x-3">
@foreach($role->users as $user)
<x-shared.avatar>
<span class="text-xs font-bold text-center text-blue-800">
{{implode('', array_map(fn($w) => strtoupper($w[0]), explode(' ', $user->name)))}}
</span>
</x-shared.avatar>
<x-shared.avatar class="text-blue-800" :placeholder="$user->name" />
@endforeach
</div>
@endscope

View File

@ -0,0 +1,138 @@
<?php
use App\Data\Ui\TableHeader;
use App\Data\Users\UserData;
use App\Data\Ui\ManageRoles\RoleData;
use App\Data\Ui\ManageRoles\ServiceData;
use App\Enums\RolesStatus;
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
new class extends Component {
/** @var DataCollection<int, TableHeader> */
#[DataCollectionOf(TableHeader::class)]
public DataCollection $header;
/** @var DataCollection<int, UserData>|null */
#[DataCollectionOf(UserData::class)]
public ?DataCollection $users = null;
public function mount(): void
{
$this->header = TableHeader::collect([
new TableHeader('name', 'User'),
new TableHeader('role.name', 'Role'),
new TableHeader('role.service', 'Service Access'),
new TableHeader('status', 'Status'),
], DataCollection::class);
$this->users = UserData::collect([
new UserData(
name: 'Priya Rao',
email: 'priya@example.com',
active: true,
roles: RoleData::collect([
new RoleData(
name: 'Super Admin',
users: UserData::collect([], DataCollection::class),
services: ServiceData::collect([
new ServiceData(name: 'AWS Core'),
new ServiceData(name: 'GitHub Enterprise'),
], DataCollection::class),
status: RolesStatus::Active,
),
new RoleData(
name: 'HR Manager',
users: UserData::collect([], DataCollection::class),
services: ServiceData::collect([
new ServiceData(name: 'Keka'),
new ServiceData(name: 'Outlook'),
], DataCollection::class),
status: RolesStatus::Active,
)
], DataCollection::class)
),
new UserData(
name: 'Kushal Saha',
email: 'kushal@example.com',
active: true,
roles: RoleData::collect([
new RoleData(
name: 'Backend Developer',
users: UserData::collect([], DataCollection::class),
services: ServiceData::collect([
new ServiceData(name: 'Forge'),
], DataCollection::class),
status: RolesStatus::Active,
)
], DataCollection::class)
),
new UserData(
name: 'Alex Johnson',
email: 'alex@example.com',
active: false,
roles: RoleData::collect([
new RoleData(
name: 'Guest Viewer',
users: UserData::collect([], DataCollection::class),
services: ServiceData::collect([
new ServiceData(name: 'Analytics Dashboard'),
], DataCollection::class),
status: RolesStatus::Active,
)
], DataCollection::class)
)
], DataCollection::class);
}
};
?>
<div>
<x-shared.card title="User Assignments" icon="lucide-users" class="p-0">
<x-slot:actions>
<x-shared.button icon="lucide-plus">
{{ __('Assign user') }}
</x-shared.button>
</x-slot:actions>
<x-table :headers="$header->toArray()" :rows="$users">
@scope('cell_name', $row)
<x-shared.avatar :placeholder="$row->name" :title="$row->name" :subtitle="$row->email" />
@endscope
@scope('cell_role.name', $row)
<div class="flex space-x-2">
@foreach ($row->roles as $role)
<x-shared.badge>
{{$role->name}}
</x-shared.badge>
@endforeach
</div>
@endscope
@scope('cell_role.service', $row)
<div class="flex space-x-2 text-sm">
@foreach ($row->roles as $role)
@foreach($role->services as $service)
{{$service->name}},
@endforeach
@endforeach
</div>
@endscope
@scope('cell_status', $row)
@if($row->active)
<x-shared.badge class="bg-lime-200 text-lime-800 font-bold">Active</x-shared.badge>
@else
<x-shared.badge class="bg-orange-200 text-orange-800 font-bold">Not Active</x-shared.badge>
@endif
@endscope
@scope('actions', $row)
<x-shared.button icon="lucide-square-pen"/>
@endscope
</x-table>
</x-shared.card>
</div>

View File

@ -1,3 +1,18 @@
<span {{$attributes->twMerge(['class' => 'bg-blue-300 w-8 h-8 border border-white rounded-full flex items-center justify-center'])}}>
{{$slot}}
@props(['placeholder', 'title', 'subtitle'])
<div class="flex space-x-2 items-center">
<span {{$attributes->twMerge(['class' => 'bg-blue-300 text-blue-800 w-8 h-8 border border-white rounded-full flex items-center justify-center'])}}>
@isset($placeholder)
@initials($placeholder)
@endisset
</span>
<div class="flex flex-col">
@isset($title)
<p class="font-semibold">
{{$title}}
</p>
@endisset
@isset($subtitle)
{{$subtitle}}
@endisset
</div>
</div>

View File

@ -18,5 +18,6 @@ class extends Component {
<livewire:dashboard.connected-services />
<livewire:dashboard.roles.manager />
<livewire:dashboard.roles.hr-permission />
<livewire:dashboard.users.assignments />
</div>