libra/resources/views/livewire/admin/consultations/index.blade.php

464 lines
19 KiB
PHP

<?php
use App\Enums\ConsultationStatus;
use App\Enums\ConsultationType;
use App\Enums\PaymentStatus;
use App\Models\AdminLog;
use App\Models\Consultation;
use App\Notifications\ConsultationCancelled;
use Illuminate\Support\Facades\DB;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component
{
use WithPagination;
public string $search = '';
public string $statusFilter = '';
public string $typeFilter = '';
public string $paymentFilter = '';
public string $dateFrom = '';
public string $dateTo = '';
public string $sortBy = 'booking_date';
public string $sortDir = 'desc';
public int $perPage = 15;
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function updatedTypeFilter(): void
{
$this->resetPage();
}
public function updatedPaymentFilter(): 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->statusFilter = '';
$this->typeFilter = '';
$this->paymentFilter = '';
$this->dateFrom = '';
$this->dateTo = '';
$this->resetPage();
}
public function markCompleted(int $id): void
{
DB::transaction(function () use ($id) {
$consultation = Consultation::lockForUpdate()->findOrFail($id);
$oldStatus = $consultation->status->value;
try {
$consultation->markAsCompleted();
$this->logStatusChange($consultation, $oldStatus, 'completed');
session()->flash('success', __('messages.marked_completed'));
} catch (\InvalidArgumentException $e) {
session()->flash('error', $e->getMessage());
}
});
}
public function markNoShow(int $id): void
{
DB::transaction(function () use ($id) {
$consultation = Consultation::lockForUpdate()->findOrFail($id);
$oldStatus = $consultation->status->value;
try {
$consultation->markAsNoShow();
$this->logStatusChange($consultation, $oldStatus, 'no_show');
session()->flash('success', __('messages.marked_no_show'));
} catch (\InvalidArgumentException $e) {
session()->flash('error', $e->getMessage());
}
});
}
public function cancel(int $id): void
{
DB::transaction(function () use ($id) {
$consultation = Consultation::lockForUpdate()->with('user')->findOrFail($id);
$oldStatus = $consultation->status->value;
try {
$consultation->cancel();
// Notify client
if ($consultation->user) {
$consultation->user->notify(new ConsultationCancelled($consultation));
}
$this->logStatusChange($consultation, $oldStatus, 'cancelled');
session()->flash('success', __('messages.consultation_cancelled'));
} catch (\InvalidArgumentException $e) {
session()->flash('error', $e->getMessage());
}
});
}
public function markPaymentReceived(int $id): void
{
DB::transaction(function () use ($id) {
$consultation = Consultation::lockForUpdate()->findOrFail($id);
try {
$consultation->markPaymentReceived();
AdminLog::create([
'admin_id' => auth()->id(),
'action' => 'payment_received',
'target_type' => 'consultation',
'target_id' => $consultation->id,
'old_values' => ['payment_status' => 'pending'],
'new_values' => ['payment_status' => 'received'],
'ip_address' => request()->ip(),
'created_at' => now(),
]);
session()->flash('success', __('messages.payment_marked_received'));
} catch (\InvalidArgumentException $e) {
session()->flash('error', $e->getMessage());
}
});
}
private function logStatusChange(Consultation $consultation, string $oldStatus, string $newStatus): void
{
AdminLog::create([
'admin_id' => auth()->id(),
'action' => 'status_change',
'target_type' => 'consultation',
'target_id' => $consultation->id,
'old_values' => ['status' => $oldStatus],
'new_values' => ['status' => $newStatus],
'ip_address' => request()->ip(),
'created_at' => now(),
]);
}
public function with(): array
{
return [
'consultations' => Consultation::query()
->with('user:id,full_name,email,phone,user_type')
->when($this->search, fn ($q) => $q->whereHas('user', fn ($uq) =>
$uq->where('full_name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%")
))
->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter))
->when($this->typeFilter, fn ($q) => $q->where('consultation_type', $this->typeFilter))
->when($this->paymentFilter, fn ($q) => $q->where('payment_status', $this->paymentFilter))
->when($this->dateFrom, fn ($q) => $q->where('booking_date', '>=', $this->dateFrom))
->when($this->dateTo, fn ($q) => $q->where('booking_date', '<=', $this->dateTo))
->orderBy($this->sortBy, $this->sortDir)
->paginate($this->perPage),
'statuses' => ConsultationStatus::cases(),
'types' => ConsultationType::cases(),
'paymentStatuses' => PaymentStatus::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">{{ __('admin.consultations') }}</flux:heading>
<p class="text-sm text-zinc-500 mt-1">{{ __('admin.consultations_description') }}</p>
</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="{{ __('admin.search_clients') }}"
icon="magnifying-glass"
/>
</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:select wire:model.live="typeFilter">
<option value="">{{ __('admin.all_types') }}</option>
@foreach($types as $type)
<option value="{{ $type->value }}">{{ $type->label() }}</option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:select wire:model.live="paymentFilter">
<option value="">{{ __('admin.all_payments') }}</option>
@foreach($paymentStatuses as $ps)
<option value="{{ $ps->value }}">{{ $ps->label() }}</option>
@endforeach
</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>
<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>
@if($search || $statusFilter || $typeFilter || $paymentFilter || $dateFrom || $dateTo)
<flux:button wire:click="clearFilters" variant="outline">
{{ __('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('booking_date')" class="flex items-center gap-1 w-32 hover:text-zinc-900">
{{ __('admin.date') }}
@if($sortBy === 'booking_date')
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
@endif
</button>
<span class="flex-1">{{ __('admin.client') }}</span>
<button wire:click="sort('status')" class="flex items-center gap-1 w-24 hover:text-zinc-900">
{{ __('admin.current_status') }}
@if($sortBy === 'status')
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
@endif
</button>
<span class="w-24">{{ __('admin.payment_status') }}</span>
<span class="w-48">{{ __('common.actions') }}</span>
</div>
<!-- Consultations List -->
<div class="space-y-0">
@forelse($consultations as $consultation)
<div wire:key="consultation-{{ $consultation->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">
<!-- Date/Time -->
<div class="lg:w-32">
<div class="text-sm font-medium text-zinc-900">
{{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('d M Y') }}
</div>
<div class="text-xs text-zinc-500">
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
</div>
</div>
<!-- Client Info -->
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<a href="{{ route('admin.consultations.show', $consultation) }}" class="font-semibold text-zinc-900 hover:text-blue-600" wire:navigate>
{{ $consultation->getClientName() }}
</a>
<flux:badge size="sm" color="{{ $consultation->consultation_type === \App\Enums\ConsultationType::Paid ? 'indigo' : 'zinc' }}">
{{ $consultation->consultation_type->label() }}
</flux:badge>
</div>
<div class="text-sm text-zinc-500">
{{ $consultation->getClientEmail() }}
</div>
</div>
<!-- Status Badge -->
<div class="lg:w-24">
@php
$statusColor = match($consultation->status) {
\App\Enums\ConsultationStatus::Pending => 'amber',
\App\Enums\ConsultationStatus::Approved => 'sky',
\App\Enums\ConsultationStatus::Completed => 'green',
\App\Enums\ConsultationStatus::Cancelled => 'red',
\App\Enums\ConsultationStatus::NoShow => 'orange',
\App\Enums\ConsultationStatus::Rejected => 'rose',
};
@endphp
<flux:badge color="{{ $statusColor }}" size="sm">
{{ $consultation->status->label() }}
</flux:badge>
</div>
<!-- Payment Status -->
<div class="lg:w-24">
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid)
@php
$paymentColor = match($consultation->payment_status) {
\App\Enums\PaymentStatus::Pending => 'amber',
\App\Enums\PaymentStatus::Received => 'green',
default => 'zinc',
};
@endphp
<flux:badge color="{{ $paymentColor }}" size="sm">
{{ $consultation->payment_status->label() }}
</flux:badge>
@else
<span class="text-xs text-zinc-400">-</span>
@endif
</div>
<!-- Actions -->
<div class="lg:w-48 flex flex-wrap gap-2">
<flux:button
href="{{ route('admin.consultations.show', $consultation) }}"
variant="outline"
size="sm"
wire:navigate
>
{{ __('common.edit') }}
</flux:button>
@if(in_array($consultation->status, [\App\Enums\ConsultationStatus::Approved, \App\Enums\ConsultationStatus::Completed, \App\Enums\ConsultationStatus::NoShow]))
<flux:dropdown>
<flux:button variant="outline" size="sm" icon="ellipsis-vertical" />
<flux:menu>
@if($consultation->status !== \App\Enums\ConsultationStatus::Completed)
<flux:menu.item
wire:click="markCompleted({{ $consultation->id }})"
wire:confirm="{{ __('admin.confirm_mark_completed') }}"
icon="check-circle"
>
{{ __('admin.mark_completed') }}
</flux:menu.item>
@endif
@if($consultation->status !== \App\Enums\ConsultationStatus::NoShow)
<flux:menu.item
wire:click="markNoShow({{ $consultation->id }})"
wire:confirm="{{ __('admin.confirm_mark_no_show') }}"
icon="x-circle"
>
{{ __('admin.mark_no_show') }}
</flux:menu.item>
@endif
@if($consultation->status === \App\Enums\ConsultationStatus::Approved)
<flux:menu.separator />
<flux:menu.item
wire:click="cancel({{ $consultation->id }})"
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
icon="trash"
variant="danger"
>
{{ __('admin.cancel_consultation') }}
</flux:menu.item>
@endif
</flux:menu>
</flux:dropdown>
@endif
@if($consultation->status === \App\Enums\ConsultationStatus::Pending)
<flux:button
wire:click="cancel({{ $consultation->id }})"
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
variant="danger"
size="sm"
>
{{ __('common.cancel') }}
</flux:button>
@endif
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid && $consultation->payment_status === \App\Enums\PaymentStatus::Pending)
<flux:button
wire:click="markPaymentReceived({{ $consultation->id }})"
wire:confirm="{{ __('admin.confirm_mark_payment') }}"
variant="primary"
size="sm"
>
{{ __('admin.payment_received') }}
</flux:button>
@endif
</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>{{ __('admin.no_consultations') }}</p>
</div>
@endforelse
</div>
<div class="mt-6">
{{ $consultations->links() }}
</div>
</div>