feature: add role permission table for HR Manager

- Add TableHeader DTO
- Refactor Toggle component to have checked prop, as wire:model does not work
- add null collasing for php 8.5 strict compatibility in scope directive
- refactor card component so that actions stay on the right
- make button component scaled small on active and change color on hover
This commit is contained in:
= 2026-05-11 07:16:31 +00:00
parent ede0064b54
commit ca628fb009
10 changed files with 129 additions and 38 deletions

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ManageRoles;
use Spatie\LaravelData\Data;
final class PermissionData extends Data
{
public function __construct(
public string $name,
public bool $view,
public bool $create,
public bool $update,
public bool $delete,
) {}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui;
use Spatie\LaravelData\Data;
final class TableHeader extends Data
{
public function __construct(
public string $key,
public string $label,
public ?array $format = null,
) {}
}

View File

@ -17,6 +17,7 @@ public function boot(): void
{
$this->registerScopeDirective();
}
public function registerScopeDirective(): void
{
/**
@ -30,7 +31,10 @@ public function registerScopeDirective(): void
$directiveArguments = preg_split("/,(?![^\(\(]*[\)\)])/", $expression);
$directiveArguments = array_map("trim", $directiveArguments);
[$name, $functionArguments] = $directiveArguments;
// PHP 8.5 Safe Unpacking (Prevents "Undefined array key" crashes)
$name = $directiveArguments[0] ?? "''";
$functionArguments = $directiveArguments[1] ?? '';
// Build function "uses" to inject extra external variables
$uses = Arr::except(array_flip($directiveArguments), [$name, $functionArguments]);
$uses = array_flip($uses);
@ -46,6 +50,7 @@ public function registerScopeDirective(): void
*/
$name = str_replace(".", "___", $name);
// PHP 8.5 Safe Loop Check
return "<?php \$__bladeCompiler = \$__bladeCompiler ?? null; \$__env->slot({$name}, function({$functionArguments}) use ({$uses}) { \$loop = !empty(\$__env->getLoopStack()) ? (object) \$__env->getLoopStack()[0] : null; ?>";
});

View File

@ -57,10 +57,10 @@
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"lint": [
"pint --parallel"
"pint --parallel --dirty"
],
"lint:check": [
"pint --parallel --test"
"pint --parallel --dirty --test"
],
"ci:check": [
"Composer\\Config::disableProcessTimeout",

View File

@ -0,0 +1,60 @@
<?php
use App\Data\Ui\ManageRoles\PermissionData;
use App\Data\Ui\TableHeader;
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
new class extends Component {
#[DataCollectionOf(TableHeader::class)]
public DataCollection $header;
#[DataCollectionOf(PermissionData::class)]
public DataCollection $rolePermissions;
public function mount(): void
{
$this->header = TableHeader::collect([
new TableHeader('name', 'Permission'),
new TableHeader('view', 'View'),
new TableHeader('create', 'Create'),
new TableHeader('update', 'Edit'),
new TableHeader('delete', 'Delete'),
], DataCollection::class);
$this->rolePermissions = PermissionData::collect([
new PermissionData('Outlook - emails', true, true, false, false),
new PermissionData('Teams - Messages', true, false, true, true),
], DataCollection::class);
}
};
?>
<div>
<x-shared.card title="Role Permission - HR Manager" icon="lucide-lock" class="p-0">
<x-slot:actions>
<x-shared.button icon="lucide-x">
{{ __('Revoke All') }}
</x-shared.button>
<x-shared.button icon="lucide-check">
{{ __('Save') }}
</x-shared.button>
</x-slot:actions>
<x-table :headers="$header->toArray()" :rows="$rolePermissions">
@scope('cell_view', $row)
<x-shared.toggle :checked="$row->view" />
@endscope
@scope('cell_create', $row)
<x-shared.toggle :checked="$row->create" />
@endscope
@scope('cell_update', $row)
<x-shared.toggle :checked="$row->update" />
@endscope
@scope('cell_delete', $row)
<x-shared.toggle :checked="$row->update" />
@endscope
</x-table>
</x-shared.card>
</div>

View File

@ -1,34 +1,24 @@
<?php
use App\Data\Ui\ManageRoles\RoleData;
use App\Data\Ui\ManageRoles\RolesCollection;
use App\Data\Ui\ManageRoles\RolesData;
use App\Data\Ui\ManageRoles\ServiceCollection;
use App\Data\Ui\ManageRoles\ServiceData;
use App\Data\Users\UserCollection;
use App\Data\Ui\TableHeader;
use App\Data\Users\UserData;
use App\Data\Users\UsersData;
use App\Enums\RolesStatus;
use Livewire\Component;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\DataCollection;
new class extends Component {
/**
* @var array<int, array{
* key: string,
* label: string,
* format: array{0: string, 1: string}|null
* }>
*/
public array $header;
#[DataCollectionOf(TableHeader::class)]
public DataCollection $header;
#[DataCollectionOf(class: RoleData::class)]
public DataCollection $roles;
public function mount(): void
{
$this->header = [
$this->header = TableHeader::collect([
[
"key" => "name",
"label" => "Role Name",
@ -45,7 +35,7 @@ public function mount(): void
"key" => "status",
"label" => "Status",
],
];
], DataCollection::class);
$this->roles = RoleData::collect(
[
new RoleData(
@ -91,7 +81,7 @@ public function mount(): void
</x-shared.button>
</x-slot:actions>
<x-table :headers="$header" :rows="$roles">
<x-table :headers="$header->toArray()" :rows="$roles">
@scope('cell_users', $role)
@php /** @var RoleData $role */ @endphp
<div class="flex -space-x-3">

View File

@ -1,5 +1,5 @@
@props(['icon' => null])
<button {{$attributes->twMerge(['class' => 'rounded-lg border border-gray-300 px-4 py-2 text-gray-700'])}}>
<button {{$attributes->twMerge(['class' => 'rounded-lg border border-gray-300 px-4 py-2 text-gray-700 active:scale-75 hover:bg-gray-100 transition duration-150 ease-in-out'])}}>
<div class="gap-2 flex items-center">
@if($icon)
<span class="block">

View File

@ -4,16 +4,16 @@
<div class="p-4 flex justify-between border-b border-gray-200">
<h2 class="font-medium flex items-center gap-x-2">
@isset($icon)
<span class="w-5 h-5">
@svg($icon)
</span>
<span class="w-5 h-5">@svg($icon)</span>
@endisset
{{ __($title) }}
</h2>
<div class="">
@isset($actions)
{{$actions}}
@endisset
</div>
</div>
@endisset
{{$slot}}
</div>

View File

@ -1,10 +1,11 @@
@props(['disabled' => false, 'label' => null])
@props(['disabled' => false, 'label' => null, 'checked' => false])
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
@checked($checked)
{{ $disabled ? 'disabled' : '' }}
{!! $attributes->merge(['class' => 'sr-only peer']) !!}
{{ $attributes->merge(['class' => 'sr-only peer']) }}
>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>

View File

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