# Story 3.7: Consultation Management ## Epic Reference **Epic 3:** Booking & Consultation System ## User Story As an **admin**, I want **to manage consultations throughout their lifecycle**, So that **I can track completed sessions, handle no-shows, and maintain accurate records**. ## Story Context ### Existing System Integration - **Integrates with:** consultations table, notifications - **Technology:** Livewire Volt, Flux UI - **Follows pattern:** Admin management dashboard - **Touch points:** Consultation status, payment tracking, admin notes ## Acceptance Criteria ### Consultations List View - [ ] View all consultations with filters: - Status (pending/approved/completed/cancelled/no_show) - Type (free/paid) - Payment status (pending/received/not_applicable) - Date range - Client name/email search - [ ] Sort by date, status, client name - [ ] Pagination (15/25/50 per page) - [ ] Quick status indicators ### Status Management - [ ] Mark consultation as completed - [ ] Mark consultation as no-show - [ ] Cancel booking on behalf of client - [ ] Status change confirmation ### Rescheduling - [ ] Reschedule appointment to new date/time - [ ] Validate new slot availability - [ ] Send notification to client - [ ] Generate new .ics file ### Payment Tracking - [ ] Mark payment as received (for paid consultations) - [ ] Payment date recorded - [ ] Payment status visible in list ### Admin Notes - [ ] Add internal admin notes - [ ] Notes not visible to client - [ ] View notes in consultation detail - [ ] Edit/delete notes ### Client History - [ ] View all consultations for a specific client - [ ] Linked from user profile - [ ] Summary statistics per client ### Quality Requirements - [ ] Audit log for all status changes - [ ] Bilingual labels - [ ] Tests for status transitions ## Technical Notes ### Status Enum ```php enum ConsultationStatus: string { case Pending = 'pending'; case Approved = 'approved'; case Completed = 'completed'; case Cancelled = 'cancelled'; case NoShow = 'no_show'; } enum PaymentStatus: string { case Pending = 'pending'; case Received = 'received'; case NotApplicable = 'not_applicable'; } ``` ### Consultation Model Methods ```php class Consultation extends Model { public function markAsCompleted(): void { $this->update(['status' => ConsultationStatus::Completed]); } public function markAsNoShow(): void { $this->update(['status' => ConsultationStatus::NoShow]); } public function cancel(): void { $this->update(['status' => ConsultationStatus::Cancelled]); } public function markPaymentReceived(): void { $this->update([ 'payment_status' => PaymentStatus::Received, 'payment_received_at' => now(), ]); } public function reschedule(string $newDate, string $newTime): void { $this->update([ 'scheduled_date' => $newDate, 'scheduled_time' => $newTime, ]); } // Scopes public function scopeUpcoming($query) { return $query->where('scheduled_date', '>=', today()) ->where('status', ConsultationStatus::Approved); } public function scopePast($query) { return $query->where('scheduled_date', '<', today()) ->orWhereIn('status', [ ConsultationStatus::Completed, ConsultationStatus::Cancelled, ConsultationStatus::NoShow, ]); } } ``` ### Volt Component for Management ```php resetPage(); } public function markCompleted(int $id): void { $consultation = Consultation::findOrFail($id); $oldStatus = $consultation->status; $consultation->markAsCompleted(); $this->logStatusChange($consultation, $oldStatus, 'completed'); session()->flash('success', __('messages.marked_completed')); } public function markNoShow(int $id): void { $consultation = Consultation::findOrFail($id); $oldStatus = $consultation->status; $consultation->markAsNoShow(); $this->logStatusChange($consultation, $oldStatus, 'no_show'); session()->flash('success', __('messages.marked_no_show')); } public function cancel(int $id): void { $consultation = Consultation::findOrFail($id); $oldStatus = $consultation->status; $consultation->cancel(); // Notify client $consultation->user->notify(new ConsultationCancelled($consultation)); $this->logStatusChange($consultation, $oldStatus, 'cancelled'); session()->flash('success', __('messages.consultation_cancelled')); } public function markPaymentReceived(int $id): void { $consultation = Consultation::findOrFail($id); $consultation->markPaymentReceived(); AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'payment_received', 'target_type' => 'consultation', 'target_id' => $consultation->id, 'ip_address' => request()->ip(), ]); session()->flash('success', __('messages.payment_marked_received')); } private function logStatusChange(Consultation $consultation, string $oldStatus, string $newStatus): void { AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'status_change', 'target_type' => 'consultation', 'target_id' => $consultation->id, 'old_values' => ['status' => $oldStatus], 'new_values' => ['status' => $newStatus], 'ip_address' => request()->ip(), ]); } public function with(): array { return [ 'consultations' => Consultation::query() ->with('user') ->when($this->search, fn($q) => $q->whereHas('user', fn($uq) => $uq->where('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('type', $this->typeFilter)) ->when($this->paymentFilter, fn($q) => $q->where('payment_status', $this->paymentFilter)) ->when($this->dateFrom, fn($q) => $q->where('scheduled_date', '>=', $this->dateFrom)) ->when($this->dateTo, fn($q) => $q->where('scheduled_date', '<=', $this->dateTo)) ->orderBy($this->sortBy, $this->sortDir) ->paginate(15), ]; } }; ``` ### Reschedule Component ```php consultation = $consultation; $this->newDate = $consultation->scheduled_date->format('Y-m-d'); } public function updatedNewDate(): void { if ($this->newDate) { $service = app(AvailabilityService::class); $this->availableSlots = $service->getAvailableSlots( Carbon::parse($this->newDate) ); $this->newTime = ''; } } public function reschedule(): void { $this->validate([ 'newDate' => ['required', 'date', 'after_or_equal:today'], 'newTime' => ['required'], ]); // 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; } $oldDate = $this->consultation->scheduled_date; $oldTime = $this->consultation->scheduled_time; $this->consultation->reschedule($this->newDate, $this->newTime); // Generate new .ics $calendarService = app(CalendarService::class); $icsContent = $calendarService->generateIcs($this->consultation->fresh()); // Notify client $this->consultation->user->notify( new ConsultationRescheduled($this->consultation, $oldDate, $oldTime, $icsContent) ); // Log AdminLog::create([ 'admin_id' => auth()->id(), 'action_type' => 'reschedule', 'target_type' => 'consultation', 'target_id' => $this->consultation->id, 'old_values' => ['date' => $oldDate, 'time' => $oldTime], 'new_values' => ['date' => $this->newDate, 'time' => $this->newTime], 'ip_address' => request()->ip(), ]); session()->flash('success', __('messages.consultation_rescheduled')); $this->redirect(route('admin.consultations.index')); } }; ``` ### Admin Notes ```php // Add admin_notes column to consultations or separate table // In Consultation model: protected $casts = [ 'admin_notes' => 'array', // [{text, admin_id, created_at}] ]; public function addNote(string $note): void { $notes = $this->admin_notes ?? []; $notes[] = [ 'text' => $note, 'admin_id' => auth()->id(), 'created_at' => now()->toISOString(), ]; $this->update(['admin_notes' => $notes]); } ``` ## Definition of Done - [ ] List view with all filters working - [ ] Can mark consultation as completed - [ ] Can mark consultation as no-show - [ ] Can cancel consultation - [ ] Can reschedule consultation - [ ] Can mark payment as received - [ ] Can add admin notes - [ ] Client notified on reschedule/cancel - [ ] New .ics sent on reschedule - [ ] Audit logging complete - [ ] Bilingual support - [ ] Tests for all status changes - [ ] Code formatted with Pint ## Dependencies - **Story 3.5:** Booking approval - **Story 3.6:** Calendar file generation - **Epic 8:** Email notifications ## Risk Assessment - **Primary Risk:** Status change on wrong consultation - **Mitigation:** Confirmation dialogs, clear identification - **Rollback:** Manual status correction, audit log ## Estimation **Complexity:** Medium-High **Estimated Effort:** 5-6 hours