= 105123bb74 chore: introduce role management functionality with DTOs and Livewire components
- Added Role and User DTOs for dynamic role handling.
- Implemented table and badge components for displaying role data.
- Registered a Blade directive for scoped slots in `ScopeServiceProvider`.
- Updated dashboard views to include role management section.
2026-05-08 13:15:00 +00:00

270 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\View\Component;
final class Table extends Component
{
public string $uuid;
public function __construct(
public array $headers,
public mixed $rows, // Supports Arrays, standard Collections, and Spatie PaginatedDataCollections
public ?string $id = null,
public bool $striped = false,
public bool $noHeaders = false,
public ?string $link = null,
public bool $withPagination = false,
public array $rowDecoration = [],
public array $cellDecoration = [],
public bool $showEmptyText = false,
public string $emptyText = 'No records found.',
public string $containerClass = 'relative overflow-x-auto rounded-2xl -pb-2',
public bool $noHover = false,
// Slots
public mixed $actions = null,
public mixed $cell = null,
public mixed $empty = null,
public mixed $footer = null,
) {
// Temp save closures to prevent Livewire/PHP 8.5 serialization crashes
$rowDecoration = $this->rowDecoration;
$cellDecoration = $this->cellDecoration;
$headers = $this->headers;
// Remove closures from serialization
unset($this->rowDecoration, $this->cellDecoration, $this->headers);
// Serialize a unique ID for component tracking
$this->uuid = "table-" . md5(serialize($this)) . $id;
// Restore closures
$this->rowDecoration = $rowDecoration;
$this->cellDecoration = $cellDecoration;
$this->headers = $headers;
}
// Check if header is hidden
public function isHidden(mixed $header): bool
{
return $header['hidden'] ?? false;
}
// Format header contents
public function format(mixed $row, mixed $field, mixed $header): mixed
{
$format = $header['format'] ?? null;
if ( ! $format) {
return $field;
}
if (is_callable($format)) {
return $format($row, $field);
}
if ('currency' === $format[0]) {
return ($format[2] ?? '') . number_format((float) $field, ...mb_str_split($format[1]));
}
if ('date' === $format[0] && $field) {
return Carbon::parse($field)->translatedFormat($format[1]);
}
return $field;
}
// Check if link should be shown in cell
public function hasLink(mixed $header): bool
{
return $this->link && empty($header['disableLink']);
}
// Build row link
public function redirectLink(mixed $row): string
{
$link = $this->link;
// Transform from `route()` pattern
$link = Str::of($link)->replace('%5B', '{')->replace('%5D', '}');
// Extract tokens like {id}, {city.name} ...
$tokens = Str::of($link)->matchAll('/\{(.*?)\}/');
// Replace tokens with actual row values
$tokens->each(function (string $token) use ($row, &$link): void {
$link = Str::of($link)->replace("{" . $token . "}", data_get($row, $token))->toString();
});
return $link;
}
public function rowClasses(mixed $row): ?string
{
$classes = [];
foreach ($this->rowDecoration as $class => $condition) {
if ($condition($row)) {
$classes[] = $class;
}
}
return Arr::join($classes, ' ');
}
public function cellClasses(mixed $row, array $header): ?string
{
$classes = Str::of($header['class'] ?? '')->explode(' ')->all();
foreach ($this->cellDecoration[$header['key']] ?? [] as $class => $condition) {
if ($condition($row)) {
$classes[] = $class;
}
}
return Arr::join($classes, ' ');
}
public function render(): View|Closure|string
{
return <<<'HTML'
<div class="w-full">
<div class="{{ $containerClass }}">
<table
{{
$attributes
->whereDoesntStartWith('wire:model')
->class([
'w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400',
])
}}
>
<thead @class([
"text-xs text-gray-700 uppercase bg-gray-50 border-b border-gray-200",
"hidden" => $noHeaders
])>
<tr>
@foreach($headers as $header)
@php
if($isHidden($header)) continue;
# Scoped slot's name like `user.city` are compiled to `user___city` through `@scope / @endscope`.
# So we use current `$header` key to find that slot on context.
$temp_key = str_replace('.', '___', $header['key'])
@endphp
<th scope="col" class="px-6 py-4 {{ $header['class'] ?? '' }}">
{{ isset(${"header_".$temp_key}) ? ${"header_".$temp_key}($header) : $header['label'] }}
</th>
@endforeach
@if($actions)
<th scope="col" class="px-6 py-3 w-1 whitespace-nowrap">
<span class="sr-only">Actions</span>
</th>
@endif
</tr>
</thead>
<tbody>
@foreach($rows as $k => $row)
<tr
wire:key="{{ $uuid }}-{{ $k }}"
@class([
'bg-white border-b',
$rowClasses($row),
'even:bg-gray-50 ' => $striped,
'hover:bg-gray-50' => !$noHover,
'cursor-pointer' => $attributes->has('@row-click') || $link
])
@if($attributes->has('@row-click'))
@click="$dispatch('row-click', {{ json_encode($row) }});"
@endif
>
@foreach($headers as $header)
@php
if($isHidden($header)) continue;
$temp_key = str_replace('.', '___', $header['key'])
@endphp
@if(isset(${"cell_".$temp_key}))
<td @class(["px-6 py-4", $cellClasses($row, $header), "p-0" => $hasLink($header)])>
@if($hasLink($header))
<a href="{{ $redirectLink($row) }}" wire:navigate class="block px-6 py-4">
@endif
{{ ${"cell_".$temp_key}($row) }}
@if($hasLink($header))
</a>
@endif
</td>
@else
<td @class(["px-6 py-4", $cellClasses($row, $header), "p-0" => $hasLink($header)])>
@if($hasLink($header))
<a href="{{ $redirectLink($row) }}" wire:navigate class="block px-6 py-4 text-inherit">
@endif
{{ $format($row, data_get($row, $header['key']), $header) }}
@if($hasLink($header))
</a>
@endif
</td>
@endif
@endforeach
@if($actions)
<td class="px-6 py-4 text-right whitespace-nowrap">
{{ $actions($row) }}
</td>
@endif
</tr>
@endforeach
</tbody>
@isset ($footer)
<tfoot @class([
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
$footer->attributes->get('class') ?? ''
])>
{{ $footer }}
</tfoot>
@endisset
</table>
@if(count($rows) === 0)
@if($showEmptyText)
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ $emptyText }}
</div>
@endif
@if($empty)
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ $empty }}
</div>
@endif
@endif
</div>
@if($withPagination && method_exists($rows, 'links'))
<div class="mt-4 px-2">
{{ $rows->links() }}
</div>
@endif
</div>
HTML;
}
}