import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; class ClassStudentsInlineWidget extends StatefulWidget { final String classId; final String className; const ClassStudentsInlineWidget({ super.key, required this.classId, required this.className, }); @override State createState() => _ClassStudentsInlineWidgetState(); } class _ClassStudentsInlineWidgetState extends State { final _searchController = TextEditingController(); String _searchQuery = ''; String? _classCode; late Stream _enrollmentsStream; @override void initState() { super.initState(); _loadClassCode(); _initStream(); } @override void didUpdateWidget(ClassStudentsInlineWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.classId != widget.classId) { _loadClassCode(); _initStream(); } } @override void dispose() { _searchController.dispose(); super.dispose(); } void _initStream() { _enrollmentsStream = FirebaseFirestore.instance .collection('enrollments') .where('classId', isEqualTo: widget.classId) .snapshots(); } Future _loadClassCode() async { final doc = await FirebaseFirestore.instance .collection('classes') .doc(widget.classId) .get(); if (mounted) { setState(() => _classCode = doc.data()?['code'] as String? ?? '—'); } } Future _confirmRemove( String enrollmentDocId, String studentName, ) async { final cs = Theme.of(context).colorScheme; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text('Remover aluno'), content: Text( 'Tens a certeza que queres remover "$studentName" desta turma?', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancelar'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: cs.error), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Remover'), ), ], ), ); if (confirmed != true) return; await _deleteEnrollment(enrollmentDocId, studentName); } Future _deleteEnrollment( String enrollmentDocId, String studentName, ) async { final cs = Theme.of(context).colorScheme; try { await FirebaseFirestore.instance .collection('enrollments') .doc(enrollmentDocId) .delete(); if (!mounted) return; ScaffoldMessenger.of(context) ..clearSnackBars() ..showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), backgroundColor: cs.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), content: Text( '"$studentName" foi removido da turma.', style: const TextStyle(color: Colors.white), ), ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Erro ao remover: $e'))); } } // ── Build ────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return StreamBuilder( stream: _enrollmentsStream, builder: (context, snapshot) { final docs = snapshot.data?.docs ?? []; final enrollments = List.from(docs) ..sort((a, b) { final aTs = (a.data() as Map)['joinedAt'] as Timestamp?; final bTs = (b.data() as Map)['joinedAt'] as Timestamp?; if (aTs == null && bTs == null) return 0; if (aTs == null) return 1; if (bTs == null) return -1; return aTs.compareTo(bTs); }); final filtered = _searchQuery.isEmpty ? enrollments : enrollments.where((doc) { final d = doc.data() as Map; final name = (d['studentName'] as String? ?? '').toLowerCase(); final email = (d['studentEmail'] as String? ?? '') .toLowerCase(); return name.contains(_searchQuery) || email.contains(_searchQuery); }).toList(); final bottomInset = MediaQuery.of(context).viewInsets.bottom; return CustomScrollView( physics: const NeverScrollableScrollPhysics(), slivers: [ SliverPadding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), sliver: SliverToBoxAdapter( child: _buildHeader(cs, enrollments.length, snapshot), ), ), SliverPadding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 0), sliver: SliverToBoxAdapter(child: _buildSearchBar()), ), if (snapshot.connectionState == ConnectionState.waiting && snapshot.data == null) const SliverFillRemaining( child: Center( child: CircularProgressIndicator(color: Colors.white), ), ) else if (filtered.isEmpty) ...[ SliverFillRemaining( hasScrollBody: false, child: Padding( padding: EdgeInsets.only(bottom: bottomInset), child: _buildEmpty(cs), ), ), ] else SliverPadding( padding: EdgeInsets.fromLTRB(20, 14, 20, 20 + bottomInset), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { final doc = filtered[index]; final data = doc.data() as Map; return Padding( padding: const EdgeInsets.only(bottom: 10), child: _buildStudentCard( cs: cs, enrollmentDocId: doc.id, studentName: data['studentName'] as String? ?? 'Aluno sem nome', studentEmail: data['studentEmail'] as String? ?? '', joinedAt: data['joinedAt'] as Timestamp?, ), ); }, childCount: filtered.length), ), ), ], ); }, ); } // ── Sub-widgets ──────────────────────────────────────────────────────────── Widget _buildHeader( ColorScheme cs, int count, AsyncSnapshot snapshot, ) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: LinearGradient( colors: [cs.primary, cs.primary.withValues(alpha: 0.8)], ), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.className, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( snapshot.connectionState == ConnectionState.waiting && snapshot.data == null ? 'A carregar…' : '$count aluno${count == 1 ? '' : 's'} inscrito${count == 1 ? '' : 's'}', style: TextStyle( color: Colors.white.withValues(alpha: 0.85), fontSize: 13, ), ), ], ), ), _buildCodeBadge(cs), ], ), ); } Widget _buildCodeBadge(ColorScheme cs) { return GestureDetector( onTap: () { if (_classCode == null || _classCode == '—') return; Clipboard.setData(ClipboardData(text: _classCode!)); ScaffoldMessenger.of(context) ..clearSnackBars() ..showSnackBar( SnackBar( behavior: SnackBarBehavior.floating, margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), backgroundColor: cs.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), duration: const Duration(seconds: 2), content: const Text( 'Código copiado!', style: TextStyle(color: Colors.white), ), ), ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withValues(alpha: 0.3)), ), child: Column( children: [ const Text( 'Código', style: TextStyle( color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( _classCode ?? '…', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2, ), ), const SizedBox(height: 2), Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.copy, color: Colors.white.withValues(alpha: 0.7), size: 10, ), const SizedBox(width: 3), Text( 'copiar', style: TextStyle( color: Colors.white.withValues(alpha: 0.7), fontSize: 9, ), ), ], ), ], ), ), ); } Widget _buildSearchBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withValues(alpha: 0.2)), ), child: Row( children: [ Icon( Icons.search, color: Colors.white.withValues(alpha: 0.7), size: 20, ), const SizedBox(width: 10), Expanded( child: Theme( data: ThemeData.dark().copyWith( textSelectionTheme: const TextSelectionThemeData( cursorColor: Colors.white, selectionColor: Colors.white24, selectionHandleColor: Colors.white, ), ), child: TextField( controller: _searchController, onChanged: (v) => setState(() => _searchQuery = v.trim().toLowerCase()), style: const TextStyle(color: Colors.white, fontSize: 14), cursorColor: Colors.white, decoration: InputDecoration( hintText: 'Pesquisar aluno…', hintStyle: TextStyle( color: Colors.white.withValues(alpha: 0.5), fontSize: 14, ), border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, ), ), ), ), if (_searchQuery.isNotEmpty) GestureDetector( onTap: () { _searchController.clear(); setState(() => _searchQuery = ''); }, child: Icon( Icons.close, color: Colors.white.withValues(alpha: 0.7), size: 18, ), ), ], ), ); } Widget _buildStudentCard({ required ColorScheme cs, required String enrollmentDocId, required String studentName, required String studentEmail, required Timestamp? joinedAt, }) { return Dismissible( key: Key(enrollmentDocId), direction: DismissDirection.endToStart, confirmDismiss: (_) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: const Text('Remover aluno'), content: Text( 'Tens a certeza que queres remover "$studentName" desta turma?', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancelar'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: cs.error), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Remover'), ), ], ), ); return confirmed ?? false; }, onDismissed: (_) => _deleteEnrollment(enrollmentDocId, studentName), background: Container( decoration: BoxDecoration( color: cs.error.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(14), ), alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), child: const Icon(Icons.delete_outline, color: Colors.white, size: 24), ), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withValues(alpha: 0.18)), ), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( studentName.isNotEmpty ? studentName[0].toUpperCase() : '?', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( studentName, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), if (studentEmail.isNotEmpty) ...[ const SizedBox(height: 2), Text( studentEmail, style: TextStyle( color: Colors.white.withValues(alpha: 0.65), fontSize: 12, ), ), ], if (joinedAt != null) ...[ const SizedBox(height: 2), Text( 'Entrou em ${DateFormat('dd/MM/yyyy').format(joinedAt.toDate())}', style: TextStyle( color: Colors.white.withValues(alpha: 0.5), fontSize: 11, ), ), ], ], ), ), IconButton( icon: Icon( Icons.person_remove_outlined, color: cs.error.withValues(alpha: 0.85), size: 20, ), tooltip: 'Remover aluno', onPressed: () => _confirmRemove(enrollmentDocId, studentName), ), ], ), ), ); } Widget _buildEmpty(ColorScheme cs) { if (_searchQuery.isNotEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search_off, size: 48, color: Colors.white.withValues(alpha: 0.4), ), const SizedBox(height: 12), Text( 'Nenhum aluno encontrado', style: TextStyle( color: Colors.white.withValues(alpha: 0.7), fontSize: 16, ), ), ], ), ); } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.people_outline, size: 56, color: Colors.white.withValues(alpha: 0.35), ), const SizedBox(height: 14), Text( 'Nenhum aluno inscrito', style: TextStyle( color: Colors.white.withValues(alpha: 0.7), fontSize: 16, ), ), const SizedBox(height: 6), Text( 'Partilha o código da turma com os alunos.', style: TextStyle( color: Colors.white.withValues(alpha: 0.45), fontSize: 13, ), textAlign: TextAlign.center, ), ], ), ); } }