16 KiB
Story 7.4: My Profile View
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 displayed
- National ID displayed
- Email address displayed
- Phone number displayed
- Preferred language displayed
- Account created date displayed
Company Client Profile
- 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 (Individual/Company badge)
- No edit capabilities (read-only view)
- Message: "Contact admin to update your information"
- Logout button with confirmation redirect
Technical Notes
File Location
resources/views/livewire/client/profile.blade.php
Route
// In routes/web.php (client authenticated routes)
Route::get('/client/profile', \Livewire\Volt\Volt::route('client.profile'))->name('client.profile');
Component Implementation
<?php
use Livewire\Volt\Component;
new class extends Component {
public function with(): array
{
return [
'user' => auth()->user(),
];
}
public function logout(): void
{
auth()->logout();
session()->invalidate();
session()->regenerateToken();
$this->redirect(route('login'));
}
}; ?>
<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">
<div>
<dt class="text-sm text-charcoal/70">{{ __('profile.full_name') }}</dt>
<dd class="text-lg font-medium">{{ $user->name }}</dd>
</div>
<div>
<dt class="text-sm text-charcoal/70">{{ __('profile.national_id') }}</dt>
<dd>{{ $user->national_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>
@else
<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>
<flux:callout class="mt-6">
{{ __('client.contact_admin_to_update') }}
</flux:callout>
<flux:button wire:click="logout" variant="danger" class="mt-6">
{{ __('auth.logout') }}
</flux:button>
</div>
Required Translation Keys
Add to lang/en/client.php:
'my_profile' => 'My Profile',
'contact_admin_to_update' => 'Contact admin to update your information',
Add to lang/en/profile.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:
'logout' => 'Logout',
Create corresponding Arabic translations in lang/ar/ files.
Test Scenarios
Unit/Feature Tests
Create tests/Feature/Client/ProfileTest.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:
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 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-3 hours
Notes
- Date formatting uses
translatedFormat()for locale-aware display - Ensure the User model has
$castsforcreated_atas datetime - The
bg-creamandtext-charcoalclasses should be defined in Tailwind config per project design system
Dev Agent Record
Status
Ready for Review
Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
Completion Notes
- Created profile Volt component at
resources/views/livewire/client/profile.blade.php - Added route at
routes/web.php(client.profile) - Created new translation file
lang/en/profile.phpandlang/ar/profile.php - Added keys to
lang/en/client.phpandlang/ar/client.php - Added logout key to
lang/en/auth.phpandlang/ar/auth.php - Created comprehensive test file
tests/Feature/Client/ProfileTest.phpwith 20 tests - All 20 profile tests pass (36 assertions)
- All 121 client tests pass
- Code formatted with Pint
File List
| File | Action |
|---|---|
resources/views/livewire/client/profile.blade.php |
Created |
routes/web.php |
Modified |
lang/en/profile.php |
Created |
lang/ar/profile.php |
Created |
lang/en/client.php |
Modified |
lang/ar/client.php |
Modified |
lang/en/auth.php |
Modified |
lang/ar/auth.php |
Modified |
tests/Feature/Client/ProfileTest.php |
Created |
Change Log
| Change | Reason |
|---|---|
Used UserType enum instead of string comparison |
Matches existing codebase pattern with typed enums |
Used full_name instead of name field |
User model uses full_name column |
Used Flux badge with color instead of variant |
Matches Flux UI Free component API |
| Added dark mode support classes | Consistent with existing client dashboard styling |
Debug Log References
None required - implementation completed without issues.
QA Results
Review Date: 2025-12-28
Reviewed By: Quinn (Test Architect)
Code Quality Assessment
The implementation is well-structured and follows Laravel/Livewire/Volt best practices. The Volt component is clean and minimal, properly delegating user type detection to the PHP layer. The template correctly uses Flux UI components and supports bilingual (AR/EN) content. Test coverage is comprehensive with 20 tests covering all acceptance criteria.
Refactoring Performed
- File:
resources/views/livewire/client/profile.blade.php- Change: Moved
UserTypeenum comparison from Blade template to PHPwith()method, passing$isIndividualboolean - Why: Pint was removing the
use App\Enums\UserTypeimport as "unused" (it doesn't analyze Blade sections), causing runtime errors - How: The enum is now used in PHP where Pint can track its usage, and the template uses a simple boolean, improving separation of concerns
- Change: Moved
Compliance Check
- Coding Standards: ✓ Passes Pint after refactoring
- Project Structure: ✓ Follows Volt single-file component pattern
- Testing Strategy: ✓ Comprehensive Pest tests with Volt::test()
- All ACs Met: ✓ All acceptance criteria verified by tests
Improvements Checklist
- Fixed Pint compliance issue with unused import (resources/views/livewire/client/profile.blade.php)
- Improved separation of concerns (type check in PHP, boolean in Blade)
Security Review
No security concerns. The profile page is:
- Read-only (no edit functionality as required)
- Protected by
authandclientmiddleware - Admin users are correctly blocked with 403
- Logout properly invalidates session and regenerates CSRF token
Performance Considerations
No performance concerns. The component:
- Makes minimal database queries (single user fetch via
auth()->user()) - Uses proper date formatting with
translatedFormat() - No N+1 queries or unnecessary data loading
Files Modified During Review
| File | Action |
|---|---|
resources/views/livewire/client/profile.blade.php |
Modified (Pint compliance fix) |
Gate Status
Gate: PASS → docs/qa/gates/7.4-my-profile-view.yml
Recommended Status
✓ Ready for Done - All acceptance criteria met, comprehensive test coverage, Pint compliant