From 10e4af9aa464d2d149fe884f8a3215217a58fdc5 Mon Sep 17 00:00:00 2001 From: 230404 <230404@epvc.pt> Date: Thu, 8 Jan 2026 10:41:31 +0000 Subject: [PATCH] melhorar o tem por filtro de ano --- lib/controllers/team_controllers.dart | 44 +++++++ lib/models/team_model.dart | 7 +- lib/pages/teams_page.dart | 92 +++++--------- lib/service/auth_service.dart | 19 +++ lib/widgets/team_widgets.dart | 171 ++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 65 deletions(-) create mode 100644 lib/controllers/team_controllers.dart create mode 100644 lib/service/auth_service.dart create mode 100644 lib/widgets/team_widgets.dart diff --git a/lib/controllers/team_controllers.dart b/lib/controllers/team_controllers.dart new file mode 100644 index 0000000..828d090 --- /dev/null +++ b/lib/controllers/team_controllers.dart @@ -0,0 +1,44 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:playmaker/service/auth_service.dart'; + +class TeamController { + final AuthService _authService = AuthService(); + final CollectionReference _teamsRef = FirebaseFirestore.instance.collection('teams'); + + + Stream get teamsStream { + final uid = _authService.currentUid; + return _teamsRef + .where('userId', isEqualTo: uid) + .orderBy('createdAt', descending: true) + .snapshots(); + } + + // --- CRIAR EQUIPA --- + Future createTeam(String name, String season, String imageUrl) async { + final uid = _authService.currentUid; + + if (uid != null) { + try { + await _teamsRef.add({ + 'name': name, + 'season': season, + 'imageUrl': imageUrl, + 'userId': uid, + 'createdAt': FieldValue.serverTimestamp(), + }); + } catch (e) { + print("Erro ao criar equipa: $e"); + } + } else { + print("Erro: Utilizador não autenticado."); + } + } + Future deleteTeam(String docId) async { + try { + await _teamsRef.doc(docId).delete(); + } catch (e) { + print("Erro ao eliminar: $e"); + } + } +} \ No newline at end of file diff --git a/lib/models/team_model.dart b/lib/models/team_model.dart index b2dcde4..5494a79 100644 --- a/lib/models/team_model.dart +++ b/lib/models/team_model.dart @@ -1,14 +1,17 @@ class Team { final String id; final String name; + final String season; // NOVO + final String imageUrl; // NOVO - Team({required this.id, required this.name}); + Team({required this.id, required this.name, required this.season, required this.imageUrl}); - // Converte de Firebase para o Flutter factory Team.fromFirestore(Map data, String id) { return Team( id: id, name: data['name'] ?? '', + season: data['season'] ?? '', + imageUrl: data['imageUrl'] ?? '', ); } } \ No newline at end of file diff --git a/lib/pages/teams_page.dart b/lib/pages/teams_page.dart index 2beef3b..9e61431 100644 --- a/lib/pages/teams_page.dart +++ b/lib/pages/teams_page.dart @@ -1,93 +1,59 @@ import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import '../models/team_model.dart'; // Importa o teu modelo +import 'package:playmaker/controllers/team_controllers.dart'; +import '../models/team_model.dart'; +import '../widgets/team_widgets.dart'; class TeamsPage extends StatelessWidget { const TeamsPage({super.key}); - // Função para abrir o Pop-up - void _showCreateTeamDialog(BuildContext context) { - final TextEditingController _nameController = TextEditingController(); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Nova Equipa'), - content: TextField( - controller: _nameController, - decoration: const InputDecoration(hintText: 'Nome da Equipa'), - autofocus: true, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancelar'), - ), - ElevatedButton( - onPressed: () async { - if (_nameController.text.isNotEmpty) { - // Guarda no Firebase - await FirebaseFirestore.instance.collection('teams').add({ - 'name': _nameController.text, - 'createdAt': FieldValue.serverTimestamp(), - }); - Navigator.pop(context); - } - }, - style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C)), - child: const Text('Criar', style: TextStyle(color: Colors.white)), - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { + // Instância única do controlador para esta página + final TeamController controller = TeamController(); + return Scaffold( - // Removi a AppBar porque a HomeScreen já tem uma, evita barra dupla body: StreamBuilder( - // Escuta o Firebase em tempo real - stream: FirebaseFirestore.instance.collection('teams').orderBy('createdAt', descending: true).snapshots(), + stream: controller.teamsStream, builder: (context, snapshot) { if (snapshot.hasError) return const Center(child: Text('Erro ao carregar')); - if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } final docs = snapshot.data!.docs; - - if (docs.isEmpty) { - return const Center(child: Text('Nenhuma equipa criada.')); - } + if (docs.isEmpty) return const Center(child: Text('Nenhuma equipa criada.')); return ListView.builder( padding: const EdgeInsets.all(16), itemCount: docs.length, itemBuilder: (context, index) { - final team = Team.fromFirestore(docs[index].data() as Map, docs[index].id); - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Color(0xFFE74C3C), - child: Icon(Icons.groups, color: Colors.white), - ), - title: Text(team.name, style: const TextStyle(fontWeight: FontWeight.bold)), - trailing: const Icon(Icons.chevron_right), - onTap: () { - // Navegar para detalhes da equipa no futuro - }, - ), + final team = Team.fromFirestore( + docs[index].data() as Map, + docs[index].id ); + + // CORREÇÃO 1: Passar o team E o controller para o Card + return TeamCard(team: team, controller: controller); }, ); }, ), floatingActionButton: FloatingActionButton( - onPressed: () => _showCreateTeamDialog(context), backgroundColor: const Color(0xFFE74C3C), child: const Icon(Icons.add, color: Colors.white), + onPressed: () { + showDialog( + context: context, + builder: (context) => CreateTeamDialog( + // CORREÇÃO 2: Receber os 3 parâmetros do formulário + onConfirm: (name, season, imageUrl) { + // Passar os 3 para a função do controlador + controller.createTeam(name, season, imageUrl); + }, + ), + ); + }, ), ); } diff --git a/lib/service/auth_service.dart b/lib/service/auth_service.dart new file mode 100644 index 0000000..6002b7f --- /dev/null +++ b/lib/service/auth_service.dart @@ -0,0 +1,19 @@ +import 'package:firebase_auth/firebase_auth.dart'; + +class AuthService { + final FirebaseAuth _auth = FirebaseAuth.instance; + + // Retorna o ID do utilizador atual + String? get currentUid => _auth.currentUser?.uid; + + // Retorna o email do utilizador (útil para mostrar no perfil) + String? get currentUserEmail => _auth.currentUser?.email; + + // Verifica se o utilizador está logado + bool get isLoggedIn => _auth.currentUser != null; + + // Função para fazer Logout (Sair) + Future signOut() async { + await _auth.signOut(); + } +} \ No newline at end of file diff --git a/lib/widgets/team_widgets.dart b/lib/widgets/team_widgets.dart new file mode 100644 index 0000000..66ed64d --- /dev/null +++ b/lib/widgets/team_widgets.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import '../models/team_model.dart'; +import '../controllers/team_controllers.dart'; + +class TeamCard extends StatelessWidget { + final Team team; + final TeamController controller; // Recebe o controlador por parâmetro + + const TeamCard({ + super.key, + required this.team, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 3, + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + + // 1. LEADING (Lado Esquerdo): Logótipo ou Emoji + leading: CircleAvatar( + backgroundColor: Colors.grey[200], + backgroundImage: (team.imageUrl.startsWith('http')) + ? NetworkImage(team.imageUrl) + : null, + child: (!team.imageUrl.startsWith('http')) + ? Text( + team.imageUrl.isEmpty ? "🏀" : team.imageUrl, + style: const TextStyle(fontSize: 20), + ) + : null, + ), + + // 2. TÍTULO E SUBTÍTULO (Centro) + title: Text( + team.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text("Temporada: ${team.season}"), + + // 3. TRAILING (Lado Direito): Botões de Status e Eliminar + trailing: SizedBox( + width: 90, // Espaço fixo para os dois ícones não quebrarem a linha + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Botão Status + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.bar_chart, color: Colors.blue), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("A abrir status de ${team.name}...")), + ); + }, + ), + const SizedBox(width: 12), + // Botão Eliminar + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.delete_outline, color: Color(0xFFE74C3C)), + onPressed: () => _confirmDelete(context), + ), + ], + ), + ), + ), + ); + } + + void _confirmDelete(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Eliminar Equipa?'), + content: Text('Tens a certeza que queres eliminar "${team.name}"? Esta ação não pode ser desfeita.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancelar'), + ), + TextButton( + onPressed: () { + controller.deleteTeam(team.id); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Equipa eliminada com sucesso")), + ); + }, + child: const Text('Eliminar', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } +} + +// --- WIDGET DO POPUP (CREATE TEAM DIALOG) --- +class CreateTeamDialog extends StatefulWidget { + final Function(String name, String season, String imageUrl) onConfirm; + + const CreateTeamDialog({super.key, required this.onConfirm}); + + @override + State createState() => _CreateTeamDialogState(); +} + +class _CreateTeamDialogState extends State { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _imageController = TextEditingController(); + String _selectedSeason = '2024/25'; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Nova Equipa'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _nameController, + decoration: const InputDecoration(labelText: 'Nome da Equipa'), + ), + const SizedBox(height: 15), + DropdownButtonFormField( + value: _selectedSeason, + decoration: const InputDecoration(labelText: 'Temporada'), + items: ['2023/24', '2024/25', '2025/26'].map((s) { + return DropdownMenuItem(value: s, child: Text(s)); + }).toList(), + onChanged: (val) => setState(() => _selectedSeason = val!), + ), + const SizedBox(height: 15), + TextField( + controller: _imageController, + decoration: const InputDecoration( + labelText: 'URL do Logótipo ou Emoji', + hintText: 'Ex: 🏀 ou link http', + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancelar'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C)), + onPressed: () { + widget.onConfirm( + _nameController.text, + _selectedSeason, + _imageController.text + ); + Navigator.pop(context); + }, + child: const Text('Criar', style: TextStyle(color: Colors.white)), + ), + ], + ); + } +} \ No newline at end of file