Version: 1.0
Last updated: December 10, 2025
Activity Logs is the core administrative module for tracking, auditing, and viewing all user actions and system events in the DTR System. Administrators can:
Key files involved:
resources/views/users/admin/activity-logs/ (index)App\Http\Controllers\Web\Admin\ActivityLogController.php (expected)Spatie\ActivityLog\Models\Activity (via spatie/laravel-activity)activity_log tablePurpose:
Scope:
Out of scope:
Required permissions:
view_activity_logs — view all activity logsview_own_activity_logs — view own activity only (for employees)export_activity_logs — export logs to CSV/Excelview_audit_reports — generate compliance reportsRecommended roles:
Implementation note:
Admin URLs:
/admin/activity-logs/admin/activity-logs?search={query}&filter={type}/admin/activity-logs?from={date}&to={date}/admin/activity-logs?user_id={id}/admin/activity-logs/{id} (modal or details page)/admin/activity-logs/export?format=csvBrowser requirements: Modern Chromium-based browsers, recent Safari/Firefox. Mobile viewing supported; recommend desktop for detailed audit analysis.
Purpose: Display all activity logs with filtering, search and export capabilities.
View file: resources/views/users/admin/activity-logs/index.blade.php
UI elements:
Top filters & search:
Date range picker:
Search bar: global search across log_name, description, model name
Filter dropdowns:
Quick actions:
Activity logs table:
Columns:
Pagination: 25 logs per page (configurable)
Sorting: by timestamp (newest first default), user, action, model
Key features:
Controller method (expected):
public function index(Request $request)
{
$query = Activity::query();
// Date range filter
if ($request->from) {
$query->where('created_at', '>=', $request->from);
}
if ($request->to) {
$query->where('created_at', '<=', $request->to);
}
// User filter
if ($request->user_id) {
$query->where('causer_id', $request->user_id);
}
// Action filter
if ($request->log_name) {
$query->where('log_name', $request->log_name);
}
// Model filter
if ($request->model_type) {
$query->where('subject_type', $request->model_type);
}
// Global search
if ($request->search) {
$query->where(function ($q) use ($request) {
$q->where('log_name', 'like', "%{$request->search}%")
->orWhere('description', 'like', "%{$request->search}%")
->orWhere('causer_type', 'like', "%{$request->search}%");
});
}
$logs = $query->latest()->paginate(25);
return view('users.admin.activity-logs.index', compact('logs'));
}
Purpose: View complete information about a specific activity log entry.
Trigger: Click "Details" or log row in index view.
Display elements:
Header:
Action section:
Subject section (what was affected):
Changes section (for update actions):
Example update log:
Field Before After
name "John Doe" "John Smith"
status "active" "suspended"
email "john@old.com" "john@new.com"
Associated logs section:
Actions:
Date range filters:
User filters:
Action filters:
Model filters:
Status filters:
Advanced filters:
Activity Log table fields (Spatie Laravel-Activity):
id — Primary key
log_name — Category/name (string, e.g., 'default', 'user_management')
description — Human-readable description (string, max 255)
subject_type — Affected model class (string, e.g., 'App\Models\User')
subject_id — Affected model ID (nullable)
causer_type — User model class (string, e.g., 'App\Models\User')
causer_id — User ID who performed action (nullable)
properties — JSON data:
{
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"method": "POST",
"url": "/admin/users",
"old": { # Before values (for updates/deletes)
"name": "John Doe",
"status": "active"
},
"attributes": { # After values (for creates/updates)
"name": "Jane Doe",
"status": "suspended"
},
"changes": { # What changed (diff)
"name": ["John Doe", "Jane Doe"],
"status": ["active", "suspended"]
},
"status": "success", # Action result status
"reason": "User suspended for inactivity"
}
created_at — Timestamp when logged
updated_at — Timestamp (usually same as created_at, immutable)
Sample records:
Activity Log 1 (User Creation):
id: 1001
log_name: "default"
description: "User created"
subject_type: "App\Models\User"
subject_id: 42
causer_type: "App\Models\User"
causer_id: 1 (Admin Jane Doe)
properties: {
"ip_address": "203.0.113.45",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"attributes": {
"first_name": "Alice",
"last_name": "Johnson",
"email": "alice@company.com",
"status": "active"
},
"status": "success"
}
created_at: "2025-01-15 09:30:45"
Activity Log 2 (User Status Change):
id: 1002
log_name: "default"
description: "User suspended"
subject_type: "App\Models\User"
subject_id: 42 (Alice Johnson)
causer_type: "App\Models\User"
causer_id: 1 (Admin Jane Doe)
properties: {
"ip_address": "203.0.113.46",
"old": {
"status": "active",
"suspended_at": null
},
"attributes": {
"status": "suspended",
"suspended_at": "2025-01-20 14:22:00"
},
"changes": {
"status": ["active", "suspended"],
"suspended_at": [null, "2025-01-20 14:22:00"]
},
"reason": "Account compromised, temporary suspension pending verification",
"status": "success"
}
created_at: "2025-01-20 14:22:30"
Activity Log 3 (Failed Login):
id: 1003
log_name: "login"
description: "Failed login attempt"
subject_type: null
subject_id: null
causer_type: null
causer_id: null
properties: {
"ip_address": "198.51.100.100",
"email": "alice@company.com",
"reason": "Invalid password",
"user_agent": "Mozilla/5.0...",
"status": "failed",
"attempt": 2
}
created_at: "2025-01-20 14:20:00"
User Management Events:
user_created — new user account createduser_updated — user profile/info modifieduser_deleted — user account deleted (soft or hard)user_suspended — user account suspendeduser_reactivated — user account reactivatedpassword_reset — password changed (by self or admin)2fa_enabled — two-factor authentication enabled2fa_disabled — two-factor authentication disabledavatar_uploaded — profile photo changedprofile_updated — profile data changed (phone, timezone, etc.)Role & Permission Events:
role_assigned — role given to userrole_removed — role removed from userrole_created — new role createdrole_updated — role modifiedrole_deleted — role deletedpermission_granted — permission added to rolepermission_revoked — permission removed from roleAuthentication Events:
login_success — successful user loginlogin_failed — failed login attemptlogout — user logged outsession_created — new session startedsession_destroyed — session endedtoken_generated — API token createdtoken_revoked — API token revoked2fa_verified — 2FA challenge passedShift Management Events:
shift_created — new shift createdshift_updated — shift modifiedshift_deleted — shift deletedshift_assigned — user assigned to shiftshift_removed — user removed from shiftData Access Events:
data_exported — data exported (CSV, Excel, PDF)data_imported — data imported from filereport_generated — report createdbulk_action — bulk operation (bulk assign, bulk delete, etc.)System Events:
settings_updated — system settings changedconfig_changed — configuration modifiedbackup_created — database backup createdsync_executed — external system sync (payroll, DTR, etc.)job_executed — background job ranerror_logged — system error occurredComplete log entry example (JSON):
{
"id": 1001,
"log_name": "default",
"description": "User suspended",
"subject_type": "App\Models\User",
"subject_id": 42,
"causer_type": "App\Models\User",
"causer_id": 1,
"properties": {
"ip_address": "203.0.113.46",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"method": "POST",
"url": "/admin/users/42/suspend",
"old": {
"status": "active",
"updated_at": "2025-01-15 10:00:00"
},
"attributes": {
"status": "suspended",
"suspended_at": "2025-01-20 14:22:00",
"updated_at": "2025-01-20 14:22:30"
},
"changes": {
"status": ["active", "suspended"],
"suspended_at": [null, "2025-01-20 14:22:00"]
},
"reason": "Account compromised, temporary suspension pending verification",
"status": "success"
},
"created_at": "2025-01-20 14:22:30",
"updated_at": "2025-01-20 14:22:30"
}
Key concepts:
GET /api/admin/activity-logs
page, per_page, search, log_name, model_type, causer_id, from, to, sort{
"data": [
{
"id": 1001,
"log_name": "default",
"description": "User suspended",
"subject_type": "App\Models\User",
"subject_id": 42,
"causer": {
"id": 1,
"name": "Jane Doe",
"email": "jane@company.com"
},
"properties": { ... },
"created_at": "2025-01-20T14:22:30Z"
}
],
"pagination": { "total": 5000, "per_page": 25, "current_page": 1 }
}GET /api/admin/activity-logs/{id}
{
"id": 1001,
"log_name": "default",
"description": "User suspended",
"subject_type": "App\Models\User",
"subject_id": 42,
"subject": {
"id": 42,
"name": "Alice Johnson",
"email": "alice@company.com"
},
"causer_type": "App\Models\User",
"causer_id": 1,
"causer": {
"id": 1,
"name": "Jane Doe",
"email": "jane@company.com",
"roles": ["admin"]
},
"properties": { ... },
"created_at": "2025-01-20T14:22:30Z"
}GET /api/admin/activity-logs/user/{userId}
page, from, toGET /api/admin/activity-logs/model/{modelType}/{modelId}
GET /api/admin/activity-logs/export
format (csv, excel, pdf), log_name, model_type, causer_id, from, toPOST /api/admin/activity-logs/report
{
"report_type": "user_activity|system_overview|security|compliance",
"from": "2025-01-01",
"to": "2025-01-31",
"group_by": "user|day|action",
"include_failed": true
}Location: app/Http/Controllers/Web/Admin/ActivityLogController.php (expected)
index() — List all activity logs
public function index(Request $request)
{
$query = Activity::query();
// Date range filter
if ($request->from) {
$query->where('created_at', '>=', \Carbon\Carbon::parse($request->from)->startOfDay());
}
if ($request->to) {
$query->where('created_at', '<=', \Carbon\Carbon::parse($request->to)->endOfDay());
}
// User (causer) filter
if ($request->causer_id) {
$query->where('causer_id', $request->causer_id);
}
// Action (log_name) filter
if ($request->log_name) {
$query->where('log_name', $request->log_name);
}
// Model type filter
if ($request->model_type) {
$query->where('subject_type', $request->model_type);
}
// Global search
if ($request->search) {
$query->where(function ($q) use ($request) {
$q->where('description', 'like', "%{$request->search}%")
->orWhereHas('causer', function ($q) use ($request) {
$q->where('name', 'like', "%{$request->search}%");
});
});
}
// Sort
$sort = $request->sort ?? 'created_at';
$direction = $request->direction ?? 'desc';
$query->orderBy($sort, $direction);
$logs = $query->with('causer')->paginate(25);
return view('users.admin.activity-logs.index', compact('logs'));
}
show() — View single log details
public function show(string $id)
{
$log = Activity::with('causer', 'subject')->findOrFail($id);
return view('users.admin.activity-logs.show', compact('log'));
}
export() — Export logs to file
public function export(Request $request)
{
$query = Activity::query();
// Apply same filters as index()
if ($request->from) {
$query->where('created_at', '>=', \Carbon\Carbon::parse($request->from));
}
if ($request->to) {
$query->where('created_at', '<=', \Carbon\Carbon::parse($request->to));
}
// ... more filters
$logs = $query->get();
$format = $request->format ?? 'csv';
if ($format === 'csv') {
return $this->exportCsv($logs);
} elseif ($format === 'excel') {
return $this->exportExcel($logs);
} elseif ($format === 'pdf') {
return $this->exportPdf($logs);
}
}
private function exportCsv($logs)
{
$filename = 'activity-logs-' . now()->format('Y-m-d-Hi') . '.csv';
$headers = [
'Content-Type' => 'text/csv',
'Content-Disposition' => "attachment; filename=\"$filename\"",
];
$callback = function () use ($logs) {
$file = fopen('php://output', 'w');
fputcsv($file, ['ID', 'Timestamp', 'User', 'Action', 'Model', 'Status', 'Description']);
foreach ($logs as $log) {
fputcsv($file, [
$log->id,
$log->created_at->format('Y-m-d H:i:s'),
$log->causer ? $log->causer->name : 'System',
$log->log_name,
class_basename($log->subject_type),
$log->properties['status'] ?? 'success',
$log->description,
]);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
report() — Generate activity report
public function report(Request $request)
{
$request->validate([
'report_type' => 'required|in:user_activity,system_overview,security,compliance',
'from' => 'required|date',
'to' => 'required|date|after:from',
'group_by' => 'nullable|in:user,day,action',
]);
$query = Activity::whereBetween('created_at', [
$request->from,
$request->to,
]);
$reportType = $request->report_type;
if ($reportType === 'user_activity') {
return $this->userActivityReport($query);
} elseif ($reportType === 'security') {
return $this->securityReport($query);
} elseif ($reportType === 'compliance') {
return $this->complianceReport($query);
}
return response()->json(['error' => 'Invalid report type'], 400);
}
private function userActivityReport($query)
{
$data = $query->groupBy('causer_id')
->selectRaw('causer_id, count(*) as total, log_name')
->with('causer')
->get();
return response()->json(['data' => $data]);
}
private function securityReport($query)
{
$data = [
'failed_logins' => $query->where('log_name', 'login_failed')->count(),
'successful_logins' => $query->where('log_name', 'login_success')->count(),
'suspicious_ips' => $query->selectRaw("JSON_EXTRACT(properties, '$.ip_address') as ip")
->groupBy('ip')
->having('count', '>', 10)
->pluck('ip'),
'role_changes' => $query->where('log_name', 'role_assigned')->orWhere('log_name', 'role_removed')->count(),
'data_exports' => $query->where('log_name', 'data_exported')->count(),
];
return response()->json(['data' => $data]);
}
Quick date filters:
Action filters:
User/Model filters:
Search capabilities:
Reports:
Export formats: CSV, Excel, PDF
User Management Integration:
Shift Management Integration:
DTR Integration:
Payroll Integration:
Leave Management Integration:
System Events:
Audit trail requirements:
Task 1: View Recent Activity
Task 2: Search for Specific Action
Task 3: Filter by User
Task 4: View Before/After Changes
Task 5: Export Logs for Audit
Task 6: Generate Security Report
Task 7: Track User Onboarding Activity
Task 8: Investigate Suspicious Activity
Compliance standards met:
Audit trail requirements:
Sensitive data handling:
Data retention policy:
Example 1: User Onboarding Audit Trail
2025-01-15 09:30:45 Admin Jane Doe user_created Alice Johnson success
2025-01-15 09:31:02 Admin Jane Doe role_assigned Alice Johnson (role: employee) success
2025-01-15 09:31:15 Alice Johnson login_success - success
2025-01-15 09:32:00 Alice Johnson profile_updated Alice Johnson success
2025-01-15 10:15:30 Admin John Smith role_assigned Alice Johnson (role: team-lead) success
Example 2: Security Incident Investigation
2025-01-20 14:15:00 Unknown IP: 198.51.100.100 login_failed - failed (invalid password)
2025-01-20 14:15:30 Unknown IP: 198.51.100.100 login_failed - failed (invalid password)
2025-01-20 14:16:00 Unknown IP: 198.51.100.100 login_failed - failed (invalid password)
2025-01-20 14:22:30 Admin Jane Doe user_suspended Alice Johnson success (reason: account compromised)
2025-01-20 15:00:00 Alice Johnson password_reset Alice Johnson success
Example 3: Role Change Tracking
Date Range: Jan 2025
User Role Assigned Date Assigned By
Alice Johnson team-lead 2025-01-15 Admin Jane Doe
Bob Smith scheduler 2025-01-17 Admin Jane Doe
Carol Davis hr-manager 2025-01-20 Admin John Smith
David Wilson team-lead 2025-01-22 Admin Jane Doe
Removals:
Eve Martinez team-lead 2025-01-25 Admin Jane Doe (reason: promotion to manager)
Frank Lee scheduler 2025-01-28 Admin John Smith (reason: role reassignment)
Example 4: Data Compliance Report
Audit Period: Q4 2024 (Oct 1 - Dec 31, 2024)
Total Activities: 15,432
- Creates: 2,145
- Updates: 11,203
- Deletes: 89
- Logins: 1,245
- Failed Logins: 34
- Role Changes: 67
- Data Exports: 23
Critical Actions:
- User Suspensions: 8
- User Deletions: 5
- Role Removals: 12
- Password Resets (forced): 3
Most Active User: Admin Jane Doe (3,456 actions)
Least Active: Manager Bob Smith (23 actions)
Peak Activity Day: Dec 15 (342 actions)
Unit tests:
Integration tests:
E2E tests (manual):
Performance tests:
Compliance tests:
Q: "Activity logs not appearing after user creation"
ActivityLogger middleware is registered in HTTP kernelapp/Models/User has LogsActivity traitactivity jobs queue is running: php artisan queue:workstorage/logs/laravel.logphp artisan activity:logs:syncQ: "Cannot find log entry for specific action"
LogsActivity configuration)view_activity_logs permissionQ: "Logs are taking too long to load"
created_at, causer_id, subject_type, subject_idphp artisan migrateQ: "Export file is empty or incomplete"
Q: "Cannot see other users' logs (permission denied)"
view_activity_logs permissionphp artisan permission:grant view_activity_logs adminQ: "Sensitive data (password, token) showing in logs"
LogsActivity::attributesToExclude() to exclude sensitive fieldsprotected static function getChangesForLogging($old, $new)
{
$exclude = ['password', 'remember_token', 'api_token'];
// Filter out excluded fields
}mask_pii optionQ: "Timezone showing incorrect time"
APP_TIMEZONE in .env (default: UTC)SQL: Find all logs for specific user (causer)
SELECT * FROM activity_log
WHERE causer_id = 42
ORDER BY created_at DESC;
SQL: Find all logs affecting specific model
SELECT * FROM activity_log
WHERE subject_type = 'App\Models\User'
AND subject_id = 42
ORDER BY created_at DESC;
SQL: Find failed login attempts (last 7 days)
SELECT * FROM activity_log
WHERE log_name = 'login_failed'
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY created_at DESC;
SQL: Count activities by type (last 30 days)
SELECT log_name, COUNT(*) as count
FROM activity_log
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY log_name
ORDER BY count DESC;
SQL: Find suspicious activity (same IP, multiple failed logins)
SELECT JSON_EXTRACT(properties, '$.ip_address') as ip_address,
COUNT(*) as attempt_count
FROM activity_log
WHERE log_name = 'login_failed'
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY ip_address
HAVING attempt_count > 5;
SQL: Create index for performance
CREATE INDEX idx_causer_id ON activity_log(causer_id);
CREATE INDEX idx_subject ON activity_log(subject_type, subject_id);
CREATE INDEX idx_log_name ON activity_log(log_name);
CREATE INDEX idx_created_at ON activity_log(created_at);
Laravel: Register activity tracking
// In AppServiceProvider or activitylog config
Activity::useLog('default');
Activity::withoutLogging(function () {
// Actions here won't be logged
});
// In Model
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class User extends Model
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['name', 'email', 'status']) // Only log these fields
->logOnlyDirty() // Only log if changed
->dontLogIfAttributesSet(['password']) // Exclude sensitive fields
->useLogName('user_management');
}
}
Laravel: Custom activity logging
// Manually log an action
activity()
->causedBy(auth()->user())
->performedOn($model)
->withProperties([
'ip_address' => request()->ip(),
'reason' => 'User requested data export',
'status' => 'success'
])
->log('data_exported');
Laravel: Query activity logs
// Get all logs for a user
$logs = Activity::whereCausedBy($user)
->latest()
->paginate(25);
// Get logs affecting a model
$logs = Activity::forSubject($model)
->latest()
->get();
// Get logs by action type
$logs = Activity::where('log_name', 'user_created')
->whereBetween('created_at', [$from, $to])
->get();
// With relationships
$logs = Activity::with('causer', 'subject')
->latest()
->paginate(25);
Blade template: Activity log table row
@foreach($logs as $log)
<tr>
<td class="text-sm">{{ $log->created_at->format('Y-m-d H:i:s') }}</td>
<td>
@if($log->causer)
<span class="badge">{{ $log->causer->name }}</span>
@else
<span class="text-muted">System</span>
@endif
</td>
<td>
<span class="badge badge-{{ $log->log_name === 'deleted' ? 'danger' : 'info' }}">
{{ $log->log_name }}
</span>
</td>
<td>{{ class_basename($log->subject_type) }}</td>
<td>{{ $log->description }}</td>
<td>
<span class="badge badge-{{ isset($log->properties['status']) && $log->properties['status'] === 'failed' ? 'warning' : 'success' }}">
{{ $log->properties['status'] ?? 'success' }}
</span>
</td>
<td>
<button class="btn btn-sm btn-info" @click="viewDetails({{ $log->id }})">View</button>
</td>
</tr>
@endforeach
Blade template: Log details modal
<!-- Activity Log Details Modal -->
<div class="modal fade" id="logDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Activity Log #<span id="logId"></span></h5>
<button type="button" class="close" data-dismiss="modal">
<span>×</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>User</h6>
<p id="logUser" class="text-muted"></p>
</div>
<div class="col-md-6">
<h6>Date/Time</h6>
<p id="logDate" class="text-muted"></p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6>Action</h6>
<p id="logAction" class="text-muted"></p>
</div>
<div class="col-md-6">
<h6>Model</h6>
<p id="logModel" class="text-muted"></p>
</div>
</div>
<hr>
<h6>Changes</h6>
<table class="table table-sm" id="changesTable">
<thead>
<tr>
<th>Field</th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody id="changesBody">
</tbody>
</table>
<hr>
<h6>Raw JSON</h6>
<pre id="logJson" class="bg-light p-3"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="copyJson()">Copy JSON</button>
</div>
</div>
</div>
</div>
Document version: 1.0
Maintainers: Security / Compliance / Engineering / Operations
Last updated: December 10, 2025