+
@@ -150,23 +289,251 @@ new class extends Component {
```
+### Required Translation Keys
+```php
+// resources/lang/en/client.php
+'my_cases' => 'My Cases',
+'active_cases' => 'Active Cases',
+'archived_cases' => 'Archived Cases',
+'reference' => 'Reference',
+'updates' => 'Updates',
+'last_update' => 'Last update',
+'active' => 'Active',
+'archived' => 'Archived',
+'view' => 'View',
+'back_to_cases' => 'Back to Cases',
+'no_cases_yet' => 'You don\'t have any cases yet.',
+'no_updates_yet' => 'No updates yet.',
+
+// resources/lang/ar/client.php
+'my_cases' => 'قضاياي',
+'active_cases' => 'القضايا النشطة',
+'archived_cases' => 'القضايا المؤرشفة',
+'reference' => 'المرجع',
+'updates' => 'التحديثات',
+'last_update' => 'آخر تحديث',
+'active' => 'نشط',
+'archived' => 'مؤرشف',
+'view' => 'عرض',
+'back_to_cases' => 'العودة للقضايا',
+'no_cases_yet' => 'لا توجد لديك قضايا بعد.',
+'no_updates_yet' => 'لا توجد تحديثات بعد.',
+```
+
+## Test Scenarios
+
+All tests should use Pest and be placed in `tests/Feature/Client/TimelineViewTest.php`.
+
+```php
+create(['user_type' => 'individual']);
+ Timeline::factory()->count(3)->create(['user_id' => $client->id]);
+
+ $this->actingAs($client)
+ ->get(route('client.timelines.index'))
+ ->assertOk()
+ ->assertSeeLivewire('pages.client.timelines.index');
+});
+
+test('client cannot view other clients timelines in list', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $otherClient = User::factory()->create(['user_type' => 'individual']);
+ $otherTimeline = Timeline::factory()->create([
+ 'user_id' => $otherClient->id,
+ 'case_name' => 'Other Client Case',
+ ]);
+
+ Volt::test('pages.client.timelines.index')
+ ->actingAs($client)
+ ->assertDontSee('Other Client Case');
+});
+
+test('client can view own timeline detail', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $timeline = Timeline::factory()->create([
+ 'user_id' => $client->id,
+ 'case_name' => 'My Contract Case',
+ ]);
+
+ $this->actingAs($client)
+ ->get(route('client.timelines.show', $timeline))
+ ->assertOk()
+ ->assertSee('My Contract Case');
+});
+
+test('client cannot view other clients timeline detail', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $otherClient = User::factory()->create(['user_type' => 'individual']);
+ $otherTimeline = Timeline::factory()->create(['user_id' => $otherClient->id]);
+
+ $this->actingAs($client)
+ ->get(route('client.timelines.show', $otherTimeline))
+ ->assertForbidden();
+});
+
+test('guest cannot access timelines', function () {
+ $this->get(route('client.timelines.index'))
+ ->assertRedirect(route('login'));
+});
+
+test('admin cannot access client timeline routes', function () {
+ $admin = User::factory()->admin()->create();
+
+ $this->actingAs($admin)
+ ->get(route('client.timelines.index'))
+ ->assertForbidden();
+});
+
+// List View Tests
+test('active timelines displayed separately from archived', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ Timeline::factory()->create([
+ 'user_id' => $client->id,
+ 'case_name' => 'Active Case',
+ 'status' => 'active',
+ ]);
+ Timeline::factory()->create([
+ 'user_id' => $client->id,
+ 'case_name' => 'Archived Case',
+ 'status' => 'archived',
+ ]);
+
+ Volt::test('pages.client.timelines.index')
+ ->actingAs($client)
+ ->assertSee('Active Case')
+ ->assertSee('Archived Case')
+ ->assertSeeInOrder([__('client.active_cases'), 'Active Case', __('client.archived_cases'), 'Archived Case']);
+});
+
+test('timeline list shows update count', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $timeline = Timeline::factory()->create(['user_id' => $client->id]);
+ TimelineUpdate::factory()->count(5)->create(['timeline_id' => $timeline->id]);
+
+ Volt::test('pages.client.timelines.index')
+ ->actingAs($client)
+ ->assertSee('5');
+});
+
+test('empty state shown when no timelines', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+
+ Volt::test('pages.client.timelines.index')
+ ->actingAs($client)
+ ->assertSee(__('client.no_cases_yet'));
+});
+
+// Detail View Tests
+test('timeline detail shows all updates chronologically', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $timeline = Timeline::factory()->create(['user_id' => $client->id]);
+
+ $oldUpdate = TimelineUpdate::factory()->create([
+ 'timeline_id' => $timeline->id,
+ 'update_text' => 'First Update',
+ 'created_at' => now()->subDays(2),
+ ]);
+ $newUpdate = TimelineUpdate::factory()->create([
+ 'timeline_id' => $timeline->id,
+ 'update_text' => 'Second Update',
+ 'created_at' => now()->subDay(),
+ ]);
+
+ Volt::test('pages.client.timelines.show', ['timeline' => $timeline])
+ ->actingAs($client)
+ ->assertSeeInOrder(['First Update', 'Second Update']);
+});
+
+test('timeline detail shows case name and reference', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $timeline = Timeline::factory()->create([
+ 'user_id' => $client->id,
+ 'case_name' => 'Property Dispute',
+ 'case_reference' => 'REF-2024-001',
+ ]);
+
+ Volt::test('pages.client.timelines.show', ['timeline' => $timeline])
+ ->actingAs($client)
+ ->assertSee('Property Dispute')
+ ->assertSee('REF-2024-001');
+});
+
+test('timeline detail shows status badge', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $activeTimeline = Timeline::factory()->create([
+ 'user_id' => $client->id,
+ 'status' => 'active',
+ ]);
+
+ Volt::test('pages.client.timelines.show', ['timeline' => $activeTimeline])
+ ->actingAs($client)
+ ->assertSee(__('client.active'));
+});
+
+test('empty updates shows no updates message', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $timeline = Timeline::factory()->create(['user_id' => $client->id]);
+
+ Volt::test('pages.client.timelines.show', ['timeline' => $timeline])
+ ->actingAs($client)
+ ->assertSee(__('client.no_updates_yet'));
+});
+
+// Read-Only Enforcement Tests
+test('client cannot edit timeline or updates', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ $timeline = Timeline::factory()->create(['user_id' => $client->id]);
+
+ // Verify no edit methods exist on the component
+ Volt::test('pages.client.timelines.show', ['timeline' => $timeline])
+ ->actingAs($client)
+ ->assertMethodDoesNotExist('edit')
+ ->assertMethodDoesNotExist('update')
+ ->assertMethodDoesNotExist('delete')
+ ->assertMethodDoesNotExist('archive');
+});
+
+// N+1 Query Prevention Test
+test('timeline list uses eager loading', function () {
+ $client = User::factory()->create(['user_type' => 'individual']);
+ Timeline::factory()->count(10)->create(['user_id' => $client->id]);
+
+ // Component should load with reasonable query count
+ $this->actingAs($client);
+ Volt::test('pages.client.timelines.index');
+ // With eager loading: ~3-4 queries (timelines, updates count, latest update)
+});
+```
+
## Definition of Done
+- [ ] Volt components created at specified file locations
+- [ ] Routes registered for client timeline views
- [ ] Client can view list of their timelines
-- [ ] Active/archived clearly separated
+- [ ] Active/archived clearly separated with visual distinction
- [ ] Can view individual timeline details
-- [ ] All updates displayed chronologically
-- [ ] Read-only (no edit capabilities)
-- [ ] Cannot view other clients' timelines
+- [ ] All updates displayed chronologically (oldest first)
+- [ ] Read-only enforced (no edit/delete methods)
+- [ ] Cannot view other clients' timelines (403 response)
+- [ ] Empty state displayed when no timelines
- [ ] Mobile responsive
-- [ ] RTL support
-- [ ] Tests pass
+- [ ] RTL support with proper positioning
+- [ ] All translation keys added (AR/EN)
+- [ ] All tests pass
- [ ] Code formatted with Pint
## Dependencies
-- **Story 4.1-4.3:** Timeline management
-- **Epic 7:** Client dashboard structure
+- **Story 4.1:** Timeline creation (`docs/stories/story-4.1-timeline-creation.md`) - Timeline model and database
+- **Story 4.2:** Timeline updates (`docs/stories/story-4.2-timeline-updates-management.md`) - TimelineUpdate model
+- **Story 4.3:** Timeline archiving (`docs/stories/story-4.3-timeline-archiving.md`) - Active/archived scopes
+- **Story 7.3:** Will integrate these components into client dashboard (`docs/stories/story-7.3-my-cases-timelines-view.md`)
## Estimation
diff --git a/docs/stories/story-4.6-timeline-update-notifications.md b/docs/stories/story-4.6-timeline-update-notifications.md
index 9112255..534f2cd 100644
--- a/docs/stories/story-4.6-timeline-update-notifications.md
+++ b/docs/stories/story-4.6-timeline-update-notifications.md
@@ -16,6 +16,31 @@ So that **I stay informed about my case progress without checking the portal**.
- **Follows pattern:** Event-driven notification pattern
- **Touch points:** Timeline update creation
+### Previous Story Context (Story 4.2)
+Story 4.2 established:
+- `TimelineUpdate` model with fields: `timeline_id`, `admin_id`, `update_text`
+- `TimelineUpdate` belongs to `Timeline` and `User` (admin)
+- `Timeline` has many `TimelineUpdate` records
+- Updates created via `addUpdate()` method in timeline management Volt component
+- Notification trigger point: after `$this->timeline->updates()->create([...])`
+
+### Prerequisites
+The following must exist before implementing this story:
+- **User model requirements:**
+ - `preferred_language` field (string, defaults to 'ar') - from Epic 2 user registration
+ - `isActive()` method returning boolean (checks `status !== 'deactivated'`) - from Epic 2
+- **Route requirement:**
+ - `client.timelines.show` route must exist (from Story 4.5)
+- **Models available:**
+ - `Timeline` model with `user()` relationship
+ - `TimelineUpdate` model with `timeline()` relationship
+
+### Implementation Files
+- **Notification:** `app/Notifications/TimelineUpdateNotification.php`
+- **Arabic Template:** `resources/views/emails/timeline/update/ar.blade.php`
+- **English Template:** `resources/views/emails/timeline/update/en.blade.php`
+- **Modify:** Story 4.2 Volt component to trigger notification
+
## Acceptance Criteria
### Notification Trigger
@@ -168,18 +193,33 @@ if ($this->timeline->user->isActive()) {
}
```
-### Testing
+### Test Scenarios
+- [ ] Notification sent when timeline update created
+- [ ] No notification to deactivated user (`status === 'deactivated'`)
+- [ ] Arabic template renders with correct content (case name, update text, date)
+- [ ] English template renders with correct content
+- [ ] Uses client's `preferred_language` for template selection (defaults to 'ar')
+- [ ] Email is queued (uses `ShouldQueue` interface)
+- [ ] Email contains valid link to timeline view
+- [ ] Subject line includes case name in correct language
+
+### Testing Examples
```php
+use App\Models\{User, Timeline, TimelineUpdate};
use App\Notifications\TimelineUpdateNotification;
use Illuminate\Support\Facades\Notification;
it('sends notification when timeline update created', function () {
Notification::fake();
- $timeline = Timeline::factory()->create();
+ $user = User::factory()->create(['status' => 'active']);
+ $timeline = Timeline::factory()->for($user)->create();
$update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
- $timeline->user->notify(new TimelineUpdateNotification($update));
+ // Simulate the trigger from Story 4.2
+ if ($timeline->user->isActive()) {
+ $timeline->user->notify(new TimelineUpdateNotification($update));
+ }
Notification::assertSentTo($timeline->user, TimelineUpdateNotification::class);
});
@@ -189,12 +229,45 @@ it('does not send notification to deactivated user', function () {
$user = User::factory()->create(['status' => 'deactivated']);
$timeline = Timeline::factory()->for($user)->create();
+ $update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
- // Add update (should check user status)
- // ...
+ // Simulate the trigger from Story 4.2 - should NOT notify
+ if ($timeline->user->isActive()) {
+ $timeline->user->notify(new TimelineUpdateNotification($update));
+ }
Notification::assertNotSentTo($user, TimelineUpdateNotification::class);
});
+
+it('uses arabic template for arabic-preferred user', function () {
+ Notification::fake();
+
+ $user = User::factory()->create(['preferred_language' => 'ar']);
+ $timeline = Timeline::factory()->for($user)->create(['case_name' => 'Test Case']);
+ $update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
+
+ $user->notify(new TimelineUpdateNotification($update));
+
+ Notification::assertSentTo($user, TimelineUpdateNotification::class, function ($notification, $channels, $notifiable) {
+ $mail = $notification->toMail($notifiable);
+ return str_contains($mail->subject, 'تحديث جديد على قضيتك');
+ });
+});
+
+it('uses english template for english-preferred user', function () {
+ Notification::fake();
+
+ $user = User::factory()->create(['preferred_language' => 'en']);
+ $timeline = Timeline::factory()->for($user)->create(['case_name' => 'Test Case']);
+ $update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
+
+ $user->notify(new TimelineUpdateNotification($update));
+
+ Notification::assertSentTo($user, TimelineUpdateNotification::class, function ($notification, $channels, $notifiable) {
+ $mail = $notification->toMail($notifiable);
+ return str_contains($mail->subject, 'New update on your case');
+ });
+});
```
## Definition of Done
@@ -211,8 +284,10 @@ it('does not send notification to deactivated user', function () {
## Dependencies
-- **Story 4.2:** Timeline updates management
-- **Epic 8:** Email infrastructure
+- **Story 4.2:** Timeline updates management (REQUIRED - notification triggered from addUpdate method)
+- **Story 4.5:** Client timeline view (REQUIRED - provides `client.timelines.show` route used in email link)
+- **Epic 2:** User model with `preferred_language` field and `isActive()` method
+- **Epic 8:** Email infrastructure (SOFT DEPENDENCY - notification will queue but email delivery requires Epic 8)
## Estimation