Vyuh CDX

Activity Tracking

Comprehensive activity tracking and audit trails for compliance and debugging

Activity Tracking and Audit Trails

The Vyuh Entity System includes comprehensive activity tracking capabilities that provide audit trails for all entity operations. This guide covers the activity tracking system, implementation patterns, and best practices for maintaining compliance and debugging.

Activity Tracking Overview

The activity tracking system automatically logs:

  • Entity creation, updates, and deletions
  • User actions and access patterns
  • System events and errors
  • Custom business events

Core Components

ActivityLog Model

class ActivityLog extends EntityBase {
  final String entityType;
  final String? entityId;
  final String action;
  final String userId;
  final String userName;
  final Map<String, dynamic>? oldValues;
  final Map<String, dynamic>? newValues;
  final Map<String, dynamic>? metadata;
  final String? ipAddress;
  final String? userAgent;
  final ActivityLogSeverity severity;
  final String? errorMessage;
  final String? stackTrace;
  
  ActivityLog({
    required super.id,
    required super.schemaType,
    required this.entityType,
    this.entityId,
    required this.action,
    required this.userId,
    required this.userName,
    this.oldValues,
    this.newValues,
    this.metadata,
    this.ipAddress,
    this.userAgent,
    this.severity = ActivityLogSeverity.info,
    this.errorMessage,
    this.stackTrace,
    super.createdAt,
    super.layout,
    super.modifiers,
  });
  
  factory ActivityLog.create({
    required String entityType,
    String? entityId,
    required String action,
    required String userId,
    required String userName,
    Map<String, dynamic>? oldValues,
    Map<String, dynamic>? newValues,
    Map<String, dynamic>? metadata,
  }) {
    return ActivityLog(
      id: const Uuid().v4(),
      schemaType: 'activity_logs',
      entityType: entityType,
      entityId: entityId,
      action: action,
      userId: userId,
      userName: userName,
      oldValues: oldValues,
      newValues: newValues,
      metadata: metadata,
      createdAt: DateTime.now(),
    );
  }
  
  // Convenience constructors
  factory ActivityLog.entityCreated({
    required String entityType,
    required String entityId,
    required Map<String, dynamic> entityData,
    required String userId,
    required String userName,
  }) {
    return ActivityLog.create(
      entityType: entityType,
      entityId: entityId,
      action: 'created',
      userId: userId,
      userName: userName,
      newValues: entityData,
    );
  }
  
  factory ActivityLog.entityUpdated({
    required String entityType,
    required String entityId,
    required Map<String, dynamic> oldValues,
    required Map<String, dynamic> newValues,
    required String userId,
    required String userName,
  }) {
    return ActivityLog.create(
      entityType: entityType,
      entityId: entityId,
      action: 'updated',
      userId: userId,
      userName: userName,
      oldValues: oldValues,
      newValues: newValues,
      metadata: {
        'changedFields': _getChangedFields(oldValues, newValues),
      },
    );
  }
  
  factory ActivityLog.entityDeleted({
    required String entityType,
    required String entityId,
    required Map<String, dynamic> entityData,
    required String userId,
    required String userName,
  }) {
    return ActivityLog.create(
      entityType: entityType,
      entityId: entityId,
      action: 'deleted',
      userId: userId,
      userName: userName,
      oldValues: entityData,
    );
  }
  
  static List<String> _getChangedFields(
    Map<String, dynamic> oldValues,
    Map<String, dynamic> newValues,
  ) {
    final changedFields = <String>[];
    
    for (final key in newValues.keys) {
      if (!oldValues.containsKey(key) || oldValues[key] != newValues[key]) {
        changedFields.add(key);
      }
    }
    
    return changedFields;
  }
  
  String get actionDisplay {
    switch (action) {
      case 'created':
        return 'Created';
      case 'updated':
        return 'Updated';
      case 'deleted':
        return 'Deleted';
      case 'viewed':
        return 'Viewed';
      case 'exported':
        return 'Exported';
      case 'imported':
        return 'Imported';
      case 'approved':
        return 'Approved';
      case 'rejected':
        return 'Rejected';
      default:
        return action.titleCase;
    }
  }
  
  String get description {
    final entityName = entityType.titleCase;
    
    switch (action) {
      case 'created':
        return '$userName created $entityName';
      case 'updated':
        final fields = metadata?['changedFields'] as List<dynamic>?;
        if (fields != null && fields.isNotEmpty) {
          return '$userName updated ${fields.join(", ")} in $entityName';
        }
        return '$userName updated $entityName';
      case 'deleted':
        return '$userName deleted $entityName';
      default:
        return '$userName $action $entityName';
    }
  }
  
  factory ActivityLog.fromJson(Map<String, dynamic> json) => 
    _$ActivityLogFromJson(json);
    
  @override
  Map<String, dynamic> toJson() => _$ActivityLogToJson(this);
}

enum ActivityLogSeverity {
  debug,
  info,
  warning,
  error,
  critical,
}

ActivityApi

class ActivityApi extends EntityApi<ActivityLog> {
  ActivityApi({required super.client});
  
  @override
  ActivityLog fromJson(Map<String, dynamic> json) => 
    ActivityLog.fromJson(json);
  
  Future<List<ActivityLog>> getEntityHistory(
    String entityType,
    String entityId, {
    int? limit,
    DateTime? startDate,
    DateTime? endDate,
  }) async {
    final response = await client.get(
      '/api/v1/activity-logs',
      queryParameters: {
        'entityType': entityType,
        'entityId': entityId,
        if (limit != null) 'limit': limit.toString(),
        if (startDate != null) 'startDate': startDate.toIso8601String(),
        if (endDate != null) 'endDate': endDate.toIso8601String(),
        'sortBy': 'createdAt',
        'sortOrder': 'desc',
      },
    );
    
    final List<dynamic> data = response.data['data'];
    return data.map((json) => fromJson(json)).toList();
  }
  
  Future<List<ActivityLog>> getUserActivity(
    String userId, {
    int? limit,
    DateTime? startDate,
    DateTime? endDate,
  }) async {
    final response = await client.get(
      '/api/v1/activity-logs',
      queryParameters: {
        'userId': userId,
        if (limit != null) 'limit': limit.toString(),
        if (startDate != null) 'startDate': startDate.toIso8601String(),
        if (endDate != null) 'endDate': endDate.toIso8601String(),
        'sortBy': 'createdAt',
        'sortOrder': 'desc',
      },
    );
    
    final List<dynamic> data = response.data['data'];
    return data.map((json) => fromJson(json)).toList();
  }
  
  Future<Map<String, dynamic>> getActivityStats({
    DateTime? startDate,
    DateTime? endDate,
  }) async {
    final response = await client.get(
      '/api/v1/activity-logs/stats',
      queryParameters: {
        if (startDate != null) 'startDate': startDate.toIso8601String(),
        if (endDate != null) 'endDate': endDate.toIso8601String(),
      },
    );
    
    return response.data;
  }
  
  Future<void> logActivity(ActivityLog activity) async {
    await createNew(activity);
  }
  
  Future<void> logBatch(List<ActivityLog> activities) async {
    await client.post(
      '/api/v1/activity-logs/batch',
      data: {
        'activities': activities.map((a) => a.toJson()).toList(),
      },
    );
  }
}

Activity Tracking Service

ActivityTrackingService

class ActivityTrackingService {
  final ActivityApi api;
  final AuthService authService;
  final Queue<ActivityLog> _queue = Queue();
  Timer? _flushTimer;
  bool _isProcessing = false;
  
  ActivityTrackingService({
    required this.api,
    required this.authService,
  }) {
    // Start periodic flush
    _flushTimer = Timer.periodic(
      const Duration(seconds: 5),
      (_) => _flushQueue(),
    );
  }
  
  Future<void> trackEntityCreated<T extends EntityBase>(T entity) async {
    final user = await authService.getCurrentUser();
    if (user == null) return;
    
    final activity = ActivityLog.entityCreated(
      entityType: T.toString(),
      entityId: entity.id,
      entityData: entity.toJson(),
      userId: user.id,
      userName: user.name,
    );
    
    _enqueue(activity);
  }
  
