From dc3a9723a434ac42b1c91a399dae90058ab89223 Mon Sep 17 00:00:00 2001 From: 230404 <230404@epvc.pt> Date: Tue, 27 Jan 2026 17:17:31 +0000 Subject: [PATCH] continuar a team_page --- lib/controllers/team_controller.dart | 56 +++++ lib/controllers/team_controllers.dart | 72 +----- .../controllers/contollers_grafico.dart | 56 ++--- lib/models/team_model.dart | 30 ++- lib/pages/home.dart | 75 +----- lib/pages/teamPage.dart | 84 +++++++ lib/pages/teams_page.dart | 58 ----- lib/screens/team_stats_page.dart | 220 ++++++++++++++---- lib/widgets/stats_widgets.dart | 90 ------- lib/widgets/team_widgets.dart | 200 ++++++++-------- 10 files changed, 469 insertions(+), 472 deletions(-) create mode 100644 lib/controllers/team_controller.dart create mode 100644 lib/pages/teamPage.dart delete mode 100644 lib/pages/teams_page.dart delete mode 100644 lib/widgets/stats_widgets.dart diff --git a/lib/controllers/team_controller.dart b/lib/controllers/team_controller.dart new file mode 100644 index 0000000..565bb39 --- /dev/null +++ b/lib/controllers/team_controller.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +class TeamController { + // --- BASE DE DADOS LOCAL (Listas Estáticas) --- + static final List> _teams = []; + + // Lista de jogadores (necessária para a contagem funcionar) + static final List> _members = []; + + // Getter para partilhar com outros controllers + static List> get members => _members; + + final _streamController = StreamController>>.broadcast(); + + // 1. STREAM + Stream>> get teamsStream { + Future.microtask(() => _streamController.add(_teams)); + return _streamController.stream; + } + + // 2. CRIAR + Future createTeam(String name, String season, String imageUrl) async { + await Future.delayed(const Duration(milliseconds: 100)); + final newTeam = { + 'id': DateTime.now().millisecondsSinceEpoch.toString(), + 'name': name, + 'season': season, + 'image_url': imageUrl, + }; + _teams.add(newTeam); + _notifyListeners(); + } + + // 3. ELIMINAR + Future deleteTeam(String id) async { + _teams.removeWhere((team) => team['id'] == id); + // Remove também os jogadores dessa equipa para não ficarem "órfãos" + _members.removeWhere((member) => member['team_id'] == id); + _notifyListeners(); + } + + // 4. CONTAR JOGADORES (Lógica Local) + Future getPlayerCount(String teamId) async { + // Filtra a lista _members e conta quantos pertencem a esta equipa + final count = _members.where((m) => m['team_id'] == teamId).length; + return count; + } + + void _notifyListeners() { + _streamController.add(_teams); + } + + void dispose() { + _streamController.close(); + } +} \ No newline at end of file diff --git a/lib/controllers/team_controllers.dart b/lib/controllers/team_controllers.dart index 63408fd..b9f78bb 100644 --- a/lib/controllers/team_controllers.dart +++ b/lib/controllers/team_controllers.dart @@ -1,71 +1 @@ -import 'package:supabase_flutter/supabase_flutter.dart'; - -class TeamController { - // Acesso ao cliente do Supabase - final SupabaseClient _supabase = Supabase.instance.client; - - // --- STREAM DE EQUIPAS (LER) --- - // Retorna uma Lista de Mapas em tempo real - Stream>> get teamsStream { - final user = _supabase.auth.currentUser; - - if (user == null) { - return const Stream.empty(); - } - - return _supabase - .from('teams') - .stream(primaryKey: ['id']) // É obrigatório definir a Primary Key para Streams - .eq('user_id', user.id) // Filtra apenas as equipas do utilizador logado - .order('created_at', ascending: false); - } - - // --- CRIAR EQUIPA --- - Future createTeam(String name, String season, String imageUrl) async { - final user = _supabase.auth.currentUser; - - if (user != null) { - try { - await _supabase.from('teams').insert({ - 'name': name, - 'season': season, - 'image_url': imageUrl, // Garante que na tabela a coluna se chama 'image_url' (snake_case) - 'user_id': user.id, // Chave estrangeira para ligar ao utilizador - // 'created_at': O Supabase preenche isto sozinho se tiver default: now() - }); - } catch (e) { - print("Erro ao criar equipa: $e"); - } - } else { - print("Erro: Utilizador não autenticado."); - } - } - - // --- ELIMINAR EQUIPA --- - Future deleteTeam(String docId) async { - try { - // Se configuraste "ON DELETE CASCADE" no Supabase, isto apaga também os jogadores - await _supabase.from('teams').delete().eq('id', docId); - } catch (e) { - print("Erro ao eliminar: $e"); - } - } - - // --- CONTAR JOGADORES --- - // No SQL não entramos dentro da equipa. Vamos à tabela 'members' e filtramos pelo team_id. - // --- CONTAR JOGADORES (CORRIGIDO) --- - Future getPlayerCount(String teamId) async { - try { - // Correção: O Supabase agora retorna o 'int' diretamente, não um objeto response - final int count = await _supabase - .from('members') - .count(CountOption.exact) // Pede o número exato - .eq('team_id', teamId); - - return count; - } catch (e) { - print("Erro ao contar jogadores: $e"); - return 0; - } - } -} \ No newline at end of file +// TODO Implement this library. \ No newline at end of file diff --git a/lib/grafico de pizza/controllers/contollers_grafico.dart b/lib/grafico de pizza/controllers/contollers_grafico.dart index 8dd9d67..145230e 100644 --- a/lib/grafico de pizza/controllers/contollers_grafico.dart +++ b/lib/grafico de pizza/controllers/contollers_grafico.dart @@ -1,29 +1,29 @@ -import 'package:flutter/material.dart'; -import 'package:playmaker/grafico%20de%20pizza/dados_grafico.dart'; + import 'package:flutter/material.dart'; + import 'package:playmaker/grafico%20de%20pizza/dados_grafico.dart'; -class PieChartController extends ChangeNotifier { - PieChartData _chartData = PieChartData(victories: 25, defeats: 10); - - PieChartData get chartData => _chartData; - - void updateData({int? victories, int? defeats, int? draws}) { - _chartData = PieChartData( - victories: victories ?? _chartData.victories, - defeats: defeats ?? _chartData.defeats, - draws: draws ?? _chartData.draws, - ); - notifyListeners(); - } - - void incrementVictories() { - updateData(victories: _chartData.victories + 1); - } - - void incrementDefeats() { - updateData(defeats: _chartData.defeats + 1); - } - - void reset() { - updateData(victories: 0, defeats: 0, draws: 0); - } -} \ No newline at end of file + class PieChartController extends ChangeNotifier { + PieChartData _chartData = PieChartData(victories: 25, defeats: 10); + + PieChartData get chartData => _chartData; + + void updateData({int? victories, int? defeats, int? draws}) { + _chartData = PieChartData( + victories: victories ?? _chartData.victories, + defeats: defeats ?? _chartData.defeats, + draws: draws ?? _chartData.draws, + ); + notifyListeners(); + } + + void incrementVictories() { + updateData(victories: _chartData.victories + 1); + } + + void incrementDefeats() { + updateData(defeats: _chartData.defeats + 1); + } + + void reset() { + updateData(victories: 0, defeats: 0, draws: 0); + } + } \ No newline at end of file diff --git a/lib/models/team_model.dart b/lib/models/team_model.dart index 9478df4..b30efff 100644 --- a/lib/models/team_model.dart +++ b/lib/models/team_model.dart @@ -3,23 +3,31 @@ class Team { final String name; final String season; final String imageUrl; - final int playerCount; // NOVO: Campo para guardar o total Team({ - required this.id, - required this.name, - required this.season, + required this.id, + required this.name, + required this.season, required this.imageUrl, - this.playerCount = 0, // Valor padrão }); - factory Team.fromFirestore(Map data, String id) { + // Converte de Mapa (o formato da nossa "memória") para Objeto + factory Team.fromMap(Map map) { return Team( - id: id, - name: data['name'] ?? '', - season: data['season'] ?? '', - imageUrl: data['imageUrl'] ?? '', - playerCount: data['playerCount'] ?? 0, // Lê do Firebase + id: map['id'] ?? '', + name: map['name'] ?? '', + season: map['season'] ?? '', + imageUrl: map['image_url'] ?? '', ); } + + // Converte de Objeto para Mapa (para guardar na lista) + Map toMap() { + return { + 'id': id, + 'name': name, + 'season': season, + 'image_url': imageUrl, + }; + } } \ No newline at end of file diff --git a/lib/pages/home.dart b/lib/pages/home.dart index b0724f1..763e574 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:playmaker/classe/home.config.dart'; -import 'package:playmaker/controllers/team_controllers.dart'; import 'package:playmaker/grafico%20de%20pizza/grafico.dart'; -// Certifica-te que o caminho do controller está correto: +import 'package:playmaker/pages/teamPage.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -13,9 +12,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { int _selectedIndex = 0; - - // 1. Instanciar o Controller para aceder ao Supabase - final TeamController _teamController = TeamController(); + late final List _pages; @@ -23,10 +20,10 @@ class _HomeScreenState extends State { void initState() { super.initState(); _pages = [ - _buildHomeContent(), // Index 0: Home - const Center(child: Text('Tela de Jogo')), // Index 1: Jogo - _buildTeamsContent(), // Index 2: Equipas (O teu StreamBuilder entra aqui) - const Center(child: Text('Tela de Status')), // Index 3: Status + _buildHomeContent(), + const Center(child: Text('Tela de Jogo')), + const TeamsPage(), + const Center(child: Text('Tela de Status')), ]; } @@ -90,64 +87,8 @@ class _HomeScreenState extends State { // --- WIDGETS DE CONTEÚDO --- - // 2. O teu StreamBuilder foi movido para aqui - Widget _buildTeamsContent() { - return StreamBuilder>>( - stream: _teamController.teamsStream, - builder: (context, snapshot) { - // Verificar estado de carregamento - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - // Verificar erros ou lista vazia - if (snapshot.hasError) { - return Center(child: Text("Erro: ${snapshot.error}")); - } - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text("Ainda não tens equipas.")); - } - - // Obter dados (Lista simples do Supabase) - final teams = snapshot.data!; - - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: teams.length, - itemBuilder: (context, index) { - final team = teams[index]; - - // Construção do Card da Equipa - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - leading: CircleAvatar( - backgroundColor: HomeConfig.primaryColor, - child: Text( - team['name'][0].toUpperCase(), - style: const TextStyle(color: Colors.white), - ), - ), - title: Text( - team['name'], - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text("Época: ${team['season']}"), - trailing: IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () { - // Confirmação antes de apagar (Opcional, mas recomendado) - _teamController.deleteTeam(team['id']); - }, - ), - ), - ); - }, - ); - }, - ); - } + // REMOVIDO: Widget _buildTeamsContent()... + // Motivo: Este código foi movido para dentro de lib/pages/teams_page.dart Widget _buildHomeContent() { return SingleChildScrollView( diff --git a/lib/pages/teamPage.dart b/lib/pages/teamPage.dart new file mode 100644 index 0000000..31d01e5 --- /dev/null +++ b/lib/pages/teamPage.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import '../controllers/team_controller.dart'; +import '../models/team_model.dart'; +import '../widgets/team_widgets.dart'; + +class TeamsPage extends StatefulWidget { + const TeamsPage({super.key}); + + @override + State createState() => _TeamsPageState(); +} + +class _TeamsPageState extends State { + final TeamController controller = TeamController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FA), + + // Título simples no topo (opcional, já tens a BottomNavBar) + // appBar: AppBar(title: Text("Minhas Equipas"), automaticallyImplyLeading: false), + + body: StreamBuilder>>( + stream: controller.teamsStream, + builder: (context, snapshot) { + // Estado de Loading + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + // Estado Vazio + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.sports_basketball_outlined, size: 60, color: Colors.grey), + SizedBox(height: 16), + Text( + 'Ainda não tens equipas.', + style: TextStyle(color: Colors.grey, fontSize: 16), + ), + Text( + 'Clica no + para criar.', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ], + ), + ); + } + + final teamsData = snapshot.data!; + + // Lista de Equipas + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: teamsData.length, + itemBuilder: (context, index) { + final team = Team.fromMap(teamsData[index]); + return TeamCard(team: team, controller: controller); + }, + ); + }, + ), + + // --- O BOTÃO FLUTUANTE QUE PEDISTE --- + floatingActionButton: FloatingActionButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => CreateTeamDialog( + onConfirm: (name, season, imageUrl) { + controller.createTeam(name, season, imageUrl); + }, + ), + ); + }, + backgroundColor: const Color(0xFFE74C3C), // Cor Vermelha + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/teams_page.dart b/lib/pages/teams_page.dart deleted file mode 100644 index 064fdb9..0000000 --- a/lib/pages/teams_page.dart +++ /dev/null @@ -1,58 +0,0 @@ -/*import 'package:flutter/material.dart'; -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}); - - @override - Widget build(BuildContext context) { - // Instância única do controlador para esta página - final TeamController controller = TeamController(); - - return Scaffold( - 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()); - } - - final docs = snapshot.data!.docs; - 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 - ); - - // CORREÇÃO 1: Passar o team E o controller para o Card - return TeamCard(team: team, controller: controller); - }, - ); - }, - ), - floatingActionButton: FloatingActionButton( - 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); - }, - ), - ); - }, - ), - ); - } -}*/ \ No newline at end of file diff --git a/lib/screens/team_stats_page.dart b/lib/screens/team_stats_page.dart index 910dbb1..aa3a7ce 100644 --- a/lib/screens/team_stats_page.dart +++ b/lib/screens/team_stats_page.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:playmaker/controllers/team_controller.dart'; +import 'dart:async'; import '../models/team_model.dart'; import '../models/person_model.dart'; -import '../controllers/stats_controller.dart'; -import '../widgets/stats_widgets.dart'; +import '../controllers/team_controllers.dart'; // Para aceder à lista estática de membros class TeamStatsPage extends StatelessWidget { final Team team; - // Agora este controller já tem o método getMembers final StatsController _controller = StatsController(); TeamStatsPage({super.key, required this.team}); @@ -17,26 +17,16 @@ class TeamStatsPage extends StatelessWidget { backgroundColor: const Color(0xFFF5F7FA), body: Column( children: [ - // Certifica-te que tens o StatsHeader no ficheiro stats_widgets.dart - StatsHeader(team: team), + _buildLocalHeader(context), Expanded( child: StreamBuilder>( - // AGORA ISTO VAI FUNCIONAR: stream: _controller.getMembers(team.id), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } - if (snapshot.hasError) { - return Center(child: Text('Erro: ${snapshot.error}')); - } - final members = snapshot.data ?? []; - if (members.isEmpty) { - return const Center(child: Text("Sem membros nesta equipa.")); - } - final coaches = members.where((m) => m.type == 'Treinador').toList(); final players = members.where((m) => m.type == 'Jogador').toList(); @@ -45,17 +35,21 @@ class TeamStatsPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SummaryCard(), // Confirma se tens este widget + _buildLocalSummaryCard(members.length), const SizedBox(height: 30), if (coaches.isNotEmpty) ...[ _buildSectionTitle("Treinadores"), ...coaches.map((c) => _buildPersonCard(context, c, isCoach: true)), const SizedBox(height: 30), ], - if (players.isNotEmpty) ...[ - _buildSectionTitle("Jogadores"), + _buildSectionTitle("Jogadores"), + if (players.isEmpty) + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("Nenhum jogador adicionado.", style: TextStyle(color: Colors.grey)), + ) + else ...players.map((p) => _buildPersonCard(context, p, isCoach: false)), - ], const SizedBox(height: 80), ], ), @@ -73,37 +67,88 @@ class TeamStatsPage extends StatelessWidget { ); } + // --- WIDGETS QUE ESTAVAM EM STATS_WIDGETS --- + + Widget _buildLocalHeader(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + decoration: const BoxDecoration( + color: Color(0xFF2C3E50), + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)), + ), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 10), + CircleAvatar( + backgroundColor: Colors.white24, + child: Text(team.imageUrl.isNotEmpty && !team.imageUrl.startsWith('http') ? team.imageUrl : "🏀"), + ), + const SizedBox(width: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)), + Text(team.season, style: const TextStyle(color: Colors.white70, fontSize: 14)), + ], + ), + ], + ), + ); + } + + Widget _buildLocalSummaryCard(int total) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient(colors: [Colors.blue.shade700, Colors.blue.shade400]), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Total de Membros", style: TextStyle(color: Colors.white, fontSize: 16)), + Text("$total", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)), + ], + ), + ), + ); + } + Widget _buildPersonCard(BuildContext context, Person person, {required bool isCoach}) { return Card( margin: const EdgeInsets.only(top: 12), elevation: 2, color: isCoach ? const Color(0xFFFFF9C4) : Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - leading: isCoach - ? const CircleAvatar(radius: 25, backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white)) - : Container( - width: 50, height: 50, - alignment: Alignment.center, - decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), - child: Text(person.number, style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 18)), - ), - title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 17)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit_outlined, size: 24, color: Colors.blue), - onPressed: () => _controller.showEditPersonDialog(context, team.id, person), - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 24, color: Colors.red), - onPressed: () => _confirmDelete(context, person), - ), - ], - ), + child: ListTile( + leading: isCoach + ? const CircleAvatar(backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white)) + : Container( + width: 45, height: 45, + alignment: Alignment.center, + decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), + child: Text(person.number, style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)), + ), + title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined, color: Colors.blue), + onPressed: () => _controller.showEditPersonDialog(context, team.id, person), + ), + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red), + onPressed: () => _confirmDelete(context, person), + ), + ], ), ), ); @@ -114,7 +159,7 @@ class TeamStatsPage extends StatelessWidget { context: context, builder: (context) => AlertDialog( title: const Text("Eliminar?"), - content: Text("Queres mesmo remover ${person.name}?"), + content: Text("Remover ${person.name}?"), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancelar")), TextButton( @@ -134,9 +179,92 @@ class TeamStatsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))), - const SizedBox(height: 10), const Divider(), ], ); } -} \ No newline at end of file +} + +// --- CONTROLLER LOCAL (SEM SUPABASE) --- + +class StatsController { + final _streamController = StreamController>.broadcast(); + + Stream> getMembers(String teamId) { + _emitMembers(teamId); + return _streamController.stream; + } + + void _emitMembers(String teamId) { + final list = TeamController.members + .where((m) => m['team_id'] == teamId) + .map((json) => Person.fromMap(json)) + .toList(); + _streamController.add(list); + } + + void deletePerson(String teamId, String personId) { + TeamController.members.removeWhere((m) => m['id'] == personId); + _emitMembers(teamId); + } + + void showAddPersonDialog(BuildContext context, String teamId) { + _showForm(context, teamId: teamId); + } + + void showEditPersonDialog(BuildContext context, String teamId, Person person) { + _showForm(context, teamId: teamId, person: person); + } + + void _showForm(BuildContext context, {required String teamId, Person? person}) { + final isEdit = person != null; + final nameCtrl = TextEditingController(text: person?.name ?? ''); + final numCtrl = TextEditingController(text: person?.number ?? ''); + String type = person?.type ?? 'Jogador'; + + showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setState) => AlertDialog( + title: Text(isEdit ? "Editar" : "Adicionar"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField(controller: nameCtrl, decoration: const InputDecoration(labelText: "Nome")), + DropdownButton( + value: type, + isExpanded: true, + items: ["Jogador", "Treinador"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(), + onChanged: (v) => setState(() => type = v!), + ), + if (type == "Jogador") + TextField(controller: numCtrl, decoration: const InputDecoration(labelText: "Número"), keyboardType: TextInputType.number), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Sair")), + ElevatedButton( + onPressed: () { + if (isEdit) { + final idx = TeamController.members.indexWhere((m) => m['id'] == person.id); + TeamController.members[idx] = {'id': person.id, 'team_id': teamId, 'name': nameCtrl.text, 'type': type, 'number': numCtrl.text}; + } else { + TeamController.members.add({ + 'id': DateTime.now().toString(), + 'team_id': teamId, + 'name': nameCtrl.text, + 'type': type, + 'number': numCtrl.text + }); + } + _emitMembers(teamId); + Navigator.pop(ctx); + }, + child: const Text("Guardar"), + ) + ], + ), + ), + ); + } + } diff --git a/lib/widgets/stats_widgets.dart b/lib/widgets/stats_widgets.dart deleted file mode 100644 index cd9ed2c..0000000 --- a/lib/widgets/stats_widgets.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import '../models/team_model.dart'; - -class StatsHeader extends StatelessWidget { - final Team team; - const StatsHeader({super.key, required this.team}); - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.only(top: 50, left: 16, right: 16, bottom: 25), - decoration: const BoxDecoration(color: Color(0xFF2196F3)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), - ), - Row( - children: [ - _buildLogo(), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), - Text("Temporada ${team.season}", style: const TextStyle(color: Colors.white70, fontSize: 14)), - ], - ), - ], - ), - ], - ), - ); - } - - Widget _buildLogo() { - return Container( - width: 60, height: 60, - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)), - child: Center( - child: team.imageUrl.startsWith('http') - ? ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.network(team.imageUrl)) - : Text(team.imageUrl.isEmpty ? "🏀" : team.imageUrl, style: const TextStyle(fontSize: 30)), - ), - ); - } -} - -class SummaryCard extends StatelessWidget { - const SummaryCard({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(15), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)], - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _StatItem(label: "Jogos", value: "0", color: Colors.black), - _StatItem(label: "Vitórias", value: "0", color: Colors.green), - _StatItem(label: "Derrotas", value: "0", color: Colors.red), - ], - ), - ); - } -} - -class _StatItem extends StatelessWidget { - final String label, value; - final Color color; - const _StatItem({required this.label, required this.value, required this.color}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text(value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color)), - Text(label, style: const TextStyle(color: Colors.grey, fontSize: 13)), - ], - ); - } -} \ No newline at end of file diff --git a/lib/widgets/team_widgets.dart b/lib/widgets/team_widgets.dart index 433024e..c243a72 100644 --- a/lib/widgets/team_widgets.dart +++ b/lib/widgets/team_widgets.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:playmaker/screens/team_stats_page.dart'; import '../models/team_model.dart'; -import '../controllers/team_controllers.dart'; - +import '../controllers/team_controller.dart'; +// Importa a tua página de stats (verifica se o caminho está correto) +import '../screens/team_stats_page.dart'; class TeamCard extends StatelessWidget { final Team team; - final TeamController controller; // Recebe o controlador por parâmetro + final TeamController controller; const TeamCard({ super.key, required this.team, required this.controller, - }); - @override + @override Widget build(BuildContext context) { return Card( elevation: 3, @@ -23,85 +22,87 @@ class TeamCard extends StatelessWidget { 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), - ), - // Dentro do build do teu TeamCard, no subtitle: -subtitle: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Row( - children: [ - // 1. JOGADORES (À ESQUERDA) - const Icon(Icons.groups, size: 16, color: Colors.grey), - const SizedBox(width: 4), - Text( - "${team.playerCount} Jogadores", - style: TextStyle( - color: team.playerCount == 0 ? Colors.orange : Colors.green, - fontWeight: FontWeight.w500, - fontSize: 13, - ), - ), - - // SEPARADOR VISUAL - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Text("•", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)), - ), - - // 2. TEMPORADA (A SEGUIR) - Text("${team.season} Temporada", - style: const TextStyle( - color: Colors.grey, - fontSize: 13, - ), - ), - ], - ), + leading: CircleAvatar( + backgroundColor: Colors.grey[200], + backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http')) + ? NetworkImage(team.imageUrl) + : null, + child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http')) + ? Text( + team.imageUrl.isEmpty ? "🏀" : team.imageUrl, + style: const TextStyle(fontSize: 20), + ) + : null, ), - // 3. TRAILING (Lado Direito): Botões de Status e Eliminar + // 2. NOME DA EQUIPA + title: Text( + team.name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + + // 3. SUBTÍTULO (CONTAGEM DE JOGADORES) + subtitle: Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Row( + children: [ + // Ícone de grupo + const Icon(Icons.groups_outlined, size: 16, color: Colors.grey), + const SizedBox(width: 4), + + // Contagem assíncrona + FutureBuilder( + future: controller.getPlayerCount(team.id), + initialData: 0, + builder: (context, snapshot) { + final count = snapshot.data ?? 0; + return Text( + "$count Jogadores", + style: TextStyle( + // Verde se tiver jogadores, Laranja se estiver vazio + color: count > 0 ? Colors.green[700] : Colors.orange, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ); + }, + ), + + const SizedBox(width: 10), + + // Temporada + Text( + "| ${team.season}", + style: const TextStyle(color: Colors.grey, fontSize: 13), + ), + ], + ), + ), + + // 4. BOTÕES DE AÇÃO (Lado Direito) trailing: SizedBox( - width: 90, // Espaço fixo para os dois ícones não quebrarem a linha + width: 96, // Espaço suficiente para 2 botões child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - // Botão Status + // --- BOTÃO DE STATUS (STATS) --- IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - icon: const Icon(Icons.bar_chart, color: Colors.blue), - onPressed: () { - // NAVEGAÇÃO PARA A NOVA PÁGINA - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TeamStatsPage(team: team), - ), - ); - }, -), - const SizedBox(width: 12), - // Botão Eliminar + tooltip: 'Ver Estatísticas', + icon: const Icon(Icons.bar_chart_rounded, color: Colors.blue), + onPressed: () { + // Navega para a página de gestão da equipa + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TeamStatsPage(team: team), + ), + ); + }, + ), + + // --- BOTÃO ELIMINAR --- IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + tooltip: 'Eliminar Equipa', icon: const Icon(Icons.delete_outline, color: Color(0xFFE74C3C)), onPressed: () => _confirmDelete(context), ), @@ -117,19 +118,16 @@ subtitle: Padding( 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.'), + content: Text('Tens a certeza que queres eliminar "${team.name}"?'), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancelar'), + 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)), ), @@ -139,7 +137,7 @@ subtitle: Padding( } } -// --- WIDGET DO POPUP (CREATE TEAM DIALOG) --- +// (O CreateTeamDialog mantém-se igual ao que já tens) class CreateTeamDialog extends StatefulWidget { final Function(String name, String season, String imageUrl) onConfirm; @@ -165,42 +163,42 @@ class _CreateTeamDialogState extends State { TextField( controller: _nameController, decoration: const InputDecoration(labelText: 'Nome da Equipa'), + textCapitalization: TextCapitalization.words, ), 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(), + items: ['2023/24', '2024/25', '2025/26'] + .map((s) => 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', + labelText: 'URL Imagem ou Emoji', + hintText: 'Ex: 🏀 ou https://...', ), ), ], ), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancelar'), - ), + 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); - }, +onPressed: () { + if (_nameController.text.trim().isNotEmpty) { + widget.onConfirm( + _nameController.text.trim(), + _selectedSeason, + _imageController.text.trim() // Trim remove espaços extras + ); + Navigator.pop(context); + } +}, child: const Text('Criar', style: TextStyle(color: Colors.white)), ), ],