356 lines
14 KiB
PHP
356 lines
14 KiB
PHP
<?php
|
|
|
|
use App\Enums\TimelineStatus;
|
|
use App\Models\AdminLog;
|
|
use App\Models\Timeline;
|
|
use App\Models\User;
|
|
use Livewire\Volt\Component;
|
|
use Livewire\WithPagination;
|
|
|
|
new class extends Component
|
|
{
|
|
use WithPagination;
|
|
|
|
public string $search = '';
|
|
public string $clientFilter = '';
|
|
public string $statusFilter = '';
|
|
public string $dateFrom = '';
|
|
public string $dateTo = '';
|
|
public string $sortBy = 'updated_at';
|
|
public string $sortDir = 'desc';
|
|
public int $perPage = 15;
|
|
|
|
public function updatedSearch(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedClientFilter(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedStatusFilter(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedDateFrom(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedDateTo(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedPerPage(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function sort(string $column): void
|
|
{
|
|
if ($this->sortBy === $column) {
|
|
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
$this->sortBy = $column;
|
|
$this->sortDir = 'asc';
|
|
}
|
|
}
|
|
|
|
public function clearFilters(): void
|
|
{
|
|
$this->search = '';
|
|
$this->clientFilter = '';
|
|
$this->statusFilter = '';
|
|
$this->dateFrom = '';
|
|
$this->dateTo = '';
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function toggleArchive(int $id): void
|
|
{
|
|
$timeline = Timeline::findOrFail($id);
|
|
|
|
if ($timeline->isArchived()) {
|
|
$timeline->unarchive();
|
|
$action = 'unarchive';
|
|
$message = __('messages.timeline_unarchived');
|
|
} else {
|
|
$timeline->archive();
|
|
$action = 'archive';
|
|
$message = __('messages.timeline_archived');
|
|
}
|
|
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action' => $action,
|
|
'target_type' => 'timeline',
|
|
'target_id' => $timeline->id,
|
|
'ip_address' => request()->ip(),
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
session()->flash('success', $message);
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'timelines' => Timeline::query()
|
|
->with(['user:id,full_name,email', 'updates' => fn ($q) => $q->latest()->limit(1)])
|
|
->withCount('updates')
|
|
->when($this->search, fn ($q) => $q->where(function ($q) {
|
|
$q->where('case_name', 'like', "%{$this->search}%")
|
|
->orWhere('case_reference', 'like', "%{$this->search}%");
|
|
}))
|
|
->when($this->clientFilter, fn ($q) => $q->where('user_id', $this->clientFilter))
|
|
->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter))
|
|
->when($this->dateFrom, fn ($q) => $q->where('created_at', '>=', $this->dateFrom))
|
|
->when($this->dateTo, fn ($q) => $q->where('created_at', '<=', $this->dateTo . ' 23:59:59'))
|
|
->orderBy($this->sortBy, $this->sortDir)
|
|
->paginate($this->perPage),
|
|
'clients' => User::query()
|
|
->whereHas('timelines')
|
|
->orderBy('full_name')
|
|
->get(['id', 'full_name']),
|
|
'statuses' => TimelineStatus::cases(),
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div class="max-w-7xl mx-auto">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
|
<div>
|
|
<flux:heading size="xl">{{ __('timelines.timelines') }}</flux:heading>
|
|
<p class="text-sm text-zinc-500 mt-1">{{ __('timelines.timelines_description') }}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<flux:button href="{{ route('admin.timelines.export') }}" icon="arrow-down-tray" wire:navigate>
|
|
{{ __('export.export_timelines') }}
|
|
</flux:button>
|
|
<flux:button href="{{ route('admin.timelines.create') }}" variant="primary" icon="plus" wire:navigate>
|
|
{{ __('timelines.create_timeline') }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
@if(session('success'))
|
|
<flux:callout variant="success" class="mb-6">
|
|
{{ session('success') }}
|
|
</flux:callout>
|
|
@endif
|
|
|
|
@if(session('error'))
|
|
<flux:callout variant="danger" class="mb-6">
|
|
{{ session('error') }}
|
|
</flux:callout>
|
|
@endif
|
|
|
|
<!-- Filters -->
|
|
<div class="bg-white rounded-lg p-4 border border-zinc-200 mb-6">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
|
<flux:field>
|
|
<flux:input
|
|
wire:model.live.debounce.300ms="search"
|
|
placeholder="{{ __('timelines.search_placeholder') }}"
|
|
icon="magnifying-glass"
|
|
/>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:select wire:model.live="clientFilter">
|
|
<option value="">{{ __('timelines.all_clients') }}</option>
|
|
@foreach($clients as $client)
|
|
<option value="{{ $client->id }}">{{ $client->full_name }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:select wire:model.live="statusFilter">
|
|
<option value="">{{ __('admin.all_statuses') }}</option>
|
|
@foreach($statuses as $status)
|
|
<option value="{{ $status->value }}">{{ $status->label() }}</option>
|
|
@endforeach
|
|
</flux:select>
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>{{ __('admin.per_page') }}</flux:label>
|
|
<flux:select wire:model.live="perPage">
|
|
<option value="15">15</option>
|
|
<option value="25">25</option>
|
|
<option value="50">50</option>
|
|
</flux:select>
|
|
</flux:field>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-4 items-end">
|
|
<flux:field class="flex-1">
|
|
<flux:label>{{ __('admin.date_from') }}</flux:label>
|
|
<flux:input type="date" wire:model.live="dateFrom" />
|
|
</flux:field>
|
|
|
|
<flux:field class="flex-1">
|
|
<flux:label>{{ __('admin.date_to') }}</flux:label>
|
|
<flux:input type="date" wire:model.live="dateTo" />
|
|
</flux:field>
|
|
|
|
@if($search || $clientFilter || $statusFilter || $dateFrom || $dateTo)
|
|
<flux:button wire:click="clearFilters" variant="ghost">
|
|
{{ __('common.clear') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sort Headers -->
|
|
<div class="hidden lg:flex bg-zinc-100 rounded-t-lg px-4 py-2 text-sm font-medium text-zinc-600 gap-4 mb-0">
|
|
<button wire:click="sort('case_name')" class="flex items-center gap-1 flex-1 hover:text-zinc-900">
|
|
{{ __('timelines.case_name') }}
|
|
@if($sortBy === 'case_name')
|
|
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
|
@endif
|
|
</button>
|
|
<button wire:click="sort('user_id')" class="flex items-center gap-1 w-40 hover:text-zinc-900">
|
|
{{ __('admin.client') }}
|
|
@if($sortBy === 'user_id')
|
|
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
|
@endif
|
|
</button>
|
|
<span class="w-24">{{ __('admin.current_status') }}</span>
|
|
<button wire:click="sort('updated_at')" class="flex items-center gap-1 w-32 hover:text-zinc-900">
|
|
{{ __('timelines.last_updated') }}
|
|
@if($sortBy === 'updated_at')
|
|
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
|
@endif
|
|
</button>
|
|
<button wire:click="sort('created_at')" class="flex items-center gap-1 w-32 hover:text-zinc-900">
|
|
{{ __('timelines.created') }}
|
|
@if($sortBy === 'created_at')
|
|
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
|
@endif
|
|
</button>
|
|
<span class="w-20 text-center">{{ __('timelines.updates_count') }}</span>
|
|
<span class="w-32">{{ __('common.actions') }}</span>
|
|
</div>
|
|
|
|
<!-- Timelines List -->
|
|
<div class="space-y-0">
|
|
@forelse($timelines as $timeline)
|
|
<div wire:key="timeline-{{ $timeline->id }}" class="bg-white p-4 border border-zinc-200 {{ $loop->first ? 'rounded-t-lg lg:rounded-t-none' : '' }} {{ $loop->last ? 'rounded-b-lg' : '' }} {{ !$loop->first ? 'border-t-0' : '' }}">
|
|
<div class="flex flex-col lg:flex-row lg:items-center gap-4">
|
|
<!-- Case Name -->
|
|
<div class="flex-1">
|
|
<a href="{{ route('admin.timelines.show', $timeline) }}" class="font-semibold text-zinc-900 hover:text-blue-600" wire:navigate>
|
|
{{ $timeline->case_name }}
|
|
</a>
|
|
@if($timeline->case_reference)
|
|
<div class="text-xs text-zinc-500 mt-1">
|
|
{{ __('timelines.reference') }}: {{ $timeline->case_reference }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Client -->
|
|
<div class="lg:w-40">
|
|
<div class="text-sm text-zinc-900">
|
|
{{ $timeline->user->full_name }}
|
|
</div>
|
|
<div class="text-xs text-zinc-500">
|
|
{{ $timeline->user->email }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Badge -->
|
|
<div class="lg:w-24">
|
|
<flux:badge :color="$timeline->isArchived() ? 'amber' : 'green'" size="sm">
|
|
{{ $timeline->status->label() }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<!-- Last Updated -->
|
|
<div class="lg:w-32">
|
|
<div class="text-sm text-zinc-900">
|
|
{{ $timeline->updated_at->diffForHumans() }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Created -->
|
|
<div class="lg:w-32">
|
|
<div class="text-sm text-zinc-500">
|
|
{{ $timeline->created_at->format('Y-m-d') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Updates Count -->
|
|
<div class="lg:w-20 text-center">
|
|
<flux:badge variant="outline" size="sm">
|
|
{{ $timeline->updates_count }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="lg:w-32 flex gap-2">
|
|
<flux:button
|
|
href="{{ route('admin.timelines.show', $timeline) }}"
|
|
variant="filled"
|
|
size="sm"
|
|
wire:navigate
|
|
>
|
|
{{ __('timelines.view') }}
|
|
</flux:button>
|
|
|
|
<flux:dropdown>
|
|
<flux:button variant="ghost" size="sm" icon="ellipsis-vertical" />
|
|
|
|
<flux:menu>
|
|
<flux:menu.item
|
|
href="{{ route('admin.timelines.show', $timeline) }}"
|
|
icon="eye"
|
|
wire:navigate
|
|
>
|
|
{{ __('timelines.view') }}
|
|
</flux:menu.item>
|
|
|
|
@if($timeline->isActive())
|
|
<flux:menu.item
|
|
href="{{ route('admin.timelines.show', $timeline) }}"
|
|
icon="plus"
|
|
wire:navigate
|
|
>
|
|
{{ __('timelines.add_update') }}
|
|
</flux:menu.item>
|
|
@endif
|
|
|
|
<flux:menu.separator />
|
|
|
|
<flux:menu.item
|
|
wire:click="toggleArchive({{ $timeline->id }})"
|
|
wire:confirm="{{ $timeline->isActive() ? __('timelines.archive_confirm_message') : __('timelines.unarchive_confirm_message') }}"
|
|
icon="{{ $timeline->isActive() ? 'archive-box' : 'archive-box-arrow-down' }}"
|
|
variant="{{ $timeline->isActive() ? 'danger' : 'default' }}"
|
|
>
|
|
{{ $timeline->isActive() ? __('timelines.archive') : __('timelines.unarchive') }}
|
|
</flux:menu.item>
|
|
</flux:menu>
|
|
</flux:dropdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="text-center py-12 text-zinc-500 bg-white rounded-lg border border-zinc-200">
|
|
<flux:icon name="inbox" class="w-12 h-12 mx-auto mb-4" />
|
|
<p>{{ __('timelines.no_timelines') }}</p>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
{{ $timelines->links() }}
|
|
</div>
|
|
</div>
|