Skip to main content

Overview

Audit Logs provide a comprehensive, searchable record of all administrative actions and system events, ensuring transparency, accountability, and compliance.
All admin actions are automatically logged with timestamps, user IDs, IP addresses, and detailed descriptions.

Audit Logs Screen

Access the audit log viewer from the admin panel:
admin/presentation/screens/audit_logs_screen.dart
class AuditLogsScreen extends StatefulWidget {
  final int adminId;

  const AuditLogsScreen({
    super.key,
    required this.adminId,
  });
}

Log Entry Structure

class AuditLog {
  final int id;
  final int? adminId;
  final int? usuarioAfectadoId;
  final String accion;
  final String? descripcion;
  final String? ipAddress;
  final String? userAgent;
  final DateTime fechaCreacion;
  final Map<String, dynamic>? metadata;
}

Database Schema

CREATE TABLE audit_logs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  admin_id INT,
  usuario_afectado_id INT,
  accion VARCHAR(100) NOT NULL,
  descripcion TEXT,
  ip_address VARCHAR(45),
  user_agent VARCHAR(255),
  metadata JSON,
  fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (admin_id) REFERENCES usuarios(id),
  FOREIGN KEY (usuario_afectado_id) REFERENCES usuarios(id),
  INDEX idx_admin_id (admin_id),
  INDEX idx_accion (accion),
  INDEX idx_fecha_creacion (fecha_creacion)
);

Logged Actions

Actions:
  • admin_login - Admin logged in
  • admin_logout - Admin logged out
  • failed_login - Failed login attempt
  • session_expired - Session timeout
Icon: Login/LogoutColor: Green (#11998e)

Audit Log Screen Implementation

Search Functionality

admin/presentation/screens/audit_logs_screen.dart
Widget _buildSearchBar() {
  return Container(
    decoration: BoxDecoration(
      color: const Color(0xFF1A1A1A).withValues(alpha: 0.8),
      borderRadius: BorderRadius.circular(20),
      border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
    ),
    child: TextField(
      controller: _searchController,
      style: TextStyle(color: Colors.white),
      decoration: InputDecoration(
        hintText: 'Buscar logs...',
        prefixIcon: Icon(Icons.search_rounded),
        suffixIcon: _searchController.text.isNotEmpty
            ? IconButton(
                icon: Icon(Icons.clear),
                onPressed: () {
                  _searchController.clear();
                  _loadLogs();
                },
              )
            : null,
        border: InputBorder.none,
      ),
      onSubmitted: (_) => _loadLogs(),
    ),
  );
}

Filter Chips

admin/presentation/screens/audit_logs_screen.dart
Widget _buildFilterChips() {
  return SingleChildScrollView(
    scrollDirection: Axis.horizontal,
    child: Row(
      children: [
        _buildFilterChip('Todos', null, Icons.all_inclusive_rounded),
        _buildFilterChip('Login', 'login', Icons.login_rounded),
        _buildFilterChip('Crear', 'crear', Icons.add_circle_outline_rounded),
        _buildFilterChip('Actualizar', 'actualizar', Icons.edit_rounded),
        _buildFilterChip('Eliminar', 'eliminar', Icons.delete_outline_rounded),
      ],
    ),
  );
}

Widget _buildFilterChip(String label, String? value, IconData icon) {
  final isSelected = _selectedFilter == value;
  return GestureDetector(
    onTap: () {
      setState(() => _selectedFilter = value);
      _loadLogs();
    },
    child: Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        color: isSelected 
            ? const Color(0xFF667eea).withValues(alpha: 0.3)
            : const Color(0xFF1A1A1A).withValues(alpha: 0.8),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: isSelected 
              ? const Color(0xFF667eea)
              : Colors.white.withValues(alpha: 0.1),
        ),
      ),
      child: Row(
        children: [
          Icon(icon, size: 18),
          const SizedBox(width: 6),
          Text(label),
        ],
      ),
    ),
  );
}

Log Card Display