  Future<void> trackEntityUpdated<T extends EntityBase>(
    T oldEntity,
    T newEntity,
  ) async {
    final user = await authService.getCurrentUser();
    if (user == null) return;
    
    final activity = ActivityLog.entityUpdated(
      entityType: T.toString(),
      entityId: newEntity.id,
      oldValues: oldEntity.toJson(),
      newValues: newEntity.toJson(),
      userId: user.id,
      userName: user.name,
    );
    
    _enqueue(activity);
  }
  
  Future<void> trackEntityDeleted<T extends EntityBase>(T entity) async {
    final user = await authService.getCurrentUser();
    if (user == null) return;
    
    final activity = ActivityLog.entityDeleted(
      entityType: T.toString(),
      entityId: entity.id,
      entityData: entity.toJson(),
      userId: user.id,
      userName: user.name,
    );
    
    _enqueue(activity);
  }
  
  Future<void> trackCustomAction({
    required String entityType,
    String? entityId,
    required String action,
    Map<String, dynamic>? metadata,
  }) async {
    final user = await authService.getCurrentUser();
    if (user == null) return;
    
    final activity = ActivityLog.create(
      entityType: entityType,
      entityId: entityId,
      action: action,
      userId: user.id,
      userName: user.name,
      metadata: metadata,
    );
    
    _enqueue(activity);
  }
  
  Future<void> trackError({
    required String entityType,
    String? entityId,
    required String action,
    required String error,
    String? stackTrace,
  }) async {
    final user = await authService.getCurrentUser();
    
    final activity = ActivityLog(
      id: const Uuid().v4(),
      schemaType: 'activity_logs',
      entityType: entityType,
      entityId: entityId,
      action: action,
      userId: user?.id ?? 'system',
      userName: user?.name ?? 'System',
      severity: ActivityLogSeverity.error,
      errorMessage: error,
      stackTrace: stackTrace,
      createdAt: DateTime.now(),
    );
    
    // Errors should be logged immediately
    try {
      await api.logActivity(activity);
    } catch (e) {
      // Log to console if API fails
      debugPrint('Failed to log error activity: $e');
      _enqueue(activity); // Try again later
    }
  }
  
  void _enqueue(ActivityLog activity) {
    _queue.add(activity);
    
    // Flush immediately if queue is getting large
    if (_queue.length >= 50) {
      _flushQueue();
    }
  }
  
  Future<void> _flushQueue() async {
    if (_isProcessing || _queue.isEmpty) return;
    
    _isProcessing = true;
    final batch = <ActivityLog>[];
    
    // Take up to 100 items from queue
    while (_queue.isNotEmpty && batch.length < 100) {
      batch.add(_queue.removeFirst());
    }
    
    try {
      await api.logBatch(batch);
    } catch (e) {
      // Put items back in queue on failure
      for (final activity in batch.reversed) {
        _queue.addFirst(activity);
      }
      debugPrint('Failed to flush activity queue: $e');
    } finally {
      _isProcessing = false;
    }
  }
  
  void dispose() {
    _flushTimer?.cancel();
    // Final flush
    _flushQueue();
  }
}

Automatic Activity Tracking

class TrackedEntityApi<T extends EntityBase> extends EntityApi<T> {
  final ActivityTrackingService trackingService;
  
  TrackedEntityApi({
    required super.client,
    required this.trackingService,
  });
  
  @override
  Future<T> performCreate(T entity) async {
    final created = await super.performCreate(entity);
    await trackingService.trackEntityCreated(created);
    return created;
  }
  
  @override
  Future<T> performUpdate(String id, T entity) async {
    // Get old entity for comparison
    final oldEntity = await performGetById(id);
    if (oldEntity == null) {
      throw NotFoundException('Entity not found: $id');
    }
    
    final updated = await super.performUpdate(id, entity);
    await trackingService.trackEntityUpdated(oldEntity, updated);
    return updated;
  }
  
  @override
  Future<void> performDelete(String id) async {
    // Get entity before deletion
    final entity = await performGetById(id);
    if (entity != null) {
      await trackingService.trackEntityDeleted(entity);
    }
    
    await super.performDelete(id);
  }
}

Activity UI Components

Activity Timeline

class ActivityTimeline extends StatelessWidget {
  final String? entityType;
  final String? entityId;
  final String? userId;
  
  const ActivityTimeline({
    this.entityType,
    this.entityId,
    this.userId,
  });
  
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<ActivityLog>>(
      future: _loadActivities(context),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }
        
        final activities = snapshot.data!;
        if (activities.isEmpty) {
          return const EmptyState(
            icon: Icons.history,
            title: 'No Activity',
            subtitle: 'No activity has been recorded yet',
          );
        }
        
        // Group activities by date
        final groupedActivities = groupBy(
          activities,
          (activity) => DateFormat('yyyy-MM-dd').format(activity.createdAt!),
        );
        
        return ListView.builder(
          itemCount: groupedActivities.length,
          itemBuilder: (context, index) {
            final date = groupedActivities.keys.elementAt(index);
            final dayActivities = groupedActivities[date]!;
            
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // Date header
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Text(
                    _formatDateHeader(date),
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(
                      color: Theme.of(context).textTheme.bodySmall?.color,
                    ),
                  ),
                ),
                // Activities for this date
                ...dayActivities.map((activity) => ActivityTimelineItem(
                  activity: activity,
                  onTap: () => _showActivityDetails(context, activity),
                )),
              ],
            );
          },
        );
      },
    );
  }
  
  Future<List<ActivityLog>> _loadActivities(BuildContext context) async {
    final api = context.read<ActivityApi>();
    
    if (entityId != null && entityType != null) {
      return api.getEntityHistory(entityType!, entityId!);
    } else if (userId != null) {
      return api.getUserActivity(userId!);
    } else {
      return api.list(limit: 100);
    }
  }
  
  String _formatDateHeader(String date) {
    final parsedDate = DateTime.parse(date);
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final yesterday = today.subtract(const Duration(days: 1));
    
    if (parsedDate.isAtSameMomentAs(today)) {
      return 'Today';
    } else if (parsedDate.isAtSameMomentAs(yesterday)) {
      return 'Yesterday';
    } else {
      return DateFormat('MMMM d, y').format(parsedDate);
    }
  }
  
  void _showActivityDetails(BuildContext context, ActivityLog activity) {
    showDialog(
      context: context,
      builder: (_) => ActivityDetailsDialog(activity: activity),
    );
  }
}

class ActivityTimelineItem extends StatelessWidget {
  final ActivityLog activity;
  final VoidCallback? onTap;
  
  const ActivityTimelineItem({
    required this.activity,
    this.onTap,
  });
  
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Time
            SizedBox(
              width: 60,
              child: Text(
                DateFormat('HH:mm').format(activity.createdAt!),
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ),
            // Icon
            Container(
              width: 32,
              height: 32,
              decoration: BoxDecoration(
                color: _getColorForAction(activity.action).withOpacity(0.1),
                shape: BoxShape.circle,
              ),
              child: Icon(
                _getIconForAction(activity.action),
                size: 16,
                color: _getColorForAction(activity.action),
              ),
            ),
            const SizedBox(width: 12),
            // Content
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    activity.description,
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                  if (activity.metadata != null && 
                      activity.metadata!.isNotEmpty)
                    Padding(
                      padding: const EdgeInsets.only(top: 4),
                      child: Text(
                        _getMetadataDisplay(activity),
                        style: Theme.of(context).textTheme.bodySmall,
                      ),
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
  
  IconData _getIconForAction(String action) {
    switch (action) {
      case 'created':
        return Icons.add_circle_outline;
      case 'updated':
        return Icons.edit_outlined;
      case 'deleted':
        return Icons.delete_outline;
      case 'viewed':
        return Icons.visibility_outlined;
      case 'exported':
        return Icons.download_outlined;
      case 'imported':
        return Icons.upload_outlined;
      default:
        return Icons.circle_outlined;
    }
  }
  
  Color _getColorForAction(String action) {
    switch (action) {
      case 'created':
        return Colors.green;
      case 'updated':
        return Colors.blue;
      case 'deleted':
        return Colors.red;
      default:
        return Colors.grey;
    }
  }
  
  String _getMetadataDisplay(ActivityLog activity) {
    final metadata = activity.metadata!;
    final parts = <String>[];
    
    if (metadata['changedFields'] != null) {
      final fields = (metadata['changedFields'] as List).join(', ');
      parts.add('Changed: $fields');
    }
    
    if (metadata['reason'] != null) {
      parts.add('Reason: ${metadata['reason']}');
    }
    
    return parts.join(' • ');
  }
}

Activity Details Dialog

class ActivityDetailsDialog extends StatelessWidget {
  final ActivityLog activity;
  
  const ActivityDetailsDialog({required this.activity});
  
  @override
  Widget build(BuildContext context) {
    return Dialog(
      child: Container(
        width: 600,
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header
            Row(
              children: [
                Icon(
                  _getIconForAction(activity.action),
                  color: _getColorForAction(activity.action),
                ),
                const SizedBox(width: 8),
                Text(
                  activity.actionDisplay,
                  style: Theme.of(context).textTheme.titleLarge,
                ),
                const Spacer(),
                IconButton(
                  icon: const Icon(Icons.close),
                  onPressed: () => Navigator.of(context).pop(),
                ),
              ],
            ),
            const Divider(height: 32),
            
            // Details
            _buildDetailRow('User', activity.userName),
            _buildDetailRow('Entity Type', activity.entityType.titleCase),
            if (activity.entityId != null)
              _buildDetailRow('Entity ID', activity.entityId!),
            _buildDetailRow(
              'Timestamp',
              DateFormat('MMM d, y HH:mm:ss').format(activity.createdAt!),
            ),
            
            // Changed fields
            if (activity.oldValues != null && activity.newValues != null) ...[
              const SizedBox(height: 16),
              Text(
                'Changes',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 8),
              ..._buildChanges(context),
            ],
            
            // Metadata
            if (activity.metadata != null && activity.metadata!.isNotEmpty) ...[
              const SizedBox(height: 16),
              Text(
                'Additional Information',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.surface,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text(
                  JsonEncoder.withIndent('  ').convert(activity.metadata),
                  style: Theme.of(context).textTheme.bodySmall?.copyWith(
                    fontFamily: 'monospace',
                  ),
                ),
              ),
            ],
            
            // Actions
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                if (activity.entityId != null)
                  TextButton(
                    onPressed: () {
                      Navigator.of(context).pop();
                      context.go('/${activity.entityType}/${activity.entityId}');
                    },
                    child: const Text('View Entity'),
                  ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('Close'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 120,
            child: Text(
              label,
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }
  
  List<Widget> _buildChanges(BuildContext context) {
    final changes = <Widget>[];
    final oldValues = activity.oldValues!;
    final newValues = activity.newValues!;
    
    for (final key in newValues.keys) {
      if (oldValues.containsKey(key) && oldValues[key] != newValues[key]) {
        changes.add(
          Container(
            margin: const EdgeInsets.only(bottom: 8),
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              border: Border.all(color: Theme.of(context).dividerColor),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  key.titleCase,
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 4),
                Row(
                  children: [
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'Old Value',
                            style: TextStyle(fontSize: 12),
                          ),
                          Text(
                            oldValues[key]?.toString() ?? 'null',
                            style: const TextStyle(
                              decoration: TextDecoration.lineThrough,
                              color: Colors.red,
                            ),
                          ),
                        ],
                      ),
                    ),
                    const Icon(Icons.arrow_forward, size: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'New Value',
                            style: TextStyle(fontSize: 12),
                          ),
                          Text(
                            newValues[key]?.toString() ?? 'null',
                            style: const TextStyle(color: Colors.green),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }
    
    return changes;
  }
}

Activity Analytics

Activity Dashboard Widget

class ActivityDashboardWidget extends StatelessWidget {
  final DateTimeRange dateRange;
  
  const ActivityDashboardWidget({required this.dateRange});
  
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Map<String, dynamic>>(
      future: _loadStats(context),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const LoadingIndicator();
        }
        
        final stats = snapshot.data!;
        
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Summary cards
            Row(
              children: [
                Expanded(
                  child: _StatCard(
                    title: 'Total Activities',
                    value: stats['total'].toString(),
                    icon: Icons.timeline,
                    color: Colors.blue,
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: _StatCard(
                    title: 'Active Users',
                    value: stats['activeUsers'].toString(),
                    icon: Icons.people,
                    color: Colors.green,
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: _StatCard(
                    title: 'Entities Modified',
                    value: stats['entitiesModified'].toString(),
                    icon: Icons.edit,
                    color: Colors.orange,
                  ),
                ),
              ],
            ),
            
            const SizedBox(height: 24),
            
            // Activity by type chart
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Activity by Type',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 16),
                    SizedBox(
                      height: 200,
                      child: ActivityTypeChart(
                        data: stats['byType'] as Map<String, int>,
                      ),
                    ),
                  ],
                ),
              ),
            ),
            
            const SizedBox(height: 16),
            
            // Recent activities
            Card(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Padding(
                    padding: const EdgeInsets.all(16),
                    child: Row(
                      children: [
                        Text(
                          'Recent Activities',
                          style: Theme.of(context).textTheme.titleMedium,
                        ),
                        const Spacer(),
                        TextButton(
                          onPressed: () => context.go('/activity-logs'),
                          child: const Text('View All'),
                        ),
                      ],
                    ),
                  ),
                  const Divider(height: 1),
                  LimitedBox(
                    maxHeight: 300,
                    child: ActivityTimeline(),
                  ),
                ],
              ),
            ),
          ],
        );
      },
    );
  }
  
  Future<Map<String, dynamic>> _loadStats(BuildContext context) async {
    final api = context.read<ActivityApi>();
    return api.getActivityStats(
      startDate: dateRange.start,
      endDate: dateRange.end,
    );
  }
}

