458 lines
19 KiB
PHP
458 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 dark:text-zinc-400 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 dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 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="ghost">
|
|
{{ __('common.clear') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sort Headers -->
|
|
<div class="hidden lg:flex bg-zinc-100 dark:bg-zinc-700 rounded-t-lg px-4 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-300 gap-4 mb-0">
|
|
<button wire:click="sort('booking_date')" class="flex items-center gap-1 w-32 hover:text-zinc-900 dark:hover:text-white">
|
|
{{ __('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 dark:hover:text-white">
|
|
{{ __('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 dark:bg-zinc-800 p-4 border border-zinc-200 dark:border-zinc-700 {{ $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 dark:text-zinc-100">
|
|
{{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('d M Y') }}
|
|
</div>
|
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
|
{{ \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 dark:text-zinc-100 hover:text-blue-600 dark:hover:text-blue-400" wire:navigate>
|
|
{{ $consultation->user?->full_name ?? __('common.unknown') }}
|
|
</a>
|
|
<flux:badge size="sm" variant="{{ $consultation->consultation_type === \App\Enums\ConsultationType::Paid ? 'primary' : 'outline' }}">
|
|
{{ $consultation->consultation_type->label() }}
|
|
</flux:badge>
|
|
</div>
|
|
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
{{ $consultation->user?->email ?? '-' }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Badge -->
|
|
<div class="lg:w-24">
|
|
@php
|
|
$statusVariant = match($consultation->status) {
|
|
\App\Enums\ConsultationStatus::Pending => 'warning',
|
|
\App\Enums\ConsultationStatus::Approved => 'primary',
|
|
\App\Enums\ConsultationStatus::Completed => 'success',
|
|
\App\Enums\ConsultationStatus::Cancelled => 'danger',
|
|
\App\Enums\ConsultationStatus::NoShow => 'danger',
|
|
\App\Enums\ConsultationStatus::Rejected => 'danger',
|
|
};
|
|
@endphp
|
|
<flux:badge variant="{{ $statusVariant }}" size="sm">
|
|
{{ $consultation->status->label() }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<!-- Payment Status -->
|
|
<div class="lg:w-24">
|
|
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid)
|
|
@php
|
|
$paymentVariant = match($consultation->payment_status) {
|
|
\App\Enums\PaymentStatus::Pending => 'warning',
|
|
\App\Enums\PaymentStatus::Received => 'success',
|
|
default => 'outline',
|
|
};
|
|
@endphp
|
|
<flux:badge variant="{{ $paymentVariant }}" 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="filled"
|
|
size="sm"
|
|
wire:navigate
|
|
>
|
|
{{ __('common.edit') }}
|
|
</flux:button>
|
|
|
|
@if($consultation->status === \App\Enums\ConsultationStatus::Approved)
|
|
<flux:dropdown>
|
|
<flux:button variant="ghost" size="sm" icon="ellipsis-vertical" />
|
|
|
|
<flux:menu>
|
|
<flux:menu.item
|
|
wire:click="markCompleted({{ $consultation->id }})"
|
|
wire:confirm="{{ __('admin.confirm_mark_completed') }}"
|
|
icon="check-circle"
|
|
>
|
|
{{ __('admin.mark_completed') }}
|
|
</flux:menu.item>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
</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 dark:text-zinc-400 bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
|
<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>
|