libra/docs/stories/story-3.7-consultation-mana...

385 lines
11 KiB
Markdown

# 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
<?php
use App\Models\Consultation;
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 = 'scheduled_date';
public string $sortDir = 'desc';
public function updatedSearch()
{
$this->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
<?php
use App\Models\Consultation;
use App\Services\{AvailabilityService, CalendarService};
use App\Notifications\ConsultationRescheduled;
use Livewire\Volt\Component;
new class extends Component {
public Consultation $consultation;
public string $newDate = '';
public string $newTime = '';
public array $availableSlots = [];
public function mount(Consultation $consultation): void
{
$this->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