reviewd epic 7 stories
This commit is contained in:
parent
b2977c00d6
commit
f6c06ec3e1
|
|
@ -8,40 +8,117 @@ As a **client**,
|
||||||
I want **a dashboard showing my key information at a glance**,
|
I want **a dashboard showing my key information at a glance**,
|
||||||
So that **I can quickly see upcoming consultations and case updates**.
|
So that **I can quickly see upcoming consultations and case updates**.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Epic Dependencies
|
||||||
|
| Epic | Dependency | Status |
|
||||||
|
|------|------------|--------|
|
||||||
|
| Epic 1 | Authentication system, base UI layout | Required |
|
||||||
|
| Epic 2 | User model with client data | Required |
|
||||||
|
| Epic 3 | Consultation model and booking system | Required |
|
||||||
|
| Epic 4 | Timeline and TimelineUpdate models | Required |
|
||||||
|
|
||||||
|
### Model Prerequisites
|
||||||
|
The following models and relationships must exist before implementation:
|
||||||
|
|
||||||
|
**User Model** (`app/Models/User.php`):
|
||||||
|
- `consultations()` - HasMany relationship to Consultation
|
||||||
|
- `timelines()` - HasMany relationship to Timeline
|
||||||
|
|
||||||
|
**Consultation Model** (`app/Models/Consultation.php`):
|
||||||
|
- `approved()` scope - filters `status = 'approved'`
|
||||||
|
- `pending()` scope - filters `status = 'pending'`
|
||||||
|
- `upcoming()` scope - filters `scheduled_date >= today()`
|
||||||
|
- `scheduled_date` column (date)
|
||||||
|
- `scheduled_time` column (time)
|
||||||
|
- `type` column (enum: 'free', 'paid')
|
||||||
|
- `status` column (enum: 'pending', 'approved', 'rejected', 'completed', 'no-show', 'cancelled')
|
||||||
|
|
||||||
|
**Timeline Model** (`app/Models/Timeline.php`):
|
||||||
|
- `active()` scope - filters `status = 'active'`
|
||||||
|
- `user_id` foreign key
|
||||||
|
- `updates()` - HasMany relationship to TimelineUpdate
|
||||||
|
|
||||||
|
**TimelineUpdate Model** (`app/Models/TimelineUpdate.php`):
|
||||||
|
- `timeline()` - BelongsTo relationship to Timeline
|
||||||
|
- `update_text` column
|
||||||
|
- `created_at` timestamp
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### Welcome Section
|
### Welcome Section
|
||||||
- [ ] Welcome message with client name
|
- [ ] Display "Welcome, {client name}" greeting
|
||||||
|
- [ ] Use localized greeting based on user's preferred language
|
||||||
|
- [ ] Show current date in user's locale format
|
||||||
|
|
||||||
### Upcoming Consultations Widget
|
### Upcoming Consultations Widget
|
||||||
- [ ] Next consultation date/time
|
- [ ] Display next approved consultation (or "No upcoming consultations" if none)
|
||||||
- [ ] Type (free/paid)
|
- [ ] Show consultation date/time formatted per locale (AR: DD/MM/YYYY, EN: MM/DD/YYYY)
|
||||||
- [ ] Status
|
- [ ] Show time in 12-hour format (AM/PM)
|
||||||
- [ ] Quick link to details
|
- [ ] Display type badge: "Free" (green) or "Paid" (gold)
|
||||||
|
- [ ] Display status badge with appropriate color
|
||||||
|
- [ ] "View Details" link to consultation details (Story 7.2)
|
||||||
|
|
||||||
### Active Cases Widget
|
### Active Cases Widget
|
||||||
- [ ] Count of active timelines
|
- [ ] Display count of active timelines
|
||||||
- [ ] Latest update preview
|
- [ ] Show preview of most recent update (truncated to ~100 chars)
|
||||||
- [ ] Link to full list
|
- [ ] "View All Cases" link to timelines list (Story 7.3)
|
||||||
|
- [ ] Empty state: "No active cases" with muted styling
|
||||||
|
|
||||||
### Recent Updates Widget
|
### Recent Updates Widget
|
||||||
- [ ] Last 3 timeline updates (across all cases)
|
- [ ] Display last 3 timeline updates across all user's cases
|
||||||
- [ ] Case name and date
|
- [ ] Each update shows: case name, update date, preview text
|
||||||
- [ ] Link to full timeline
|
- [ ] "View Timeline" link for each update
|
||||||
|
- [ ] Empty state: "No recent updates"
|
||||||
|
|
||||||
### Booking Status Widget
|
### Booking Status Widget
|
||||||
- [ ] Pending booking requests
|
- [ ] Display count of pending booking requests
|
||||||
- [ ] Daily booking limit indicator
|
- [ ] Show booking limit indicator:
|
||||||
- [ ] Quick book button
|
- Can book: "You can book a consultation today" (green)
|
||||||
|
- Cannot book: "You already have a booking for today" (amber)
|
||||||
|
- [ ] "Book Consultation" button linking to booking page (Story 7.5)
|
||||||
|
- [ ] Disable button if daily limit reached
|
||||||
|
|
||||||
### Design
|
### Design Requirements
|
||||||
- [ ] Clean, card-based layout
|
- [ ] Card-based layout using Flux UI components
|
||||||
- [ ] Mobile-first responsive
|
- [ ] Color scheme: Navy (#0A1F44) background sections, Gold (#D4AF37) accents
|
||||||
- [ ] Bilingual content
|
- [ ] Mobile-first responsive (stack cards vertically on mobile)
|
||||||
|
- [ ] All text content bilingual (Arabic RTL / English LTR)
|
||||||
|
- [ ] Use consistent spacing: `gap-6` between cards, `p-6` card padding
|
||||||
|
|
||||||
## Technical Notes
|
### Edge Cases & Empty States
|
||||||
|
- [ ] No consultations: Show empty state with "Book your first consultation" CTA
|
||||||
|
- [ ] No timelines: Show empty state "No cases assigned yet"
|
||||||
|
- [ ] No updates: Show empty state "No recent updates"
|
||||||
|
- [ ] Loading state: Show skeleton loaders while data fetches
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Files to Create/Modify
|
||||||
|
|
||||||
|
| File | Action | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `resources/views/livewire/client/dashboard.blade.php` | Create | Main Volt component |
|
||||||
|
| `routes/web.php` | Modify | Add client dashboard route |
|
||||||
|
| `resources/views/components/layouts/client.blade.php` | Create/Verify | Client layout if not exists |
|
||||||
|
|
||||||
|
### Route Configuration
|
||||||
```php
|
```php
|
||||||
|
// routes/web.php
|
||||||
|
Route::middleware(['auth', 'verified'])->prefix('client')->group(function () {
|
||||||
|
Route::get('/dashboard', function () {
|
||||||
|
return view('livewire.client.dashboard');
|
||||||
|
})->name('client.dashboard');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\TimelineUpdate;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
public function with(): array
|
public function with(): array
|
||||||
{
|
{
|
||||||
|
|
@ -51,6 +128,8 @@ new class extends Component {
|
||||||
'upcomingConsultation' => $user->consultations()
|
'upcomingConsultation' => $user->consultations()
|
||||||
->approved()
|
->approved()
|
||||||
->upcoming()
|
->upcoming()
|
||||||
|
->orderBy('scheduled_date')
|
||||||
|
->orderBy('scheduled_time')
|
||||||
->first(),
|
->first(),
|
||||||
'activeTimelinesCount' => $user->timelines()->active()->count(),
|
'activeTimelinesCount' => $user->timelines()->active()->count(),
|
||||||
'recentUpdates' => TimelineUpdate::whereHas('timeline', fn($q) => $q->where('user_id', $user->id))
|
'recentUpdates' => TimelineUpdate::whereHas('timeline', fn($q) => $q->where('user_id', $user->id))
|
||||||
|
|
@ -58,22 +137,163 @@ new class extends Component {
|
||||||
->take(3)
|
->take(3)
|
||||||
->with('timeline')
|
->with('timeline')
|
||||||
->get(),
|
->get(),
|
||||||
'pendingBookings' => $user->consultations()->pending()->count(),
|
'pendingBookingsCount' => $user->consultations()->pending()->count(),
|
||||||
'canBookToday' => !$user->consultations()
|
'canBookToday' => !$user->consultations()
|
||||||
->whereDate('scheduled_date', today())
|
->whereDate('scheduled_date', today())
|
||||||
->whereIn('status', ['pending', 'approved'])
|
->whereIn('status', ['pending', 'approved'])
|
||||||
->exists(),
|
->exists(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{-- Welcome Section --}}
|
||||||
|
{{-- Widgets Grid --}}
|
||||||
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Flux UI Components to Use
|
||||||
|
- `<flux:card>` - Widget containers
|
||||||
|
- `<flux:heading>` - Section titles
|
||||||
|
- `<flux:badge>` - Status/type indicators
|
||||||
|
- `<flux:button>` - CTAs
|
||||||
|
- `<flux:text>` - Body content
|
||||||
|
- `<flux:skeleton>` - Loading states
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
- Create/update `resources/lang/en/client.php` and `resources/lang/ar/client.php`
|
||||||
|
- Keys needed: `dashboard.welcome`, `dashboard.upcoming`, `dashboard.cases`, `dashboard.updates`, `dashboard.booking`, empty state messages
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
### Test File
|
||||||
|
`tests/Feature/Client/DashboardTest.php`
|
||||||
|
|
||||||
|
### Test Scenarios
|
||||||
|
```php
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Consultation;
|
||||||
|
use App\Models\Timeline;
|
||||||
|
use App\Models\TimelineUpdate;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
test('client can view their dashboard', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(route('client.dashboard'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeLivewire('client.dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard shows only authenticated user consultations', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
|
||||||
|
$myConsultation = Consultation::factory()->approved()->upcoming()->for($user)->create();
|
||||||
|
$otherConsultation = Consultation::factory()->approved()->upcoming()->for($otherUser)->create();
|
||||||
|
|
||||||
|
Volt::test('client.dashboard')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSee($myConsultation->scheduled_date->format('...'))
|
||||||
|
->assertDontSee($otherConsultation->scheduled_date->format('...'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard shows correct active timelines count', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Timeline::factory()->active()->count(3)->for($user)->create();
|
||||||
|
Timeline::factory()->archived()->count(2)->for($user)->create();
|
||||||
|
|
||||||
|
Volt::test('client.dashboard')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSee('3'); // Only active count
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard shows last 3 timeline updates', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$timeline = Timeline::factory()->for($user)->create();
|
||||||
|
TimelineUpdate::factory()->count(5)->for($timeline)->create();
|
||||||
|
|
||||||
|
Volt::test('client.dashboard')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertViewHas('recentUpdates', fn($updates) => $updates->count() === 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('canBookToday is false when user has booking today', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Consultation::factory()->approved()->for($user)->create([
|
||||||
|
'scheduled_date' => today(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('client.dashboard')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSet('canBookToday', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('canBookToday is true when user has no booking today', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
Volt::test('client.dashboard')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSet('canBookToday', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard handles empty state gracefully', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
Volt::test('client.dashboard')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSee(__('client.dashboard.no_upcoming'))
|
||||||
|
->assertSee(__('client.dashboard.no_cases'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unauthenticated user cannot access dashboard', function () {
|
||||||
|
$this->get(route('client.dashboard'))
|
||||||
|
->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Factory Requirements
|
||||||
|
Ensure factories exist with states:
|
||||||
|
- `Consultation::factory()->approved()`, `->pending()`, `->upcoming()`
|
||||||
|
- `Timeline::factory()->active()`, `->archived()`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### PRD Sections
|
||||||
|
- **Section 5.8** - Client Dashboard requirements and components
|
||||||
|
- **Section 7.1** - Design requirements (color scheme, typography)
|
||||||
|
- **Section 5.4** - Booking rules (1 per day limit, consultation types)
|
||||||
|
- **Section 5.5** - Timeline system structure
|
||||||
|
|
||||||
|
### Design Specifications
|
||||||
|
- Primary: Navy Blue `#0A1F44`
|
||||||
|
- Accent: Gold `#D4AF37`
|
||||||
|
- Card styling: `shadow-sm`, `rounded-lg`, `border border-gray-200`
|
||||||
|
- Spacing scale per PRD Section 7.1
|
||||||
|
|
||||||
|
### Related Stories
|
||||||
|
- Story 7.2: My Consultations View (link target for upcoming consultation)
|
||||||
|
- Story 7.3: My Cases/Timelines View (link target for cases widget)
|
||||||
|
- Story 7.5: New Booking Interface (link target for book button)
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
- [ ] All widgets display correctly
|
- [ ] Volt component created at `resources/views/livewire/client/dashboard.blade.php`
|
||||||
- [ ] Data scoped to logged-in user
|
- [ ] Route registered and protected with auth middleware
|
||||||
- [ ] Mobile responsive
|
- [ ] All 5 widgets display correctly with real data
|
||||||
- [ ] Bilingual
|
- [ ] Data strictly scoped to authenticated user (security verified)
|
||||||
- [ ] Tests pass
|
- [ ] Empty states display appropriately for each widget
|
||||||
|
- [ ] Mobile responsive (tested on 375px viewport)
|
||||||
|
- [ ] Bilingual content working (AR/EN toggle)
|
||||||
|
- [ ] All test scenarios pass
|
||||||
|
- [ ] Code formatted with `vendor/bin/pint --dirty`
|
||||||
|
- [ ] No console errors or warnings
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
**Complexity:** Medium | **Effort:** 4-5 hours
|
**Complexity:** Medium | **Effort:** 4-5 hours
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
- Consultation detail view (Story 7.2)
|
||||||
|
- Timeline detail view (Story 7.3)
|
||||||
|
- Booking form functionality (Story 7.5)
|
||||||
|
- Real-time updates via websockets
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,24 @@ As a **client**,
|
||||||
I want **to view all my consultations**,
|
I want **to view all my consultations**,
|
||||||
So that **I can track upcoming appointments and review past sessions**.
|
So that **I can track upcoming appointments and review past sessions**.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- **Story 7.1:** Client Dashboard Overview (navigation context)
|
||||||
|
- **Epic 3:** Consultation model with scopes (`approved()`, `pending()`)
|
||||||
|
- **Story 3.6:** Calendar file generation (.ics) for download functionality
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### Upcoming Consultations Section
|
### Upcoming Consultations Section
|
||||||
- [ ] Date and time
|
- [ ] Date and time (formatted per locale)
|
||||||
- [ ] Consultation type (free/paid)
|
- [ ] Consultation type (free/paid)
|
||||||
- [ ] Status (approved/pending)
|
- [ ] Status (approved/pending)
|
||||||
- [ ] Payment status (for paid)
|
- [ ] Payment status (for paid consultations)
|
||||||
- [ ] Download .ics calendar file button
|
- [ ] Download .ics calendar file button
|
||||||
|
|
||||||
### Pending Requests Section
|
### Pending Requests Section
|
||||||
- [ ] Submitted bookings awaiting approval
|
- [ ] Submitted bookings awaiting approval
|
||||||
- [ ] Submission date
|
- [ ] Submission date
|
||||||
- [ ] Problem summary preview
|
- [ ] Problem summary preview (truncated)
|
||||||
- [ ] Status: "Pending Review"
|
- [ ] Status: "Pending Review"
|
||||||
|
|
||||||
### Past Consultations Section
|
### Past Consultations Section
|
||||||
|
|
@ -29,14 +34,38 @@ So that **I can track upcoming appointments and review past sessions**.
|
||||||
- [ ] Date and type
|
- [ ] Date and type
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- [ ] Visual status indicators
|
- [ ] Visual status indicators (badges with colors)
|
||||||
- [ ] Sort by date (default: newest first for past)
|
- [ ] Sort by date (default: newest first for past)
|
||||||
- [ ] Pagination if many consultations
|
- [ ] Pagination if many consultations (10 per page)
|
||||||
- [ ] No edit/cancel capabilities (read-only)
|
- [ ] No edit/cancel capabilities (read-only)
|
||||||
|
|
||||||
|
### Empty States
|
||||||
|
- [ ] "No upcoming consultations" message with link to book
|
||||||
|
- [ ] "No pending requests" message
|
||||||
|
- [ ] "No past consultations" message
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
|
|
||||||
|
### Files to Create/Modify
|
||||||
|
- `resources/views/livewire/client/consultations.blade.php` - Main Volt component
|
||||||
|
- `routes/web.php` - Add route within client middleware group
|
||||||
|
|
||||||
|
### Route Definition
|
||||||
```php
|
```php
|
||||||
|
// In routes/web.php, within authenticated client routes
|
||||||
|
Route::get('/client/consultations', function () {
|
||||||
|
return view('livewire.client.consultations');
|
||||||
|
})->name('client.consultations');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volt Component Structure
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Consultation;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
use WithPagination;
|
use WithPagination;
|
||||||
|
|
||||||
|
|
@ -57,20 +86,96 @@ new class extends Component {
|
||||||
->get(),
|
->get(),
|
||||||
'past' => $user->consultations()
|
'past' => $user->consultations()
|
||||||
->whereIn('status', ['completed', 'cancelled', 'no_show'])
|
->whereIn('status', ['completed', 'cancelled', 'no_show'])
|
||||||
->orWhere(fn($q) => $q->approved()->where('scheduled_date', '<', today()))
|
->orWhere(fn($q) => $q
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->approved()
|
||||||
|
->where('scheduled_date', '<', today()))
|
||||||
->latest('scheduled_date')
|
->latest('scheduled_date')
|
||||||
->paginate(10),
|
->paginate(10),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
public function downloadCalendar(Consultation $consultation): \Symfony\Component\HttpFoundation\StreamedResponse
|
||||||
|
{
|
||||||
|
// Reuse .ics generation from Story 3.6
|
||||||
|
return $consultation->generateIcsDownload();
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Model Scopes Required (from Epic 3)
|
||||||
|
The Consultation model should have these scopes:
|
||||||
|
- `scopeApproved($query)` - `where('status', 'approved')`
|
||||||
|
- `scopePending($query)` - `where('status', 'pending')`
|
||||||
|
|
||||||
|
### Flux UI Components to Use
|
||||||
|
- `<flux:badge>` - Status indicators (approved=green, pending=yellow, completed=gray, cancelled=red)
|
||||||
|
- `<flux:button>` - Download calendar button
|
||||||
|
- `<flux:heading>` - Section headings
|
||||||
|
- `<flux:text>` - Consultation details
|
||||||
|
|
||||||
|
### Status Badge Colors
|
||||||
|
| Status | Color | Variant |
|
||||||
|
|--------|-------|---------|
|
||||||
|
| approved | green | `variant="success"` |
|
||||||
|
| pending | yellow | `variant="warning"` |
|
||||||
|
| completed | gray | `variant="subtle"` |
|
||||||
|
| cancelled | red | `variant="danger"` |
|
||||||
|
| no_show | red | `variant="danger"` |
|
||||||
|
|
||||||
|
### Payment Status Display
|
||||||
|
- For paid consultations, show payment status:
|
||||||
|
- "Payment Pending" (yellow badge)
|
||||||
|
- "Payment Received" (green badge)
|
||||||
|
- For free consultations, show "Free" (blue badge)
|
||||||
|
|
||||||
|
## References
|
||||||
|
- `docs/epics/epic-3-booking-consultation.md` - Consultation model, statuses, payment handling
|
||||||
|
- `docs/epics/epic-3-booking-consultation.md#story-36` - .ics calendar file generation
|
||||||
|
- `docs/stories/story-7.1-client-dashboard-overview.md` - Dashboard navigation context
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
- [ ] Unauthenticated users redirected to login
|
||||||
|
- [ ] User sees only their own consultations (not other users')
|
||||||
|
|
||||||
|
### Upcoming Section
|
||||||
|
- [ ] Shows approved consultations with `scheduled_date >= today`
|
||||||
|
- [ ] Sorted by date ascending (soonest first)
|
||||||
|
- [ ] Displays type, status, payment status correctly
|
||||||
|
- [ ] Calendar download button triggers .ics download
|
||||||
|
|
||||||
|
### Pending Section
|
||||||
|
- [ ] Shows consultations with status='pending'
|
||||||
|
- [ ] Displays submission date and problem summary
|
||||||
|
- [ ] Summary truncated if too long
|
||||||
|
|
||||||
|
### Past Section
|
||||||
|
- [ ] Shows completed, cancelled, no_show consultations
|
||||||
|
- [ ] Shows approved consultations with `scheduled_date < today`
|
||||||
|
- [ ] Sorted by date descending (newest first)
|
||||||
|
- [ ] Pagination works correctly (10 per page)
|
||||||
|
|
||||||
|
### Empty States
|
||||||
|
- [ ] Empty upcoming shows appropriate message
|
||||||
|
- [ ] Empty pending shows appropriate message
|
||||||
|
- [ ] Empty past shows appropriate message
|
||||||
|
|
||||||
|
### Calendar Download
|
||||||
|
- [ ] Download generates valid .ics file
|
||||||
|
- [ ] File contains correct consultation details
|
||||||
|
- [ ] Only available for approved upcoming consultations
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
- [ ] All sections display correctly
|
- [ ] All sections display correctly
|
||||||
- [ ] Calendar download works
|
- [ ] Calendar download works
|
||||||
- [ ] Status indicators clear
|
- [ ] Status indicators clear and color-coded
|
||||||
- [ ] Read-only (no actions)
|
- [ ] Read-only (no actions except download)
|
||||||
- [ ] Pagination works
|
- [ ] Pagination works for past consultations
|
||||||
|
- [ ] Empty states display appropriately
|
||||||
|
- [ ] Mobile responsive
|
||||||
|
- [ ] Bilingual support
|
||||||
- [ ] Tests pass
|
- [ ] Tests pass
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,245 @@
|
||||||
# Story 7.3: My Cases/Timelines View
|
# Story 7.3: My Cases/Timelines View (Dashboard Integration)
|
||||||
|
|
||||||
## Epic Reference
|
## Epic Reference
|
||||||
**Epic 7:** Client Dashboard
|
**Epic 7:** Client Dashboard
|
||||||
|
|
||||||
## User Story
|
## User Story
|
||||||
As a **client**,
|
As a **client**,
|
||||||
I want **to view my case timelines and their updates**,
|
I want **to access my case timelines from the dashboard navigation**,
|
||||||
So that **I can track the progress of my legal matters**.
|
So that **I can easily track the progress of my legal matters from one central location**.
|
||||||
|
|
||||||
|
## Story Context
|
||||||
|
|
||||||
|
### Relationship to Story 4.5
|
||||||
|
Story 4.5 (`docs/stories/story-4.5-client-timeline-view.md`) already implements the **complete timeline viewing functionality**:
|
||||||
|
- Routes: `client.timelines.index` and `client.timelines.show`
|
||||||
|
- Components: `pages/client/timelines/index.blade.php` and `show.blade.php`
|
||||||
|
- Active/archived separation with visual distinction
|
||||||
|
- Individual timeline detail view with chronological updates
|
||||||
|
- Authorization, tests, and translations
|
||||||
|
|
||||||
|
**This story (7.3) focuses solely on dashboard navigation integration** - ensuring clients can access the existing timeline views from the Epic 7 client dashboard structure.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- **Story 4.5:** Client Timeline View - MUST be complete (provides all timeline components)
|
||||||
|
- **Story 7.1:** Client Dashboard Overview - MUST be complete (provides dashboard layout and navigation)
|
||||||
|
|
||||||
|
### What This Story Does NOT Do
|
||||||
|
- Does NOT recreate timeline list or detail views (use Story 4.5's components)
|
||||||
|
- Does NOT add new timeline functionality
|
||||||
|
- Does NOT modify existing timeline components
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### Active Cases Section
|
### Navigation Integration
|
||||||
- [ ] List of active timelines
|
- [ ] "My Cases" navigation item added to client dashboard sidebar/nav
|
||||||
- [ ] Case name and reference
|
- [ ] Navigation links to `route('client.timelines.index')`
|
||||||
- [ ] Last update date
|
- [ ] Active state shown when on timeline routes
|
||||||
- [ ] Update count
|
- [ ] Icon: folder or briefcase icon for cases
|
||||||
- [ ] "View" button
|
|
||||||
|
|
||||||
### Archived Cases Section
|
### Dashboard Widget (on Story 7.1's dashboard)
|
||||||
- [ ] Clearly separated from active
|
- [ ] "My Cases" widget card displays:
|
||||||
- [ ] Different visual styling (muted)
|
- Count of active cases
|
||||||
- [ ] Still accessible for viewing
|
- Latest update preview (case name + date)
|
||||||
|
- "View All" link to timeline index
|
||||||
|
- [ ] Widget shows empty state if no cases exist
|
||||||
|
|
||||||
### Individual Timeline View
|
### Layout Consistency
|
||||||
- [ ] Case name and reference
|
- [ ] Timeline pages use client dashboard layout (consistent header/nav)
|
||||||
- [ ] Status badge (active/archived)
|
- [ ] Breadcrumbs: Dashboard > My Cases (on index)
|
||||||
- [ ] All updates in chronological order
|
- [ ] Breadcrumbs: Dashboard > My Cases > [Case Name] (on show)
|
||||||
- [ ] Each update shows:
|
|
||||||
- Date and time
|
|
||||||
- Update content
|
|
||||||
- [ ] Read-only (no interactions)
|
|
||||||
|
|
||||||
### Navigation
|
### Bilingual Support
|
||||||
- [ ] Back to cases list
|
- [ ] Navigation label translated (AR/EN)
|
||||||
- [ ] Responsive layout
|
- [ ] Widget content translated (AR/EN)
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
|
|
||||||
Reuse components from Story 4.5.
|
### File Structure
|
||||||
|
```
|
||||||
|
Files to Modify:
|
||||||
|
resources/views/components/layouts/client.blade.php (add nav item)
|
||||||
|
OR resources/views/livewire/pages/client/dashboard.blade.php (add widget)
|
||||||
|
|
||||||
|
Files from Story 4.5 (DO NOT MODIFY - just ensure they exist):
|
||||||
|
resources/views/livewire/pages/client/timelines/index.blade.php
|
||||||
|
resources/views/livewire/pages/client/timelines/show.blade.php
|
||||||
|
|
||||||
|
Tests to Create:
|
||||||
|
tests/Feature/Client/DashboardTimelineIntegrationTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation Item Addition
|
||||||
|
Add to client dashboard navigation (location depends on Story 7.1's implementation):
|
||||||
|
|
||||||
```php
|
```php
|
||||||
new class extends Component {
|
{{-- In client layout/navigation component --}}
|
||||||
public function with(): array
|
<flux:navbar.item
|
||||||
{
|
href="{{ route('client.timelines.index') }}"
|
||||||
return [
|
:active="request()->routeIs('client.timelines.*')"
|
||||||
'activeTimelines' => auth()->user()
|
icon="folder"
|
||||||
->timelines()
|
>
|
||||||
->active()
|
{{ __('client.my_cases') }}
|
||||||
->withCount('updates')
|
</flux:navbar.item>
|
||||||
->latest('updated_at')
|
```
|
||||||
->get(),
|
|
||||||
'archivedTimelines' => auth()->user()
|
### Dashboard Widget Component
|
||||||
->timelines()
|
Add to Story 7.1's dashboard view:
|
||||||
->archived()
|
|
||||||
->withCount('updates')
|
```php
|
||||||
->latest('updated_at')
|
{{-- My Cases Widget --}}
|
||||||
->get(),
|
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||||
];
|
<div class="flex justify-between items-center mb-4">
|
||||||
}
|
<h3 class="font-semibold text-charcoal">{{ __('client.my_cases') }}</h3>
|
||||||
};
|
<flux:badge>{{ $activeTimelinesCount }} {{ __('client.active') }}</flux:badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($latestTimelineUpdate)
|
||||||
|
<div class="text-sm text-charcoal/70 mb-4">
|
||||||
|
<p class="font-medium">{{ $latestTimelineUpdate->timeline->case_name }}</p>
|
||||||
|
<p class="text-xs">{{ __('client.last_update') }}: {{ $latestTimelineUpdate->created_at->diffForHumans() }}</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-sm text-charcoal/50 mb-4">{{ __('client.no_cases_yet') }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<flux:button size="sm" href="{{ route('client.timelines.index') }}">
|
||||||
|
{{ __('client.view_all_cases') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data for Widget (add to Story 7.1's dashboard component)
|
||||||
|
```php
|
||||||
|
// In dashboard component's with() method
|
||||||
|
'activeTimelinesCount' => auth()->user()->timelines()->active()->count(),
|
||||||
|
'latestTimelineUpdate' => TimelineUpdate::whereHas('timeline',
|
||||||
|
fn($q) => $q->where('user_id', auth()->id())->active()
|
||||||
|
)
|
||||||
|
->with('timeline:id,case_name')
|
||||||
|
->latest()
|
||||||
|
->first(),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Translation Keys
|
||||||
|
```php
|
||||||
|
// Add to resources/lang/en/client.php (if not already from 4.5)
|
||||||
|
'view_all_cases' => 'View All Cases',
|
||||||
|
|
||||||
|
// Add to resources/lang/ar/client.php
|
||||||
|
'view_all_cases' => 'عرض جميع القضايا',
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// tests/Feature/Client/DashboardTimelineIntegrationTest.php
|
||||||
|
|
||||||
|
use App\Models\{User, Timeline, TimelineUpdate};
|
||||||
|
|
||||||
|
test('client dashboard has my cases navigation link', function () {
|
||||||
|
$client = User::factory()->create(['user_type' => 'individual']);
|
||||||
|
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.dashboard'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee(__('client.my_cases'))
|
||||||
|
->assertSee(route('client.timelines.index'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('client dashboard shows active cases count in widget', function () {
|
||||||
|
$client = User::factory()->create(['user_type' => 'individual']);
|
||||||
|
Timeline::factory()->count(3)->create([
|
||||||
|
'user_id' => $client->id,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
Timeline::factory()->create([
|
||||||
|
'user_id' => $client->id,
|
||||||
|
'status' => 'archived',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.dashboard'))
|
||||||
|
->assertSee('3'); // Only active cases counted
|
||||||
|
});
|
||||||
|
|
||||||
|
test('client dashboard shows latest timeline update', function () {
|
||||||
|
$client = User::factory()->create(['user_type' => 'individual']);
|
||||||
|
$timeline = Timeline::factory()->create([
|
||||||
|
'user_id' => $client->id,
|
||||||
|
'case_name' => 'Property Dispute Case',
|
||||||
|
]);
|
||||||
|
TimelineUpdate::factory()->create([
|
||||||
|
'timeline_id' => $timeline->id,
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.dashboard'))
|
||||||
|
->assertSee('Property Dispute Case');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('client dashboard shows empty state when no cases', function () {
|
||||||
|
$client = User::factory()->create(['user_type' => 'individual']);
|
||||||
|
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.dashboard'))
|
||||||
|
->assertSee(__('client.no_cases_yet'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('my cases navigation is active on timeline routes', function () {
|
||||||
|
$client = User::factory()->create(['user_type' => 'individual']);
|
||||||
|
$timeline = Timeline::factory()->create(['user_id' => $client->id]);
|
||||||
|
|
||||||
|
// Test on index
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.timelines.index'))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
// Test on show
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.timelines.show', $timeline))
|
||||||
|
->assertOk();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('timeline pages use client dashboard layout', function () {
|
||||||
|
$client = User::factory()->create(['user_type' => 'individual']);
|
||||||
|
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('client.timelines.index'))
|
||||||
|
->assertSee(__('client.my_cases')); // Nav item visible = layout applied
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
- [ ] Active cases display correctly
|
- [ ] "My Cases" navigation item added to client dashboard
|
||||||
- [ ] Archived cases separated
|
- [ ] Navigation links to existing `client.timelines.index` route
|
||||||
- [ ] Timeline detail view works
|
- [ ] Active state shows on timeline routes
|
||||||
- [ ] Updates display chronologically
|
- [ ] Dashboard widget displays active case count
|
||||||
- [ ] Read-only enforced
|
- [ ] Dashboard widget shows latest update preview
|
||||||
- [ ] Tests pass
|
- [ ] Dashboard widget links to timeline index
|
||||||
|
- [ ] Empty state handled in widget
|
||||||
|
- [ ] Translation keys added for new strings
|
||||||
|
- [ ] Timeline pages render within client dashboard layout
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Code formatted with Pint
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Story 4.5:** Client Timeline View (REQUIRED - provides timeline components and routes)
|
||||||
|
- **Story 7.1:** Client Dashboard Overview (REQUIRED - provides dashboard layout)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
This story is intentionally minimal because the heavy lifting was done in Story 4.5. The developer should:
|
||||||
|
1. Verify Story 4.5 is complete and routes work
|
||||||
|
2. Add navigation item to client layout
|
||||||
|
3. Add widget to dashboard
|
||||||
|
4. Ensure layout consistency
|
||||||
|
5. Write integration tests
|
||||||
|
|
||||||
|
Do NOT duplicate or recreate the timeline components from Story 4.5.
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
**Complexity:** Medium | **Effort:** 3-4 hours
|
**Complexity:** Low | **Effort:** 1-2 hours
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,79 @@
|
||||||
## Epic Reference
|
## Epic Reference
|
||||||
**Epic 7:** Client Dashboard
|
**Epic 7:** Client Dashboard
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- **Epic 2:** User Management (user model with all client profile fields must exist)
|
||||||
|
- **Story 7.1:** Client Dashboard Overview (layout and navigation context)
|
||||||
|
|
||||||
## User Story
|
## User Story
|
||||||
As a **client**,
|
As a **client**,
|
||||||
I want **to view my profile information**,
|
I want **to view my profile information**,
|
||||||
So that **I can verify my account details are correct**.
|
So that **I can verify my account details are correct**.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Required User Model Fields
|
||||||
|
The `users` table must include these columns (from Epic 2):
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `user_type` | enum('individual','company') | Distinguishes client type |
|
||||||
|
| `name` | string | Full name (individual) or company name |
|
||||||
|
| `national_id` | string | National ID for individuals |
|
||||||
|
| `email` | string | Email address |
|
||||||
|
| `phone` | string | Phone number |
|
||||||
|
| `preferred_language` | enum('ar','en') | User's language preference |
|
||||||
|
| `company_name` | string, nullable | Company name (company clients) |
|
||||||
|
| `company_cert_number` | string, nullable | Company registration number |
|
||||||
|
| `contact_person_name` | string, nullable | Contact person (company clients) |
|
||||||
|
| `contact_person_id` | string, nullable | Contact person's ID |
|
||||||
|
| `created_at` | timestamp | Account creation date |
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### Individual Client Profile
|
### Individual Client Profile
|
||||||
- [ ] Full name
|
- [ ] Full name displayed
|
||||||
- [ ] National ID
|
- [ ] National ID displayed
|
||||||
- [ ] Email address
|
- [ ] Email address displayed
|
||||||
- [ ] Phone number
|
- [ ] Phone number displayed
|
||||||
- [ ] Preferred language
|
- [ ] Preferred language displayed
|
||||||
- [ ] Account created date
|
- [ ] Account created date displayed
|
||||||
|
|
||||||
### Company Client Profile
|
### Company Client Profile
|
||||||
- [ ] Company name
|
- [ ] Company name displayed
|
||||||
- [ ] Registration number
|
- [ ] Company certificate/registration number displayed
|
||||||
- [ ] Contact person name
|
- [ ] Contact person name displayed
|
||||||
- [ ] Contact person ID
|
- [ ] Contact person ID displayed
|
||||||
- [ ] Email address
|
- [ ] Email address displayed
|
||||||
- [ ] Phone number
|
- [ ] Phone number displayed
|
||||||
- [ ] Preferred language
|
- [ ] Preferred language displayed
|
||||||
- [ ] Account created date
|
- [ ] Account created date displayed
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- [ ] Account type indicator
|
- [ ] Account type indicator (Individual/Company badge)
|
||||||
- [ ] No edit capabilities (read-only)
|
- [ ] No edit capabilities (read-only view)
|
||||||
- [ ] Message: "Contact admin to update your information"
|
- [ ] Message: "Contact admin to update your information"
|
||||||
- [ ] Logout button
|
- [ ] Logout button with confirmation redirect
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
|
|
||||||
|
### File Location
|
||||||
|
```
|
||||||
|
resources/views/livewire/client/profile.blade.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route
|
||||||
```php
|
```php
|
||||||
|
// In routes/web.php (client authenticated routes)
|
||||||
|
Route::get('/client/profile', \Livewire\Volt\Volt::route('client.profile'))->name('client.profile');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Implementation
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
public function with(): array
|
public function with(): array
|
||||||
{
|
{
|
||||||
|
|
@ -58,6 +97,11 @@ new class extends Component {
|
||||||
<div class="max-w-2xl mx-auto">
|
<div class="max-w-2xl mx-auto">
|
||||||
<flux:heading>{{ __('client.my_profile') }}</flux:heading>
|
<flux:heading>{{ __('client.my_profile') }}</flux:heading>
|
||||||
|
|
||||||
|
<!-- Account Type Badge -->
|
||||||
|
<flux:badge class="mt-4" variant="{{ $user->user_type === 'individual' ? 'primary' : 'secondary' }}">
|
||||||
|
{{ $user->user_type === 'individual' ? __('profile.individual_account') : __('profile.company_account') }}
|
||||||
|
</flux:badge>
|
||||||
|
|
||||||
<div class="bg-cream rounded-lg p-6 mt-6">
|
<div class="bg-cream rounded-lg p-6 mt-6">
|
||||||
@if($user->user_type === 'individual')
|
@if($user->user_type === 'individual')
|
||||||
<dl class="space-y-4">
|
<dl class="space-y-4">
|
||||||
|
|
@ -69,10 +113,58 @@ new class extends Component {
|
||||||
<dt class="text-sm text-charcoal/70">{{ __('profile.national_id') }}</dt>
|
<dt class="text-sm text-charcoal/70">{{ __('profile.national_id') }}</dt>
|
||||||
<dd>{{ $user->national_id }}</dd>
|
<dd>{{ $user->national_id }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<!-- More fields... -->
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.email') }}</dt>
|
||||||
|
<dd>{{ $user->email }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.phone') }}</dt>
|
||||||
|
<dd>{{ $user->phone }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.preferred_language') }}</dt>
|
||||||
|
<dd>{{ $user->preferred_language === 'ar' ? __('profile.arabic') : __('profile.english') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.member_since') }}</dt>
|
||||||
|
<dd>{{ $user->created_at->translatedFormat('F j, Y') }}</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
@else
|
@else
|
||||||
<!-- Company fields -->
|
<dl class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.company_name') }}</dt>
|
||||||
|
<dd class="text-lg font-medium">{{ $user->company_name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.registration_number') }}</dt>
|
||||||
|
<dd>{{ $user->company_cert_number }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.contact_person') }}</dt>
|
||||||
|
<dd>{{ $user->contact_person_name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.contact_person_id') }}</dt>
|
||||||
|
<dd>{{ $user->contact_person_id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.email') }}</dt>
|
||||||
|
<dd>{{ $user->email }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.phone') }}</dt>
|
||||||
|
<dd>{{ $user->phone }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.preferred_language') }}</dt>
|
||||||
|
<dd>{{ $user->preferred_language === 'ar' ? __('profile.arabic') : __('profile.english') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-sm text-charcoal/70">{{ __('profile.member_since') }}</dt>
|
||||||
|
<dd>{{ $user->created_at->translatedFormat('F j, Y') }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -86,13 +178,188 @@ new class extends Component {
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Required Translation Keys
|
||||||
|
|
||||||
|
Add to `lang/en/client.php`:
|
||||||
|
```php
|
||||||
|
'my_profile' => 'My Profile',
|
||||||
|
'contact_admin_to_update' => 'Contact admin to update your information',
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `lang/en/profile.php`:
|
||||||
|
```php
|
||||||
|
'full_name' => 'Full Name',
|
||||||
|
'national_id' => 'National ID',
|
||||||
|
'email' => 'Email Address',
|
||||||
|
'phone' => 'Phone Number',
|
||||||
|
'preferred_language' => 'Preferred Language',
|
||||||
|
'member_since' => 'Member Since',
|
||||||
|
'company_name' => 'Company Name',
|
||||||
|
'registration_number' => 'Registration Number',
|
||||||
|
'contact_person' => 'Contact Person',
|
||||||
|
'contact_person_id' => 'Contact Person ID',
|
||||||
|
'individual_account' => 'Individual Account',
|
||||||
|
'company_account' => 'Company Account',
|
||||||
|
'arabic' => 'Arabic',
|
||||||
|
'english' => 'English',
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `lang/en/auth.php`:
|
||||||
|
```php
|
||||||
|
'logout' => 'Logout',
|
||||||
|
```
|
||||||
|
|
||||||
|
Create corresponding Arabic translations in `lang/ar/` files.
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### Unit/Feature Tests
|
||||||
|
Create `tests/Feature/Client/ProfileTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use App\Models\User;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
test('client can view individual profile page', function () {
|
||||||
|
$user = User::factory()->individual()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(route('client.profile'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeLivewire('client.profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('individual profile displays all required fields', function () {
|
||||||
|
$user = User::factory()->individual()->create([
|
||||||
|
'name' => 'Test User',
|
||||||
|
'national_id' => '123456789',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'phone' => '+970599999999',
|
||||||
|
'preferred_language' => 'en',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSee('Test User')
|
||||||
|
->assertSee('123456789')
|
||||||
|
->assertSee('test@example.com')
|
||||||
|
->assertSee('+970599999999')
|
||||||
|
->assertSee('English');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('company profile displays all required fields', function () {
|
||||||
|
$user = User::factory()->company()->create([
|
||||||
|
'company_name' => 'Test Company',
|
||||||
|
'company_cert_number' => 'REG-12345',
|
||||||
|
'contact_person_name' => 'John Doe',
|
||||||
|
'contact_person_id' => '987654321',
|
||||||
|
'email' => 'company@example.com',
|
||||||
|
'phone' => '+970599888888',
|
||||||
|
'preferred_language' => 'ar',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSee('Test Company')
|
||||||
|
->assertSee('REG-12345')
|
||||||
|
->assertSee('John Doe')
|
||||||
|
->assertSee('987654321')
|
||||||
|
->assertSee('company@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('profile page shows correct account type badge', function () {
|
||||||
|
$individual = User::factory()->individual()->create();
|
||||||
|
$company = User::factory()->company()->create();
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($individual)
|
||||||
|
->assertSee(__('profile.individual_account'));
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($company)
|
||||||
|
->assertSee(__('profile.company_account'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('profile page has no edit functionality', function () {
|
||||||
|
$user = User::factory()->individual()->create();
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertDontSee('Edit')
|
||||||
|
->assertDontSee('Update')
|
||||||
|
->assertDontSee('wire:model');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('profile page shows contact admin message', function () {
|
||||||
|
$user = User::factory()->individual()->create();
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($user)
|
||||||
|
->assertSee(__('client.contact_admin_to_update'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logout button logs out user and redirects to login', function () {
|
||||||
|
$user = User::factory()->individual()->create();
|
||||||
|
|
||||||
|
Volt::test('client.profile')
|
||||||
|
->actingAs($user)
|
||||||
|
->call('logout')
|
||||||
|
->assertRedirect(route('login'));
|
||||||
|
|
||||||
|
$this->assertGuest();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unauthenticated users cannot access profile', function () {
|
||||||
|
$this->get(route('client.profile'))
|
||||||
|
->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Factory States Required
|
||||||
|
Ensure `database/factories/UserFactory.php` has these states:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function individual(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'user_type' => 'individual',
|
||||||
|
'national_id' => fake()->numerify('#########'),
|
||||||
|
'company_name' => null,
|
||||||
|
'company_cert_number' => null,
|
||||||
|
'contact_person_name' => null,
|
||||||
|
'contact_person_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function company(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'user_type' => 'company',
|
||||||
|
'company_name' => fake()->company(),
|
||||||
|
'company_cert_number' => fake()->numerify('REG-#####'),
|
||||||
|
'contact_person_name' => fake()->name(),
|
||||||
|
'contact_person_id' => fake()->numerify('#########'),
|
||||||
|
'national_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
- [ ] Individual profile displays correctly
|
- [ ] Individual profile displays all fields correctly
|
||||||
- [ ] Company profile displays correctly
|
- [ ] Company profile displays all fields correctly
|
||||||
- [ ] No edit functionality
|
- [ ] Account type badge shows correctly for both types
|
||||||
- [ ] Contact admin message shown
|
- [ ] No edit functionality present (read-only)
|
||||||
- [ ] Logout works
|
- [ ] Contact admin message displayed
|
||||||
- [ ] Tests pass
|
- [ ] Logout button works and redirects to login
|
||||||
|
- [ ] All test scenarios pass
|
||||||
|
- [ ] Bilingual support (AR/EN) working
|
||||||
|
- [ ] Responsive design on mobile
|
||||||
|
- [ ] Code formatted with Pint
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
**Complexity:** Low | **Effort:** 2 hours
|
**Complexity:** Low | **Effort:** 2-3 hours
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Date formatting uses `translatedFormat()` for locale-aware display
|
||||||
|
- Ensure the User model has `$casts` for `created_at` as datetime
|
||||||
|
- The `bg-cream` and `text-charcoal` classes should be defined in Tailwind config per project design system
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
## Epic Reference
|
## Epic Reference
|
||||||
**Epic 7:** Client Dashboard
|
**Epic 7:** Client Dashboard
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- **Story 7.5:** New Booking Interface (provides booking page where indicator displays)
|
||||||
|
- **Story 3.3:** Availability Calendar (calendar component to integrate with)
|
||||||
|
|
||||||
## User Story
|
## User Story
|
||||||
As a **client**,
|
As a **client**,
|
||||||
I want **to see my booking status and limits clearly**,
|
I want **to see my booking status and limits clearly**,
|
||||||
|
|
@ -11,25 +15,46 @@ So that **I understand when I can book consultations**.
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### Display Locations
|
### Display Locations
|
||||||
- [ ] Dashboard widget
|
- [ ] Dashboard widget showing booking status
|
||||||
- [ ] Booking page
|
- [ ] Booking page status banner
|
||||||
|
|
||||||
### Status Messages
|
### Status Messages
|
||||||
- [ ] "You can book a consultation today"
|
- [ ] "You can book a consultation today" (when no booking exists for today)
|
||||||
- [ ] "You already have a booking for today"
|
- [ ] "You already have a booking for today" (when pending/approved booking exists for today)
|
||||||
- [ ] "You have a pending request for [date]"
|
- [ ] "You have a pending request for [date]" (shows first pending request date)
|
||||||
|
|
||||||
### Calendar Integration
|
### Calendar Integration
|
||||||
- [ ] Calendar shows booked days as unavailable
|
- [ ] Pass `bookedDates` array to availability calendar component
|
||||||
- [ ] Visual indicator for user's booked dates
|
- [ ] Calendar marks user's booked dates as unavailable (distinct styling)
|
||||||
|
- [ ] Visual indicator differentiates "user already booked" from "no slots available"
|
||||||
|
|
||||||
### Information
|
### Information
|
||||||
- [ ] Clear messaging about 1-per-day limit
|
- [ ] Clear messaging about 1-per-day limit
|
||||||
- [ ] Bilingual messages
|
- [ ] Bilingual messages (Arabic/English)
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- [ ] Handle multiple pending requests (show count or list)
|
||||||
|
- [ ] Handle cancelled bookings (should not block new booking)
|
||||||
|
- [ ] Loading state while fetching booking status
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
|
|
||||||
|
### Files to Create
|
||||||
|
- `resources/views/livewire/client/booking-status.blade.php` - Reusable status component
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
- `resources/views/livewire/client/dashboard.blade.php` - Add booking status widget
|
||||||
|
- `resources/views/livewire/client/booking.blade.php` - Add status banner (from Story 7.5)
|
||||||
|
- `resources/lang/en/booking.php` - Add translation keys
|
||||||
|
- `resources/lang/ar/booking.php` - Add translation keys
|
||||||
|
|
||||||
|
### Component Implementation
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
new class extends Component {
|
new class extends Component {
|
||||||
public function getBookingStatus(): array
|
public function getBookingStatus(): array
|
||||||
{
|
{
|
||||||
|
|
@ -41,11 +66,13 @@ new class extends Component {
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$pendingRequests = $user->consultations()
|
$pendingRequests = $user->consultations()
|
||||||
->pending()
|
->where('status', 'pending')
|
||||||
|
->where('scheduled_date', '>=', today())
|
||||||
|
->orderBy('scheduled_date')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$upcomingApproved = $user->consultations()
|
$upcomingApproved = $user->consultations()
|
||||||
->approved()
|
->where('status', 'approved')
|
||||||
->where('scheduled_date', '>=', today())
|
->where('scheduled_date', '>=', today())
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|
@ -62,43 +89,136 @@ new class extends Component {
|
||||||
->toArray(),
|
->toArray(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Template
|
public function with(): array
|
||||||
```blade
|
{
|
||||||
<div class="bg-cream rounded-lg p-4">
|
return $this->getBookingStatus();
|
||||||
@if($canBookToday)
|
}
|
||||||
<div class="flex items-center gap-2 text-success">
|
}; ?>
|
||||||
<flux:icon name="check-circle" class="w-5 h-5" />
|
|
||||||
<span>{{ __('booking.can_book_today') }}</span>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div class="flex items-center gap-2 text-warning">
|
|
||||||
<flux:icon name="exclamation-circle" class="w-5 h-5" />
|
|
||||||
<span>{{ __('booking.already_booked_today') }}</span>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($pendingRequests->isNotEmpty())
|
<div>
|
||||||
<p class="mt-2 text-sm text-charcoal/70">
|
<!-- Template below -->
|
||||||
{{ __('booking.pending_for_date', ['date' => $pendingRequests->first()->scheduled_date->format('d/m/Y')]) }}
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<p class="mt-2 text-sm text-charcoal/70">
|
|
||||||
{{ __('booking.limit_message') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
```blade
|
||||||
|
<div class="bg-cream rounded-lg p-4">
|
||||||
|
{{-- Loading State --}}
|
||||||
|
<div wire:loading class="animate-pulse">
|
||||||
|
<div class="h-5 bg-charcoal/20 rounded w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div wire:loading.remove>
|
||||||
|
@if($canBookToday)
|
||||||
|
<div class="flex items-center gap-2 text-success">
|
||||||
|
<flux:icon name="check-circle" class="w-5 h-5" />
|
||||||
|
<span>{{ __('booking.can_book_today') }}</span>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex items-center gap-2 text-warning">
|
||||||
|
<flux:icon name="exclamation-circle" class="w-5 h-5" />
|
||||||
|
<span>{{ __('booking.already_booked_today') }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($pendingRequests->isNotEmpty())
|
||||||
|
<div class="mt-2 text-sm text-charcoal/70">
|
||||||
|
@if($pendingRequests->count() === 1)
|
||||||
|
<p>{{ __('booking.pending_for_date', ['date' => $pendingRequests->first()->scheduled_date->format('d/m/Y')]) }}</p>
|
||||||
|
@else
|
||||||
|
<p>{{ __('booking.pending_count', ['count' => $pendingRequests->count()]) }}</p>
|
||||||
|
<ul class="mt-1 list-disc list-inside">
|
||||||
|
@foreach($pendingRequests->take(3) as $request)
|
||||||
|
<li>{{ $request->scheduled_date->format('d/m/Y') }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p class="mt-2 text-sm text-charcoal/70">
|
||||||
|
{{ __('booking.limit_message') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Translation Keys
|
||||||
|
|
||||||
|
```php
|
||||||
|
// resources/lang/en/booking.php
|
||||||
|
return [
|
||||||
|
'can_book_today' => 'You can book a consultation today',
|
||||||
|
'already_booked_today' => 'You already have a booking for today',
|
||||||
|
'pending_for_date' => 'You have a pending request for :date',
|
||||||
|
'pending_count' => 'You have :count pending requests',
|
||||||
|
'limit_message' => 'Note: You can book a maximum of 1 consultation per day.',
|
||||||
|
];
|
||||||
|
|
||||||
|
// resources/lang/ar/booking.php
|
||||||
|
return [
|
||||||
|
'can_book_today' => 'يمكنك حجز استشارة اليوم',
|
||||||
|
'already_booked_today' => 'لديك حجز بالفعل لهذا اليوم',
|
||||||
|
'pending_for_date' => 'لديك طلب معلق بتاريخ :date',
|
||||||
|
'pending_count' => 'لديك :count طلبات معلقة',
|
||||||
|
'limit_message' => 'ملاحظة: يمكنك حجز استشارة واحدة كحد أقصى في اليوم.',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calendar Integration
|
||||||
|
|
||||||
|
Pass `bookedDates` to the availability calendar from Story 3.3:
|
||||||
|
|
||||||
|
```blade
|
||||||
|
{{-- In booking page --}}
|
||||||
|
<livewire:availability-calendar :disabled-dates="$bookedDates" />
|
||||||
|
```
|
||||||
|
|
||||||
|
The calendar should style user's booked dates differently (e.g., with a badge or distinct color) from dates with no available slots.
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- `getBookingStatus()` returns correct structure
|
||||||
|
- `canBookToday` is true when no booking exists for today
|
||||||
|
- `canBookToday` is false when pending booking exists for today
|
||||||
|
- `canBookToday` is false when approved booking exists for today
|
||||||
|
- Cancelled bookings do not affect `canBookToday`
|
||||||
|
- `bookedDates` only includes pending and approved bookings
|
||||||
|
|
||||||
|
### Feature Tests
|
||||||
|
- Status displays "can book today" message for new users
|
||||||
|
- Status displays "already booked" when user has today's booking
|
||||||
|
- Status displays pending request date correctly
|
||||||
|
- Multiple pending requests display count and dates
|
||||||
|
- Component renders on dashboard page
|
||||||
|
- Component renders on booking page
|
||||||
|
- Messages display correctly in Arabic locale
|
||||||
|
- Messages display correctly in English locale
|
||||||
|
|
||||||
|
### Browser Tests (optional)
|
||||||
|
- Calendar visually shows user's booked dates as unavailable
|
||||||
|
- Status updates after successful booking submission
|
||||||
|
|
||||||
|
## References
|
||||||
|
- **PRD Section 5.8:** Client Dashboard - Booking limit status requirement
|
||||||
|
- **PRD Section 5.4:** "Maximum 1 consultation per client per day"
|
||||||
|
- **Epic 7 Success Criteria:** "Booking limit enforcement (1 per day)"
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
|
- [ ] Booking status component created
|
||||||
- [ ] Status displays on dashboard
|
- [ ] Status displays on dashboard
|
||||||
- [ ] Status displays on booking page
|
- [ ] Status displays on booking page
|
||||||
- [ ] Calendar highlights booked dates
|
- [ ] Calendar highlights user's booked dates
|
||||||
- [ ] Messages are accurate
|
- [ ] Messages are accurate for all states
|
||||||
- [ ] Bilingual support
|
- [ ] Bilingual support (AR/EN)
|
||||||
- [ ] Tests pass
|
- [ ] Loading state implemented
|
||||||
|
- [ ] Edge cases handled (multiple pending, cancelled)
|
||||||
|
- [ ] Unit tests pass
|
||||||
|
- [ ] Feature tests pass
|
||||||
|
- [ ] Code formatted with Pint
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
**Complexity:** Low | **Effort:** 2 hours
|
**Complexity:** Low | **Effort:** 2-3 hours
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue