import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/load_state.dart'; import '../providers/providers.dart'; import '../widgets/load_error_view.dart'; /// Экран просмотра лога событий. class EventLogScreen extends ConsumerStatefulWidget { const EventLogScreen({super.key}); @override ConsumerState createState() => _EventLogScreenState(); } class _EventLogScreenState extends ConsumerState { int _limit = 100; @override void initState() { super.initState(); _load(); } Future _load() async { await ref.read(eventLogProvider.notifier).load(limit: _limit); } @override Widget build(BuildContext context) { final eventsState = ref.watch(eventLogProvider); final events = eventsState.data; return Scaffold( appBar: AppBar( title: const Text('ЛОГ СОБЫТИЙ'), actions: [ PopupMenuButton( icon: const Icon(Icons.filter_list), tooltip: 'Количество записей', onSelected: (v) { _limit = v; _load(); }, itemBuilder: (_) => [50, 100, 200, 500] .map((n) => PopupMenuItem(value: n, child: Text('$n записей'))) .toList(), ), ], ), body: _buildContent(eventsState, events), ); } Widget _buildContent( LoadState> eventsState, List events, ) { if ((eventsState.isIdle || eventsState.isLoading) && events.isEmpty) { return const Center( child: CircularProgressIndicator(color: Colors.deepOrange), ); } if (eventsState.hasError && events.isEmpty) { return LoadErrorView( title: 'Не удалось загрузить лог событий', message: eventsState.errorMessage, icon: Icons.list_alt, onRetry: _load, ); } if (events.isEmpty) { return const Center( child: Text('Нет событий', style: TextStyle(color: Colors.white54)), ); } final hasStatusHeader = eventsState.isLoading || eventsState.hasError; final statusHeaderCount = hasStatusHeader ? 1 : 0; return RefreshIndicator( color: Colors.deepOrange, onRefresh: _load, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(8), itemCount: events.length + statusHeaderCount, itemBuilder: (context, index) { if (hasStatusHeader && index == 0) { if (eventsState.isLoading) { return const Padding( padding: EdgeInsets.only(bottom: 12), child: LinearProgressIndicator(color: Colors.deepOrange), ); } return LoadErrorBanner( title: 'Не удалось обновить лог событий', message: eventsState.errorMessage, onRetry: _load, ); } final eventIndex = index - statusHeaderCount; final event = events[eventIndex]; return _EventRow( event: event is Map ? Map.from(event) : {}, ); }, ), ); } } class _EventRow extends StatelessWidget { final Map event; const _EventRow({required this.event}); @override Widget build(BuildContext context) { final timestamp = event['timestamp'] ?? event['time'] ?? event['created_at'] ?? ''; final action = event['action'] ?? event['command'] ?? event['type'] ?? ''; final targetId = event['target_id'] ?? event['target'] ?? event['group_id'] ?? ''; final params = event['params'] ?? event['details'] ?? ''; final actor = event['actor'] ?? event['user'] ?? event['key_name'] ?? ''; final formattedTime = _formatTime(timestamp.toString()); return Card( margin: const EdgeInsets.only(bottom: 4), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Время SizedBox( width: 80, child: Text( formattedTime, style: const TextStyle( fontSize: 11, color: Colors.white38, fontFamily: 'monospace', ), ), ), const SizedBox(width: 8), // Контент Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$action - $targetId', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), ), if (params.toString().isNotEmpty) Text( params.toString(), style: const TextStyle( fontSize: 11, color: Colors.white38, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (actor.toString().isNotEmpty) Text( actor.toString(), style: const TextStyle( fontSize: 10, color: Colors.white24, ), ), ], ), ), ], ), ), ); } String _formatTime(String iso) { if (iso.isEmpty) return ''; try { final d = DateTime.parse(iso); String pad(int n) => n.toString().padLeft(2, '0'); return '${pad(d.day)}.${pad(d.month)} ${pad(d.hour)}:${pad(d.minute)}:${pad(d.second)}'; } catch (_) { return iso; } } }