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

435 lines
17 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;
new class extends Component
{
public Consultation $consultation;
public string $consultationType = 'free';
public ?string $paymentAmount = null;
public string $paymentInstructions = '';
public string $rejectionReason = '';
public bool $showApproveModal = false;
public bool $showRejectModal = false;
public function mount(Consultation $consultation): void
{
$this->consultation = $consultation->load(['user']);
}
public function openApproveModal(): void
{
$this->showApproveModal = true;
}
public function openRejectModal(): void
{
$this->showRejectModal = true;
}
public function approve(): void
{
if ($this->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 = $this->consultation->status->value;
$type = $this->consultationType === 'paid' ? ConsultationType::Paid : ConsultationType::Free;
$this->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
$icsContent = null;
try {
$calendarService = app(CalendarService::class);
$icsContent = $calendarService->generateIcs($this->consultation);
} catch (\Exception $e) {
Log::error('Failed to generate calendar file', [
'consultation_id' => $this->consultation->id,
'error' => $e->getMessage(),
]);
}
// Send appropriate notification/email based on guest/client
if ($this->consultation->isGuest()) {
Mail::to($this->consultation->guest_email)->queue(
new GuestBookingApprovedMail(
$this->consultation,
app()->getLocale(),
$this->paymentInstructions ?: null
)
);
} elseif ($this->consultation->user) {
$this->consultation->user->notify(
new BookingApproved(
$this->consultation,
$icsContent ?? '',
$this->paymentInstructions ?: null
)
);
}
// Log action
AdminLog::create([
'admin_id' => auth()->id(),
'action' => 'approve',
'target_type' => 'consultation',
'target_id' => $this->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(),
]);
session()->flash('success', __('admin.booking_approved'));
$this->redirect(route('admin.bookings.pending'), navigate: true);
}
public function reject(): void
{
if ($this->consultation->status !== ConsultationStatus::Pending) {
session()->flash('error', __('admin.booking_already_processed'));
$this->showRejectModal = false;
return;
}
$this->validate([
'rejectionReason' => ['nullable', 'string', 'max:1000'],
]);
$oldStatus = $this->consultation->status->value;
$this->consultation->update([
'status' => ConsultationStatus::Rejected,
]);
// Send appropriate notification/email based on guest/client
if ($this->consultation->isGuest()) {
Mail::to($this->consultation->guest_email)->queue(
new GuestBookingRejectedMail(
$this->consultation,
app()->getLocale(),
$this->rejectionReason ?: null
)
);
} elseif ($this->consultation->user) {
$this->consultation->user->notify(
new BookingRejected($this->consultation, $this->rejectionReason ?: null)
);
}
// Log action
AdminLog::create([
'admin_id' => auth()->id(),
'action' => 'reject',
'target_type' => 'consultation',
'target_id' => $this->consultation->id,
'old_values' => ['status' => $oldStatus],
'new_values' => [
'status' => ConsultationStatus::Rejected->value,
'reason' => $this->rejectionReason,
],
'ip_address' => request()->ip(),
'created_at' => now(),
]);
session()->flash('success', __('admin.booking_rejected'));
$this->redirect(route('admin.bookings.pending'), navigate: true);
}
public function with(): array
{
// Guest bookings don't have consultation history
if ($this->consultation->isGuest()) {
return ['consultationHistory' => collect()];
}
return [
'consultationHistory' => Consultation::query()
->where('user_id', $this->consultation->user_id)
->where('id', '!=', $this->consultation->id)
->orderBy('booking_date', 'desc')
->limit(5)
->get(),
];
}
}; ?>
<div class="max-w-4xl mx-auto">
<div class="flex items-center gap-4 mb-6">
<flux:button href="{{ route('admin.bookings.pending') }}" variant="ghost" wire:navigate>
<flux:icon name="arrow-left" class="w-4 h-4 rtl:rotate-180" />
{{ __('common.back') }}
</flux:button>
<flux:heading size="xl">{{ __('admin.review_booking') }}</flux:heading>
</div>
@if(session('error'))
<flux:callout variant="danger" class="mb-6">
{{ session('error') }}
</flux:callout>
@endif
@if($consultation->status !== ConsultationStatus::Pending)
<flux:callout variant="warning" class="mb-6">
{{ __('admin.booking_already_processed_info', ['status' => $consultation->status->label()]) }}
</flux:callout>
@endif
<!-- Client/Guest Information -->
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700 mb-6">
<div class="flex items-center gap-2 mb-4">
<flux:heading size="lg">{{ __('admin.client_information') }}</flux:heading>
@if($consultation->isGuest())
<flux:badge color="amber" size="sm">{{ __('admin.guest') }}</flux:badge>
@endif
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_name') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{{ $consultation->getClientName() }}
</p>
</div>
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_email') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
<a href="mailto:{{ $consultation->getClientEmail() }}" class="text-primary hover:underline">
{{ $consultation->getClientEmail() }}
</a>
</p>
</div>
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_phone') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
@if($consultation->getClientPhone())
<a href="tel:{{ $consultation->getClientPhone() }}" class="text-primary hover:underline">
{{ $consultation->getClientPhone() }}
</a>
@else
-
@endif
</p>
</div>
@unless($consultation->isGuest())
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_type') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{{ ucfirst($consultation->user?->user_type?->value ?? '-') }}
</p>
</div>
@endunless
</div>
</div>
<!-- Booking Details -->
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700 mb-6">
<flux:heading size="lg" class="mb-4">{{ __('admin.booking_details') }}</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.requested_date') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
</p>
</div>
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.requested_time') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
</p>
</div>
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.submission_date') }}</p>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{{ $consultation->created_at->translatedFormat('d M Y, g:i A') }}
</p>
</div>
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.current_status') }}</p>
<flux:badge :variant="match($consultation->status) {
ConsultationStatus::Pending => 'warning',
ConsultationStatus::Approved => 'success',
ConsultationStatus::Rejected => 'danger',
default => 'default',
}">
{{ $consultation->status->label() }}
</flux:badge>
</div>
</div>
<div>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-2">{{ __('admin.problem_summary') }}</p>
<p class="text-zinc-900 dark:text-zinc-100 whitespace-pre-wrap">
{{ $consultation->problem_summary }}
</p>
</div>
</div>
<!-- Consultation History -->
@if($consultationHistory->count() > 0)
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700 mb-6">
<flux:heading size="lg" class="mb-4">{{ __('admin.consultation_history') }}</flux:heading>
<div class="space-y-3">
@foreach($consultationHistory as $history)
<div class="flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-700 rounded-lg">
<div>
<p class="font-medium text-zinc-900 dark:text-zinc-100">
{{ \Carbon\Carbon::parse($history->booking_date)->translatedFormat('d M Y') }}
</p>
<p class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $history->consultation_type?->value ?? '-' }}
</p>
</div>
<flux:badge :variant="match($history->status) {
ConsultationStatus::Pending => 'warning',
ConsultationStatus::Approved => 'success',
ConsultationStatus::Completed => 'default',
ConsultationStatus::Rejected => 'danger',
ConsultationStatus::Cancelled => 'danger',
ConsultationStatus::NoShow => 'danger',
}" size="sm">
{{ $history->status->label() }}
</flux:badge>
</div>
@endforeach
</div>
</div>
@endif
<!-- Action Buttons -->
@if($consultation->status === ConsultationStatus::Pending)
<div class="flex gap-4">
<flux:button wire:click="openApproveModal" variant="primary">
{{ __('admin.approve') }}
</flux:button>
<flux:button wire:click="openRejectModal" variant="danger">
{{ __('admin.reject') }}
</flux:button>
</div>
@endif
<!-- Approve Modal -->
<flux:modal wire:model="showApproveModal">
<div class="space-y-6">
<flux:heading size="lg">{{ __('admin.approve_booking') }}</flux:heading>
<!-- Client Info Summary -->
<div class="bg-zinc-50 dark:bg-zinc-700 p-4 rounded-lg">
<p><strong>{{ __('admin.client') }}:</strong> {{ $consultation->getClientName() }}</p>
<p><strong>{{ __('admin.date') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}</p>
<p><strong>{{ __('admin.time') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}</p>
</div>
<!-- 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="approve">
{{ __('admin.approve') }}
</flux:button>
</div>
</div>
</flux:modal>
<!-- Reject Modal -->
<flux:modal wire:model="showRejectModal">
<div class="space-y-6">
<flux:heading size="lg">{{ __('admin.reject_booking') }}</flux:heading>
<!-- Client Info Summary -->
<div class="bg-zinc-50 dark:bg-zinc-700 p-4 rounded-lg">
<p><strong>{{ __('admin.client') }}:</strong> {{ $consultation->getClientName() }}</p>
<p><strong>{{ __('admin.date') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}</p>
<p><strong>{{ __('admin.time') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}</p>
</div>
<flux:field>
<flux:label>{{ __('admin.rejection_reason') }} ({{ __('common.optional') }})</flux:label>
<flux:textarea
wire:model="rejectionReason"
rows="3"
placeholder="{{ __('admin.rejection_reason_placeholder') }}"
/>
<flux:error name="rejectionReason" />
</flux:field>
<div class="flex gap-3 justify-end">
<flux:button wire:click="$set('showRejectModal', false)">
{{ __('common.cancel') }}
</flux:button>
<flux:button variant="danger" wire:click="reject">
{{ __('admin.reject') }}
</flux:button>
</div>
</div>
</flux:modal>
</div>