- 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.
270 lines
10 KiB
PHP
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;
|
|
}
|
|
}
|