# Story 6.10: Audit Log Viewer ## Epic Reference **Epic 6:** Admin Dashboard ## User Story As an **admin**, I want **to view admin action history**, So that **I can maintain accountability and track changes**. ## Dependencies - **Story 1.1:** Project Setup & Database Schema (admin_logs table and AdminLog model) - **Story 1.2:** Authentication & Role System (admin auth middleware) - **Story 6.1:** Dashboard Overview & Statistics (admin layout, navigation patterns) ## Navigation Context - Accessible from admin dashboard sidebar/navigation - Route: `/admin/audit-logs` - Named route: `admin.audit-logs` ## Background Context ### What Gets Logged The `admin_logs` table captures significant admin actions for accountability: - **User Management:** Create, update, delete, deactivate/reactivate users - **Consultation Management:** Approve, reject, mark complete, mark no-show, cancel bookings - **Timeline Management:** Create, update, archive timelines and timeline entries - **Posts Management:** Create, update, delete posts - **Settings Changes:** System configuration modifications ### AdminLog Model Schema Reference: PRD ยง16.1 Database Schema ``` admin_logs table: - id: bigint (primary key) - admin_id: bigint (nullable, foreign key to users - null for system actions) - action: string (create, update, delete, approve, reject, etc.) - target_type: string (user, consultation, timeline, timeline_update, post, setting) - target_id: bigint (ID of the affected record) - old_values: json (nullable - previous state for updates) - new_values: json (nullable - new state for creates/updates) - ip_address: string (IPv4/IPv6 address of admin) - created_at: timestamp ``` ## Acceptance Criteria ### Display Requirements - [ ] Paginated table of audit log entries (25 per page) - [ ] Each row displays: - Timestamp (formatted per locale: d/m/Y H:i for AR, m/d/Y H:i for EN) - Admin name (or "System" for automated actions) - Action type badge (color-coded: green=create, blue=update, red=delete) - Target type and ID (e.g., "User #123") - IP address - Details button to view old/new values ### Filtering - [ ] Filter by action type (dropdown with available actions) - [ ] Filter by target type (dropdown with available targets) - [ ] Filter by date range (date pickers for from/to) - [ ] Filters apply without page reload (Livewire) - [ ] Reset filters button ### Search - [ ] Search by target ID (exact match) - [ ] Search updates results in real-time with debounce ### Detail Modal - [ ] Modal displays full log entry details - [ ] Shows old values in readable format (JSON pretty-printed) - [ ] Shows new values in readable format - [ ] Highlights differences between old/new for updates - [ ] Bilingual labels ### CSV Export - [ ] Export current filtered results to CSV - [ ] CSV includes all displayed columns - [ ] CSV filename includes export date - [ ] Bilingual column headers based on admin's language preference ### Empty State - [ ] Friendly message when no logs exist - [ ] Friendly message when filters return no results ## Technical Notes ### Files to Create/Modify - `resources/views/livewire/admin/audit-logs.blade.php` - Main Volt component - `app/Models/AdminLog.php` - Eloquent model (if not already created in Story 1.1) - `routes/web.php` - Add admin route (or admin routes file) - `lang/ar/audit.php` - Arabic translations - `lang/en/audit.php` - English translations ### AdminLog Model ```php class AdminLog extends Model { protected $fillable = [ 'admin_id', 'action', 'target_type', 'target_id', 'old_values', 'new_values', 'ip_address', ]; protected function casts(): array { return [ 'old_values' => 'array', 'new_values' => 'array', ]; } public function admin(): BelongsTo { return $this->belongsTo(User::class, 'admin_id'); } } ``` ### Volt Component Structure ```php resetPage(); } public function updatedTargetFilter(): void { $this->resetPage(); } public function updatedDateFrom(): void { $this->resetPage(); } public function updatedDateTo(): void { $this->resetPage(); } public function updatedSearch(): void { $this->resetPage(); } public function resetFilters(): void { $this->reset(['actionFilter', 'targetFilter', 'dateFrom', 'dateTo', 'search']); $this->resetPage(); } public function showDetails(int $logId): void { $this->selectedLogId = $logId; $this->dispatch('open-modal', name: 'log-details'); } public function with(): array { return [ 'logs' => $this->getFilteredQuery()->paginate(25), 'actionTypes' => AdminLog::distinct()->pluck('action'), 'targetTypes' => AdminLog::distinct()->pluck('target_type'), 'selectedLog' => $this->selectedLogId ? AdminLog::with('admin')->find($this->selectedLogId) : null, ]; } public function exportCsv(): StreamedResponse { $logs = $this->getFilteredQuery()->get(); return response()->streamDownload(function () use ($logs) { $handle = fopen('php://output', 'w'); // Header row (bilingual based on locale) fputcsv($handle, [ __('audit.timestamp'), __('audit.admin'), __('audit.action'), __('audit.target_type'), __('audit.target_id'), __('audit.ip_address'), ]); foreach ($logs as $log) { fputcsv($handle, [ $log->created_at->format('Y-m-d H:i:s'), $log->admin?->name ?? __('audit.system'), $log->action, $log->target_type, $log->target_id, $log->ip_address, ]); } fclose($handle); }, 'audit-log-' . now()->format('Y-m-d') . '.csv'); } private function getFilteredQuery() { return AdminLog::query() ->with('admin') ->when($this->actionFilter, fn($q) => $q->where('action', $this->actionFilter)) ->when($this->targetFilter, fn($q) => $q->where('target_type', $this->targetFilter)) ->when($this->dateFrom, fn($q) => $q->whereDate('created_at', '>=', $this->dateFrom)) ->when($this->dateTo, fn($q) => $q->whereDate('created_at', '<=', $this->dateTo)) ->when($this->search, fn($q) => $q->where('target_id', $this->search)) ->latest(); } }; ?>
| {{ __('audit.timestamp') }} | {{ __('audit.admin') }} | {{ __('audit.action') }} | {{ __('audit.target') }} | {{ __('audit.ip_address') }} | |
|---|---|---|---|---|---|
| {{ $log->created_at->translatedFormat(app()->getLocale() === 'ar' ? 'd/m/Y H:i' : 'm/d/Y H:i') }} | {{ $log->admin?->name ?? __('audit.system') }} |
|
{{ __("audit.target_{$log->target_type}") }} #{{ $log->target_id }} | {{ $log->ip_address }} |
|
{{ json_encode($selectedLog->old_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}
{{ json_encode($selectedLog->new_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}