Export and Compliance

Activity Export

class ActivityExporter {
  final ActivityApi api;
  
  const ActivityExporter({required this.api});
  
  Future<String> exportToCsv({
    String? entityType,
    String? entityId,
    String? userId,
    DateTime? startDate,
    DateTime? endDate,
  }) async {
    // Load activities
    final activities = await api.list(
      filters: {
        if (entityType != null) 'entityType': entityType,
        if (entityId != null) 'entityId': entityId,
        if (userId != null) 'userId': userId,
      },
      sortBy: 'createdAt',
      sortOrder: 'desc',
    );
    
    // Build CSV
    final csv = StringBuffer();
    
    // Header
    csv.writeln(
      'Timestamp,User,Action,Entity Type,Entity ID,Description,Changes'
    );
    
    // Data
    for (final activity in activities) {
      final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss')
        .format(activity.createdAt!);
      final changes = _getChangeSummary(activity);
      
      csv.writeln(
        '$timestamp,"${activity.userName}","${activity.action}",'
        '"${activity.entityType}","${activity.entityId ?? ""}",'
        '"${activity.description}","$changes"'
      );
    }
    
    return csv.toString();
  }
  
  Future<Uint8List> exportToPdf({
    String? entityType,
    String? entityId,
    DateTime? startDate,
    DateTime? endDate,
  }) async {
    // Implementation depends on PDF library
    throw UnimplementedError();
  }
  
  String _getChangeSummary(ActivityLog activity) {
    if (activity.oldValues == null || activity.newValues == null) {
      return '';
    }
    
    final changes = <String>[];
    final changedFields = activity.metadata?['changedFields'] as List?;
    
    if (changedFields != null) {
      for (final field in changedFields) {
        final oldValue = activity.oldValues![field];
        final newValue = activity.newValues![field];
        changes.add('$field: $oldValue$newValue');
      }
    }
    
    return changes.join('; ');
  }
}

Compliance Reports

class ComplianceReportGenerator {
  final ActivityApi api;
  
  const ComplianceReportGenerator({required this.api});
  