admin/presentation/screens/audit_logs_screen.dart
Widget _buildLogCard(Map<String, dynamic> log) {
  final accion = log['accion'] ?? '';
  final descripcion = log['descripcion'] ?? '';
  final usuario = '${log['nombre'] ?? ''} ${log['apellido'] ?? ''}'.trim();
  final email = log['email'] ?? '';
  final fecha = _formatDate(log['fecha_creacion']);
  final actionColor = _getActionColor(accion);
  final actionIcon = _getActionIcon(accion);

  return ClipRRect(
    borderRadius: BorderRadius.circular(20),
    child: BackdropFilter(
      filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
      child: Container(
        decoration: BoxDecoration(
          color: const Color(0xFF1A1A1A).withValues(alpha: 0.8),
          borderRadius: BorderRadius.circular(20),
          border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
        ),
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            borderRadius: BorderRadius.circular(20),
            onTap: () => _showLogDetails(log),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  Row(
                    children: [
                      Container(
                        padding: const EdgeInsets.all(10),
                        decoration: BoxDecoration(
                          color: actionColor.withValues(alpha: 0.2),
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Icon(actionIcon, color: actionColor, size: 20),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              accion.toUpperCase(),
                              style: TextStyle(
                                color: actionColor,
                                fontSize: 12,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            Text(
                              descripcion,
                              style: TextStyle(fontSize: 14),
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                  if (usuario.isNotEmpty || email.isNotEmpty)
                    _buildUserInfo(usuario, email),
                ],
              ),
            ),
          ),
        ),
      ),
    ),
  );
}

Action Color Coding

admin/presentation/screens/audit_logs_screen.dart
Color _getActionColor(String accion) {
  final lower = accion.toLowerCase();
  if (lower.contains('login') || lower.contains('acceso')) {
    return const Color(0xFF11998e); // Green
  }
  if (lower.contains('crear') || lower.contains('registro')) {
    return const Color(0xFF667eea); // Blue
  }
  if (lower.contains('actualizar') || lower.contains('editar')) {
    return AppColors.primary; // Yellow
  }
  if (lower.contains('eliminar') || lower.contains('desactivar')) {
    return const Color(0xFFf5576c); // Red
  }
  return const Color(0xFF667eea); // Default blue
}

IconData _getActionIcon(String accion) {
  final lower = accion.toLowerCase();
  if (lower.contains('login') || lower.contains('acceso')) {
    return Icons.login_rounded;
  }
  if (lower.contains('crear') || lower.contains('registro')) {
    return Icons.add_circle_rounded;
  }
  if (lower.contains('actualizar') || lower.contains('editar')) {
    return Icons.edit_rounded;
  }
  if (lower.contains('eliminar') || lower.contains('desactivar')) {
    return Icons.delete_rounded;
  }
  return Icons.info_rounded;
}

Log Details Modal

admin/presentation/screens/audit_logs_screen.dart
void _showLogDetails(Map<String, dynamic> log) {
  showModalBottomSheet(
    context: context,
    backgroundColor: Colors.transparent,
    isScrollControlled: true,
    builder: (context) => ClipRRect(
      borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
        child: Container(
          padding: const EdgeInsets.all(24),
          decoration: BoxDecoration(
            color: const Color(0xFF1A1A1A).withValues(alpha: 0.95),
            borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              _buildDetailRow('Acción', log['accion']),
              _buildDetailRow('Descripción', log['descripcion']),
              _buildDetailRow('Usuario', '${log['nombre']} ${log['apellido']}'),
              _buildDetailRow('Email', log['email']),
              _buildDetailRow('IP', log['ip_address']),
              _buildDetailRow('User Agent', log['user_agent']),
              _buildDetailRow('Fecha', _formatFullDate(log['fecha_creacion'])),
            ],
          ),
        ),
      ),
    ),
  );
}

Widget _buildDetailRow(String label, String? value) {
  if (value == null || value.isEmpty) return const SizedBox.shrink();
  
  return Padding(
    padding: const EdgeInsets.only(bottom: 16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: TextStyle(fontSize: 12, color: Colors.white60)),
        const SizedBox(height: 6),
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: Colors.white.withValues(alpha: 0.05),
            borderRadius: BorderRadius.circular(10),
          ),
          child: Text(value, style: TextStyle(fontSize: 14)),
        ),
      ],
    ),
  );
}

Time Formatting

