import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; /// Экран управления гостевыми API-ключами. /// Доступен только администраторам. class ApiKeysScreen extends ConsumerStatefulWidget { const ApiKeysScreen({super.key}); @override ConsumerState createState() => _ApiKeysScreenState(); } class _ApiKeysScreenState extends ConsumerState { bool _loading = true; String? _lastCreatedKey; @override void initState() { super.initState(); _load(); } Future _load() async { await ref.read(apiKeysProvider.notifier).load(); if (mounted) setState(() => _loading = false); } @override Widget build(BuildContext context) { final keys = ref.watch(apiKeysProvider); return Scaffold( appBar: AppBar( title: const Text('API-КЛЮЧИ'), ), body: _loading ? const Center( child: CircularProgressIndicator(color: Colors.deepOrange), ) : Column( children: [ // ─── Последний созданный ключ (для копирования) ─── if (_lastCreatedKey != null) Container( margin: const EdgeInsets.all(12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.deepOrange.withOpacity(0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.deepOrange.withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Новый ключ создан! Скопируйте его сейчас:', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.deepOrange, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: Text( _lastCreatedKey!, style: const TextStyle( fontSize: 12, fontFamily: 'monospace', color: Colors.white70, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.copy, size: 20), onPressed: () { Clipboard.setData( ClipboardData(text: _lastCreatedKey!)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ скопирован'), duration: Duration(seconds: 1), ), ); }, ), ], ), ], ), ), // ─── Список ключей ─── Expanded( child: keys.isEmpty ? const Center( child: Text( 'Нет гостевых ключей', style: TextStyle(color: Colors.white54), ), ) : RefreshIndicator( color: Colors.deepOrange, onRefresh: _load, child: ListView.builder( padding: const EdgeInsets.all(12), itemCount: keys.length, itemBuilder: (context, index) { final k = keys[index]; final map = k is Map ? Map.from(k) : {}; return _ApiKeyCard( data: map, onRevoke: () => _revokeKey(map), onActivate: () => _activateKey(map), ); }, ), ), ), ], ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.deepOrange, onPressed: () => _showCreateDialog(context), child: const Icon(Icons.add), ), ); } void _showCreateDialog(BuildContext context) { final nameCtrl = TextEditingController(); bool isAdmin = false; showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( title: const Text('Новый API-ключ'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameCtrl, decoration: const InputDecoration( labelText: 'Имя ключа', hintText: 'Например: "Гость"', ), autofocus: true, ), const SizedBox(height: 12), SwitchListTile( title: const Text('Администратор'), value: isAdmin, activeColor: Colors.deepOrange, onChanged: (v) => setDialogState(() => isAdmin = v), contentPadding: EdgeInsets.zero, ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Отмена'), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.deepOrange, ), onPressed: () async { final name = nameCtrl.text.trim(); if (name.isEmpty) return; Navigator.of(ctx).pop(); final key = await ref .read(apiKeysProvider.notifier) .create(name, isAdmin: isAdmin); if (key != null && mounted) { setState(() => _lastCreatedKey = key); } }, child: const Text('Создать'), ), ], ), ), ); } Future _revokeKey(Map data) async { final key = (data['key'] ?? data['token'] ?? '').toString(); final name = (data['name'] ?? '').toString(); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Отозвать ключ?'), content: Text('Отозвать "$name"?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Отмена'), ), TextButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Отозвать', style: TextStyle(color: Colors.redAccent)), ), ], ), ); if (confirmed == true) { await ref.read(apiKeysProvider.notifier).revoke(key); } } Future _activateKey(Map data) async { final key = (data['key'] ?? data['token'] ?? '').toString(); await ref.read(apiKeysProvider.notifier).activate(key); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ключ активирован'), duration: Duration(seconds: 1), ), ); } } } /// Карточка одного API-ключа class _ApiKeyCard extends StatelessWidget { final Map data; final VoidCallback onRevoke; final VoidCallback onActivate; const _ApiKeyCard({ required this.data, required this.onRevoke, required this.onActivate, }); @override Widget build(BuildContext context) { final name = (data['name'] ?? 'Без имени').toString(); final isAdmin = data['is_admin'] == true; final isActive = data['is_active'] ?? data['active'] ?? true; final createdAt = data['created_at'] ?? ''; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( Icons.vpn_key, color: isActive ? (isAdmin ? Colors.amber : Colors.deepOrange) : Colors.white24, ), title: Row( children: [ Text( name, style: TextStyle( fontWeight: FontWeight.bold, color: isActive ? Colors.white : Colors.white38, ), ), if (isAdmin) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.amber.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( 'admin', style: TextStyle(fontSize: 10, color: Colors.amber), ), ), ], if (!isActive) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.redAccent.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: const Text( 'отозван', style: TextStyle(fontSize: 10, color: Colors.redAccent), ), ), ], ], ), subtitle: createdAt.toString().isNotEmpty ? Text( 'Создан: ${_formatDate(createdAt.toString())}', style: const TextStyle(fontSize: 11, color: Colors.white30), ) : null, trailing: isActive ? IconButton( icon: const Icon(Icons.block, size: 20, color: Colors.redAccent), tooltip: 'Отозвать', onPressed: onRevoke, ) : IconButton( icon: const Icon(Icons.check_circle_outline, size: 20, color: Colors.green), tooltip: 'Активировать', onPressed: onActivate, ), ), ); } String _formatDate(String iso) { try { final d = DateTime.parse(iso); final pad = (int n) => n.toString().padLeft(2, '0'); return '${pad(d.day)}.${pad(d.month)}.${d.year}'; } catch (_) { return iso; } } }