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**,
|
||||
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
|
||||
|
||||
### 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
|
||||
- [ ] Next consultation date/time
|
||||
- [ ] Type (free/paid)
|
||||
- [ ] Status
|
||||
- [ ] Quick link to details
|
||||
- [ ] Display next approved consultation (or "No upcoming consultations" if none)
|
||||
- [ ] Show consultation date/time formatted per locale (AR: DD/MM/YYYY, EN: MM/DD/YYYY)
|
||||
- [ ] Show time in 12-hour format (AM/PM)
|
||||
- [ ] 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
|
||||
- [ ] Count of active timelines
|
||||
- [ ] Latest update preview
|
||||
- [ ] Link to full list
|
||||
- [ ] Display count of active timelines
|
||||
- [ ] Show preview of most recent update (truncated to ~100 chars)
|
||||
- [ ] "View All Cases" link to timelines list (Story 7.3)
|
||||
- [ ] Empty state: "No active cases" with muted styling
|
||||
|
||||
### Recent Updates Widget
|
||||
- [ ] Last 3 timeline updates (across all cases)
|
||||
- [ ] Case name and date
|
||||
- [ ] Link to full timeline
|
||||
- [ ] Display last 3 timeline updates across all user's cases
|
||||
- [ ] Each update shows: case name, update date, preview text
|
||||
- [ ] "View Timeline" link for each update
|
||||
- [ ] Empty state: "No recent updates"
|
||||
|
||||
### Booking Status Widget
|
||||
- [ ] Pending booking requests
|
||||
- [ ] Daily booking limit indicator
|
||||
- [ ] Quick book button
|
||||
- [ ] Display count of pending booking requests
|
||||
- [ ] Show booking limit indicator:
|
||||
- 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
|
||||
- [ ] Clean, card-based layout
|
||||
- [ ] Mobile-first responsive
|
||||
- [ ] Bilingual content
|
||||
### Design Requirements
|
||||
- [ ] Card-based layout using Flux UI components
|
||||
- [ ] Color scheme: Navy (#0A1F44) background sections, Gold (#D4AF37) accents
|
||||
- [ ] 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
|
||||
// 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 {
|
||||
public function with(): array
|
||||
{
|
||||
|
|
@ -51,6 +128,8 @@ new class extends Component {
|
|||
'upcomingConsultation' => $user->consultations()
|
||||
->approved()
|
||||
->upcoming()
|
||||
->orderBy('scheduled_date')
|
||||
->orderBy('scheduled_time')
|
||||
->first(),
|
||||
'activeTimelinesCount' => $user->timelines()->active()->count(),
|
||||
'recentUpdates' => TimelineUpdate::whereHas('timeline', fn($q) => $q->where('user_id', $user->id))
|
||||
|
|
@ -58,22 +137,163 @@ new class extends Component {
|
|||
->take(3)
|
||||
->with('timeline')
|
||||
->get(),
|
||||
'pendingBookings' => $user->consultations()->pending()->count(),
|
||||
'pendingBookingsCount' => $user->consultations()->pending()->count(),
|
||||
'canBookToday' => !$user->consultations()
|
||||
->whereDate('scheduled_date', today())
|
||||
->whereIn('status', ['pending', 'approved'])
|
||||
->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
|
||||
- [ ] All widgets display correctly
|
||||
- [ ] Data scoped to logged-in user
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Bilingual
|
||||
- [ ] Tests pass
|
||||
- [ ] Volt component created at `resources/views/livewire/client/dashboard.blade.php`
|
||||
- [ ] Route registered and protected with auth middleware
|
||||
- [ ] All 5 widgets display correctly with real data
|
||||
- [ ] Data strictly scoped to authenticated user (security verified)
|
||||
- [ ] 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
|
||||
**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**,
|
||||
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
|
||||
|
||||
### Upcoming Consultations Section
|
||||
- [ ] Date and time
|
||||
- [ ] Date and time (formatted per locale)
|
||||
- [ ] Consultation type (free/paid)
|
||||
- [ ] Status (approved/pending)
|
||||
- [ ] Payment status (for paid)
|
||||
- [ ] Payment status (for paid consultations)
|
||||
- [ ] Download .ics calendar file button
|
||||
|
||||
### Pending Requests Section
|
||||
- [ ] Submitted bookings awaiting approval
|
||||
- [ ] Submission date
|
||||
- [ ] Problem summary preview
|
||||
- [ ] Problem summary preview (truncated)
|
||||
- [ ] Status: "Pending Review"
|
||||
|
||||
### Past Consultations Section
|
||||
|
|
@ -29,14 +34,38 @@ So that **I can track upcoming appointments and review past sessions**.
|
|||
- [ ] Date and type
|
||||
|
||||
### Features
|
||||
- [ ] Visual status indicators
|
||||
- [ ] Visual status indicators (badges with colors)
|
||||
- [ ] 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)
|
||||
|
||||
### Empty States
|
||||
- [ ] "No upcoming consultations" message with link to book
|
||||
- [ ] "No pending requests" message
|
||||
- [ ] "No past consultations" message
|
||||
|
||||
## 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
|
||||
// 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 {
|
||||
use WithPagination;
|
||||
|
||||
|
|
@ -57,20 +86,96 @@ new class extends Component {
|
|||
->get(),
|
||||
'past' => $user->consultations()
|
||||
->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')
|
||||
->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
|
||||
- [ ] All sections display correctly
|
||||
- [ ] Calendar download works
|
||||
- [ ] Status indicators clear
|
||||
- [ ] Read-only (no actions)
|
||||
- [ ] Pagination works
|
||||
- [ ] Status indicators clear and color-coded
|
||||
- [ ] Read-only (no actions except download)
|
||||
- [ ] Pagination works for past consultations
|
||||
- [ ] Empty states display appropriately
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Bilingual support
|
||||
- [ ] Tests pass
|
||||
|
||||
## Estimation
|
||||
|
|
|
|||
|
|
@ -1,73 +1,245 @@
|
|||
# Story 7.3: My Cases/Timelines View
|
||||
# Story 7.3: My Cases/Timelines View (Dashboard Integration)
|
||||
|
||||
## Epic Reference
|
||||
**Epic 7:** Client Dashboard
|
||||
|
||||
## User Story
|
||||
As a **client**,
|
||||
I want **to view my case timelines and their updates**,
|
||||
So that **I can track the progress of my legal matters**.
|
||||
I want **to access my case timelines from the dashboard navigation**,
|
||||
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
|
||||
|
||||
### Active Cases Section
|
||||
- [ ] List of active timelines
|
||||
- [ ] Case name and reference
|
||||
- [ ] Last update date
|
||||
- [ ] Update count
|
||||
- [ ] "View" button
|
||||
### Navigation Integration
|
||||
- [ ] "My Cases" navigation item added to client dashboard sidebar/nav
|
||||
- [ ] Navigation links to `route('client.timelines.index')`
|
||||
- [ ] Active state shown when on timeline routes
|
||||
- [ ] Icon: folder or briefcase icon for cases
|
||||
|
||||
### Archived Cases Section
|
||||
- [ ] Clearly separated from active
|
||||
- [ ] Different visual styling (muted)
|
||||
- [ ] Still accessible for viewing
|
||||
### Dashboard Widget (on Story 7.1's dashboard)
|
||||
- [ ] "My Cases" widget card displays:
|
||||
- Count of active cases
|
||||
- Latest update preview (case name + date)
|
||||
- "View All" link to timeline index
|
||||
- [ ] Widget shows empty state if no cases exist
|
||||
|
||||
### Individual Timeline View
|
||||
- [ ] Case name and reference
|
||||
- [ ] Status badge (active/archived)
|
||||
- [ ] All updates in chronological order
|
||||
- [ ] Each update shows:
|
||||
- Date and time
|
||||
- Update content
|
||||
- [ ] Read-only (no interactions)
|
||||
### Layout Consistency
|
||||
- [ ] Timeline pages use client dashboard layout (consistent header/nav)
|
||||
- [ ] Breadcrumbs: Dashboard > My Cases (on index)
|
||||
- [ ] Breadcrumbs: Dashboard > My Cases > [Case Name] (on show)
|
||||
|
||||
### Navigation
|
||||
- [ ] Back to cases list
|
||||
- [ ] Responsive layout
|
||||
### Bilingual Support
|
||||
- [ ] Navigation label translated (AR/EN)
|
||||
- [ ] Widget content translated (AR/EN)
|
||||
|
||||
## 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
|
||||
new class extends Component {
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'activeTimelines' => auth()->user()
|
||||
->timelines()
|
||||
->active()
|
||||
->withCount('updates')
|
||||
->latest('updated_at')
|
||||
->get(),
|
||||
'archivedTimelines' => auth()->user()
|
||||
->timelines()
|
||||
->archived()
|
||||
->withCount('updates')
|
||||
->latest('updated_at')
|
||||
->get(),
|
||||
];
|
||||
}
|
||||
};
|
||||
{{-- In client layout/navigation component --}}
|
||||
<flux:navbar.item
|
||||
href="{{ route('client.timelines.index') }}"
|
||||
:active="request()->routeIs('client.timelines.*')"
|
||||
icon="folder"
|
||||
>
|
||||
{{ __('client.my_cases') }}
|
||||
</flux:navbar.item>
|
||||
```
|
||||
|
||||
### Dashboard Widget Component
|
||||
Add to Story 7.1's dashboard view:
|
||||
|
||||
```php
|
||||
{{-- My Cases Widget --}}
|
||||
<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
|
||||
- [ ] Active cases display correctly
|
||||
- [ ] Archived cases separated
|
||||
- [ ] Timeline detail view works
|
||||
- [ ] Updates display chronologically
|
||||
- [ ] Read-only enforced
|
||||
- [ ] Tests pass
|
||||
- [ ] "My Cases" navigation item added to client dashboard
|
||||
- [ ] Navigation links to existing `client.timelines.index` route
|
||||
- [ ] Active state shows on timeline routes
|
||||
- [ ] Dashboard widget displays active case count
|
||||
- [ ] Dashboard widget shows latest update preview
|
||||
- [ ] 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
|
||||
**Complexity:** Medium | **Effort:** 3-4 hours
|
||||
**Complexity:** Low | **Effort:** 1-2 hours
|
||||
|
|
|
|||
|
|
@ -3,40 +3,79 @@
|
|||
## Epic Reference
|
||||
**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
|
||||
As a **client**,
|
||||
I want **to view my profile information**,
|
||||
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
|
||||
|
||||
### Individual Client Profile
|
||||
- [ ] Full name
|
||||
- [ ] National ID
|
||||
- [ ] Email address
|
||||
- [ ] Phone number
|
||||
- [ ] Preferred language
|
||||
- [ ] Account created date
|
||||
- [ ] Full name displayed
|
||||
- [ ] National ID displayed
|
||||
- [ ] Email address displayed
|
||||
- [ ] Phone number displayed
|
||||
- [ ] Preferred language displayed
|
||||
- [ ] Account created date displayed
|
||||
|
||||
### Company Client Profile
|
||||
- [ ] Company name
|
||||
- [ ] Registration number
|
||||
- [ ] Contact person name
|
||||
- [ ] Contact person ID
|
||||
- [ ] Email address
|
||||
- [ ] Phone number
|
||||
- [ ] Preferred language
|
||||
- [ ] Account created date
|
||||
- [ ] Company name displayed
|
||||
- [ ] Company certificate/registration number displayed
|
||||
- [ ] Contact person name displayed
|
||||
- [ ] Contact person ID displayed
|
||||
- [ ] Email address displayed
|
||||
- [ ] Phone number displayed
|
||||
- [ ] Preferred language displayed
|
||||
- [ ] Account created date displayed
|
||||
|
||||
### Features
|
||||
- [ ] Account type indicator
|
||||
- [ ] No edit capabilities (read-only)
|
||||
- [ ] Account type indicator (Individual/Company badge)
|
||||
- [ ] No edit capabilities (read-only view)
|
||||
- [ ] Message: "Contact admin to update your information"
|
||||
- [ ] Logout button
|
||||
- [ ] Logout button with confirmation redirect
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### File Location
|
||||
```
|
||||
resources/views/livewire/client/profile.blade.php
|
||||
```
|
||||
|
||||
### Route
|
||||
```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 {
|
||||
public function with(): array
|
||||
{
|
||||
|
|
@ -58,6 +97,11 @@ new class extends Component {
|
|||
<div class="max-w-2xl mx-auto">
|
||||
<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">
|
||||
@if($user->user_type === 'individual')
|
||||
<dl class="space-y-4">
|
||||
|
|
@ -69,10 +113,58 @@ new class extends Component {
|
|||
<dt class="text-sm text-charcoal/70">{{ __('profile.national_id') }}</dt>
|
||||
<dd>{{ $user->national_id }}</dd>
|
||||
</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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
|
|
@ -86,13 +178,188 @@ new class extends Component {
|
|||
</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
|
||||
- [ ] Individual profile displays correctly
|
||||
- [ ] Company profile displays correctly
|
||||
- [ ] No edit functionality
|
||||
- [ ] Contact admin message shown
|
||||
- [ ] Logout works
|
||||
- [ ] Tests pass
|
||||
- [ ] Individual profile displays all fields correctly
|
||||
- [ ] Company profile displays all fields correctly
|
||||
- [ ] Account type badge shows correctly for both types
|
||||
- [ ] No edit functionality present (read-only)
|
||||
- [ ] Contact admin message displayed
|
||||
- [ ] 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
|
||||
**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 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
|
||||
As a **client**,
|
||||
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
|
||||
|
||||
### Display Locations
|
||||
- [ ] Dashboard widget
|
||||
- [ ] Booking page
|
||||
- [ ] Dashboard widget showing booking status
|
||||
- [ ] Booking page status banner
|
||||
|
||||
### Status Messages
|
||||
- [ ] "You can book a consultation today"
|
||||
- [ ] "You already have a booking for today"
|
||||
- [ ] "You have a pending request for [date]"
|
||||
- [ ] "You can book a consultation today" (when no booking exists for today)
|
||||
- [ ] "You already have a booking for today" (when pending/approved booking exists for today)
|
||||
- [ ] "You have a pending request for [date]" (shows first pending request date)
|
||||
|
||||
### Calendar Integration
|
||||
- [ ] Calendar shows booked days as unavailable
|
||||
- [ ] Visual indicator for user's booked dates
|
||||
- [ ] Pass `bookedDates` array to availability calendar component
|
||||
- [ ] Calendar marks user's booked dates as unavailable (distinct styling)
|
||||
- [ ] Visual indicator differentiates "user already booked" from "no slots available"
|
||||
|
||||
### Information
|
||||
- [ ] 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
|
||||
|
||||
### 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
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public function getBookingStatus(): array
|
||||
{
|
||||
|
|
@ -41,11 +66,13 @@ new class extends Component {
|
|||
->first();
|
||||
|
||||
$pendingRequests = $user->consultations()
|
||||
->pending()
|
||||
->where('status', 'pending')
|
||||
->where('scheduled_date', '>=', today())
|
||||
->orderBy('scheduled_date')
|
||||
->get();
|
||||
|
||||
$upcomingApproved = $user->consultations()
|
||||
->approved()
|
||||
->where('status', 'approved')
|
||||
->where('scheduled_date', '>=', today())
|
||||
->get();
|
||||
|
||||
|
|
@ -62,43 +89,136 @@ new class extends Component {
|
|||
->toArray(),
|
||||
];
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Template
|
||||
```blade
|
||||
<div class="bg-cream rounded-lg p-4">
|
||||
@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
|
||||
public function with(): array
|
||||
{
|
||||
return $this->getBookingStatus();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
@if($pendingRequests->isNotEmpty())
|
||||
<p class="mt-2 text-sm text-charcoal/70">
|
||||
{{ __('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>
|
||||
<!-- Template below -->
|
||||
</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
|
||||
- [ ] Booking status component created
|
||||
- [ ] Status displays on dashboard
|
||||
- [ ] Status displays on booking page
|
||||
- [ ] Calendar highlights booked dates
|
||||
- [ ] Messages are accurate
|
||||
- [ ] Bilingual support
|
||||
- [ ] Tests pass
|
||||
- [ ] Calendar highlights user's booked dates
|
||||
- [ ] Messages are accurate for all states
|
||||
- [ ] Bilingual support (AR/EN)
|
||||
- [ ] Loading state implemented
|
||||
- [ ] Edge cases handled (multiple pending, cancelled)
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Feature tests pass
|
||||
- [ ] Code formatted with Pint
|
||||
|
||||
## Estimation
|
||||
**Complexity:** Low | **Effort:** 2 hours
|
||||
**Complexity:** Low | **Effort:** 2-3 hours
|
||||
|
|
|
|||
Loading…
Reference in New Issue