admin/presentation/screens/audit_logs_screen.dart
String _formatDate(String? dateStr) {
  if (dateStr == null) return '';
  try {
    final date = DateTime.parse(dateStr);
    final now = DateTime.now();
    final diff = now.difference(date);

    if (diff.inMinutes < 1) return 'Ahora';
    if (diff.inMinutes < 60) return 'Hace ${diff.inMinutes}m';
    if (diff.inHours < 24) return 'Hace ${diff.inHours}h';
    if (diff.inDays < 7) return 'Hace ${diff.inDays}d';
    
    return DateFormat('dd/MM/yy').format(date);
  } catch (e) {
    return dateStr;
  }
}

String _formatFullDate(String? dateStr) {
  if (dateStr == null) return '';
  try {
    final date = DateTime.parse(dateStr);
    return DateFormat('dd/MM/yyyy HH:mm:ss').format(date);
  } catch (e) {
    return dateStr;
  }
}

Creating Audit Log Entries

PHP Backend Implementation

backend/utils/audit_logger.php
function logAuditAction(
  $pdo,
  $adminId,
  $accion,
  $descripcion,
  $usuarioAfectadoId = null,
  $metadata = null
) {
  $ipAddress = $_SERVER['REMOTE_ADDR'] ?? null;
  $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
  
  $stmt = $pdo->prepare("
    INSERT INTO audit_logs (
      admin_id,
      usuario_afectado_id,
      accion,
      descripcion,
      ip_address,
      user_agent,
      metadata
    ) VALUES (?, ?, ?, ?, ?, ?, ?)
  ");
  
  $stmt->execute([
    $adminId,
    $usuarioAfectadoId,
    $accion,
    $descripcion,
    $ipAddress,
    $userAgent,
    json_encode($metadata)
  ]);
}

Usage Example

// When approving a driver
logAuditAction(
  $pdo,
  $adminId,
  'approve_driver',
  "Conductor aprobado: {$conductor['nombre']} (ID: {$conductorId})",
  $conductorId,
  ['previous_status' => 'pendiente', 'new_status' => 'aprobado']
);

// When rejecting a driver
logAuditAction(
  $pdo,
  $adminId,
  'reject_driver',
  "Conductor rechazado: {$conductor['nombre']} - Motivo: {$motivo}",
  $conductorId,
  ['motivo' => $motivo, 'previous_status' => 'pendiente']
);

Retrieving Audit Logs

Backend API Endpoint

backend/admin/get_audit_logs.php
// GET /admin/get_audit_logs.php?admin_id=1&page=1&per_page=50&filter=login

$stmt = $pdo->prepare("
  SELECT 
    al.*,
    u.nombre,
    u.apellido,
    u.email
  FROM audit_logs al
  LEFT JOIN usuarios u ON al.usuario_afectado_id = u.id
  WHERE al.admin_id = ?
    AND (? IS NULL OR al.accion LIKE ?)
  ORDER BY al.fecha_creacion DESC
  LIMIT ? OFFSET ?
");

$filter = $_GET['filter'] ?? null;
$filterParam = $filter ? "%{$filter}%" : null;
$limit = intval($_GET['per_page'] ?? 50);
$offset = (intval($_GET['page'] ?? 1) - 1) * $limit;

$stmt->execute([$adminId, $filterParam, $filterParam, $limit, $offset]);

Export Audit Logs

Export functionality is planned for future releases. Will support CSV and PDF formats for compliance reporting.

Retention Policy

Define how long to keep audit logs:
-- Delete logs older than 1 year
DELETE FROM audit_logs 
WHERE fecha_creacion < DATE_SUB(NOW(), INTERVAL 1 YEAR);

-- Archive old logs to separate table
INSERT INTO audit_logs_archive
SELECT * FROM audit_logs
WHERE fecha_creacion < DATE_SUB(NOW(), INTERVAL 6 MONTH);

DELETE FROM audit_logs
WHERE fecha_creacion < DATE_SUB(NOW(), INTERVAL 6 MONTH);

Best Practices

Ensure every administrative action is logged for complete audit trail.
Log not just what action was taken, but why and by whom, with before/after states.
Audit logs should be write-only for admins - no deletion or modification allowed.
Periodically review audit logs to identify suspicious patterns or security issues.
Maintain logs for required retention period for regulatory compliance.

Dashboard

View recent activity summary

User Management

User change tracking

Driver Management

Driver approval history

Build docs developers (and LLMs) love