328 lines
13 KiB
PHP
328 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Enums\PaymentStatus;
|
|
use App\Mail\BookingSubmittedMail;
|
|
use App\Mail\NewBookingAdminEmail;
|
|
use App\Models\AdminLog;
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use App\Services\AvailabilityService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Livewire\Volt\Component;
|
|
|
|
new class extends Component
|
|
{
|
|
public ?string $selectedDate = null;
|
|
|
|
public ?string $selectedTime = null;
|
|
|
|
public string $problemSummary = '';
|
|
|
|
public bool $showConfirmation = false;
|
|
|
|
public function getBookingStatus(): array
|
|
{
|
|
$user = auth()->user();
|
|
|
|
$todayBooking = $user->consultations()
|
|
->whereDate('booking_date', today())
|
|
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
|
|
->first();
|
|
|
|
$pendingRequests = $user->consultations()
|
|
->where('status', ConsultationStatus::Pending)
|
|
->where('booking_date', '>=', today())
|
|
->orderBy('booking_date')
|
|
->get();
|
|
|
|
$bookedDates = $user->consultations()
|
|
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
|
|
->where('booking_date', '>=', today())
|
|
->pluck('booking_date')
|
|
->map(fn ($d) => $d->format('Y-m-d'))
|
|
->toArray();
|
|
|
|
return [
|
|
'canBookToday' => is_null($todayBooking),
|
|
'todayBooking' => $todayBooking,
|
|
'pendingRequests' => $pendingRequests,
|
|
'bookedDates' => $bookedDates,
|
|
];
|
|
}
|
|
|
|
public function selectSlot(string $date, string $time): void
|
|
{
|
|
$this->selectedDate = $date;
|
|
$this->selectedTime = $time;
|
|
}
|
|
|
|
public function clearSelection(): void
|
|
{
|
|
$this->selectedDate = null;
|
|
$this->selectedTime = null;
|
|
$this->showConfirmation = false;
|
|
}
|
|
|
|
public function showConfirm(): void
|
|
{
|
|
$this->validate([
|
|
'selectedDate' => ['required', 'date', 'after_or_equal:today'],
|
|
'selectedTime' => ['required'],
|
|
'problemSummary' => ['required', 'string', 'min:20', 'max:2000'],
|
|
]);
|
|
|
|
// Check 1-per-day limit
|
|
$existingBooking = Consultation::query()
|
|
->where('user_id', auth()->id())
|
|
->whereDate('booking_date', $this->selectedDate)
|
|
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
|
|
->exists();
|
|
|
|
if ($existingBooking) {
|
|
$this->addError('selectedDate', __('booking.already_booked_this_day'));
|
|
|
|
return;
|
|
}
|
|
|
|
// Verify slot still available
|
|
$service = app(AvailabilityService::class);
|
|
$availableSlots = $service->getAvailableSlots(Carbon::parse($this->selectedDate));
|
|
|
|
if (! in_array($this->selectedTime, $availableSlots)) {
|
|
$this->addError('selectedTime', __('booking.slot_no_longer_available'));
|
|
|
|
return;
|
|
}
|
|
|
|
$this->showConfirmation = true;
|
|
}
|
|
|
|
public function submit(): void
|
|
{
|
|
try {
|
|
DB::transaction(function () {
|
|
// Check slot one more time with lock
|
|
$slotTaken = Consultation::query()
|
|
->whereDate('booking_date', $this->selectedDate)
|
|
->where('booking_time', $this->selectedTime)
|
|
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
|
|
->lockForUpdate()
|
|
->exists();
|
|
|
|
if ($slotTaken) {
|
|
throw new \Exception(__('booking.slot_taken'));
|
|
}
|
|
|
|
// Check 1-per-day again with lock
|
|
$userHasBooking = Consultation::query()
|
|
->where('user_id', auth()->id())
|
|
->whereDate('booking_date', $this->selectedDate)
|
|
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
|
|
->lockForUpdate()
|
|
->exists();
|
|
|
|
if ($userHasBooking) {
|
|
throw new \Exception(__('booking.already_booked_this_day'));
|
|
}
|
|
|
|
// Create booking
|
|
$consultation = Consultation::create([
|
|
'user_id' => auth()->id(),
|
|
'booking_date' => $this->selectedDate,
|
|
'booking_time' => $this->selectedTime,
|
|
'problem_summary' => $this->problemSummary,
|
|
'status' => ConsultationStatus::Pending,
|
|
'payment_status' => PaymentStatus::NotApplicable,
|
|
]);
|
|
|
|
// Send email to client
|
|
Mail::to(auth()->user())->queue(
|
|
new BookingSubmittedMail($consultation)
|
|
);
|
|
|
|
// Send email to admin
|
|
$admin = User::query()->where('user_type', 'admin')->first();
|
|
if ($admin) {
|
|
Mail::to($admin)->queue(
|
|
new NewBookingAdminEmail($consultation)
|
|
);
|
|
} else {
|
|
Log::warning('No admin user found to notify about new booking', [
|
|
'consultation_id' => $consultation->id,
|
|
]);
|
|
}
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => null,
|
|
'action' => 'create',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $consultation->id,
|
|
'new_values' => $consultation->toArray(),
|
|
'ip_address' => request()->ip(),
|
|
'created_at' => now(),
|
|
]);
|
|
});
|
|
|
|
session()->flash('success', __('booking.submitted_successfully'));
|
|
$this->redirect(route('client.consultations.index'));
|
|
|
|
} catch (\Exception $e) {
|
|
$this->addError('selectedTime', $e->getMessage());
|
|
$this->showConfirmation = false;
|
|
}
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return $this->getBookingStatus();
|
|
}
|
|
}; ?>
|
|
|
|
<div class="max-w-4xl mx-auto">
|
|
<flux:heading size="xl" class="mb-6 text-xl sm:text-2xl">{{ __('booking.request_consultation') }}</flux:heading>
|
|
|
|
{{-- Booking Status Banner --}}
|
|
<div class="mb-6 rounded-lg border p-4 {{ $canBookToday ? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-900/20' : 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-900/20' }}">
|
|
<div class="flex items-start gap-3">
|
|
@if($canBookToday)
|
|
<flux:icon name="check-circle" class="mt-0.5 h-5 w-5 flex-shrink-0 text-green-600 dark:text-green-400" />
|
|
<div>
|
|
<p class="font-medium text-green-700 dark:text-green-400">{{ __('booking.can_book_today') }}</p>
|
|
</div>
|
|
@else
|
|
<flux:icon name="information-circle" class="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
|
|
<div>
|
|
<p class="font-medium text-amber-700 dark:text-amber-400">{{ __('booking.already_booked_today') }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if($pendingRequests->isNotEmpty())
|
|
<div class="mt-3 border-t border-zinc-200 pt-3 dark:border-zinc-700">
|
|
@if($pendingRequests->count() === 1)
|
|
<p class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
{{ __('booking.pending_for_date', ['date' => $pendingRequests->first()->booking_date->format('d/m/Y')]) }}
|
|
</p>
|
|
@else
|
|
<p class="text-sm text-zinc-600 dark:text-zinc-400">
|
|
{{ __('booking.pending_count', ['count' => $pendingRequests->count()]) }}
|
|
</p>
|
|
<ul class="mt-1 list-inside list-disc text-sm text-zinc-500 dark:text-zinc-400">
|
|
@foreach($pendingRequests->take(3) as $request)
|
|
<li>{{ $request->booking_date->format('d/m/Y') }}</li>
|
|
@endforeach
|
|
@if($pendingRequests->count() > 3)
|
|
<li>...</li>
|
|
@endif
|
|
</ul>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<p class="mt-3 text-xs text-zinc-500 dark:text-zinc-400">
|
|
{{ __('booking.limit_message') }}
|
|
</p>
|
|
</div>
|
|
|
|
@if(!$selectedDate || !$selectedTime)
|
|
<!-- Step 1: Calendar Selection -->
|
|
<div class="mt-6">
|
|
<p class="mb-4 text-zinc-600 dark:text-zinc-400">{{ __('booking.select_date_time') }}</p>
|
|
<livewire:availability-calendar :booked-dates="$bookedDates" />
|
|
</div>
|
|
@else
|
|
<!-- Step 2: Problem Summary -->
|
|
<div class="mt-6">
|
|
<!-- Selected Time Display -->
|
|
<div class="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg mb-6 border border-amber-200 dark:border-amber-800">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<p class="font-semibold text-zinc-900 dark:text-zinc-100">{{ __('booking.selected_time') }}</p>
|
|
<p class="text-zinc-600 dark:text-zinc-400">{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</p>
|
|
<p class="text-zinc-600 dark:text-zinc-400">{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</p>
|
|
</div>
|
|
<flux:button size="sm" wire:click="clearSelection">
|
|
{{ __('common.change') }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
@error('selectedDate')
|
|
<flux:callout variant="danger" class="mb-4">
|
|
{{ $message }}
|
|
</flux:callout>
|
|
@enderror
|
|
|
|
@if(!$showConfirmation)
|
|
<!-- Problem Summary Form -->
|
|
<flux:field>
|
|
<flux:label class="required">{{ __('booking.problem_summary') }}</flux:label>
|
|
<flux:textarea
|
|
wire:model="problemSummary"
|
|
rows="6"
|
|
placeholder="{{ __('booking.problem_summary_placeholder') }}"
|
|
/>
|
|
<flux:description>
|
|
{{ __('booking.problem_summary_help') }}
|
|
</flux:description>
|
|
<flux:error name="problemSummary" />
|
|
</flux:field>
|
|
|
|
<flux:button
|
|
wire:click="showConfirm"
|
|
class="mt-4 w-full sm:w-auto min-h-[44px]"
|
|
wire:loading.attr="disabled"
|
|
>
|
|
<span wire:loading.remove wire:target="showConfirm">{{ __('booking.continue') }}</span>
|
|
<span wire:loading wire:target="showConfirm">{{ __('common.loading') }}</span>
|
|
</flux:button>
|
|
@else
|
|
<!-- Confirmation Step -->
|
|
<flux:callout>
|
|
<flux:heading size="sm">{{ __('booking.confirm_booking') }}</flux:heading>
|
|
<p class="text-zinc-600 dark:text-zinc-400">{{ __('booking.confirm_message') }}</p>
|
|
|
|
<div class="mt-4 space-y-2">
|
|
<p><strong>{{ __('booking.date') }}:</strong>
|
|
{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</p>
|
|
<p><strong>{{ __('booking.time') }}:</strong>
|
|
{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</p>
|
|
<p><strong>{{ __('booking.duration') }}:</strong> 45 {{ __('common.minutes') }}</p>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<p><strong>{{ __('booking.problem_summary') }}:</strong></p>
|
|
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">{{ $problemSummary }}</p>
|
|
</div>
|
|
</flux:callout>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-3 mt-4">
|
|
<flux:button wire:click="$set('showConfirmation', false)" class="w-full sm:w-auto min-h-[44px] justify-center">
|
|
{{ __('common.back') }}
|
|
</flux:button>
|
|
<flux:button
|
|
wire:click="submit"
|
|
variant="primary"
|
|
wire:loading.attr="disabled"
|
|
class="w-full sm:w-auto min-h-[44px] justify-center"
|
|
>
|
|
<span wire:loading.remove wire:target="submit">{{ __('booking.submit_request') }}</span>
|
|
<span wire:loading wire:target="submit">{{ __('common.submitting') }}</span>
|
|
</flux:button>
|
|
</div>
|
|
|
|
@error('selectedTime')
|
|
<flux:callout variant="danger" class="mt-4">
|
|
{{ $message }}
|
|
</flux:callout>
|
|
@enderror
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|