libra/docs/stories/story-6.8-system-settings.md

331 lines
9.8 KiB
Markdown

# Story 6.8: System Settings
## Epic Reference
**Epic 6:** Admin Dashboard
## User Story
As an **admin**,
I want **to configure system-wide settings**,
So that **I can customize the platform to my needs**.
## Dependencies
- **Story 1.2:** Authentication & Role System (admin auth, User model)
- **Story 8.1:** Email Infrastructure Setup (mail configuration for test email)
## Navigation Context
- Accessible from admin dashboard sidebar/navigation
- Route: `/admin/settings`
- Named route: `admin.settings`
## Acceptance Criteria
### Profile Settings
- [ ] Admin name
- [ ] Email
- [ ] Password change
- [ ] Preferred language
### Email Settings
- [ ] Display current sender email from `config('mail.from.address')`
- [ ] Display current sender name from `config('mail.from.name')`
- [ ] "Send Test Email" button that sends a test email to admin's email address
- [ ] Success/error feedback after test email attempt
### Notification Preferences (Future Enhancement - Not in Scope)
> **Note:** The following are documented for future consideration but are NOT required for this story's completion. All admin notifications are currently mandatory per PRD.
- Toggle admin notifications (future)
- Summary email frequency (future)
### Behavior
- [ ] Settings saved and applied immediately
- [ ] Validation for all inputs
- [ ] Flash messages for success/error states
- [ ] Password fields cleared after successful update
## Technical Notes
### Database Migration Required
Add `preferred_language` column to users table:
```php
// database/migrations/xxxx_add_preferred_language_to_users_table.php
Schema::table('users', function (Blueprint $table) {
$table->string('preferred_language', 2)->default('ar')->after('remember_token');
});
```
### Files to Create/Modify
| File | Action | Purpose |
|------|--------|---------|
| `resources/views/livewire/admin/settings.blade.php` | Create | Main settings Volt component |
| `app/Mail/TestEmail.php` | Create | Test email mailable |
| `resources/views/emails/test.blade.php` | Create | Test email template |
| `database/migrations/xxxx_add_preferred_language_to_users_table.php` | Create | Migration |
| `routes/web.php` | Modify | Add admin settings route |
| `app/Models/User.php` | Modify | Add `preferred_language` to fillable |
### TestEmail Mailable
```php
// app/Mail/TestEmail.php
namespace App\Mail;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
class TestEmail extends Mailable
{
public function envelope(): Envelope
{
return new Envelope(
subject: __('messages.test_email_subject'),
);
}
public function content(): Content
{
return new Content(
view: 'emails.test',
);
}
}
```
### Volt Component Structure
```php
<?php
// resources/views/livewire/admin/settings.blade.php
use App\Mail\TestEmail;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Rule;
use Livewire\Volt\Component;
new class extends Component {
public string $name = '';
public string $email = '';
public string $current_password = '';
public string $password = '';
public string $password_confirmation = '';
public string $preferred_language = 'ar';
public function mount(): void
{
$user = auth()->user();
$this->name = $user->name;
$this->email = $user->email;
$this->preferred_language = $user->preferred_language ?? 'ar';
}
public function updateProfile(): void
{
$this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', Rule::unique('users')->ignore(auth()->id())],
'preferred_language' => ['required', 'in:ar,en'],
]);
auth()->user()->update([
'name' => $this->name,
'email' => $this->email,
'preferred_language' => $this->preferred_language,
]);
session()->flash('success', __('messages.profile_updated'));
}
public function updatePassword(): void
{
$this->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
auth()->user()->update([
'password' => Hash::make($this->password),
]);
$this->reset(['current_password', 'password', 'password_confirmation']);
session()->flash('success', __('messages.password_updated'));
}
public function sendTestEmail(): void
{
try {
Mail::to(auth()->user())->send(new TestEmail());
session()->flash('success', __('messages.test_email_sent'));
} catch (\Exception $e) {
session()->flash('error', __('messages.test_email_failed'));
}
}
}; ?>
<div>
{{-- UI Template Here --}}
</div>
```
### Edge Cases & Error Handling
- **Wrong current password:** Validation rule `current_password` handles this automatically
- **Duplicate email:** `Rule::unique` with `ignore(auth()->id())` prevents self-collision
- **Email send failure:** Wrap in try/catch, show user-friendly error message
- **Empty preferred_language:** Default to 'ar' in mount() if null
## Testing Requirements
### Test File
`tests/Feature/Admin/SettingsTest.php`
### Test Scenarios
**Profile Update Tests:**
```php
test('admin can view settings page', function () {
$admin = User::factory()->create();
$this->actingAs($admin)
->get(route('admin.settings'))
->assertOk()
->assertSeeLivewire('admin.settings');
});
test('admin can update profile information', function () {
$admin = User::factory()->create();
Volt::test('admin.settings')
->actingAs($admin)
->set('name', 'Updated Name')
->set('email', 'updated@example.com')
->set('preferred_language', 'en')
->call('updateProfile')
->assertHasNoErrors();
expect($admin->fresh())
->name->toBe('Updated Name')
->email->toBe('updated@example.com')
->preferred_language->toBe('en');
});
test('profile update validates required fields', function () {
$admin = User::factory()->create();
Volt::test('admin.settings')
->actingAs($admin)
->set('name', '')
->set('email', '')
->call('updateProfile')
->assertHasErrors(['name', 'email']);
});
test('profile update prevents duplicate email', function () {
$existingUser = User::factory()->create(['email' => 'taken@example.com']);
$admin = User::factory()->create();
Volt::test('admin.settings')
->actingAs($admin)
->set('email', 'taken@example.com')
->call('updateProfile')
->assertHasErrors(['email']);
});
```
**Password Update Tests:**
```php
test('admin can update password with correct current password', function () {
$admin = User::factory()->create([
'password' => Hash::make('old-password'),
]);
Volt::test('admin.settings')
->actingAs($admin)
->set('current_password', 'old-password')
->set('password', 'new-password')
->set('password_confirmation', 'new-password')
->call('updatePassword')
->assertHasNoErrors();
expect(Hash::check('new-password', $admin->fresh()->password))->toBeTrue();
});
test('password update fails with wrong current password', function () {
$admin = User::factory()->create([
'password' => Hash::make('correct-password'),
]);
Volt::test('admin.settings')
->actingAs($admin)
->set('current_password', 'wrong-password')
->set('password', 'new-password')
->set('password_confirmation', 'new-password')
->call('updatePassword')
->assertHasErrors(['current_password']);
});
test('password update requires confirmation match', function () {
$admin = User::factory()->create([
'password' => Hash::make('old-password'),
]);
Volt::test('admin.settings')
->actingAs($admin)
->set('current_password', 'old-password')
->set('password', 'new-password')
->set('password_confirmation', 'different-password')
->call('updatePassword')
->assertHasErrors(['password']);
});
```
**Test Email Tests:**
```php
test('admin can send test email', function () {
Mail::fake();
$admin = User::factory()->create();
Volt::test('admin.settings')
->actingAs($admin)
->call('sendTestEmail')
->assertHasNoErrors();
Mail::assertSent(TestEmail::class, fn ($mail) =>
$mail->hasTo($admin->email)
);
});
test('test email failure shows error message', function () {
Mail::fake();
Mail::shouldReceive('to->send')->andThrow(new \Exception('SMTP error'));
$admin = User::factory()->create();
Volt::test('admin.settings')
->actingAs($admin)
->call('sendTestEmail')
->assertSessionHas('error');
});
```
## Definition of Done
- [ ] Migration created and run for `preferred_language` column
- [ ] User model updated with `preferred_language` in fillable
- [ ] Settings Volt component created at `resources/views/livewire/admin/settings.blade.php`
- [ ] TestEmail mailable created at `app/Mail/TestEmail.php`
- [ ] Test email template created at `resources/views/emails/test.blade.php`
- [ ] Route added: `Route::get('/admin/settings', ...)->name('admin.settings')`
- [ ] Profile update works with validation
- [ ] Password change works with current password verification
- [ ] Language preference persists across sessions
- [ ] Test email sends successfully (or shows error on failure)
- [ ] Email settings display current sender info from config
- [ ] All flash messages display correctly (success/error)
- [ ] UI follows Flux UI component patterns
- [ ] All tests pass (`php artisan test --filter=SettingsTest`)
- [ ] Code formatted with Pint
## Estimation
**Complexity:** Medium | **Effort:** 3-4 hours