libra/resources/views/livewire/admin/bookings/pending.blade.php

369 lines
14 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 = '';
// Quick approve modal state
public bool $showApproveModal = false;
public ?int $approvingBookingId = null;
public string $consultationType = 'free';
public ?string $paymentAmount = null;
public string $paymentInstructions = '';
public function updatedDateFrom(): void
{
$this->resetPage();
}
public function updatedDateTo(): void
{
$this->resetPage();
}
public function clearFilters(): void
{
$this->dateFrom = '';
$this->dateTo = '';
$this->resetPage();
}
public function openApproveModal(int $id): void
{
$this->approvingBookingId = $id;
$this->consultationType = 'free';
$this->paymentAmount = null;
$this->paymentInstructions = '';
$this->showApproveModal = true;
}
public function quickApprove(): void
{
$consultation = Consultation::with('user')->findOrFail($this->approvingBookingId);
if ($consultation->status !== ConsultationStatus::Pending) {
session()->flash('error', __('admin.booking_already_processed'));
$this->showApproveModal = false;
return;
}
$this->validate([
'consultationType' => ['required', 'in:free,paid'],
'paymentAmount' => ['required_if:consultationType,paid', 'nullable', 'numeric', 'min:0'],
'paymentInstructions' => ['nullable', 'string', 'max:1000'],
]);
$oldStatus = $consultation->status->value;
$type = $this->consultationType === 'paid' ? ConsultationType::Paid : ConsultationType::Free;
$consultation->update([
'status' => ConsultationStatus::Approved,
'consultation_type' => $type,
'payment_amount' => $type === ConsultationType::Paid ? $this->paymentAmount : null,
'payment_status' => $type === ConsultationType::Paid ? PaymentStatus::Pending : 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(),
$this->paymentInstructions ?: null
)
);
} elseif ($consultation->user) {
$consultation->user->notify(
new BookingApproved($consultation, $icsContent ?? '', $this->paymentInstructions ?: 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' => $type->value,
'payment_amount' => $this->paymentAmount,
],
'ip_address' => request()->ip(),
'created_at' => now(),
]);
$this->showApproveModal = false;
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="outline">
{{ __('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 color="amber" 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="openApproveModal({{ $booking->id }})"
variant="outline"
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>
<!-- Approve Modal -->
<flux:modal wire:model="showApproveModal">
<div class="space-y-6">
<flux:heading size="lg">{{ __('admin.approve_booking') }}</flux:heading>
<!-- Consultation Type -->
<flux:field>
<flux:label>{{ __('admin.consultation_type') }}</flux:label>
<flux:radio.group wire:model.live="consultationType">
<flux:radio value="free" label="{{ __('admin.free_consultation') }}" />
<flux:radio value="paid" label="{{ __('admin.paid_consultation') }}" />
</flux:radio.group>
</flux:field>
<!-- Payment Amount (if paid) -->
@if($consultationType === 'paid')
<flux:field>
<flux:label>{{ __('admin.payment_amount') }} *</flux:label>
<flux:input
type="number"
wire:model="paymentAmount"
step="0.01"
min="0"
/>
<flux:error name="paymentAmount" />
</flux:field>
<flux:field>
<flux:label>{{ __('admin.payment_instructions') }}</flux:label>
<flux:textarea
wire:model="paymentInstructions"
rows="3"
placeholder="{{ __('admin.payment_instructions_placeholder') }}"
/>
</flux:field>
@endif
<div class="flex gap-3 justify-end">
<flux:button wire:click="$set('showApproveModal', false)">
{{ __('common.cancel') }}
</flux:button>
<flux:button variant="primary" wire:click="quickApprove">
{{ __('admin.approve') }}
</flux:button>
</div>
</div>
</flux:modal>
</div>