libra/resources/views/livewire/admin/consultations/show.blade.php

691 lines
28 KiB
PHP

<?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="outline" 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="outline" 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 rounded-lg p-6 border border-zinc-200">
<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">{{ __('admin.requested_date') }}</dt>
<dd class="text-zinc-900 font-medium">
{{ $consultation->booking_date->translatedFormat('l, d M Y') }}
</dd>
</div>
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.requested_time') }}</dt>
<dd class="text-zinc-900 font-medium">
{{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
</dd>
</div>
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.current_status') }}</dt>
<dd>
@php
$statusColor = match($consultation->status) {
\App\Enums\ConsultationStatus::Pending => 'amber',
\App\Enums\ConsultationStatus::Approved => 'sky',
\App\Enums\ConsultationStatus::Completed => 'green',
\App\Enums\ConsultationStatus::Cancelled => 'red',
\App\Enums\ConsultationStatus::NoShow => 'orange',
\App\Enums\ConsultationStatus::Rejected => 'rose',
};
@endphp
<flux:badge color="{{ $statusColor }}">
{{ $consultation->status->label() }}
</flux:badge>
</dd>
</div>
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.consultation_type') }}</dt>
<dd>
<flux:badge color="{{ $consultation->consultation_type === \App\Enums\ConsultationType::Paid ? 'indigo' : 'zinc' }}">
{{ $consultation->consultation_type->label() }}
</flux:badge>
</dd>
</div>
</div>
@if($consultation->problem_summary)
<div class="mt-4 pt-4 border-t border-zinc-200">
<dt class="text-sm text-zinc-500 mb-2">{{ __('admin.problem_summary') }}</dt>
<dd class="text-zinc-900">{{ $consultation->problem_summary }}</dd>
</div>
@endif
</div>
<!-- Payment Info (for paid consultations) -->
@if($consultation->consultation_type === \App\Enums\ConsultationType::Paid)
<div class="bg-white rounded-lg p-6 border border-zinc-200">
<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">{{ __('admin.payment_amount') }}</dt>
<dd class="text-zinc-900 font-medium">
{{ number_format($consultation->payment_amount, 2) }} {{ __('common.currency') }}
</dd>
</div>
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.payment_status') }}</dt>
<dd>
@php
$paymentColor = match($consultation->payment_status) {
\App\Enums\PaymentStatus::Pending => 'amber',
\App\Enums\PaymentStatus::Received => 'green',
default => 'zinc',
};
@endphp
<flux:badge color="{{ $paymentColor }}">
{{ $consultation->payment_status->label() }}
</flux:badge>
</dd>
</div>
@if($consultation->payment_received_at)
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.payment_received_at') }}</dt>
<dd class="text-zinc-900">
{{ $consultation->payment_received_at->translatedFormat('d M Y, g:i A') }}
</dd>
</div>
@endif
</div>
</div>
@endif
<!-- Admin Notes -->
<div class="bg-white rounded-lg p-6 border border-zinc-200">
<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 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="outline" 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 mb-2">{{ $note['text'] }}</p>
<div class="flex justify-between items-center text-xs text-zinc-500">
<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="outline" 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 text-center py-4">{{ __('admin.no_notes') }}</p>
@endforelse
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Client Info -->
<div class="bg-white rounded-lg p-6 border border-zinc-200">
<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">{{ __('admin.client_name') }}</dt>
<dd class="text-zinc-900 font-medium">
{{ $consultation->user->full_name }}
</dd>
</div>
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.client_email') }}</dt>
<dd class="text-zinc-900">{{ $consultation->user->email }}</dd>
</div>
@if($consultation->user->phone)
<div>
<dt class="text-sm text-zinc-500">{{ __('admin.client_phone') }}</dt>
<dd class="text-zinc-900">{{ $consultation->user->phone }}</dd>
</div>
@endif
</div>
<div class="mt-4 pt-4 border-t border-zinc-200">
<flux:button
href="{{ route('admin.clients.consultation-history', $consultation->user) }}"
variant="outline"
size="sm"
class="w-full"
wire:navigate
>
{{ __('admin.view_client_history') }}
</flux:button>
</div>
@else
<p class="text-zinc-500">{{ __('messages.client_account_not_found') }}</p>
@endif
</div>
<!-- Status Actions -->
@if(in_array($consultation->status, [\App\Enums\ConsultationStatus::Approved, \App\Enums\ConsultationStatus::Completed, \App\Enums\ConsultationStatus::NoShow]))
<div class="bg-white rounded-lg p-6 border border-zinc-200">
<flux:heading size="lg" class="mb-4">{{ __('common.actions') }}</flux:heading>
<div class="space-y-2">
@if($consultation->status !== \App\Enums\ConsultationStatus::Completed)
<flux:button
wire:click="markCompleted"
wire:confirm="{{ __('admin.confirm_mark_completed') }}"
variant="outline"
class="w-full"
icon="check-circle"
>
{{ __('admin.mark_completed') }}
</flux:button>
@endif
@if($consultation->status !== \App\Enums\ConsultationStatus::NoShow)
<flux:button
wire:click="markNoShow"
wire:confirm="{{ __('admin.confirm_mark_no_show') }}"
variant="outline"
class="w-full"
icon="x-circle"
>
{{ __('admin.mark_no_show') }}
</flux:button>
@endif
@if($consultation->status === \App\Enums\ConsultationStatus::Approved)
<flux:button
wire:click="cancel"
wire:confirm="{{ __('admin.confirm_cancel_consultation') }}"
variant="danger"
class="w-full"
icon="trash"
>
{{ __('admin.cancel_consultation') }}
</flux:button>
@endif
</div>
</div>
@endif
@if($consultation->status === \App\Enums\ConsultationStatus::Pending)
<div class="bg-white rounded-lg p-6 border border-zinc-200">
<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 rounded-lg">
<p class="text-sm text-zinc-500 mb-1">{{ __('admin.current_schedule') }}</p>
<p class="text-zinc-900 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">{{ __('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="outline">
{{ __('common.cancel') }}
</flux:button>
<flux:button wire:click="reschedule" variant="primary" :disabled="!$newDate || !$newTime">
{{ __('admin.reschedule') }}
</flux:button>
</div>
</div>
</flux:modal>
</div>