385 lines
11 KiB
Markdown
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
|