import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../domain/entities/server.dart'; import '../../blocs/auth/auth_bloc.dart'; import '../../blocs/auth/auth_event.dart'; import '../../blocs/auth/auth_state.dart'; import '../../blocs/server/server_bloc.dart'; import '../../blocs/server/server_event.dart'; import '../../blocs/server/server_state.dart'; import '../../widgets/app_drawer.dart'; class ServersManagementScreen extends StatefulWidget { const ServersManagementScreen({super.key}); @override State createState() => _ServersManagementScreenState(); } class _ServersManagementScreenState extends State { final TextEditingController _searchController = TextEditingController(); final Set _selectedServers = {}; bool _selectAll = false; // Filter states String _filterType = 'all'; // 'all', 'gcore', 'geviscope' bool? _filterEnabled; // null = all, true = enabled only, false = disabled only String _searchQuery = ''; // Sort states int? _sortColumnIndex; bool _sortAscending = true; @override void dispose() { _searchController.dispose(); super.dispose(); } List _getFilteredServers(List allServers) { var filtered = allServers; // Apply type filter if (_filterType == 'gcore') { filtered = filtered.where((s) => s.type == ServerType.gcore).toList(); } else if (_filterType == 'geviscope') { filtered = filtered.where((s) => s.type == ServerType.geviscope).toList(); } // Apply enabled/disabled filter if (_filterEnabled != null) { filtered = filtered.where((s) => s.enabled == _filterEnabled).toList(); } // Apply search filter if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); filtered = filtered.where((s) { return s.alias.toLowerCase().contains(query) || s.host.toLowerCase().contains(query) || s.user.toLowerCase().contains(query); }).toList(); } // Apply sorting if (_sortColumnIndex != null) { filtered.sort((a, b) { int comparison = 0; switch (_sortColumnIndex) { case 0: // Alias comparison = a.alias.toLowerCase().compareTo(b.alias.toLowerCase()); break; case 1: // Host comparison = a.host.toLowerCase().compareTo(b.host.toLowerCase()); break; case 2: // User comparison = a.user.toLowerCase().compareTo(b.user.toLowerCase()); break; case 3: // Type comparison = a.type.toString().compareTo(b.type.toString()); break; case 4: // Status comparison = a.enabled == b.enabled ? 0 : (a.enabled ? -1 : 1); break; } return _sortAscending ? comparison : -comparison; }); } return filtered; } void _toggleSelectAll(List servers) { setState(() { if (_selectAll) { _selectedServers.clear(); } else { _selectedServers.addAll(servers.map((s) => s.id)); } _selectAll = !_selectAll; }); } void _showBatchDeleteDialog(BuildContext context) { if (_selectedServers.isEmpty) return; showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text('Delete ${_selectedServers.length} Servers'), content: Text( 'Are you sure you want to delete ${_selectedServers.length} selected server${_selectedServers.length != 1 ? 's' : ''}?' ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), TextButton( onPressed: () { // Get all selected servers to determine their types final bloc = context.read(); final state = bloc.state; if (state is ServerLoaded) { for (final id in _selectedServers) { final server = state.servers.firstWhere((s) => s.id == id); bloc.add(DeleteServerEvent(id, server.type)); } } setState(() { _selectedServers.clear(); _selectAll = false; }); Navigator.of(dialogContext).pop(); }, child: const Text('Delete All', style: TextStyle(color: Colors.red)), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( drawer: const AppDrawer(currentRoute: '/servers'), appBar: AppBar( title: Row( children: [ const Icon(Icons.dns, size: 24), const SizedBox(width: 8), const Text('Server Management'), ], ), actions: [ // Sync button with dirty count badge BlocBuilder( builder: (context, state) { final dirtyCount = state is ServerLoaded ? state.dirtyCount : 0; return Stack( children: [ IconButton( icon: const Icon(Icons.sync), onPressed: dirtyCount > 0 ? () { context.read().add(const SyncServersEvent()); } : null, tooltip: dirtyCount > 0 ? 'Sync $dirtyCount unsaved change${dirtyCount != 1 ? 's' : ''}' : 'No changes to sync', ), if (dirtyCount > 0) Positioned( right: 4, top: 4, child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: Text( '$dirtyCount', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ), ], ); }, ), // Download/refresh button Builder( builder: (context) => IconButton( icon: const Icon(Icons.cloud_download), onPressed: () { context.read().add(const DownloadServersEvent()); }, tooltip: 'Download latest from server', ), ), const SizedBox(width: 8), BlocBuilder( builder: (context, state) { if (state is Authenticated) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( children: [ Icon( state.user.role == 'Administrator' ? Icons.admin_panel_settings : Icons.person, size: 20, ), const SizedBox(width: 8), Text(state.user.username), const SizedBox(width: 16), IconButton( icon: const Icon(Icons.logout), onPressed: () { context.read().add(const LogoutRequested()); }, tooltip: 'Logout', ), ], ), ); } return const SizedBox.shrink(); }, ), ], ), body: Column( children: [ // Toolbar with search, filters, and batch actions Container( padding: const EdgeInsets.all(16.0), color: Colors.grey[100], child: Column( children: [ // Search and Add button row Row( children: [ Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search by alias, host, or user...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { _searchController.clear(); _searchQuery = ''; }); }, ) : null, border: const OutlineInputBorder(), filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), onChanged: (value) { setState(() { _searchQuery = value; }); }, ), ), const SizedBox(width: 16), ElevatedButton.icon( onPressed: () { _showAddServerDialog(context); }, icon: const Icon(Icons.add), label: const Text('Add Server'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ], ), const SizedBox(height: 12), // Filter chips and batch actions Row( children: [ // Type filter chips const Text('Type: ', style: TextStyle(fontWeight: FontWeight.w500)), const SizedBox(width: 8), ChoiceChip( label: const Text('All'), selected: _filterType == 'all', onSelected: (selected) { setState(() { _filterType = 'all'; }); }, ), const SizedBox(width: 8), ChoiceChip( label: const Text('G-Core'), selected: _filterType == 'gcore', onSelected: (selected) { setState(() { _filterType = selected ? 'gcore' : 'all'; }); }, ), const SizedBox(width: 8), ChoiceChip( label: const Text('GeViScope'), selected: _filterType == 'geviscope', onSelected: (selected) { setState(() { _filterType = selected ? 'geviscope' : 'all'; }); }, ), const SizedBox(width: 24), // Status filter chips const Text('Status: ', style: TextStyle(fontWeight: FontWeight.w500)), const SizedBox(width: 8), ChoiceChip( label: const Text('All'), selected: _filterEnabled == null, onSelected: (selected) { setState(() { _filterEnabled = null; }); }, ), const SizedBox(width: 8), ChoiceChip( label: const Text('Enabled'), selected: _filterEnabled == true, onSelected: (selected) { setState(() { _filterEnabled = selected ? true : null; }); }, ), const SizedBox(width: 8), ChoiceChip( label: const Text('Disabled'), selected: _filterEnabled == false, onSelected: (selected) { setState(() { _filterEnabled = selected ? false : null; }); }, ), const Spacer(), // Batch actions if (_selectedServers.isNotEmpty) ...[ Text( '${_selectedServers.length} selected', style: const TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(width: 8), TextButton( onPressed: () { setState(() { _selectedServers.clear(); _selectAll = false; }); }, child: const Text('Clear'), ), const SizedBox(width: 16), OutlinedButton.icon( onPressed: () => _showBatchDeleteDialog(context), icon: const Icon(Icons.delete, size: 18), label: const Text('Delete Selected'), style: OutlinedButton.styleFrom( foregroundColor: Colors.red, ), ), ], ], ), ], ), ), // Table Expanded( child: BlocConsumer( listener: (context, state) { if (state is ServerError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.red, ), ); } else if (state is ServerOperationSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.green, ), ); } else if (state is ServerSyncSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.green, ), ); } }, builder: (context, state) { if (state is ServerLoading) { return const Center(child: CircularProgressIndicator()); } else if (state is ServerSyncing) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text( state.message, style: Theme.of(context).textTheme.titleMedium, ), ], ), ); } else if (state is ServerDownloading) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Downloading servers...'), ], ), ); } else if (state is ServerLoaded) { final filteredServers = _getFilteredServers(state.servers); if (filteredServers.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.dns_outlined, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( _searchQuery.isNotEmpty || _filterEnabled != null || _filterType != 'all' ? 'No matching servers' : 'No servers found', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.grey[600], ), ), const SizedBox(height: 8), Text( _searchQuery.isNotEmpty || _filterEnabled != null || _filterType != 'all' ? 'Try adjusting your search or filters' : 'Add a server to get started', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey[500], ), ), ], ), ); } return _buildCompactTable(context, filteredServers); } else if (state is ServerError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red[300]), const SizedBox(height: 16), Text( 'Error loading servers', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( state.message, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey[600], ), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: () { context.read().add(const LoadServers()); }, icon: const Icon(Icons.refresh), label: const Text('Retry'), ), ], ), ); } return const Center(child: CircularProgressIndicator()); }, ), ), ], ), ); } Widget _buildCompactTable(BuildContext context, List servers) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: DataTable( columnSpacing: 24, horizontalMargin: 12, headingRowHeight: 48, dataRowHeight: 56, showCheckboxColumn: true, sortColumnIndex: _sortColumnIndex, sortAscending: _sortAscending, columns: [ DataColumn( label: const Text('Alias', style: TextStyle(fontWeight: FontWeight.bold)), onSort: (columnIndex, ascending) { setState(() { _sortColumnIndex = columnIndex; _sortAscending = ascending; }); }, ), DataColumn( label: const Text('Host', style: TextStyle(fontWeight: FontWeight.bold)), onSort: (columnIndex, ascending) { setState(() { _sortColumnIndex = columnIndex; _sortAscending = ascending; }); }, ), DataColumn( label: const Text('User', style: TextStyle(fontWeight: FontWeight.bold)), onSort: (columnIndex, ascending) { setState(() { _sortColumnIndex = columnIndex; _sortAscending = ascending; }); }, ), DataColumn( label: const Text('Type', style: TextStyle(fontWeight: FontWeight.bold)), onSort: (columnIndex, ascending) { setState(() { _sortColumnIndex = columnIndex; _sortAscending = ascending; }); }, ), DataColumn( label: const Text('Status', style: TextStyle(fontWeight: FontWeight.bold)), onSort: (columnIndex, ascending) { setState(() { _sortColumnIndex = columnIndex; _sortAscending = ascending; }); }, ), const DataColumn( label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold)), ), ], rows: servers.map((server) { final isSelected = _selectedServers.contains(server.id); return DataRow( selected: isSelected, onSelectChanged: (selected) { setState(() { if (selected == true) { _selectedServers.add(server.id); } else { _selectedServers.remove(server.id); } _selectAll = _selectedServers.length == servers.length; }); }, cells: [ // Alias DataCell( Text( server.alias, style: const TextStyle(fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // Host DataCell( Text( server.host, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // User DataCell( Text( server.user, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // Type DataCell( Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: server.type == ServerType.gcore ? Colors.green.withOpacity(0.1) : Colors.purple.withOpacity(0.1), border: Border.all( color: server.type == ServerType.gcore ? Colors.green : Colors.purple, ), borderRadius: BorderRadius.circular(12), ), child: Text( server.type == ServerType.gcore ? 'G-Core' : 'GeViScope', style: TextStyle( color: server.type == ServerType.gcore ? Colors.green[700] : Colors.purple[700], fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ), // Status DataCell( Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: server.enabled ? Colors.green.withOpacity(0.1) : Colors.grey.withOpacity(0.1), border: Border.all( color: server.enabled ? Colors.green : Colors.grey, ), borderRadius: BorderRadius.circular(12), ), child: Text( server.enabled ? 'Enabled' : 'Disabled', style: TextStyle( color: server.enabled ? Colors.green[700] : Colors.grey[700], fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ), // Actions DataCell( Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit, size: 20), onPressed: () { context.push('/servers/edit/${server.id}', extra: server); }, tooltip: 'Edit', padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.delete, size: 20, color: Colors.red), onPressed: () { _showDeleteConfirmation(context, server); }, tooltip: 'Delete', padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ], ); }).toList(), ), ), ); } void _showDeleteConfirmation(BuildContext context, Server server) { showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Delete Server'), content: Text('Are you sure you want to delete "${server.alias}"?'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), TextButton( onPressed: () { context.read().add(DeleteServerEvent(server.id, server.type)); setState(() { _selectedServers.remove(server.id); }); Navigator.of(dialogContext).pop(); }, child: const Text('Delete', style: TextStyle(color: Colors.red)), ), ], ), ); } void _showAddServerDialog(BuildContext context) { showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Add Server'), content: const Text('Choose the server type:'), actions: [ TextButton( onPressed: () { Navigator.of(dialogContext).pop(); context.push('/servers/create?type=gcore'); }, child: const Text('G-Core Server'), ), TextButton( onPressed: () { Navigator.of(dialogContext).pop(); context.push('/servers/create?type=geviscope'); }, child: const Text('GeViScope Server'), ), TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), ], ), ); } }