reviewd epic 7 stories

This commit is contained in:
Naser Mansour 2025-12-21 00:20:35 +02:00
parent b2977c00d6
commit f6c06ec3e1
5 changed files with 1037 additions and 153 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,12 +89,28 @@ new class extends Component {
->toArray(), ->toArray(),
]; ];
} }
};
public function with(): array
{
return $this->getBookingStatus();
}
}; ?>
<div>
<!-- Template below -->
</div>
``` ```
### Template ### Template
```blade ```blade
<div class="bg-cream rounded-lg p-4"> <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) @if($canBookToday)
<div class="flex items-center gap-2 text-success"> <div class="flex items-center gap-2 text-success">
<flux:icon name="check-circle" class="w-5 h-5" /> <flux:icon name="check-circle" class="w-5 h-5" />
@ -81,24 +124,101 @@ new class extends Component {
@endif @endif
@if($pendingRequests->isNotEmpty()) @if($pendingRequests->isNotEmpty())
<p class="mt-2 text-sm text-charcoal/70"> <div class="mt-2 text-sm text-charcoal/70">
{{ __('booking.pending_for_date', ['date' => $pendingRequests->first()->scheduled_date->format('d/m/Y')]) }} @if($pendingRequests->count() === 1)
</p> <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 @endif
<p class="mt-2 text-sm text-charcoal/70"> <p class="mt-2 text-sm text-charcoal/70">
{{ __('booking.limit_message') }} {{ __('booking.limit_message') }}
</p> </p>
</div>
</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