import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:it0_app/l10n/app_localizations.dart'; import '../../../../core/config/api_endpoints.dart'; import '../../../../core/network/dio_client.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/widgets/empty_state.dart'; import '../../../../core/widgets/error_view.dart'; import '../../../../core/widgets/status_badge.dart'; // --------------------------------------------------------------------------- // Provider – fetch servers from inventory API // --------------------------------------------------------------------------- /// Fetches the full list of servers from the backend. final serversProvider = FutureProvider>>((ref) async { final dio = ref.watch(dioClientProvider); final response = await dio.get(ApiEndpoints.servers); final data = response.data; if (data is List) { return data.cast>(); } if (data is Map && data.containsKey('items')) { return (data['items'] as List).cast>(); } return []; }); // --------------------------------------------------------------------------- // Servers page – ConsumerStatefulWidget (needs local filter state) // --------------------------------------------------------------------------- class ServersPage extends ConsumerStatefulWidget { const ServersPage({super.key}); @override ConsumerState createState() => _ServersPageState(); } class _ServersPageState extends ConsumerState { String _envFilter = 'all'; static const _environments = ['all', 'dev', 'staging', 'prod']; @override Widget build(BuildContext context) { final serversAsync = ref.watch(serversProvider); return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context).serversPageTitle), ), body: RefreshIndicator( onRefresh: () async => ref.invalidate(serversProvider), child: Column( children: [ // Environment filter chips _buildFilterBar(), // Server list Expanded( child: serversAsync.when( data: (servers) { final filtered = _filterServers(servers); if (filtered.isEmpty) { return EmptyState( icon: Icons.dns_outlined, title: AppLocalizations.of(context).noServersTitle, subtitle: AppLocalizations.of(context).noServersFiltered, ); } return ListView.builder( padding: const EdgeInsets.all(12), itemCount: filtered.length, itemBuilder: (context, index) { final server = filtered[index]; return _ServerCard( server: server, onTap: () => _showServerDetails(context, server), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => ErrorView( error: e, onRetry: () => ref.invalidate(serversProvider), ), ), ), ], ), ), ); } // ---- Environment filter bar ----------------------------------------------- Widget _buildFilterBar() { return SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: _environments.map((env) { final selected = _envFilter == env; return Padding( padding: const EdgeInsets.only(right: 8), child: FilterChip( label: Text(env == 'all' ? AppLocalizations.of(context).allEnvironments : env), selected: selected, onSelected: (_) => setState(() => _envFilter = env), selectedColor: _envColor(env).withOpacity(0.25), checkmarkColor: _envColor(env), labelStyle: TextStyle( color: selected ? _envColor(env) : AppColors.textSecondary, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, ), side: BorderSide( color: selected ? _envColor(env).withOpacity(0.5) : AppColors.surfaceLight, ), backgroundColor: AppColors.surface, ), ); }).toList(), ), ); } List> _filterServers( List> servers) { if (_envFilter == 'all') return servers; return servers.where((s) { final env = (s['environment'] as String? ?? '').toLowerCase(); return env == _envFilter; }).toList(); } // ---- Server details bottom sheet ------------------------------------------ void _showServerDetails( BuildContext context, Map server) { final hostname = server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel; final ip = server['ip_address'] as String? ?? server['ipAddress'] as String? ?? server['ip'] as String? ?? 'N/A'; final env = server['environment'] as String? ?? 'unknown'; final status = server['status'] as String? ?? 'unknown'; final role = server['role'] as String? ?? ''; final os = server['os'] as String? ?? server['operating_system'] as String? ?? ''; final cpu = server['cpu'] as String? ?? server['cpu_cores']?.toString() ?? ''; final memory = server['memory'] as String? ?? server['memory_gb']?.toString() ?? ''; final region = server['region'] as String? ?? ''; final provider = server['provider'] as String? ?? server['cloud_provider'] as String? ?? ''; final createdAt = server['created_at'] as String? ?? server['createdAt'] as String? ?? ''; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: AppColors.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) => DraggableScrollableSheet( initialChildSize: 0.55, minChildSize: 0.3, maxChildSize: 0.85, expand: false, builder: (context, scrollController) => SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.fromLTRB(20, 12, 20, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle bar Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: AppColors.textMuted, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), // Hostname + status Row( children: [ Expanded( child: Text( hostname, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold), ), ), StatusBadge(status: status), ], ), const SizedBox(height: 16), // Environment + Role row Row( children: [ _EnvironmentTag(environment: env), if (role.isNotEmpty) ...[ const SizedBox(width: 8), StatusBadge(status: role, color: AppColors.info), ], ], ), const SizedBox(height: 20), // Detail rows _DetailRow(label: AppLocalizations.of(context).ipAddressLabel, value: ip), if (os.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).osLabel, value: os), if (cpu.isNotEmpty) _DetailRow(label: 'CPU', value: cpu), if (memory.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).memoryLabel, value: memory), if (region.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).regionLabel, value: region), if (provider.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).cloudProviderLabel, value: provider), if (createdAt.isNotEmpty) _DetailRow(label: AppLocalizations.of(context).createdAtLabel, value: createdAt), ], ), ), ), ); } // ---- Helpers -------------------------------------------------------------- static Color _envColor(String env) { switch (env.toLowerCase()) { case 'dev': return AppColors.info; case 'staging': return AppColors.warning; case 'prod': return AppColors.error; default: return AppColors.primary; } } } // --------------------------------------------------------------------------- // Server card widget // --------------------------------------------------------------------------- class _ServerCard extends StatelessWidget { final Map server; final VoidCallback onTap; const _ServerCard({required this.server, required this.onTap}); @override Widget build(BuildContext context) { final hostname = server['hostname'] as String? ?? server['name'] as String? ?? AppLocalizations.of(context).unknownLabel; final ip = server['ip_address'] as String? ?? server['ipAddress'] as String? ?? server['ip'] as String? ?? 'N/A'; final env = server['environment'] as String? ?? 'unknown'; final status = server['status'] as String? ?? 'unknown'; final role = server['role'] as String? ?? ''; final isOnline = _isOnline(status); return Card( color: AppColors.surface, margin: const EdgeInsets.only(bottom: 10), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Hostname + status dot Row( children: [ // Status indicator dot Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: isOnline ? AppColors.success : AppColors.error, boxShadow: [ BoxShadow( color: (isOnline ? AppColors.success : AppColors.error) .withOpacity(0.4), blurRadius: 6, spreadRadius: 1, ), ], ), ), const SizedBox(width: 10), Expanded( child: Text( hostname, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), _EnvironmentTag(environment: env), ], ), const SizedBox(height: 8), // IP + role row Row( children: [ const Icon(Icons.lan_outlined, size: 14, color: AppColors.textMuted), const SizedBox(width: 4), Text( ip, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13), ), if (role.isNotEmpty) ...[ const Spacer(), Icon(Icons.workspaces_outline, size: 14, color: AppColors.textMuted), const SizedBox(width: 4), Text( role, style: const TextStyle( color: AppColors.textSecondary, fontSize: 13), ), ], ], ), ], ), ), ), ); } bool _isOnline(String status) { final lower = status.toLowerCase(); return lower == 'online' || lower == 'running' || lower == 'active' || lower == 'healthy'; } } // --------------------------------------------------------------------------- // Environment tag chip // --------------------------------------------------------------------------- class _EnvironmentTag extends StatelessWidget { final String environment; const _EnvironmentTag({required this.environment}); Color get _color { switch (environment.toLowerCase()) { case 'dev': return AppColors.info; case 'staging': return AppColors.warning; case 'prod': return AppColors.error; default: return AppColors.textMuted; } } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _color.withOpacity(0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: _color.withOpacity(0.3)), ), child: Text( environment.toUpperCase(), style: TextStyle( color: _color, fontSize: 11, fontWeight: FontWeight.w600, ), ), ); } } // --------------------------------------------------------------------------- // Detail row for bottom sheet // --------------------------------------------------------------------------- class _DetailRow extends StatelessWidget { final String label; final String value; const _DetailRow({required this.label, required this.value}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 100, child: Text( label, style: const TextStyle( color: AppColors.textMuted, fontSize: 13, fontWeight: FontWeight.w500), ), ), Expanded( child: Text( value, style: const TextStyle( color: AppColors.textPrimary, fontSize: 13), ), ), ], ), ); } }