complete story 3.7 with qa test
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
@php
|
||||
$locale = $user->preferred_language ?? 'ar';
|
||||
@endphp
|
||||
@component('mail::message')
|
||||
@if($locale === 'ar')
|
||||
<div dir="rtl" style="text-align: right;">
|
||||
# {{ __('emails.consultation_cancelled_title', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_cancelled_greeting', ['name' => $user->company_name ?? $user->full_name], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_cancelled_body', [], $locale) }}
|
||||
|
||||
**{{ __('emails.cancelled_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.consultation_cancelled_rebook', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_cancelled_contact', [], $locale) }}
|
||||
|
||||
{{ __('emails.regards', [], $locale) }}<br>
|
||||
{{ config('app.name') }}
|
||||
</div>
|
||||
@else
|
||||
# {{ __('emails.consultation_cancelled_title', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_cancelled_greeting', ['name' => $user->company_name ?? $user->full_name], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_cancelled_body', [], $locale) }}
|
||||
|
||||
**{{ __('emails.cancelled_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.consultation_cancelled_rebook', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_cancelled_contact', [], $locale) }}
|
||||
|
||||
{{ __('emails.regards', [], $locale) }}<br>
|
||||
{{ config('app.name') }}
|
||||
@endif
|
||||
@endcomponent
|
||||
@@ -0,0 +1,56 @@
|
||||
@php
|
||||
$locale = $user->preferred_language ?? 'ar';
|
||||
@endphp
|
||||
@component('mail::message')
|
||||
@if($locale === 'ar')
|
||||
<div dir="rtl" style="text-align: right;">
|
||||
# {{ __('emails.consultation_rescheduled_title', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_rescheduled_greeting', ['name' => $user->company_name ?? $user->full_name], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_rescheduled_body', [], $locale) }}
|
||||
|
||||
**{{ __('emails.old_booking_details', [], $locale) }}**
|
||||
|
||||
- **{{ __('emails.booking_date', [], $locale) }}** {{ $oldDate->translatedFormat('l, d M Y') }}
|
||||
- **{{ __('emails.booking_time', [], $locale) }}** {{ \Carbon\Carbon::parse($oldTime)->format('g:i A') }}
|
||||
|
||||
**{{ __('emails.new_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.consultation_rescheduled_calendar', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_rescheduled_contact', [], $locale) }}
|
||||
|
||||
{{ __('emails.regards', [], $locale) }}<br>
|
||||
{{ config('app.name') }}
|
||||
</div>
|
||||
@else
|
||||
# {{ __('emails.consultation_rescheduled_title', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_rescheduled_greeting', ['name' => $user->company_name ?? $user->full_name], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_rescheduled_body', [], $locale) }}
|
||||
|
||||
**{{ __('emails.old_booking_details', [], $locale) }}**
|
||||
|
||||
- **{{ __('emails.booking_date', [], $locale) }}** {{ $oldDate->translatedFormat('l, d M Y') }}
|
||||
- **{{ __('emails.booking_time', [], $locale) }}** {{ \Carbon\Carbon::parse($oldTime)->format('g:i A') }}
|
||||
|
||||
**{{ __('emails.new_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.consultation_rescheduled_calendar', [], $locale) }}
|
||||
|
||||
{{ __('emails.consultation_rescheduled_contact', [], $locale) }}
|
||||
|
||||
{{ __('emails.regards', [], $locale) }}<br>
|
||||
{{ config('app.name') }}
|
||||
@endif
|
||||
@endcomponent
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\User;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public User $user;
|
||||
|
||||
public function mount(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'consultations' => Consultation::query()
|
||||
->where('user_id', $this->user->id)
|
||||
->orderBy('booking_date', 'desc')
|
||||
->orderBy('booking_time', 'desc')
|
||||
->paginate(15),
|
||||
'statistics' => [
|
||||
'total' => Consultation::where('user_id', $this->user->id)->count(),
|
||||
'completed' => Consultation::where('user_id', $this->user->id)
|
||||
->where('status', ConsultationStatus::Completed)->count(),
|
||||
'cancelled' => Consultation::where('user_id', $this->user->id)
|
||||
->where('status', ConsultationStatus::Cancelled)->count(),
|
||||
'no_show' => Consultation::where('user_id', $this->user->id)
|
||||
->where('status', ConsultationStatus::NoShow)->count(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="mb-6">
|
||||
@if($user->user_type->value === 'individual')
|
||||
<flux:button href="{{ route('admin.clients.individual.show', $user) }}" variant="ghost" icon="arrow-left" wire:navigate>
|
||||
{{ __('common.back') }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button href="{{ route('admin.clients.company.show', $user) }}" variant="ghost" icon="arrow-left" wire:navigate>
|
||||
{{ __('common.back') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('admin.client_consultations') }}</flux:heading>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ $user->full_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 text-center">
|
||||
<p class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{{ $statistics['total'] }}</p>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.total_consultations') }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 text-center">
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">{{ $statistics['completed'] }}</p>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.completed_consultations') }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 text-center">
|
||||
<p class="text-2xl font-bold text-red-600 dark:text-red-400">{{ $statistics['cancelled'] }}</p>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.cancelled_consultations') }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 text-center">
|
||||
<p class="text-2xl font-bold text-amber-600 dark:text-amber-400">{{ $statistics['no_show'] }}</p>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.no_show_consultations') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consultations List -->
|
||||
<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 flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{{ $consultation->booking_date->translatedFormat('l, d M Y') }}
|
||||
</span>
|
||||
<span class="text-zinc-500 dark:text-zinc-400">
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$statusVariant = match($consultation->status) {
|
||||
\App\Enums\ConsultationStatus::Pending => 'warning',
|
||||
\App\Enums\ConsultationStatus::Approved => 'primary',
|
||||
\App\Enums\ConsultationStatus::Completed => 'success',
|
||||
\App\Enums\ConsultationStatus::Cancelled => 'danger',
|
||||
\App\Enums\ConsultationStatus::NoShow => 'danger',
|
||||
\App\Enums\ConsultationStatus::Rejected => 'danger',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge variant="{{ $statusVariant }}" size="sm">
|
||||
{{ $consultation->status->label() }}
|
||||
</flux:badge>
|
||||
|
||||
<flux:badge variant="{{ $consultation->consultation_type === \App\Enums\ConsultationType::Paid ? 'primary' : 'outline' }}" size="sm">
|
||||
{{ $consultation->consultation_type->label() }}
|
||||
</flux:badge>
|
||||
|
||||
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid)
|
||||
@php
|
||||
$paymentVariant = match($consultation->payment_status) {
|
||||
\App\Enums\PaymentStatus::Pending => 'warning',
|
||||
\App\Enums\PaymentStatus::Received => 'success',
|
||||
default => 'outline',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge variant="{{ $paymentVariant }}" size="sm">
|
||||
{{ $consultation->payment_status->label() }}
|
||||
</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($consultation->problem_summary)
|
||||
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-2">
|
||||
{{ Str::limit($consultation->problem_summary, 150) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<flux:button
|
||||
href="{{ route('admin.consultations.show', $consultation) }}"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('common.edit') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400 bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:icon name="inbox" class="w-12 h-12 mx-auto mb-4" />
|
||||
<p>{{ __('admin.no_consultations') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
{{ $consultations->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\ConsultationType;
|
||||
use App\Enums\PaymentStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\Consultation;
|
||||
use App\Notifications\ConsultationCancelled;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $statusFilter = '';
|
||||
public string $typeFilter = '';
|
||||
public string $paymentFilter = '';
|
||||
public string $dateFrom = '';
|
||||
public string $dateTo = '';
|
||||
public string $sortBy = 'booking_date';
|
||||
public string $sortDir = 'desc';
|
||||
public int $perPage = 15;
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedStatusFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedTypeFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedPaymentFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedDateFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedDateTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedPerPage(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function sort(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDir = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->statusFilter = '';
|
||||
$this->typeFilter = '';
|
||||
$this->paymentFilter = '';
|
||||
$this->dateFrom = '';
|
||||
$this->dateTo = '';
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function markCompleted(int $id): void
|
||||
{
|
||||
DB::transaction(function () use ($id) {
|
||||
$consultation = Consultation::lockForUpdate()->findOrFail($id);
|
||||
$oldStatus = $consultation->status->value;
|
||||
|
||||
try {
|
||||
$consultation->markAsCompleted();
|
||||
|
||||
$this->logStatusChange($consultation, $oldStatus, 'completed');
|
||||
|
||||
session()->flash('success', __('messages.marked_completed'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function markNoShow(int $id): void
|
||||
{
|
||||
DB::transaction(function () use ($id) {
|
||||
$consultation = Consultation::lockForUpdate()->findOrFail($id);
|
||||
$oldStatus = $consultation->status->value;
|
||||
|
||||
try {
|
||||
$consultation->markAsNoShow();
|
||||
|
||||
$this->logStatusChange($consultation, $oldStatus, 'no_show');
|
||||
|
||||
session()->flash('success', __('messages.marked_no_show'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function cancel(int $id): void
|
||||
{
|
||||
DB::transaction(function () use ($id) {
|
||||
$consultation = Consultation::lockForUpdate()->with('user')->findOrFail($id);
|
||||
$oldStatus = $consultation->status->value;
|
||||
|
||||
try {
|
||||
$consultation->cancel();
|
||||
|
||||
// Notify client
|
||||
if ($consultation->user) {
|
||||
$consultation->user->notify(new ConsultationCancelled($consultation));
|
||||
}
|
||||
|
||||
$this->logStatusChange($consultation, $oldStatus, 'cancelled');
|
||||
|
||||
session()->flash('success', __('messages.consultation_cancelled'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function markPaymentReceived(int $id): void
|
||||
{
|
||||
DB::transaction(function () use ($id) {
|
||||
$consultation = Consultation::lockForUpdate()->findOrFail($id);
|
||||
|
||||
try {
|
||||
$consultation->markPaymentReceived();
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'payment_received',
|
||||
'target_type' => 'consultation',
|
||||
'target_id' => $consultation->id,
|
||||
'old_values' => ['payment_status' => 'pending'],
|
||||
'new_values' => ['payment_status' => 'received'],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
session()->flash('success', __('messages.payment_marked_received'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function logStatusChange(Consultation $consultation, string $oldStatus, string $newStatus): void
|
||||
{
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'status_change',
|
||||
'target_type' => 'consultation',
|
||||
'target_id' => $consultation->id,
|
||||
'old_values' => ['status' => $oldStatus],
|
||||
'new_values' => ['status' => $newStatus],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'consultations' => Consultation::query()
|
||||
->with('user:id,full_name,email,phone,user_type')
|
||||
->when($this->search, fn ($q) => $q->whereHas('user', fn ($uq) =>
|
||||
$uq->where('full_name', 'like', "%{$this->search}%")
|
||||
->orWhere('email', 'like', "%{$this->search}%")
|
||||
))
|
||||
->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter))
|
||||
->when($this->typeFilter, fn ($q) => $q->where('consultation_type', $this->typeFilter))
|
||||
->when($this->paymentFilter, fn ($q) => $q->where('payment_status', $this->paymentFilter))
|
||||
->when($this->dateFrom, fn ($q) => $q->where('booking_date', '>=', $this->dateFrom))
|
||||
->when($this->dateTo, fn ($q) => $q->where('booking_date', '<=', $this->dateTo))
|
||||
->orderBy($this->sortBy, $this->sortDir)
|
||||
->paginate($this->perPage),
|
||||
'statuses' => ConsultationStatus::cases(),
|
||||
'types' => ConsultationType::cases(),
|
||||
'paymentStatuses' => PaymentStatus::cases(),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('admin.consultations') }}</flux:heading>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ __('admin.consultations_description') }}</p>
|
||||
</div>
|
||||
</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 dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
<flux:field>
|
||||
<flux:input
|
||||
wire:model.live.debounce.300ms="search"
|
||||
placeholder="{{ __('admin.search_clients') }}"
|
||||
icon="magnifying-glass"
|
||||
/>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:select wire:model.live="statusFilter">
|
||||
<option value="">{{ __('admin.all_statuses') }}</option>
|
||||
@foreach($statuses as $status)
|
||||
<option value="{{ $status->value }}">{{ $status->label() }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:select wire:model.live="typeFilter">
|
||||
<option value="">{{ __('admin.all_types') }}</option>
|
||||
@foreach($types as $type)
|
||||
<option value="{{ $type->value }}">{{ $type->label() }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:select wire:model.live="paymentFilter">
|
||||
<option value="">{{ __('admin.all_payments') }}</option>
|
||||
@foreach($paymentStatuses as $ps)
|
||||
<option value="{{ $ps->value }}">{{ $ps->label() }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.per_page') }}</flux:label>
|
||||
<flux:select wire:model.live="perPage">
|
||||
<option value="15">15</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
@if($search || $statusFilter || $typeFilter || $paymentFilter || $dateFrom || $dateTo)
|
||||
<flux:button wire:click="clearFilters" variant="ghost">
|
||||
{{ __('common.clear') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort Headers -->
|
||||
<div class="hidden lg:flex bg-zinc-100 dark:bg-zinc-700 rounded-t-lg px-4 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-300 gap-4 mb-0">
|
||||
<button wire:click="sort('booking_date')" class="flex items-center gap-1 w-32 hover:text-zinc-900 dark:hover:text-white">
|
||||
{{ __('admin.date') }}
|
||||
@if($sortBy === 'booking_date')
|
||||
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
<span class="flex-1">{{ __('admin.client') }}</span>
|
||||
<button wire:click="sort('status')" class="flex items-center gap-1 w-24 hover:text-zinc-900 dark:hover:text-white">
|
||||
{{ __('admin.current_status') }}
|
||||
@if($sortBy === 'status')
|
||||
<flux:icon name="{{ $sortDir === 'asc' ? 'chevron-up' : 'chevron-down' }}" class="w-4 h-4" />
|
||||
@endif
|
||||
</button>
|
||||
<span class="w-24">{{ __('admin.payment_status') }}</span>
|
||||
<span class="w-48">{{ __('common.actions') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Consultations List -->
|
||||
<div class="space-y-0">
|
||||
@forelse($consultations as $consultation)
|
||||
<div wire:key="consultation-{{ $consultation->id }}" class="bg-white dark:bg-zinc-800 p-4 border border-zinc-200 dark:border-zinc-700 {{ $loop->first ? 'rounded-t-lg lg:rounded-t-none' : '' }} {{ $loop->last ? 'rounded-b-lg' : '' }} {{ !$loop->first ? 'border-t-0' : '' }}">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
<!-- Date/Time -->
|
||||
<div class="lg:w-32">
|
||||
<div class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('d M Y') }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Info -->
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<a href="{{ route('admin.consultations.show', $consultation) }}" class="font-semibold text-zinc-900 dark:text-zinc-100 hover:text-blue-600 dark:hover:text-blue-400" wire:navigate>
|
||||
{{ $consultation->user?->full_name ?? __('common.unknown') }}
|
||||
</a>
|
||||
<flux:badge size="sm" variant="{{ $consultation->consultation_type === \App\Enums\ConsultationType::Paid ? 'primary' : 'outline' }}">
|
||||
{{ $consultation->consultation_type->label() }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{{ $consultation->user?->email ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="lg:w-24">
|
||||
@php
|
||||
$statusVariant = match($consultation->status) {
|
||||
\App\Enums\ConsultationStatus::Pending => 'warning',
|
||||
\App\Enums\ConsultationStatus::Approved => 'primary',
|
||||
\App\Enums\ConsultationStatus::Completed => 'success',
|
||||
\App\Enums\ConsultationStatus::Cancelled => 'danger',
|
||||
\App\Enums\ConsultationStatus::NoShow => 'danger',
|
||||
\App\Enums\ConsultationStatus::Rejected => 'danger',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge variant="{{ $statusVariant }}" size="sm">
|
||||
{{ $consultation->status->label() }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
<!-- Payment Status -->
|
||||
<div class="lg:w-24">
|
||||
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid)
|
||||
@php
|
||||
$paymentVariant = match($consultation->payment_status) {
|
||||
\App\Enums\PaymentStatus::Pending => 'warning',
|
||||
\App\Enums\PaymentStatus::Received => 'success',
|
||||
default => 'outline',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge variant="{{ $paymentVariant }}" size="sm">
|
||||
{{ $consultation->payment_status->label() }}
|
||||
</flux:badge>
|
||||
@else
|
||||
<span class="text-xs text-zinc-400">-</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="lg:w-48 flex flex-wrap gap-2">
|
||||
<flux:button
|
||||
href="{{ route('admin.consultations.show', $consultation) }}"
|
||||
variant="filled"
|
||||
size="sm"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('common.edit') }}
|
||||
</flux:button>
|
||||
|
||||
@if($consultation->status === \App\Enums\ConsultationStatus::Approved)
|
||||
<flux:dropdown>
|
||||
<flux:button variant="ghost" size="sm" icon="ellipsis-vertical" />
|
||||
|
||||
<flux:menu>
|
||||
<flux:menu.item
|
||||
wire:click="markCompleted({{ $consultation->id }})"
|
||||
wire:confirm="{{ __('admin.confirm_mark_completed') }}"
|
||||
icon="check-circle"
|
||||
>
|
||||
{{ __('admin.mark_completed') }}
|
||||
</flux:menu.item>
|
||||
|
||||
<flux:menu.item
|
||||
wire:click="markNoShow({{ $consultation->id }})"
|
||||
wire:confirm="{{ __('admin.confirm_mark_no_show') }}"
|
||||
icon="x-circle"
|
||||
>
|
||||
{{ __('admin.mark_no_show') }}
|
||||
</flux:menu.item>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.item
|
||||
wire:click="cancel({{ $consultation->id }})"
|
||||
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
|
||||
icon="trash"
|
||||
variant="danger"
|
||||
>
|
||||
{{ __('admin.cancel_consultation') }}
|
||||
</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
@endif
|
||||
|
||||
@if($consultation->status === \App\Enums\ConsultationStatus::Pending)
|
||||
<flux:button
|
||||
wire:click="cancel({{ $consultation->id }})"
|
||||
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
|
||||
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid && $consultation->payment_status === \App\Enums\PaymentStatus::Pending)
|
||||
<flux:button
|
||||
wire:click="markPaymentReceived({{ $consultation->id }})"
|
||||
wire:confirm="{{ __('admin.confirm_mark_payment') }}"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('admin.payment_received') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400 bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:icon name="inbox" class="w-12 h-12 mx-auto mb-4" />
|
||||
<p>{{ __('admin.no_consultations') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
{{ $consultations->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,684 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\ConsultationType;
|
||||
use App\Enums\PaymentStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\User;
|
||||
use App\Notifications\ConsultationCancelled;
|
||||
use App\Notifications\ConsultationRescheduled;
|
||||
use App\Services\AvailabilityService;
|
||||
use App\Services\CalendarService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
public Consultation $consultation;
|
||||
|
||||
// Reschedule
|
||||
public bool $showRescheduleModal = false;
|
||||
public string $newDate = '';
|
||||
public string $newTime = '';
|
||||
public array $availableSlots = [];
|
||||
|
||||
// Notes
|
||||
public string $newNote = '';
|
||||
public ?int $editingNoteIndex = null;
|
||||
public string $editingNoteText = '';
|
||||
|
||||
public function mount(Consultation $consultation): void
|
||||
{
|
||||
$this->consultation = $consultation->load('user');
|
||||
$this->newDate = $consultation->booking_date->format('Y-m-d');
|
||||
}
|
||||
|
||||
// Status Actions
|
||||
public function markCompleted(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$consultation = Consultation::lockForUpdate()->findOrFail($this->consultation->id);
|
||||
$oldStatus = $consultation->status->value;
|
||||
|
||||
try {
|
||||
$consultation->markAsCompleted();
|
||||
$this->consultation = $consultation->fresh();
|
||||
|
||||
$this->logStatusChange($oldStatus, 'completed');
|
||||
|
||||
session()->flash('success', __('messages.marked_completed'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function markNoShow(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$consultation = Consultation::lockForUpdate()->findOrFail($this->consultation->id);
|
||||
$oldStatus = $consultation->status->value;
|
||||
|
||||
try {
|
||||
$consultation->markAsNoShow();
|
||||
$this->consultation = $consultation->fresh();
|
||||
|
||||
$this->logStatusChange($oldStatus, 'no_show');
|
||||
|
||||
session()->flash('success', __('messages.marked_no_show'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function cancel(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$consultation = Consultation::lockForUpdate()->with('user')->findOrFail($this->consultation->id);
|
||||
$oldStatus = $consultation->status->value;
|
||||
|
||||
try {
|
||||
$consultation->cancel();
|
||||
$this->consultation = $consultation->fresh()->load('user');
|
||||
|
||||
if ($consultation->user) {
|
||||
$consultation->user->notify(new ConsultationCancelled($consultation));
|
||||
}
|
||||
|
||||
$this->logStatusChange($oldStatus, 'cancelled');
|
||||
|
||||
session()->flash('success', __('messages.consultation_cancelled'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function markPaymentReceived(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$consultation = Consultation::lockForUpdate()->findOrFail($this->consultation->id);
|
||||
|
||||
try {
|
||||
$consultation->markPaymentReceived();
|
||||
$this->consultation = $consultation->fresh();
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'payment_received',
|
||||
'target_type' => 'consultation',
|
||||
'target_id' => $consultation->id,
|
||||
'old_values' => ['payment_status' => 'pending'],
|
||||
'new_values' => ['payment_status' => 'received'],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
session()->flash('success', __('messages.payment_marked_received'));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reschedule
|
||||
public function openRescheduleModal(): void
|
||||
{
|
||||
$this->showRescheduleModal = true;
|
||||
$this->newDate = $this->consultation->booking_date->format('Y-m-d');
|
||||
$this->newTime = '';
|
||||
$this->loadAvailableSlots();
|
||||
}
|
||||
|
||||
public function closeRescheduleModal(): void
|
||||
{
|
||||
$this->showRescheduleModal = false;
|
||||
$this->newDate = '';
|
||||
$this->newTime = '';
|
||||
$this->availableSlots = [];
|
||||
}
|
||||
|
||||
public function updatedNewDate(): void
|
||||
{
|
||||
$this->loadAvailableSlots();
|
||||
$this->newTime = '';
|
||||
}
|
||||
|
||||
private function loadAvailableSlots(): void
|
||||
{
|
||||
if ($this->newDate) {
|
||||
$service = app(AvailabilityService::class);
|
||||
$this->availableSlots = $service->getAvailableSlots(Carbon::parse($this->newDate));
|
||||
}
|
||||
}
|
||||
|
||||
public function reschedule(): void
|
||||
{
|
||||
$this->validate([
|
||||
'newDate' => ['required', 'date', 'after_or_equal:today'],
|
||||
'newTime' => ['required'],
|
||||
]);
|
||||
|
||||
$oldDate = $this->consultation->booking_date;
|
||||
$oldTime = $this->consultation->booking_time;
|
||||
|
||||
// Check if same date/time
|
||||
if ($oldDate->format('Y-m-d') === $this->newDate && $oldTime === $this->newTime) {
|
||||
session()->flash('info', __('messages.no_changes_made'));
|
||||
$this->closeRescheduleModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify slot available
|
||||
$service = app(AvailabilityService::class);
|
||||
$slots = $service->getAvailableSlots(Carbon::parse($this->newDate));
|
||||
|
||||
if (!in_array($this->newTime, $slots)) {
|
||||
$this->addError('newTime', __('booking.slot_not_available'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against missing user
|
||||
if (!$this->consultation->user) {
|
||||
session()->flash('error', __('messages.client_account_not_found'));
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($oldDate, $oldTime) {
|
||||
$this->consultation->reschedule($this->newDate, $this->newTime);
|
||||
$this->consultation = $this->consultation->fresh()->load('user');
|
||||
|
||||
// Generate new .ics
|
||||
$icsContent = '';
|
||||
try {
|
||||
$calendarService = app(CalendarService::class);
|
||||
$icsContent = $calendarService->generateIcs($this->consultation);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to generate ICS on reschedule', [
|
||||
'consultation_id' => $this->consultation->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Notify client
|
||||
try {
|
||||
$this->consultation->user->notify(
|
||||
new ConsultationRescheduled($this->consultation, $oldDate, $oldTime, $icsContent)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send reschedule notification', [
|
||||
'consultation_id' => $this->consultation->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Log
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'reschedule',
|
||||
'target_type' => 'consultation',
|
||||
'target_id' => $this->consultation->id,
|
||||
'old_values' => ['date' => $oldDate->format('Y-m-d'), 'time' => $oldTime],
|
||||
'new_values' => ['date' => $this->newDate, 'time' => $this->newTime],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
session()->flash('success', __('messages.consultation_rescheduled'));
|
||||
$this->closeRescheduleModal();
|
||||
}
|
||||
|
||||
// Notes
|
||||
public function addNote(): void
|
||||
{
|
||||
$this->validate([
|
||||
'newNote' => ['required', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
$this->consultation->addNote($this->newNote, auth()->id());
|
||||
$this->consultation = $this->consultation->fresh();
|
||||
$this->newNote = '';
|
||||
|
||||
session()->flash('success', __('messages.note_added'));
|
||||
}
|
||||
|
||||
public function startEditNote(int $index): void
|
||||
{
|
||||
$notes = $this->consultation->admin_notes ?? [];
|
||||
if (isset($notes[$index])) {
|
||||
$this->editingNoteIndex = $index;
|
||||
$this->editingNoteText = $notes[$index]['text'];
|
||||
}
|
||||
}
|
||||
|
||||
public function cancelEditNote(): void
|
||||
{
|
||||
$this->editingNoteIndex = null;
|
||||
$this->editingNoteText = '';
|
||||
}
|
||||
|
||||
public function updateNote(): void
|
||||
{
|
||||
$this->validate([
|
||||
'editingNoteText' => ['required', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
if ($this->editingNoteIndex !== null) {
|
||||
$this->consultation->updateNote($this->editingNoteIndex, $this->editingNoteText);
|
||||
$this->consultation = $this->consultation->fresh();
|
||||
$this->cancelEditNote();
|
||||
|
||||
session()->flash('success', __('messages.note_updated'));
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteNote(int $index): void
|
||||
{
|
||||
$this->consultation->deleteNote($index);
|
||||
$this->consultation = $this->consultation->fresh();
|
||||
|
||||
session()->flash('success', __('messages.note_deleted'));
|
||||
}
|
||||
|
||||
private function logStatusChange(string $oldStatus, string $newStatus): void
|
||||
{
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'status_change',
|
||||
'target_type' => 'consultation',
|
||||
'target_id' => $this->consultation->id,
|
||||
'old_values' => ['status' => $oldStatus],
|
||||
'new_values' => ['status' => $newStatus],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'adminUsers' => User::query()
|
||||
->where('user_type', 'admin')
|
||||
->pluck('full_name', 'id'),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<flux:button href="{{ route('admin.consultations.index') }}" variant="ghost" icon="arrow-left" wire:navigate>
|
||||
{{ __('common.back') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<flux:heading size="xl">{{ __('admin.consultation_detail') }}</flux:heading>
|
||||
|
||||
@if($consultation->status === \App\Enums\ConsultationStatus::Approved)
|
||||
<div class="flex gap-2">
|
||||
<flux:button wire:click="openRescheduleModal" variant="filled" icon="calendar">
|
||||
{{ __('admin.reschedule') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</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
|
||||
|
||||
@if(session('info'))
|
||||
<flux:callout variant="warning" class="mb-6">
|
||||
{{ session('info') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Main Details -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Booking Info -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('admin.booking_details') }}</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.requested_date') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100 font-medium">
|
||||
{{ $consultation->booking_date->translatedFormat('l, d M Y') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.requested_time') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100 font-medium">
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.current_status') }}</dt>
|
||||
<dd>
|
||||
@php
|
||||
$statusVariant = match($consultation->status) {
|
||||
\App\Enums\ConsultationStatus::Pending => 'warning',
|
||||
\App\Enums\ConsultationStatus::Approved => 'primary',
|
||||
\App\Enums\ConsultationStatus::Completed => 'success',
|
||||
\App\Enums\ConsultationStatus::Cancelled => 'danger',
|
||||
\App\Enums\ConsultationStatus::NoShow => 'danger',
|
||||
\App\Enums\ConsultationStatus::Rejected => 'danger',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge variant="{{ $statusVariant }}">
|
||||
{{ $consultation->status->label() }}
|
||||
</flux:badge>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.consultation_type') }}</dt>
|
||||
<dd>
|
||||
<flux:badge variant="{{ $consultation->consultation_type === \App\Enums\ConsultationType::Paid ? 'primary' : 'outline' }}">
|
||||
{{ $consultation->consultation_type->label() }}
|
||||
</flux:badge>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($consultation->problem_summary)
|
||||
<div class="mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400 mb-2">{{ __('admin.problem_summary') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100">{{ $consultation->problem_summary }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Payment Info (for paid consultations) -->
|
||||
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid)
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<flux:heading size="lg">{{ __('admin.payment_details') }}</flux:heading>
|
||||
@if($consultation->payment_status === \App\Enums\PaymentStatus::Pending)
|
||||
<flux:button
|
||||
wire:click="markPaymentReceived"
|
||||
wire:confirm="{{ __('admin.confirm_mark_payment') }}"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('admin.mark_payment_received') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.payment_amount') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100 font-medium">
|
||||
{{ number_format($consultation->payment_amount, 2) }} {{ __('common.currency') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.payment_status') }}</dt>
|
||||
<dd>
|
||||
@php
|
||||
$paymentVariant = match($consultation->payment_status) {
|
||||
\App\Enums\PaymentStatus::Pending => 'warning',
|
||||
\App\Enums\PaymentStatus::Received => 'success',
|
||||
default => 'outline',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge variant="{{ $paymentVariant }}">
|
||||
{{ $consultation->payment_status->label() }}
|
||||
</flux:badge>
|
||||
</dd>
|
||||
</div>
|
||||
@if($consultation->payment_received_at)
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.payment_received_at') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100">
|
||||
{{ $consultation->payment_received_at->translatedFormat('d M Y, g:i A') }}
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Admin Notes -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('admin.admin_notes') }}</flux:heading>
|
||||
|
||||
<!-- Add Note Form -->
|
||||
<div class="mb-6">
|
||||
<flux:field>
|
||||
<flux:textarea
|
||||
wire:model="newNote"
|
||||
rows="3"
|
||||
placeholder="{{ __('admin.note_placeholder') }}"
|
||||
/>
|
||||
@error('newNote')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<flux:button wire:click="addNote" variant="primary" size="sm">
|
||||
{{ __('admin.add_note') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes List -->
|
||||
<div class="space-y-4">
|
||||
@forelse($consultation->admin_notes ?? [] as $index => $note)
|
||||
<div wire:key="note-{{ $index }}" class="p-4 bg-zinc-50 dark:bg-zinc-700/50 rounded-lg">
|
||||
@if($editingNoteIndex === $index)
|
||||
<flux:field>
|
||||
<flux:textarea
|
||||
wire:model="editingNoteText"
|
||||
rows="3"
|
||||
/>
|
||||
@error('editingNoteText')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
<div class="mt-2 flex gap-2 justify-end">
|
||||
<flux:button wire:click="cancelEditNote" variant="ghost" size="sm">
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button wire:click="updateNote" variant="primary" size="sm">
|
||||
{{ __('common.save') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-zinc-900 dark:text-zinc-100 mb-2">{{ $note['text'] }}</p>
|
||||
<div class="flex justify-between items-center text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<span>
|
||||
{{ __('admin.added_by') }}: {{ $adminUsers[$note['admin_id']] ?? __('common.unknown') }}
|
||||
@if(isset($note['updated_at']))
|
||||
({{ __('admin.updated') }})
|
||||
@endif
|
||||
</span>
|
||||
<span>{{ \Carbon\Carbon::parse($note['created_at'])->translatedFormat('d M Y, g:i A') }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2 justify-end">
|
||||
<flux:button wire:click="startEditNote({{ $index }})" variant="ghost" size="sm">
|
||||
{{ __('common.edit') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
wire:click="deleteNote({{ $index }})"
|
||||
wire:confirm="{{ __('admin.confirm_delete_note') }}"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('common.delete') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-zinc-500 dark:text-zinc-400 text-center py-4">{{ __('admin.no_notes') }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Client Info -->
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('admin.client_information') }}</flux:heading>
|
||||
|
||||
@if($consultation->user)
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_name') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100 font-medium">
|
||||
{{ $consultation->user->full_name }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_email') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100">{{ $consultation->user->email }}</dd>
|
||||
</div>
|
||||
@if($consultation->user->phone)
|
||||
<div>
|
||||
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_phone') }}</dt>
|
||||
<dd class="text-zinc-900 dark:text-zinc-100">{{ $consultation->user->phone }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<flux:button
|
||||
href="{{ route('admin.clients.consultation-history', $consultation->user) }}"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('admin.view_client_history') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-zinc-500 dark:text-zinc-400">{{ __('messages.client_account_not_found') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Status Actions -->
|
||||
@if($consultation->status === \App\Enums\ConsultationStatus::Approved)
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('common.actions') }}</flux:heading>
|
||||
|
||||
<div class="space-y-2">
|
||||
<flux:button
|
||||
wire:click="markCompleted"
|
||||
wire:confirm="{{ __('admin.confirm_mark_completed') }}"
|
||||
variant="filled"
|
||||
class="w-full"
|
||||
icon="check-circle"
|
||||
>
|
||||
{{ __('admin.mark_completed') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button
|
||||
wire:click="markNoShow"
|
||||
wire:confirm="{{ __('admin.confirm_mark_no_show') }}"
|
||||
variant="ghost"
|
||||
class="w-full"
|
||||
icon="x-circle"
|
||||
>
|
||||
{{ __('admin.mark_no_show') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button
|
||||
wire:click="cancel"
|
||||
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
|
||||
variant="danger"
|
||||
class="w-full"
|
||||
icon="trash"
|
||||
>
|
||||
{{ __('admin.cancel_consultation') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($consultation->status === \App\Enums\ConsultationStatus::Pending)
|
||||
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('common.actions') }}</flux:heading>
|
||||
|
||||
<flux:button
|
||||
wire:click="cancel"
|
||||
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
|
||||
variant="danger"
|
||||
class="w-full"
|
||||
icon="trash"
|
||||
>
|
||||
{{ __('admin.cancel_consultation') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reschedule Modal -->
|
||||
<flux:modal wire:model="showRescheduleModal" name="reschedule-modal" class="max-w-lg">
|
||||
<div class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('admin.reschedule_consultation') }}</flux:heading>
|
||||
|
||||
<div class="mb-4 p-4 bg-zinc-50 dark:bg-zinc-700/50 rounded-lg">
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-1">{{ __('admin.current_schedule') }}</p>
|
||||
<p class="text-zinc-900 dark:text-zinc-100 font-medium">
|
||||
{{ $consultation->booking_date->translatedFormat('l, d M Y') }}
|
||||
{{ __('admin.to') }}
|
||||
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.new_date') }}</flux:label>
|
||||
<flux:input type="date" wire:model.live="newDate" min="{{ now()->format('Y-m-d') }}" />
|
||||
@error('newDate')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('admin.new_time') }}</flux:label>
|
||||
@if(count($availableSlots) > 0)
|
||||
<flux:select wire:model="newTime">
|
||||
<option value="">{{ __('admin.select_time') }}</option>
|
||||
@foreach($availableSlots as $slot)
|
||||
<option value="{{ $slot }}">{{ \Carbon\Carbon::parse($slot)->format('g:i A') }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
@else
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.no_slots_available') }}</p>
|
||||
@endif
|
||||
@error('newTime')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-2 justify-end">
|
||||
<flux:button wire:click="closeRescheduleModal" variant="ghost">
|
||||
{{ __('common.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button wire:click="reschedule" variant="primary" :disabled="!$newDate || !$newTime">
|
||||
{{ __('admin.reschedule') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
Reference in New Issue
Block a user