294 lines
11 KiB
PHP
294 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Enums\ConsultationType;
|
|
use App\Enums\PaymentStatus;
|
|
use App\Mail\GuestBookingApprovedMail;
|
|
use App\Mail\GuestBookingRejectedMail;
|
|
use App\Models\AdminLog;
|
|
use App\Models\Consultation;
|
|
use App\Notifications\BookingApproved;
|
|
use App\Notifications\BookingRejected;
|
|
use App\Services\CalendarService;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Livewire\Volt\Component;
|
|
use Livewire\WithPagination;
|
|
|
|
new class extends Component
|
|
{
|
|
use WithPagination;
|
|
|
|
public string $dateFrom = '';
|
|
public string $dateTo = '';
|
|
|
|
public function updatedDateFrom(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedDateTo(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function clearFilters(): void
|
|
{
|
|
$this->dateFrom = '';
|
|
$this->dateTo = '';
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function quickApprove(int $id): void
|
|
{
|
|
$consultation = Consultation::with('user')->findOrFail($id);
|
|
|
|
if ($consultation->status !== ConsultationStatus::Pending) {
|
|
session()->flash('error', __('admin.booking_already_processed'));
|
|
|
|
return;
|
|
}
|
|
|
|
$oldStatus = $consultation->status->value;
|
|
|
|
$consultation->update([
|
|
'status' => ConsultationStatus::Approved,
|
|
'consultation_type' => ConsultationType::Free,
|
|
'payment_status' => PaymentStatus::NotApplicable,
|
|
]);
|
|
|
|
// Generate calendar file and send notification
|
|
$icsContent = null;
|
|
try {
|
|
$calendarService = app(CalendarService::class);
|
|
$icsContent = $calendarService->generateIcs($consultation);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to generate calendar file', [
|
|
'consultation_id' => $consultation->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
// Send appropriate notification/email based on guest/client
|
|
if ($consultation->isGuest()) {
|
|
Mail::to($consultation->guest_email)->queue(
|
|
new GuestBookingApprovedMail(
|
|
$consultation,
|
|
app()->getLocale()
|
|
)
|
|
);
|
|
} elseif ($consultation->user) {
|
|
$consultation->user->notify(
|
|
new BookingApproved($consultation, $icsContent ?? '', null)
|
|
);
|
|
}
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action' => 'approve',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $consultation->id,
|
|
'old_values' => ['status' => $oldStatus],
|
|
'new_values' => [
|
|
'status' => ConsultationStatus::Approved->value,
|
|
'consultation_type' => ConsultationType::Free->value,
|
|
],
|
|
'ip_address' => request()->ip(),
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
session()->flash('success', __('admin.booking_approved'));
|
|
}
|
|
|
|
public function quickReject(int $id): void
|
|
{
|
|
$consultation = Consultation::with('user')->findOrFail($id);
|
|
|
|
if ($consultation->status !== ConsultationStatus::Pending) {
|
|
session()->flash('error', __('admin.booking_already_processed'));
|
|
|
|
return;
|
|
}
|
|
|
|
$oldStatus = $consultation->status->value;
|
|
|
|
$consultation->update([
|
|
'status' => ConsultationStatus::Rejected,
|
|
]);
|
|
|
|
// Send appropriate notification/email based on guest/client
|
|
if ($consultation->isGuest()) {
|
|
Mail::to($consultation->guest_email)->queue(
|
|
new GuestBookingRejectedMail(
|
|
$consultation,
|
|
app()->getLocale(),
|
|
null
|
|
)
|
|
);
|
|
} elseif ($consultation->user) {
|
|
$consultation->user->notify(
|
|
new BookingRejected($consultation, null)
|
|
);
|
|
}
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action' => 'reject',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $consultation->id,
|
|
'old_values' => ['status' => $oldStatus],
|
|
'new_values' => [
|
|
'status' => ConsultationStatus::Rejected->value,
|
|
],
|
|
'ip_address' => request()->ip(),
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
session()->flash('success', __('admin.booking_rejected'));
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'bookings' => Consultation::query()
|
|
->where('status', ConsultationStatus::Pending)
|
|
->when($this->dateFrom, fn ($q) => $q->where('booking_date', '>=', $this->dateFrom))
|
|
->when($this->dateTo, fn ($q) => $q->where('booking_date', '<=', $this->dateTo))
|
|
->with('user:id,full_name,email,phone,user_type')
|
|
->orderBy('booking_date')
|
|
->orderBy('booking_time')
|
|
->paginate(15),
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div class="max-w-6xl mx-auto">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
|
<flux:heading size="xl">{{ __('admin.pending_bookings') }}</flux:heading>
|
|
</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="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($dateFrom || $dateTo)
|
|
<flux:button wire:click="clearFilters" variant="ghost">
|
|
{{ __('common.clear') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bookings List -->
|
|
<div class="space-y-4">
|
|
@forelse($bookings as $booking)
|
|
<div wire:key="booking-{{ $booking->id }}" class="bg-white rounded-lg p-4 border border-zinc-200">
|
|
<div class="flex flex-col lg:flex-row lg:items-start justify-between gap-4">
|
|
<!-- Booking Info -->
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
@if($booking->isGuest())
|
|
<flux:badge color="amber" size="sm">{{ __('admin.guest') }}</flux:badge>
|
|
@endif
|
|
<span class="font-semibold text-zinc-900">
|
|
{{ $booking->getClientName() }}
|
|
</span>
|
|
<flux:badge variant="warning" size="sm">
|
|
{{ $booking->status->label() }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-zinc-600">
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="calendar" class="w-4 h-4" />
|
|
{{ \Carbon\Carbon::parse($booking->booking_date)->translatedFormat('l, d M Y') }}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="clock" class="w-4 h-4" />
|
|
{{ \Carbon\Carbon::parse($booking->booking_time)->format('g:i A') }}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="envelope" class="w-4 h-4" />
|
|
<a href="mailto:{{ $booking->getClientEmail() }}" class="hover:underline">
|
|
{{ $booking->getClientEmail() }}
|
|
</a>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="document-text" class="w-4 h-4" />
|
|
{{ __('admin.submitted') }}: {{ $booking->created_at->translatedFormat('d M Y') }}
|
|
</div>
|
|
</div>
|
|
|
|
<p class="mt-3 text-sm text-zinc-600 line-clamp-2">
|
|
{{ Str::limit($booking->problem_summary, 150) }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex flex-wrap gap-2 lg:flex-col">
|
|
<flux:button
|
|
href="{{ route('admin.bookings.review', $booking) }}"
|
|
variant="primary"
|
|
size="sm"
|
|
wire:navigate
|
|
>
|
|
{{ __('admin.review') }}
|
|
</flux:button>
|
|
|
|
<flux:button
|
|
wire:click="quickApprove({{ $booking->id }})"
|
|
wire:confirm="{{ __('admin.confirm_quick_approve') }}"
|
|
variant="filled"
|
|
size="sm"
|
|
class="!bg-emerald-600 !text-white hover:!bg-emerald-700"
|
|
>
|
|
{{ __('admin.quick_approve') }}
|
|
</flux:button>
|
|
|
|
<flux:button
|
|
wire:click="quickReject({{ $booking->id }})"
|
|
wire:confirm="{{ __('admin.confirm_quick_reject') }}"
|
|
variant="danger"
|
|
size="sm"
|
|
>
|
|
{{ __('admin.quick_reject') }}
|
|
</flux:button>
|
|
</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_pending_bookings') }}</p>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
{{ $bookings->links() }}
|
|
</div>
|
|
</div>
|