445 lines
15 KiB
Dart
445 lines
15 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../../../core/services/auth_service.dart';
|
|
import '../../../../features/materials/presentation/pages/teacher_materials_page.dart';
|
|
import 'dashboard_action_card.dart';
|
|
|
|
/// Quick access cards for teacher actions
|
|
class TeacherQuickActionsWidget extends StatefulWidget {
|
|
const TeacherQuickActionsWidget({super.key});
|
|
|
|
@override
|
|
State<TeacherQuickActionsWidget> createState() =>
|
|
_TeacherQuickActionsWidgetState();
|
|
}
|
|
|
|
class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
|
bool _isCreatingClass = false;
|
|
|
|
/// Mesmas dimensões dos cards em "As Minhas Disciplinas".
|
|
static const double _scrollCardWidth = 200;
|
|
static const double _scrollRowHeight = 150;
|
|
static const double _cardMinHeight = 150;
|
|
static const EdgeInsets _cardPadding = EdgeInsets.all(16);
|
|
static const double _titleFontSize = 16;
|
|
static const double _subtitleFontSize = 13;
|
|
static const double _iconSize = 24;
|
|
static const double _iconPadding = 10;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cards = [
|
|
_buildUploadContentCard(context),
|
|
_buildCreateClassCard(context),
|
|
_buildCreateQuizCard(context),
|
|
_buildViewAnalyticsCard(context),
|
|
];
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Ações Rápidas',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: _scrollRowHeight,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
clipBehavior: Clip.none,
|
|
padding: const EdgeInsets.only(right: 16),
|
|
itemCount: cards.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
itemBuilder: (context, index) =>
|
|
SizedBox(width: _scrollCardWidth, child: cards[index]),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildUploadContentCard(BuildContext context) {
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: DashboardActionCard(
|
|
title: 'Upload Conteúdo',
|
|
subtitle: 'PDFs, textos, imagens',
|
|
icon: Icons.upload_file,
|
|
useGradient: true,
|
|
minHeight: _cardMinHeight,
|
|
iconSize: _iconSize,
|
|
iconPadding: _iconPadding,
|
|
titleFontSize: _titleFontSize,
|
|
subtitleFontSize: _subtitleFontSize,
|
|
padding: _cardPadding,
|
|
onTap: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const TeacherMaterialsPage()),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateQuizCard(BuildContext context) {
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: DashboardActionCardSurface(
|
|
title: 'Criar Quiz',
|
|
subtitle: 'Avaliações interativas',
|
|
icon: Icons.quiz,
|
|
minHeight: _cardMinHeight,
|
|
titleFontSize: _titleFontSize,
|
|
subtitleFontSize: _subtitleFontSize,
|
|
iconSize: _iconSize,
|
|
padding: _cardPadding,
|
|
onTap: () => context.go('/teacher/quiz/create'),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildViewAnalyticsCard(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
return DashboardActionCardSurface(
|
|
title: 'Analytics',
|
|
subtitle: 'Desempenho da disciplina',
|
|
icon: Icons.analytics,
|
|
minHeight: _cardMinHeight,
|
|
titleFontSize: _titleFontSize,
|
|
subtitleFontSize: _subtitleFontSize,
|
|
iconSize: _iconSize,
|
|
padding: _cardPadding,
|
|
iconColor: cs.primary,
|
|
leadingWidget: Container(
|
|
padding: const EdgeInsets.all(_iconPadding),
|
|
decoration: BoxDecoration(
|
|
color: cs.primary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(Icons.analytics, color: cs.primary, size: _iconSize),
|
|
),
|
|
onTap: () => context.go('/teacher/analytics'),
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateClassCard(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: DashboardActionCardSurface(
|
|
title: 'Criar Disciplina',
|
|
subtitle: 'Gerar código de acesso',
|
|
icon: Icons.school,
|
|
minHeight: _cardMinHeight,
|
|
titleFontSize: _titleFontSize,
|
|
subtitleFontSize: _subtitleFontSize,
|
|
iconSize: _iconSize,
|
|
padding: _cardPadding,
|
|
iconColor: cs.primary,
|
|
onTapDisabled: _isCreatingClass,
|
|
onTap: () => _showCreateClassDialog(context),
|
|
leadingWidget: Container(
|
|
padding: const EdgeInsets.all(_iconPadding),
|
|
decoration: BoxDecoration(
|
|
color: cs.primary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: _isCreatingClass
|
|
? SizedBox(
|
|
width: _iconSize,
|
|
height: _iconSize,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: cs.primary,
|
|
),
|
|
)
|
|
: Icon(Icons.school, color: cs.primary, size: _iconSize),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCreateClassDialog(BuildContext context) {
|
|
final TextEditingController nameController = TextEditingController();
|
|
String? selectedSchoolClassId;
|
|
List<Map<String, String>> schoolClasses = [];
|
|
bool isLoadingClasses = true;
|
|
|
|
// Carregar school_classes antes de mostrar o dialog
|
|
FirebaseFirestore.instance
|
|
.collection('school_classes')
|
|
.where('active', isEqualTo: true)
|
|
.orderBy('year')
|
|
.orderBy('section')
|
|
.get()
|
|
.then((snapshot) {
|
|
schoolClasses = snapshot.docs.map((doc) {
|
|
final data = doc.data();
|
|
return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)};
|
|
}).toList();
|
|
isLoadingClasses = false;
|
|
})
|
|
.catchError((_) {
|
|
isLoadingClasses = false;
|
|
});
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext dialogContext) {
|
|
return StatefulBuilder(
|
|
builder: (context, setDialogState) {
|
|
// Atualizar estado do dialog quando as classes carregarem
|
|
if (isLoadingClasses) {
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
if (dialogContext.mounted) setDialogState(() {});
|
|
});
|
|
}
|
|
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
title: Text(
|
|
'Criar Nova Disciplina',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Nome da disciplina:',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: nameController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Ex: Matemática, Física...',
|
|
filled: true,
|
|
fillColor: Theme.of(
|
|
context,
|
|
).colorScheme.surfaceContainerHighest,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Ano letivo:',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
isLoadingClasses
|
|
? Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
'A carregar disciplinas...',
|
|
style: TextStyle(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.onSurfaceVariant,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: DropdownButtonFormField<String>(
|
|
value: selectedSchoolClassId,
|
|
isExpanded: true,
|
|
decoration: InputDecoration(
|
|
filled: true,
|
|
fillColor: Theme.of(
|
|
context,
|
|
).colorScheme.surfaceContainerHighest,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
width: 2,
|
|
),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 10,
|
|
),
|
|
),
|
|
hint: Text(
|
|
'Seleciona o ano letivo',
|
|
style: TextStyle(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
items: schoolClasses
|
|
.map(
|
|
(c) => DropdownMenuItem<String>(
|
|
value: c['id'],
|
|
child: Text(c['name']!),
|
|
),
|
|
)
|
|
.toList(),
|
|
onChanged: (value) => setDialogState(
|
|
() => selectedSchoolClassId = value,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: Text(
|
|
'Cancelar',
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
final className = nameController.text.trim();
|
|
if (className.isNotEmpty) {
|
|
Navigator.of(dialogContext).pop();
|
|
_createClass(
|
|
className,
|
|
schoolClassId: selectedSchoolClassId,
|
|
);
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
child: const Text('Criar'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
String _generateClassCode() {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
final random = Random();
|
|
return String.fromCharCodes(
|
|
Iterable.generate(
|
|
6,
|
|
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _createClass(String className, {String? schoolClassId}) async {
|
|
setState(() {
|
|
_isCreatingClass = true;
|
|
});
|
|
|
|
try {
|
|
final currentUser = AuthService.currentUser;
|
|
if (currentUser == null) {
|
|
throw Exception('Utilizador não autenticado');
|
|
}
|
|
|
|
final classCode = _generateClassCode();
|
|
final firestore = FirebaseFirestore.instance;
|
|
|
|
final Map<String, dynamic> classData = {
|
|
'name': className,
|
|
'teacherId': currentUser.uid,
|
|
'code': classCode,
|
|
'createdAt': FieldValue.serverTimestamp(),
|
|
};
|
|
if (schoolClassId != null && schoolClassId.isNotEmpty) {
|
|
classData['schoolClassId'] = schoolClassId;
|
|
}
|
|
|
|
await firestore.collection('classes').add(classData);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Disciplina "$className" criada com sucesso! Código: $classCode',
|
|
),
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erro ao criar disciplina: $e'),
|
|
backgroundColor: Colors.red,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
setState(() {
|
|
_isCreatingClass = false;
|
|
});
|
|
}
|
|
}
|
|
}
|