import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../../core/api/client.dart'; import '../../../core/api/endpoints.dart'; import '../../../core/api/exceptions.dart'; import '../../../core/theme/app_theme.dart'; import '../../../models/admin.dart'; import '../../../models/tariff.dart'; import '../../../models/auth.dart'; import '../../../widgets/loading_indicator.dart'; import '../../../widgets/error_display.dart'; import '../../../widgets/empty_state.dart'; class AdminScreen extends ConsumerStatefulWidget { const AdminScreen({super.key}); @override ConsumerState createState() => _AdminScreenState(); } class _AdminScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 7, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Админ-панель'), bottom: TabBar( controller: _tabController, isScrollable: true, tabs: const [ Tab(text: 'Статистика'), Tab(text: 'Пользователи'), Tab(text: 'Агенты'), Tab(text: 'Логи'), Tab(text: 'Тарифы'), Tab(text: 'Бот'), Tab(text: 'Сервисы'), ], ), ), body: TabBarView( controller: _tabController, children: const [ _StatsTab(), _UsersTab(), _AgentsTab(), _LogsTab(), _TariffsTab(), _BotTab(), _ServicesTab(), ], ), ); } } class _StatsTab extends ConsumerWidget { const _StatsTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminStats); return AdminStats.fromJson(resp.data as Map); }, builder: (stats) => GridView.count( padding: const EdgeInsets.all(16), crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.5, children: [ _StatCard(label: 'Пользователи', value: '${stats.totalUsers}', icon: Icons.people), _StatCard(label: 'Идеи', value: '${stats.totalIdeas}', icon: Icons.lightbulb), _StatCard(label: 'Голосовые сессии', value: '${stats.totalVoiceSessions}', icon: Icons.mic), _StatCard(label: 'Раб. пространства', value: '${stats.totalWorkspaces}', icon: Icons.workspaces), _StatCard(label: 'Активные агенты', value: '${stats.activeAgents}', icon: Icons.smart_toy), _StatCard(label: 'Уведомления', value: '${stats.totalNotifications}', icon: Icons.notifications), ], ), ); } } class _StatCard extends StatelessWidget { final String label; final String value; final IconData icon; const _StatCard({ required this.label, required this.value, required this.icon, }); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: VoIdeaColors.primary, size: 28), const SizedBox(height: 8), Text(value, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, )), Text(label, style: const TextStyle(fontSize: 12, color: VoIdeaColors.muted)), ], ), ), ); } } class _UsersTab extends ConsumerWidget { const _UsersTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder>( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminUsers); return (resp.data as List) .map((e) => User.fromJson(e as Map)) .toList(); }, builder: (users) => ListView.builder( padding: const EdgeInsets.all(16), itemCount: users.length, itemBuilder: (_, i) { final u = users[i]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar( backgroundColor: VoIdeaColors.primary.withOpacity(0.1), child: Text(u.name.isNotEmpty ? u.name[0].toUpperCase() : '?', style: const TextStyle(color: VoIdeaColors.primary)), ), title: Text(u.name), subtitle: Text('${u.email} · ${DateFormat('d MMM yyyy').format(u.createdAt)}', style: const TextStyle(fontSize: 12)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (u.isAdmin) const Chip(label: Text('Admin', style: TextStyle(fontSize: 10))), if (u.isTwoFactorEnabled) const Padding( padding: EdgeInsets.only(left: 4), child: Icon(Icons.security, size: 16, color: VoIdeaColors.success), ), ], ), ), ); }, ), ); } } class _AgentsTab extends ConsumerWidget { const _AgentsTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder>( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminAgents); return (resp.data as List) .map((e) => AgentConfig.fromJson(e as Map)) .toList(); }, builder: (agents) => Column( children: [ Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => _runAllAgents(ref), icon: const Icon(Icons.play_arrow), label: const Text('Запустить всех'), ), ), ], ), ), Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: agents.length, itemBuilder: (_, i) { final agent = agents[i]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ExpansionTile( leading: Icon( agent.isActive ? Icons.smart_toy : Icons.smart_toy_outlined, color: agent.isActive ? VoIdeaColors.success : VoIdeaColors.muted, ), title: Text(agent.name), subtitle: Text(agent.description, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.play_arrow, size: 20), onPressed: () => _runAgent(ref, agent.name), ), const Icon(Icons.expand_more), ], ), children: [ Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Расписание: ${agent.schedule}', style: const TextStyle(fontSize: 12)), const SizedBox(height: 8), Text('Активен: ${agent.isActive ? "Да" : "Нет"}', style: const TextStyle(fontSize: 12)), const SizedBox(height: 8), Text('Конфиг: ${agent.config.toString()}', style: const TextStyle(fontSize: 12)), ], ), ), ], ), ); }, ), ), ], ), ); } void _runAgent(WidgetRef ref, String name) async { try { final api = ref.read(apiClientProvider); await api.post(Endpoints.adminAgentRun(name)); } catch (_) {} } void _runAllAgents(WidgetRef ref) async { try { final api = ref.read(apiClientProvider); await api.post(Endpoints.adminAgentsRunAll); } catch (_) {} } } class _LogsTab extends ConsumerWidget { const _LogsTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder>( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminLogs); return (resp.data as List) .map((e) => LogEntry.fromJson(e as Map)) .toList(); }, builder: (logs) => ListView.builder( padding: const EdgeInsets.all(16), itemCount: logs.length, itemBuilder: (_, i) { final log = logs[i]; final color = log.level == 'ERROR' ? VoIdeaColors.error : log.level == 'WARNING' ? VoIdeaColors.warning : VoIdeaColors.muted; return Card( margin: const EdgeInsets.only(bottom: 4), child: ListTile( dense: true, leading: Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), title: Text(log.message, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13)), subtitle: Row( children: [ Text(log.level, style: TextStyle(fontSize: 11, color: color)), const SizedBox(width: 8), if (log.source != null) Text(log.source!, style: const TextStyle(fontSize: 11, color: VoIdeaColors.muted)), const Spacer(), Text(DateFormat('HH:mm:ss').format(log.createdAt), style: const TextStyle(fontSize: 11, color: VoIdeaColors.muted)), ], ), ), ); }, ), ); } } class _TariffsTab extends ConsumerWidget { const _TariffsTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder>( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminTariffs); return (resp.data as List) .map((e) => Tariff.fromJson(e as Map)) .toList(); }, builder: (tariffs) => ListView.builder( padding: const EdgeInsets.all(16), itemCount: tariffs.length, itemBuilder: (_, i) { final t = tariffs[i]; return Card( margin: const EdgeInsets.only(bottom: 12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(t.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, )), const Spacer(), Text('${t.price} ${t.currency}/мес', style: TextStyle( color: VoIdeaColors.primary, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 8), ...t.features.entries.map((e) => Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ Icon(Icons.check, size: 14, color: VoIdeaColors.success), const SizedBox(width: 8), Text('${e.key}: ${e.value}', style: const TextStyle(fontSize: 13)), ], ), )), ], ), ), ); }, ), ); } } class _BotTab extends ConsumerWidget { const _BotTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder>( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminBotCommands); return (resp.data as List) .map((e) => BotCommand.fromJson(e as Map)) .toList(); }, builder: (commands) => ListView.builder( padding: const EdgeInsets.all(16), itemCount: commands.length, itemBuilder: (_, i) { final cmd = commands[i]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( cmd.isEnabled ? Icons.check_circle : Icons.cancel, color: cmd.isEnabled ? VoIdeaColors.success : VoIdeaColors.error, ), title: Text('/${cmd.command}'), subtitle: Text(cmd.description, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: Switch( value: cmd.isEnabled, onChanged: (v) => _toggleCommand(ref, cmd.id, v), ), ), ); }, ), ); } void _toggleCommand(WidgetRef ref, String id, bool enabled) async { try { final api = ref.read(apiClientProvider); await api.patch('${Endpoints.adminBotCommands}/$id', data: { 'is_enabled': enabled, }); } catch (_) {} } } class _ServicesTab extends ConsumerWidget { const _ServicesTab(); @override Widget build(BuildContext context, WidgetRef ref) { return _FutureBuilder( future: () async { final api = ref.read(apiClientProvider); final resp = await api.get(Endpoints.adminHealth); return HealthStatus.fromJson(resp.data as Map); }, builder: (health) => ListView( padding: const EdgeInsets.all(16), children: [ Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( health.status == 'ok' ? Icons.check_circle : Icons.error, color: health.status == 'ok' ? VoIdeaColors.success : VoIdeaColors.error, ), const SizedBox(width: 8), Text('Статус: ${health.status}', style: Theme.of(context).textTheme.titleMedium), ], ), const SizedBox(height: 12), Text('Версия: ${health.version}', style: const TextStyle(fontSize: 13)), const SizedBox(height: 4), Text('Uptime: ${health.uptimeSeconds.toStringAsFixed(0)}с', style: const TextStyle(fontSize: 13)), ], ), ), ), const SizedBox(height: 16), const Text('Сервисы', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)), const SizedBox(height: 8), ...health.services.entries.map((e) => Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( e.value == 'ok' || e.value == true ? Icons.check_circle : Icons.error, color: e.value == 'ok' || e.value == true ? VoIdeaColors.success : VoIdeaColors.error, size: 20, ), title: Text(e.key), subtitle: Text('${e.value}'), ), )), ], ), ); } } class _FutureBuilder extends StatelessWidget { final Future Function() future; final Widget Function(T data) builder; const _FutureBuilder({required this.future, required this.builder}); @override Widget build(BuildContext context) { return FutureBuilder( future: future(), builder: (ctx, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const LoadingIndicator(); } if (snapshot.hasError) { return ErrorDisplay(message: snapshot.error.toString()); } if (!snapshot.hasData) { return const EmptyState( icon: Icons.info_outline, title: 'Нет данных'); } return builder(snapshot.data!); }, ); } }