  Future<ComplianceReport> generateReport({
    required DateTime startDate,
    required DateTime endDate,
    List<String>? entityTypes,
    List<String>? actions,
  }) async {
    final activities = await api.list(
      filters: {
        'startDate': startDate.toIso8601String(),
        'endDate': endDate.toIso8601String(),
        if (entityTypes != null) 'entityTypes': entityTypes,
        if (actions != null) 'actions': actions,
      },
    );
    
    return ComplianceReport(
      period: DateTimeRange(start: startDate, end: endDate),
      totalActivities: activities.length,
      uniqueUsers: activities.map((a) => a.userId).toSet().length,
      activitiesByType: _groupByType(activities),
      activitiesByUser: _groupByUser(activities),
      sensitiveOperations: _findSensitiveOperations(activities),
      anomalies: _detectAnomalies(activities),
    );
  }
  
  Map<String, int> _groupByType(List<ActivityLog> activities) {
    final grouped = <String, int>{};
    
    for (final activity in activities) {
      final key = '${activity.entityType}.${activity.action}';
      grouped[key] = (grouped[key] ?? 0) + 1;
    }
    
    return grouped;
  }
  
  Map<String, int> _groupByUser(List<ActivityLog> activities) {
    final grouped = <String, int>{};
    
    for (final activity in activities) {
      grouped[activity.userName] = (grouped[activity.userName] ?? 0) + 1;
    }
    
    return grouped;
  }
  
  List<ActivityLog> _findSensitiveOperations(List<ActivityLog> activities) {
    return activities.where((activity) {
      // Define sensitive operations
      return activity.action == 'deleted' ||
             activity.entityType == 'users' ||
             activity.entityType == 'permissions' ||
             activity.severity == ActivityLogSeverity.critical;
    }).toList();
  }
  
  List<ComplianceAnomaly> _detectAnomalies(List<ActivityLog> activities) {
    final anomalies = <ComplianceAnomaly>[];
    
    // Check for unusual activity patterns
    final userActivityCount = <String, int>{};
    for (final activity in activities) {
      userActivityCount[activity.userId] = 
        (userActivityCount[activity.userId] ?? 0) + 1;
    }
    
    // Flag users with excessive activity
    final avgActivity = activities.length / userActivityCount.length;
    for (final entry in userActivityCount.entries) {
      if (entry.value > avgActivity * 3) {
        anomalies.add(ComplianceAnomaly(
          type: 'excessive_activity',
          userId: entry.key,
          description: 'User has ${entry.value} activities, '
                      '3x more than average',
          severity: AnomalySeverity.medium,
        ));
      }
    }
    
    // Check for after-hours activity
    for (final activity in activities) {
      final hour = activity.createdAt!.hour;
      if (hour < 6 || hour > 22) {
        anomalies.add(ComplianceAnomaly(
          type: 'after_hours',
          userId: activity.userId,
          description: 'Activity at ${activity.createdAt}',
          severity: AnomalySeverity.low,
          activityId: activity.id,
        ));
      }
    }
    
    return anomalies;
  }
}

Configuration

Activity Tracking Settings

class ActivityTrackingSettings {
  final bool enabled;
  final bool trackReads;
  final bool trackFieldChanges;
  final List<String> excludedEntityTypes;
  final List<String> excludedFields;
  final Duration retentionPeriod;
  final int batchSize;
  final Duration flushInterval;
  
  const ActivityTrackingSettings({
    this.enabled = true,
    this.trackReads = false,
    this.trackFieldChanges = true,
    this.excludedEntityTypes = const [],
    this.excludedFields = const ['password', 'token', 'secret'],
    this.retentionPeriod = const Duration(days: 365),
    this.batchSize = 50,
    this.flushInterval = const Duration(seconds: 5),
  });
}

// Usage
final settings = ActivityTrackingSettings(
  trackReads: true, // Enable read tracking
  excludedEntityTypes: ['temp_data'], // Don't track temporary entities
  excludedFields: ['password', 'apiKey', 'secret'], // Sensitive fields
  retentionPeriod: const Duration(days: 730), // 2 years
);

Best Practices

  1. Performance - Use batch logging for better performance
  2. Privacy - Exclude sensitive fields from logs
  3. Retention - Define clear retention policies
  4. Compliance - Regular compliance reports
  5. Security - Protect activity logs from tampering
  6. Analysis - Use activity data for insights
  7. Real-time - Consider real-time monitoring for critical actions

Next: Help System - Context-sensitive help integration