generated sotries
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user