complete 3.4 with qa tests
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
@php
|
||||
$locale = $user->preferred_language ?? 'en';
|
||||
@endphp
|
||||
@component('mail::message')
|
||||
@if($locale === 'ar')
|
||||
<div dir="rtl" style="text-align: right;">
|
||||
# {{ __('emails.booking_submitted_title', [], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_greeting', ['name' => $user->company_name ?? $user->full_name], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_body', [], $locale) }}
|
||||
|
||||
**{{ __('emails.booking_details', [], $locale) }}**
|
||||
|
||||
- **{{ __('emails.booking_date', [], $locale) }}** {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
|
||||
- **{{ __('emails.booking_time', [], $locale) }}** {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
- **{{ __('emails.booking_duration', [], $locale) }}** 45 {{ __('common.minutes', [], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_next_steps', [], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_contact', [], $locale) }}
|
||||
|
||||
{{ __('emails.regards', [], $locale) }}<br>
|
||||
{{ config('app.name') }}
|
||||
</div>
|
||||
@else
|
||||
# {{ __('emails.booking_submitted_title', [], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_greeting', ['name' => $user->company_name ?? $user->full_name], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_body', [], $locale) }}
|
||||
|
||||
**{{ __('emails.booking_details', [], $locale) }}**
|
||||
|
||||
- **{{ __('emails.booking_date', [], $locale) }}** {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
|
||||
- **{{ __('emails.booking_time', [], $locale) }}** {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
- **{{ __('emails.booking_duration', [], $locale) }}** 45 {{ __('common.minutes', [], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_next_steps', [], $locale) }}
|
||||
|
||||
{{ __('emails.booking_submitted_contact', [], $locale) }}
|
||||
|
||||
{{ __('emails.regards', [], $locale) }}<br>
|
||||
{{ config('app.name') }}
|
||||
@endif
|
||||
@endcomponent
|
||||
@@ -0,0 +1,27 @@
|
||||
@component('mail::message')
|
||||
# {{ __('emails.new_booking_request_title') }}
|
||||
|
||||
{{ __('emails.new_booking_request_body') }}
|
||||
|
||||
**{{ __('emails.client_details') }}**
|
||||
|
||||
- **{{ __('emails.client_name') }}** {{ $client->company_name ?? $client->full_name }}
|
||||
- **{{ __('emails.client_email') }}** {{ $client->email }}
|
||||
- **{{ __('emails.client_phone') }}** {{ $client->phone }}
|
||||
|
||||
**{{ __('emails.booking_details') }}**
|
||||
|
||||
- **{{ __('emails.booking_date') }}** {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
|
||||
- **{{ __('emails.booking_time') }}** {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
|
||||
**{{ __('emails.problem_summary') }}**
|
||||
|
||||
{{ $consultation->problem_summary }}
|
||||
|
||||
@component('mail::button', ['url' => route('admin.dashboard')])
|
||||
{{ __('emails.view_in_dashboard') }}
|
||||
@endcomponent
|
||||
|
||||
{{ __('emails.regards') }}<br>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\PaymentStatus;
|
||||
use App\Mail\BookingSubmittedMail;
|
||||
use App\Mail\NewBookingRequestMail;
|
||||
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\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 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 NewBookingRequestMail($consultation)
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<flux:heading size="xl" class="mb-6">{{ __('booking.request_consultation') }}</flux:heading>
|
||||
|
||||
@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 />
|
||||
</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>{{ __('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"
|
||||
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 gap-3 mt-4">
|
||||
<flux:button wire:click="$set('showConfirmation', false)">
|
||||
{{ __('common.back') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
wire:click="submit"
|
||||
variant="primary"
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
<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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Models\Consultation;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'consultations' => Consultation::query()
|
||||
->where('user_id', auth()->id())
|
||||
->orderBy('booking_date', 'desc')
|
||||
->paginate(10),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<flux:heading size="xl">{{ __('booking.my_consultations') }}</flux:heading>
|
||||
<flux:button href="{{ route('client.consultations.book') }}" variant="primary">
|
||||
{{ __('booking.request_consultation') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<flux:callout variant="success" class="mb-6">
|
||||
{{ session('success') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="space-y-4">
|
||||
@forelse($consultations as $consultation)
|
||||
<div wire:key="consultation-{{ $consultation->id }}" class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<p class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}
|
||||
</p>
|
||||
<p class="text-zinc-600 dark:text-zinc-400">
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
</p>
|
||||
</div>
|
||||
<flux:badge :variant="match($consultation->status) {
|
||||
ConsultationStatus::Pending => 'warning',
|
||||
ConsultationStatus::Approved => 'success',
|
||||
ConsultationStatus::Completed => 'default',
|
||||
ConsultationStatus::Cancelled => 'danger',
|
||||
ConsultationStatus::NoShow => 'danger',
|
||||
default => 'default',
|
||||
}">
|
||||
{{ $consultation->status->label() }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-2">
|
||||
{{ $consultation->problem_summary }}
|
||||
</p>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
||||
<p>{{ __('booking.no_consultations') }}</p>
|
||||
<flux:button href="{{ route('client.consultations.book') }}" class="mt-4">
|
||||
{{ __('booking.book_first_consultation') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
{{ $consultations->links() }}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user