From f5d7e8814988b63e07c154d4f8c7a4e672e02e83 Mon Sep 17 00:00:00 2001 From: Diogo Date: Thu, 12 Mar 2026 00:57:01 +0000 Subject: [PATCH] esta melhor des que comecei --- lib/controllers/game_controller.dart | 41 +- lib/controllers/placar_controller.dart | 63 +- lib/controllers/team_controller.dart | 23 +- lib/models/game_model.dart | 22 +- lib/pages/gamePage.dart | 399 +++++++++++- lib/pages/home.dart | 863 +++++++++++++------------ lib/pages/status_page.dart | 282 ++++++++ lib/pages/teamPage.dart | 250 ++++++- lib/screens/game_screen.dart | 1 - lib/widgets/team_widgets.dart | 10 +- 10 files changed, 1447 insertions(+), 507 deletions(-) create mode 100644 lib/pages/status_page.dart delete mode 100644 lib/screens/game_screen.dart diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index ffa141a..b55decf 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -4,25 +4,34 @@ import '../models/game_model.dart'; class GameController { final _supabase = Supabase.instance.client; - // 1. LER JOGOS (Stream em Tempo Real) -Stream> get gamesStream { + // 1. LER JOGOS (Com Filtros Opcionais) + Stream> getFilteredGames({String? teamFilter, String? seasonFilter}) { return _supabase - .from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!) + .from('games') .stream(primaryKey: ['id']) .asyncMap((event) async { - // 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado), - // vamos buscar os dados já misturados com as imagens à nossa View. - final viewData = await _supabase - .from('games_with_logos') - .select() - .order('game_date', ascending: false); + + // 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games' + var query = _supabase.from('games').select(); + + // Aplica o filtro de Temporada + if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') { + query = query.eq('season', seasonFilter); + } + + // Aplica o filtro de Equipa (Procura em casa ou fora) + if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') { + query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter'); + } + + // Executa a query com a ordenação por data + final viewData = await query.order('game_date', ascending: false); - // 3. Convertemos para a nossa lista de objetos Game return viewData.map((json) => Game.fromMap(json)).toList(); }); } + // 2. CRIAR JOGO - // Retorna o ID do jogo criado para podermos navegar para o placar Future createGame(String myTeam, String opponent, String season) async { try { final response = await _supabase.from('games').insert({ @@ -31,18 +40,16 @@ Stream> get gamesStream { 'season': season, 'my_score': 0, 'opponent_score': 0, - 'status': 'Decorrer', // Começa como "Decorrer" + 'status': 'Decorrer', 'game_date': DateTime.now().toIso8601String(), - }).select().single(); // .select().single() retorna o objeto criado + }).select().single(); - return response['id']; // Retorna o UUID gerado pelo Supabase + return response['id']; } catch (e) { print("Erro ao criar jogo: $e"); return null; } } - void dispose() { - // Não é necessário fechar streams do Supabase manualmente aqui - } + void dispose() {} } \ No newline at end of file diff --git a/lib/controllers/placar_controller.dart b/lib/controllers/placar_controller.dart index 3bba4cc..feb2475 100644 --- a/lib/controllers/placar_controller.dart +++ b/lib/controllers/placar_controller.dart @@ -187,16 +187,16 @@ class PlacarController { timer.cancel(); isRunning = false; if (currentQuarter < 4) { - currentQuarter++; - duration = const Duration(minutes: 10); - myFouls = 0; - opponentFouls = 0; - myTimeoutsUsed = 0; - opponentTimeoutsUsed = 0; - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue)); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red)); - } + currentQuarter++; + duration = const Duration(minutes: 10); + myFouls = 0; + opponentFouls = 0; + myTimeoutsUsed = 0; + opponentTimeoutsUsed = 0; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue)); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red)); + } } onUpdate(); }); @@ -355,7 +355,34 @@ class PlacarController { bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0; String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado'; - // 1. Atualizar o Jogo na BD + // 👇👇👇 0. CÉREBRO: CALCULAR OS LÍDERES E MVP DO JOGO 👇👇👇 + String topPtsName = '---'; int maxPts = -1; + String topAstName = '---'; int maxAst = -1; + String topRbsName = '---'; int maxRbs = -1; + String topDefName = '---'; int maxDef = -1; + String mvpName = '---'; int maxMvpScore = -1; + + // Passa por todos os jogadores e calcula a matemática + playerStats.forEach((playerName, stats) { + int pts = stats['pts'] ?? 0; + int ast = stats['ast'] ?? 0; + int rbs = stats['rbs'] ?? 0; + int stl = stats['stl'] ?? 0; + int blk = stats['blk'] ?? 0; + + int defScore = stl + blk; // Defesa: Roubos + Cortes + int mvpScore = pts + ast + rbs + defScore; // Impacto Total (MVP) + + // Compara com o máximo atual e substitui se for maior + if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; } + if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; } + if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; } + if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; } + if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; } // MVP não leva nº à frente, fica mais limpo + }); + // 👆👆👆 FIM DO CÉREBRO 👆👆👆 + + // 1. Atualizar o Jogo na BD (Agora inclui os Reis da partida!) await supabase.from('games').update({ 'my_score': myScore, 'opponent_score': opponentScore, @@ -364,12 +391,18 @@ class PlacarController { 'opp_timeouts': opponentTimeoutsUsed, 'current_quarter': currentQuarter, 'status': newStatus, + + // ENVIA A MATEMÁTICA PARA A TUA BASE DE DADOS + 'top_pts_name': topPtsName, + 'top_ast_name': topAstName, + 'top_rbs_name': topRbsName, + 'top_def_name': topDefName, + 'mvp_name': mvpName, }).eq('id', gameId); - // 👇 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES 👇 + // 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) { - // Vai buscar os dados atuais das equipas final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]); Map myTeamUpdate = {}; @@ -380,7 +413,6 @@ class PlacarController { if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t); } - // Calcula os resultados if (myScore > opponentScore) { myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1; oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1; @@ -388,12 +420,10 @@ class PlacarController { myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1; oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1; } else { - // Empate myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1; oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1; } - // Envia as atualizações para a tabela 'teams' await supabase.from('teams').update({ 'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws'] }).eq('id', myTeamDbId!); @@ -402,7 +432,6 @@ class PlacarController { 'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws'] }).eq('id', oppTeamDbId!); - // Bloqueia o trinco para não contar 2 vezes se o utilizador clicar "Guardar" outra vez gameWasAlreadyFinished = true; } diff --git a/lib/controllers/team_controller.dart b/lib/controllers/team_controller.dart index bf45320..6a376d9 100644 --- a/lib/controllers/team_controller.dart +++ b/lib/controllers/team_controller.dart @@ -5,7 +5,6 @@ class TeamController { final _supabase = Supabase.instance.client; // 1. STREAM (Realtime) - // Adicionei o .map() no final para garantir que o Dart entende que é uma List Stream>> get teamsStream { return _supabase .from('teams') @@ -15,7 +14,6 @@ class TeamController { } // 2. CRIAR - // Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem Future createTeam(String name, String season, String? imageUrl) async { try { await _supabase.from('teams').insert({ @@ -51,21 +49,14 @@ class TeamController { } } - // 5. CONTAR JOGADORES - // CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora. - Future getPlayerCount(String teamId) async { - try { - final count = await _supabase - .from('members') - .count() // Retorna diretamente o número inteiro - .eq('team_id', teamId); - return count; - } catch (e) { - print("Erro ao contar jogadores: $e"); - return 0; - } + // 5. CONTAR JOGADORES (AGORA EM TEMPO REAL COM STREAM!) + Stream getPlayerCountStream(String teamId) { + return _supabase + .from('members') + .stream(primaryKey: ['id']) + .eq('team_id', teamId) + .map((data) => data.length); // O tamanho da lista é o número de jogadores } - // Mantemos o dispose vazio para não quebrar a chamada na TeamsPage void dispose() {} } \ No newline at end of file diff --git a/lib/models/game_model.dart b/lib/models/game_model.dart index 64f2f79..763c33e 100644 --- a/lib/models/game_model.dart +++ b/lib/models/game_model.dart @@ -2,8 +2,6 @@ class Game { final String id; final String myTeam; final String opponentTeam; - final String? myTeamLogo; // URL da imagem - final String? opponentTeamLogo; // URL da imagem final String myScore; final String opponentScore; final String status; @@ -13,26 +11,22 @@ class Game { required this.id, required this.myTeam, required this.opponentTeam, - this.myTeamLogo, - this.opponentTeamLogo, required this.myScore, required this.opponentScore, required this.status, required this.season, }); - // No seu factory, certifique-se de mapear os campos da tabela (ou de um JOIN) factory Game.fromMap(Map map) { return Game( - id: map['id'], - myTeam: map['my_team_name'], - opponentTeam: map['opponent_team_name'], - myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto - opponentTeamLogo: map['opponent_team_logo'], - myScore: map['my_score'].toString(), - opponentScore: map['opponent_score'].toString(), - status: map['status'], - season: map['season'], + // O "?." converte para texto com segurança, e o "?? '...'" diz o que mostrar se for nulo (vazio) + id: map['id']?.toString() ?? '', + myTeam: map['my_team']?.toString() ?? 'Desconhecida', + opponentTeam: map['opponent_team']?.toString() ?? 'Adversário', + myScore: map['my_score']?.toString() ?? '0', + opponentScore: map['opponent_score']?.toString() ?? '0', + status: map['status']?.toString() ?? 'Terminado', + season: map['season']?.toString() ?? 'Sem Época', ); } } \ No newline at end of file diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 86c7373..37c1465 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -1,10 +1,230 @@ import 'package:flutter/material.dart'; +import 'package:playmaker/pages/PlacarPage.dart'; import '../controllers/game_controller.dart'; import '../controllers/team_controller.dart'; import '../models/game_model.dart'; -import '../widgets/game_widgets.dart'; -import 'dart:math' as math; // <-- IMPORTANTE: Para o cálculo da escala +import 'dart:math' as math; +// --- CARD DE EXIBIÇÃO DO JOGO --- +class GameResultCard extends StatelessWidget { + final String gameId; + final String myTeam, opponentTeam, myScore, opponentScore, status, season; + final String? myTeamLogo; + final String? opponentTeamLogo; + final double sf; + + const GameResultCard({ + super.key, + required this.gameId, + required this.myTeam, + required this.opponentTeam, + required this.myScore, + required this.opponentScore, + required this.status, + required this.season, + this.myTeamLogo, + this.opponentTeamLogo, + required this.sf, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 16 * sf), + padding: EdgeInsets.all(16 * sf), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20 * sf), + boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)), + _buildScoreCenter(context, gameId, sf), + Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)), + ], + ), + ); + } + + Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) { + return Column( + children: [ + CircleAvatar( + radius: 24 * sf, + backgroundColor: color, + backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null, + child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null, + ), + SizedBox(height: 6 * sf), + Text(name, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf), + textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, + ), + ], + ); + } + + Widget _buildScoreCenter(BuildContext context, String id, double sf) { + return Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _scoreBox(myScore, Colors.green, sf), + Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)), + _scoreBox(opponentScore, Colors.grey, sf), + ], + ), + SizedBox(height: 10 * sf), + TextButton.icon( + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam))); + }, + icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)), + label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)), + style: TextButton.styleFrom( + backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), + padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), + visualDensity: VisualDensity.compact, + ), + ), + SizedBox(height: 6 * sf), + Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)), + ], + ); + } + + Widget _scoreBox(String pts, Color c, double sf) => Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf), + decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)), + child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)), + ); +} + +// --- POPUP DE CRIAÇÃO --- +class CreateGameDialogManual extends StatefulWidget { + final TeamController teamController; + final GameController gameController; + final double sf; + + const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf}); + + @override + State createState() => _CreateGameDialogManualState(); +} + +class _CreateGameDialogManualState extends State { + late TextEditingController _seasonController; + final TextEditingController _myTeamController = TextEditingController(); + final TextEditingController _opponentController = TextEditingController(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _seasonController = TextEditingController(text: _calculateSeason()); + } + + String _calculateSeason() { + final now = DateTime.now(); + return now.month >= 7 ? "${now.year}/${(now.year + 1).toString().substring(2)}" : "${now.year - 1}/${now.year.toString().substring(2)}"; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)), + title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _seasonController, style: TextStyle(fontSize: 14 * widget.sf), + decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)), + ), + SizedBox(height: 15 * widget.sf), + _buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf), + Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))), + _buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)), + onPressed: _isLoading ? null : () async { + if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { + setState(() => _isLoading = true); + String? newGameId = await widget.gameController.createGame(_myTeamController.text, _opponentController.text, _seasonController.text); + setState(() => _isLoading = false); + if (newGameId != null && context.mounted) { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: newGameId, myTeam: _myTeamController.text, opponentTeam: _opponentController.text))); + } + } + }, + child: _isLoading ? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)), + ), + ], + ); + } + + Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) { + return StreamBuilder>>( + stream: widget.teamController.teamsStream, + builder: (context, snapshot) { + List> teamList = snapshot.hasData ? snapshot.data! : []; + return Autocomplete>( + displayStringForOption: (Map option) => option['name'].toString(), + optionsBuilder: (TextEditingValue val) { + if (val.text.isEmpty) return const Iterable>.empty(); + return teamList.where((t) => t['name'].toString().toLowerCase().contains(val.text.toLowerCase())); + }, + onSelected: (Map selection) { controller.text = selection['name'].toString(); }, + optionsViewBuilder: (context, onSelected, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, borderRadius: BorderRadius.circular(8 * sf), + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 250 * sf, maxWidth: MediaQuery.of(context).size.width * 0.7), + child: ListView.builder( + padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final option = options.elementAt(index); + final String name = option['name'].toString(); + final String? imageUrl = option['image_url']; + return ListTile( + leading: CircleAvatar(radius: 20 * sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * sf) : null), + title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * sf)), + onTap: () { onSelected(option); }, + ); + }, + ), + ), + ), + ); + }, + fieldViewBuilder: (ctx, txtCtrl, node, submit) { + if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { txtCtrl.text = controller.text; } + txtCtrl.addListener(() { controller.text = txtCtrl.text; }); + return TextField( + controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * sf), + decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * sf), prefixIcon: Icon(Icons.search, size: 20 * sf), border: const OutlineInputBorder()), + ); + }, + ); + }, + ); + } +} + +// --- PÁGINA PRINCIPAL DOS JOGOS COM FILTROS --- class GamePage extends StatefulWidget { const GamePage({super.key}); @@ -16,40 +236,68 @@ class _GamePageState extends State { final GameController gameController = GameController(); final TeamController teamController = TeamController(); + // Variáveis para os filtros + String selectedSeason = 'Todas'; + String selectedTeam = 'Todas'; + @override Widget build(BuildContext context) { - // 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇 final double wScreen = MediaQuery.of(context).size.width; final double hScreen = MediaQuery.of(context).size.height; final double sf = math.min(wScreen, hScreen) / 400; + // Verifica se algum filtro está ativo para mudar a cor do ícone + bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas'; + return Scaffold( backgroundColor: const Color(0xFFF5F7FA), appBar: AppBar( title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * sf)), backgroundColor: Colors.white, elevation: 0, + actions: [ + // 👇 BOTÃO DE FILTRO NA APP BAR 👇 + Padding( + padding: EdgeInsets.only(right: 8.0 * sf), + child: IconButton( + icon: Icon( + isFilterActive ? Icons.filter_list_alt : Icons.filter_list, + color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87, + size: 26 * sf, + ), + onPressed: () => _showFilterPopup(context, sf), + ), + ) + ], ), - // 1º STREAM: Lemos as equipas para ter as imagens + body: StreamBuilder>>( stream: teamController.teamsStream, builder: (context, teamSnapshot) { final List> teamsList = teamSnapshot.data ?? []; - // 2º STREAM: Lemos os jogos return StreamBuilder>( - stream: gameController.gamesStream, + stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason), builder: (context, gameSnapshot) { if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (gameSnapshot.hasError) { - return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf))); + return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf))); } if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { - return Center(child: Text("Nenhum jogo registado.", style: TextStyle(fontSize: 16 * sf))); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.search_off, size: 48 * sf, color: Colors.grey.shade300), + SizedBox(height: 10 * sf), + Text("Nenhum jogo encontrado para este filtro.", style: TextStyle(fontSize: 14 * sf, color: Colors.grey.shade600)), + ], + ) + ); } return ListView.builder( @@ -58,17 +306,12 @@ class _GamePageState extends State { itemBuilder: (context, index) { final game = gameSnapshot.data![index]; - // --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME --- String? myLogo; String? oppLogo; for (var team in teamsList) { - if (team['name'] == game.myTeam) { - myLogo = team['image_url']; - } - if (team['name'] == game.opponentTeam) { - oppLogo = team['image_url']; - } + if (team['name'] == game.myTeam) { myLogo = team['image_url']; } + if (team['name'] == game.opponentTeam) { oppLogo = team['image_url']; } } return GameResultCard( @@ -81,7 +324,7 @@ class _GamePageState extends State { season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo, - sf: sf, // <-- Passamos a escala para o Cartão + sf: sf, ); }, ); @@ -97,13 +340,135 @@ class _GamePageState extends State { ); } + // 👇 O POPUP DE FILTROS 👇 + void _showFilterPopup(BuildContext context, double sf) { + // Variáveis temporárias para o Popup (para não atualizar a lista antes de clicar em "Aplicar") + String tempSeason = selectedSeason; + String tempTeam = selectedTeam; + + showDialog( + context: context, + builder: (context) { + // StatefulBuilder permite atualizar a interface APENAS dentro do Popup + return StatefulBuilder( + builder: (context, setPopupState) { + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * sf)), + IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: () => Navigator.pop(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ) + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Filtro de Temporada + Text("Temporada", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), + SizedBox(height: 6 * sf), + Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: tempSeason, + style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold), + items: ['Todas', '2024/25', '2025/26'].map((String value) { + return DropdownMenuItem(value: value, child: Text(value)); + }).toList(), + onChanged: (newValue) { + setPopupState(() => tempSeason = newValue!); + }, + ), + ), + ), + + SizedBox(height: 20 * sf), + + // 2. Filtro de Equipa + Text("Equipa", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), + SizedBox(height: 6 * sf), + Container( + padding: EdgeInsets.symmetric(horizontal: 12 * sf), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)), + child: StreamBuilder>>( + stream: teamController.teamsStream, + builder: (context, snapshot) { + List teamNames = ['Todas']; + if (snapshot.hasData) { + teamNames.addAll(snapshot.data!.map((t) => t['name'].toString())); + } + + if (!teamNames.contains(tempTeam)) tempTeam = 'Todas'; + + return DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: tempTeam, + style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold), + items: teamNames.map((String value) { + return DropdownMenuItem(value: value, child: Text(value, overflow: TextOverflow.ellipsis)); + }).toList(), + onChanged: (newValue) { + setPopupState(() => tempTeam = newValue!); + }, + ), + ); + } + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + // Limpar Filtros + setState(() { + selectedSeason = 'Todas'; + selectedTeam = 'Todas'; + }); + Navigator.pop(context); + }, + child: Text('LIMPAR', style: TextStyle(fontSize: 12 * sf, color: Colors.grey)) + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE74C3C), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * sf)), + ), + onPressed: () { + // Aplicar Filtros (atualiza a página principal) + setState(() { + selectedSeason = tempSeason; + selectedTeam = tempTeam; + }); + Navigator.pop(context); + }, + child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * sf)), + ), + ], + ); + } + ); + }, + ); + } + void _showCreateDialog(BuildContext context, double sf) { showDialog( context: context, builder: (context) => CreateGameDialogManual( teamController: teamController, gameController: gameController, - sf: sf, // <-- Passamos a escala para o Pop-up + sf: sf, ), ); } diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 2c4481b..bbb9390 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -1,448 +1,473 @@ - import 'package:flutter/material.dart'; - import 'package:playmaker/classe/home.config.dart'; - import 'package:playmaker/grafico%20de%20pizza/grafico.dart'; - import 'package:playmaker/pages/gamePage.dart'; - import 'package:playmaker/pages/teamPage.dart'; - import 'package:playmaker/controllers/team_controller.dart'; - import 'package:supabase_flutter/supabase_flutter.dart'; - import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:playmaker/classe/home.config.dart'; +import 'package:playmaker/grafico%20de%20pizza/grafico.dart'; +import 'package:playmaker/pages/gamePage.dart'; +import 'package:playmaker/pages/teamPage.dart'; +import 'package:playmaker/controllers/team_controller.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:playmaker/pages/status_page.dart'; +import 'dart:math' as math; - import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart'; +import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart'; - class HomeScreen extends StatefulWidget { - const HomeScreen({super.key}); +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); - @override - State createState() => _HomeScreenState(); + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + int _selectedIndex = 0; + final TeamController _teamController = TeamController(); + String? _selectedTeamId; + String _selectedTeamName = "Selecionar Equipa"; + + int _teamWins = 0; + int _teamLosses = 0; + int _teamDraws = 0; + + final _supabase = Supabase.instance.client; + + @override + Widget build(BuildContext context) { + final double wScreen = MediaQuery.of(context).size.width; + final double hScreen = MediaQuery.of(context).size.height; + final double sf = math.min(wScreen, hScreen) / 400; + + final List pages = [ + _buildHomeContent(sf, wScreen), + const GamePage(), + const TeamsPage(), + const StatusPage(), + ]; + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)), + backgroundColor: HomeConfig.primaryColor, + foregroundColor: Colors.white, + leading: IconButton( + icon: Icon(Icons.person, size: 24 * sf), + onPressed: () {}, + ), + ), + + body: IndexedStack( + index: _selectedIndex, + children: pages, + ), + + bottomNavigationBar: NavigationBar( + selectedIndex: _selectedIndex, + onDestinationSelected: (index) => setState(() => _selectedIndex = index), + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, + elevation: 1, + height: 70 * math.min(sf, 1.2), + destinations: const [ + NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), + NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'), + NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Equipas'), + NavigationDestination(icon: Icon(Icons.insights_outlined), selectedIcon: Icon(Icons.insights), label: 'Status'), + ], + ), + ); } - class _HomeScreenState extends State { - int _selectedIndex = 0; - final TeamController _teamController = TeamController(); - String? _selectedTeamId; - String _selectedTeamName = "Selecionar Equipa"; + void _showTeamSelector(BuildContext context, double sf) { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))), + builder: (context) { + return StreamBuilder>>( + stream: _teamController.teamsStream, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * sf, child: const Center(child: Text("Nenhuma equipa criada."))); - int _teamWins = 0; - int _teamLosses = 0; - int _teamDraws = 0; + final teams = snapshot.data!; + return ListView.builder( + shrinkWrap: true, + itemCount: teams.length, + itemBuilder: (context, index) { + final team = teams[index]; + return ListTile( + title: Text(team['name']), + onTap: () { + setState(() { + _selectedTeamId = team['id']; + _selectedTeamName = team['name']; + _teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0; + _teamLosses = team['losses'] != null ? int.tryParse(team['losses'].toString()) ?? 0 : 0; + _teamDraws = team['draws'] != null ? int.tryParse(team['draws'].toString()) ?? 0 : 0; + }); + Navigator.pop(context); + }, + ); + }, + ); + }, + ); + }, + ); + } - final _supabase = Supabase.instance.client; + Widget _buildHomeContent(double sf, double wScreen) { + final double cardHeight = (wScreen / 2) * 1.0; - @override - Widget build(BuildContext context) { - final double wScreen = MediaQuery.of(context).size.width; - final double hScreen = MediaQuery.of(context).size.height; - final double sf = math.min(wScreen, hScreen) / 400; + return StreamBuilder>>( + stream: _selectedTeamId != null + ? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!) + : const Stream.empty(), + builder: (context, snapshot) { + Map leaders = _calculateLeaders(snapshot.data ?? []); - final List pages = [ - _buildHomeContent(sf, wScreen), - const GamePage(), - const TeamsPage(), - const Center(child: Text('Tela de Status')), - ]; + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () => _showTeamSelector(context, sf), + child: Container( + padding: EdgeInsets.all(12 * sf), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * sf), SizedBox(width: 10 * sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold))]), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + SizedBox(height: 20 * sf), - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)), - backgroundColor: HomeConfig.primaryColor, - foregroundColor: Colors.white, - leading: IconButton( - icon: Icon(Icons.person, size: 24 * sf), - onPressed: () {}, + SizedBox( + height: cardHeight, + child: Row( + children: [ + Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), + SizedBox(width: 12 * sf), + Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))), + ], + ), + ), + SizedBox(height: 12 * sf), + + SizedBox( + height: cardHeight, + child: Row( + children: [ + Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), + SizedBox(width: 12 * sf), + Expanded( + child: PieChartCard( + victories: _teamWins, + defeats: _teamLosses, + draws: _teamDraws, + title: 'DESEMPENHO', + subtitle: 'Temporada', + backgroundColor: const Color(0xFFC62828), + sf: sf + ), + ), + ], + ), + ), + SizedBox(height: 40 * sf), + + Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])), + SizedBox(height: 16 * sf), + + // 👇 HISTÓRICO LIGADO À BASE DE DADOS (COM AS COLUNAS CORRIGIDAS) 👇 +// 👇 LIGAÇÃO CORRIGIDA: Agora usa a coluna 'nome' como pediste 👇 +// 👇 HISTÓRICO DINÂMICO (Qualquer equipa) 👇 + _selectedTeamName == "Selecionar Equipa" + ? Container( + padding: EdgeInsets.all(20 * sf), + alignment: Alignment.center, + child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)), + ) + : StreamBuilder>>( + // Pede os jogos ordenados pela data (sem filtros rígidos aqui) + stream: _supabase.from('games').stream(primaryKey: ['id']) + .order('game_date', ascending: false), + builder: (context, gameSnapshot) { + + if (gameSnapshot.hasError) { + return Text("Erro ao carregar jogos: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); + } + + if (gameSnapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + // 👇 O CÉREBRO DA APP: Filtro inteligente no Flutter 👇 + final todosOsJogos = gameSnapshot.data ?? []; + + final gamesList = todosOsJogos.where((game) { + String myT = game['my_team']?.toString() ?? ''; + String oppT = game['opponent_team']?.toString() ?? ''; + String status = game['status']?.toString() ?? ''; + + // O jogo tem de envolver a equipa selecionada E estar Terminado + bool isPlaying = (myT == _selectedTeamName || oppT == _selectedTeamName); + bool isFinished = status == 'Terminado'; + + return isPlaying && isFinished; + }).take(3).toList(); // Pega apenas nos 3 mais recentes + + if (gamesList.isEmpty) { + return Container( + padding: EdgeInsets.all(20 * sf), + decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)), + alignment: Alignment.center, + child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)), + ); + } + + return Column( + children: gamesList.map((game) { + + // Lê os dados brutos da base de dados + String dbMyTeam = game['my_team']?.toString() ?? ''; + String dbOppTeam = game['opponent_team']?.toString() ?? ''; + int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0; + int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0; + + String opponent; + int myScore; + int oppScore; + + // 🔄 MAGIA DA INVERSÃO DE RESULTADOS 🔄 + // Garante que os pontos da equipa selecionada aparecem sempre do lado esquerdo + if (dbMyTeam == _selectedTeamName) { + // A equipa que escolhemos está guardada no 'my_team' + opponent = dbOppTeam; + myScore = dbMyScore; + oppScore = dbOppScore; + } else { + // A equipa que escolhemos está guardada no 'opponent_team' + opponent = dbMyTeam; + myScore = dbOppScore; + oppScore = dbMyScore; + } + + // Limpa a data (Remove as horas e deixa só YYYY-MM-DD) + String rawDate = game['game_date']?.toString() ?? '---'; + String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate; + + // Calcula Vitória, Empate ou Derrota para a equipa selecionada + String result = 'E'; + if (myScore > oppScore) result = 'V'; + if (myScore < oppScore) result = 'D'; + + return _buildGameHistoryCard( + opponent: opponent, + result: result, + myScore: myScore, + oppScore: oppScore, + date: date, + sf: sf, + topPts: game['top_pts_name'] ?? '---', + topAst: game['top_ast_name'] ?? '---', + topRbs: game['top_rbs_name'] ?? '---', + topDef: game['top_def_name'] ?? '---', + mvp: game['mvp_name'] ?? '---', + ); + }).toList(), + ); + }, + ), + + SizedBox(height: 20 * sf), + ], + ), ), - ), - - body: IndexedStack( - index: _selectedIndex, - children: pages, - ), + ); + }, + ); + } - bottomNavigationBar: NavigationBar( - selectedIndex: _selectedIndex, - onDestinationSelected: (index) => setState(() => _selectedIndex = index), - backgroundColor: Theme.of(context).colorScheme.surface, - surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, - elevation: 1, - height: 70 * math.min(sf, 1.2), - destinations: const [ - NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'), - NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'), - NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Equipas'), - NavigationDestination(icon: Icon(Icons.insights_outlined), selectedIcon: Icon(Icons.insights), label: 'Status'), - ], - ), - ); + Map _calculateLeaders(List> data) { + Map ptsMap = {}; Map astMap = {}; Map rbsMap = {}; Map namesMap = {}; + for (var row in data) { + String pid = row['member_id'].toString(); + namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido"; + ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0); + astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0); + rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0); } + if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; + String getBest(Map map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; } + int getBestVal(Map map) => map.values.reduce((a, b) => a > b ? a : b); + return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)}; + } - void _showTeamSelector(BuildContext context, double sf) { - showModalBottomSheet( - context: context, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))), - builder: (context) { - return StreamBuilder>>( - stream: _teamController.teamsStream, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); - if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * sf, child: const Center(child: Text("Nenhuma equipa criada."))); - - final teams = snapshot.data!; - return ListView.builder( - shrinkWrap: true, - itemCount: teams.length, - itemBuilder: (context, index) { - final team = teams[index]; - return ListTile( - title: Text(team['name']), - onTap: () { - setState(() { - _selectedTeamId = team['id']; - _selectedTeamName = team['name']; - _teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0; - _teamLosses = team['losses'] != null ? int.tryParse(team['losses'].toString()) ?? 0 : 0; - _teamDraws = team['draws'] != null ? int.tryParse(team['draws'].toString()) ?? 0 : 0; - }); - Navigator.pop(context); - }, - ); - }, - ); - }, - ); - }, - ); - } - - Widget _buildHomeContent(double sf, double wScreen) { - final double cardHeight = (wScreen / 2) * 1.0; - - return StreamBuilder>>( - stream: _selectedTeamId != null - ? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!) - : const Stream.empty(), - builder: (context, snapshot) { - Map leaders = _calculateLeaders(snapshot.data ?? []); - - return SingleChildScrollView( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf), + Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { + return Card( + elevation: 4, margin: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none), + child: Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])), + child: LayoutBuilder( + builder: (context, constraints) { + final double ch = constraints.maxHeight; + final double cw = constraints.maxWidth; + + return Padding( + padding: EdgeInsets.all(cw * 0.06), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - InkWell( - onTap: () => _showTeamSelector(context, sf), - child: Container( - padding: EdgeInsets.all(12 * sf), - decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)), - child: Row( + Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis), + SizedBox(height: ch * 0.011), + SizedBox( + width: double.infinity, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)), + ), + ), + const Spacer(), + Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))), + SizedBox(height: ch * 0.015), + Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), + const Spacer(), + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: ch * 0.035), + decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), + child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold))) + ), + ], + ), + ); + } + ), + ), + ); + } + + Widget _buildGameHistoryCard({ + required String opponent, required String result, required int myScore, required int oppScore, required String date, required double sf, + required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp + }) { + bool isWin = result == 'V'; + bool isDraw = result == 'E'; + Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red); + + return Container( + margin: EdgeInsets.only(bottom: 14 * sf), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(14 * sf), + child: Row( + children: [ + Container( + width: 36 * sf, height: 36 * sf, + decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), + child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * sf))), + ), + SizedBox(width: 14 * sf), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(date, style: TextStyle(fontSize: 11 * sf, color: Colors.grey, fontWeight: FontWeight.w600)), + SizedBox(height: 6 * sf), + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row(children: [Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * sf), SizedBox(width: 10 * sf), Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold))]), - const Icon(Icons.arrow_drop_down), + Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8 * sf), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf), + decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), + child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), + ), + ), + Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), ], ), - ), + ], ), - SizedBox(height: 20 * sf), - - SizedBox( - height: cardHeight, - child: Row( - children: [ - Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)), - SizedBox(width: 12 * sf), - Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))), - ], - ), - ), - SizedBox(height: 12 * sf), - - SizedBox( - height: cardHeight, - child: Row( - children: [ - Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))), - SizedBox(width: 12 * sf), - Expanded( - child: PieChartCard( - victories: _teamWins, - defeats: _teamLosses, - draws: _teamDraws, - title: 'DESEMPENHO', - subtitle: 'Temporada', - backgroundColor: const Color(0xFFC62828), - sf: sf - ), - ), - ], - ), - ), - SizedBox(height: 40 * sf), - - Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])), - SizedBox(height: 16 * sf), - - // 👇 MAGIA ACONTECE AQUI: Ligação à Base de Dados para os Jogos 👇 - _selectedTeamId == null - ? Container( - padding: EdgeInsets.all(20 * sf), - alignment: Alignment.center, - child: Text("Seleciona uma equipa para ver os jogos.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)), - ) - : StreamBuilder>>( - // ⚠️ ATENÇÃO: Substitui 'games' pelo nome real da tua tabela de jogos na Supabase - stream: _supabase.from('games').stream(primaryKey: ['id']) - .eq('team_id', _selectedTeamId!) - // ⚠️ ATENÇÃO: Substitui 'date' pelo nome da coluna da data do jogo - .order('date', ascending: false) - .limit(3), // Mostra só os 3 últimos jogos - builder: (context, gameSnapshot) { - if (gameSnapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) { - return Container( - padding: EdgeInsets.all(20 * sf), - decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)), - alignment: Alignment.center, - child: Column( - children: [ - Icon(Icons.sports_basketball, size: 40 * sf, color: Colors.grey.shade300), - SizedBox(height: 10 * sf), - Text("Ainda não há jogos registados.", style: TextStyle(color: Colors.grey.shade600, fontSize: 14 * sf, fontWeight: FontWeight.bold)), - ], - ), - ); - } - - final gamesList = gameSnapshot.data!; - - return Column( - children: gamesList.map((game) { - // ⚠️ ATENÇÃO: Confirma se os nomes entre parênteses retos [ ] - // batem certo com as tuas colunas na tabela Supabase! - - String opponent = game['opponent_name']?.toString() ?? 'Adversário'; - int myScore = game['my_score'] != null ? int.tryParse(game['my_score'].toString()) ?? 0 : 0; - int oppScore = game['opponent_score'] != null ? int.tryParse(game['opponent_score'].toString()) ?? 0 : 0; - String date = game['date']?.toString() ?? 'Sem Data'; - - // Calcula Vitória (V), Derrota (D) ou Empate (E) automaticamente - String result = 'E'; - if (myScore > oppScore) result = 'V'; - if (myScore < oppScore) result = 'D'; - - // ⚠️ Destaques da Partida. Se ainda não tiveres estas colunas na tabela 'games', - // podes deixar assim e ele mostra '---' sem dar erro. - String topPts = game['top_pts']?.toString() ?? '---'; - String topAst = game['top_ast']?.toString() ?? '---'; - String topRbs = game['top_rbs']?.toString() ?? '---'; - String topDef = game['top_def']?.toString() ?? '---'; - String mvp = game['mvp']?.toString() ?? '---'; - - return _buildGameHistoryCard( - opponent: opponent, - result: result, - myScore: myScore, - oppScore: oppScore, - date: date, - sf: sf, - topPts: topPts, - topAst: topAst, - topRbs: topRbs, - topDef: topDef, - mvp: mvp - ); - }).toList(), - ); - }, - ), - - SizedBox(height: 20 * sf), - ], - ), + ), + ], ), - ); - }, - ); - } + ), + + Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5), - Map _calculateLeaders(List> data) { - Map ptsMap = {}; Map astMap = {}; Map rbsMap = {}; Map namesMap = {}; - for (var row in data) { - String pid = row['member_id'].toString(); - namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido"; - ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0); - astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0); - rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0); - } - if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; - String getBest(Map map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; } - int getBestVal(Map map) => map.values.reduce((a, b) => a > b ? a : b); - return {'pts_name': getBest(ptsMap), 'pts_val': getBestVal(ptsMap), 'ast_name': getBest(astMap), 'ast_val': getBestVal(astMap), 'rbs_name': getBest(rbsMap), 'rbs_val': getBestVal(rbsMap)}; - } - - Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { - return Card( - elevation: 4, margin: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none), - child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])), - child: LayoutBuilder( - builder: (context, constraints) { - final double ch = constraints.maxHeight; - final double cw = constraints.maxWidth; - - return Padding( - padding: EdgeInsets.all(cw * 0.06), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 12 * sf), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)), + ), + child: Column( + children: [ + Row( children: [ - Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis), - SizedBox(height: ch * 0.011), - SizedBox( - width: double.infinity, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)), - ), - ), - const Spacer(), - Center(child: FittedBox(fit: BoxFit.scaleDown, child: Text(statValue, style: TextStyle(fontSize: ch * 0.18, fontWeight: FontWeight.bold, color: Colors.white, height: 1.0)))), - SizedBox(height: ch * 0.015), - Center(child: Text(statLabel, style: TextStyle(fontSize: ch * 0.05, color: Colors.white70))), - const Spacer(), - Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: ch * 0.035), - decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(ch * 0.03)), - child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: ch * 0.05, fontWeight: FontWeight.bold))) - ), + Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, sf, isMvp: true)), + Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, sf)), ], ), - ); - } - ), - ), - ); - } - - Widget _buildGameHistoryCard({ - required String opponent, required String result, required int myScore, required int oppScore, required String date, required double sf, - required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp - }) { - bool isWin = result == 'V'; - bool isDraw = result == 'E'; - Color statusColor = isWin ? Colors.green : (isDraw ? Colors.yellow.shade700 : Colors.red); - - return Container( - margin: EdgeInsets.only(bottom: 14 * sf), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 4))], - ), - child: Column( - children: [ - Padding( - padding: EdgeInsets.all(14 * sf), - child: Row( - children: [ - Container( - width: 36 * sf, height: 36 * sf, - decoration: BoxDecoration(color: statusColor.withOpacity(0.15), shape: BoxShape.circle), - child: Center(child: Text(result, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontSize: 16 * sf))), - ), - SizedBox(width: 14 * sf), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(date, style: TextStyle(fontSize: 11 * sf, color: Colors.grey, fontWeight: FontWeight.w600)), - SizedBox(height: 6 * sf), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)), - Padding( - padding: EdgeInsets.symmetric(horizontal: 8 * sf), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf), - decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)), - child: Text('$myScore - $oppScore', style: TextStyle(fontSize: 15 * sf, fontWeight: FontWeight.w900, letterSpacing: 1.5, color: Colors.black87)), - ), - ), - Expanded(child: Text(opponent, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis)), - ], - ), - ], - ), - ), - ], - ), + SizedBox(height: 8 * sf), + Row( + children: [ + Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, sf)), + Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, sf)), + ], + ), + SizedBox(height: 8 * sf), + Row( + children: [ + Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, sf)), + const Expanded(child: SizedBox()), + ], + ), + ], ), - - Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5), - - Container( - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 12 * sf), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)), - ), - child: Column( - children: [ - Row( - children: [ - Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, sf, isMvp: true)), - Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, sf)), - ], - ), - SizedBox(height: 8 * sf), - Row( - children: [ - Expanded(child: _buildGridStatRow(Icons.bolt, Colors.blue.shade700, "Pontos", topPts, sf)), - Expanded(child: _buildGridStatRow(Icons.trending_up, Colors.purple.shade700, "Rebotes", topRbs, sf)), - ], - ), - SizedBox(height: 8 * sf), - Row( - children: [ - Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, sf)), - const Expanded(child: SizedBox()), - ], - ), - ], - ), - ) - ], - ), - ); - } - - Widget _buildGridStatRow(IconData icon, Color color, String label, String value, double sf, {bool isMvp = false}) { - return Row( - children: [ - Icon(icon, size: 14 * sf, color: color), - SizedBox(width: 4 * sf), - Text('$label: ', style: TextStyle(fontSize: 11 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), - Expanded( - child: Text( - value, - style: TextStyle( - fontSize: 11 * sf, - color: isMvp ? Colors.amber.shade900 : Colors.black87, - fontWeight: FontWeight.bold - ), - maxLines: 1, - overflow: TextOverflow.ellipsis - ) - ), + ) ], - ); - } - } \ No newline at end of file + ), + ); + } + + Widget _buildGridStatRow(IconData icon, Color color, String label, String value, double sf, {bool isMvp = false}) { + return Row( + children: [ + Icon(icon, size: 14 * sf, color: color), + SizedBox(width: 4 * sf), + Text('$label: ', style: TextStyle(fontSize: 11 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)), + Expanded( + child: Text( + value, + style: TextStyle( + fontSize: 11 * sf, + color: isMvp ? Colors.amber.shade900 : Colors.black87, + fontWeight: FontWeight.bold + ), + maxLines: 1, + overflow: TextOverflow.ellipsis + ) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/pages/status_page.dart b/lib/pages/status_page.dart new file mode 100644 index 0000000..1a0cf6d --- /dev/null +++ b/lib/pages/status_page.dart @@ -0,0 +1,282 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../controllers/team_controller.dart'; +import 'dart:math' as math; + +class StatusPage extends StatefulWidget { + const StatusPage({super.key}); + + @override + State createState() => _StatusPageState(); +} + +class _StatusPageState extends State { + final TeamController _teamController = TeamController(); + final _supabase = Supabase.instance.client; + + String? _selectedTeamId; + String _selectedTeamName = "Selecionar Equipa"; + String _sortColumn = 'pts'; + bool _isAscending = false; + + @override + Widget build(BuildContext context) { + final double sf = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) / 400; + + return Column( + children: [ + // --- SELETOR DE EQUIPA --- + Padding( + padding: EdgeInsets.all(16.0 * sf), + child: InkWell( + onTap: () => _showTeamSelector(context, sf), + child: Container( + padding: EdgeInsets.all(12 * sf), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15 * sf), + border: Border.all(color: Colors.grey.shade300), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)] + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * sf), + SizedBox(width: 10 * sf), + Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold)) + ]), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + ), + + // --- TABELA DE ESTATÍSTICAS (AGORA EM TEMPO REAL) --- + Expanded( + child: _selectedTeamId == null + ? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf))) + + // 👇 STREAM 1: LÊ AS ESTATÍSTICAS 👇 + : StreamBuilder>>( + stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), + builder: (context, statsSnapshot) { + + // 👇 STREAM 2: LÊ OS JOGOS (Para os MVPs e contagem de jogos da equipa) 👇 + return StreamBuilder>>( + stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName), + builder: (context, gamesSnapshot) { + + // 👇 STREAM 3: LÊ TODOS OS MEMBROS DO PLANTEL 👇 + // 👇 A CORREÇÃO ESTÁ AQUI: Remover o .eq('type', 'Jogador') +return StreamBuilder>>( + stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!), + builder: (context, membersSnapshot) { + + // Verifica se ALGUM dos 3 streams ainda está a carregar + if (statsSnapshot.connectionState == ConnectionState.waiting || + gamesSnapshot.connectionState == ConnectionState.waiting || + membersSnapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C))); + } + + final membersData = membersSnapshot.data ?? []; + + if (membersData.isEmpty) { + return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf))); + } + + final statsData = statsSnapshot.data ?? []; + final gamesData = gamesSnapshot.data ?? []; + + // Conta o total de jogos terminados da equipa + final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length; + + // Agrega os dados + final List> playerTotals = _aggregateStats(statsData, gamesData, membersData); + + // Calcula os Totais da Equipa + final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam); + + // Ordenação + playerTotals.sort((a, b) { + var valA = a[_sortColumn] ?? 0; + var valB = b[_sortColumn] ?? 0; + return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA); + }); + + return _buildStatsGrid(playerTotals, teamTotals, sf); + } + ); + } + ); + } + ), + ), + ], + ); + } + + // --- CÉREBRO CORRIGIDO --- + List> _aggregateStats(List stats, List games, List members) { + Map> aggregated = {}; + + // 1. Mete a malta toda do plantel com ZERO JOGOS e ZERO STATS + for (var member in members) { + String name = member['name']?.toString() ?? "Desconhecido"; + aggregated[name] = { + 'name': name, + 'j': 0, + 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0, + }; + } + + // 2. Se o jogador tiver linha nas estatísticas, soma +1 Jogo e os pontos dele + for (var row in stats) { + String name = row['player_name']?.toString() ?? "Desconhecido"; + + if (!aggregated.containsKey(name)) { + aggregated[name] = { + 'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0, + }; + } + + aggregated[name]!['j'] += 1; + aggregated[name]!['pts'] += (row['pts'] ?? 0); + aggregated[name]!['ast'] += (row['ast'] ?? 0); + aggregated[name]!['rbs'] += (row['rbs'] ?? 0); + aggregated[name]!['stl'] += (row['stl'] ?? 0); + aggregated[name]!['blk'] += (row['blk'] ?? 0); + } + + // 3. Conta os troféus + for (var game in games) { + String? mvp = game['mvp_name']; + String? defRaw = game['top_def_name']; + + if (mvp != null && aggregated.containsKey(mvp)) { + aggregated[mvp]!['mvp'] += 1; + } + + if (defRaw != null) { + String defName = defRaw.split(' (')[0].trim(); + if (aggregated.containsKey(defName)) { + aggregated[defName]!['def'] += 1; + } + } + } + return aggregated.values.toList(); + } + + Map _calculateTeamTotals(List> players, int teamGames) { + int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0; + for (var p in players) { + tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int); + tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int); + tDef += (p['def'] as int); + } + return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef}; + } + + Widget _buildStatsGrid(List> players, Map teamTotals, double sf) { + return Container( + color: Colors.white, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columnSpacing: 25 * sf, + headingRowColor: MaterialStateProperty.all(Colors.grey.shade100), + dataRowHeight: 60 * sf, + columns: [ + DataColumn(label: const Text('JOGADOR')), + _buildSortableColumn('J', 'j', sf), + _buildSortableColumn('PTS', 'pts', sf), + _buildSortableColumn('AST', 'ast', sf), + _buildSortableColumn('RBS', 'rbs', sf), + _buildSortableColumn('STL', 'stl', sf), + _buildSortableColumn('BLK', 'blk', sf), + _buildSortableColumn('DEF 🛡️', 'def', sf), + _buildSortableColumn('MVP 🏆', 'mvp', sf), + ], + rows: [ + ...players.map((player) => DataRow(cells: [ + DataCell(Row(children: [ + CircleAvatar(radius: 15 * sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * sf)), + SizedBox(width: 10 * sf), + Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf)), + ])), + DataCell(Center(child: Text(player['j'].toString()))), + _buildStatCell(player['pts'], sf, isHighlight: true), + _buildStatCell(player['ast'], sf), + _buildStatCell(player['rbs'], sf), + _buildStatCell(player['stl'], sf), + _buildStatCell(player['blk'], sf), + _buildStatCell(player['def'], sf, isBlue: true), + _buildStatCell(player['mvp'], sf, isGold: true), + ])), + + DataRow( + color: MaterialStateProperty.all(Colors.grey.shade50), + cells: [ + DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * sf))), + DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))), + _buildStatCell(teamTotals['pts'], sf, isHighlight: true), + _buildStatCell(teamTotals['ast'], sf), + _buildStatCell(teamTotals['rbs'], sf), + _buildStatCell(teamTotals['stl'], sf), + _buildStatCell(teamTotals['blk'], sf), + _buildStatCell(teamTotals['def'], sf, isBlue: true), + _buildStatCell(teamTotals['mvp'], sf, isGold: true), + ] + ) + ], + ), + ), + ), + ); + } + + DataColumn _buildSortableColumn(String title, String sortKey, double sf) { + return DataColumn(label: InkWell( + onTap: () => setState(() { + if (_sortColumn == sortKey) _isAscending = !_isAscending; + else { _sortColumn = sortKey; _isAscending = false; } + }), + child: Row(children: [ + Text(title, style: TextStyle(fontSize: 12 * sf, fontWeight: FontWeight.bold)), + if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * sf, color: const Color(0xFFE74C3C)), + ]), + )); + } + + DataCell _buildStatCell(int value, double sf, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) { + return DataCell(Center(child: Container( + padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf), + decoration: BoxDecoration( + color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent), + borderRadius: BorderRadius.circular(6), + ), + child: Text(value == 0 ? "-" : value.toString(), style: TextStyle( + fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600, + fontSize: 14 * sf, + color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87)) + )), + ))); + } + + void _showTeamSelector(BuildContext context, double sf) { + showModalBottomSheet(context: context, builder: (context) => StreamBuilder>>( + stream: _teamController.teamsStream, + builder: (context, snapshot) { + final teams = snapshot.data ?? []; + return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile( + title: Text(teams[i]['name']), + onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); }, + )); + }, + )); + } +} \ No newline at end of file diff --git a/lib/pages/teamPage.dart b/lib/pages/teamPage.dart index b387dd8..7940b1d 100644 --- a/lib/pages/teamPage.dart +++ b/lib/pages/teamPage.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:playmaker/screens/team_stats_page.dart'; import '../controllers/team_controller.dart'; import '../models/team_model.dart'; -import '../widgets/team_widgets.dart'; import 'dart:math' as math; // <-- IMPORTANTE: Adicionar para o cálculo class TeamsPage extends StatefulWidget { @@ -139,7 +138,8 @@ class _TeamsPageState extends State { // 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇 final double wScreen = MediaQuery.of(context).size.width; final double hScreen = MediaQuery.of(context).size.height; -final double sf = math.min(wScreen, hScreen) / 400; + final double sf = math.min(wScreen, hScreen) / 400; + return Scaffold( backgroundColor: const Color(0xFFF5F7FA), appBar: AppBar( @@ -256,4 +256,250 @@ final double sf = math.min(wScreen, hScreen) / 400; ), ); } +} + +// --- TEAM CARD --- +class TeamCard extends StatelessWidget { + final Team team; + final TeamController controller; + final VoidCallback onFavoriteTap; + final double sf; // <-- Variável de escala + + const TeamCard({ + super.key, + required this.team, + required this.controller, + required this.onFavoriteTap, + required this.sf, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.white, + elevation: 3, + margin: EdgeInsets.only(bottom: 12 * sf), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), + child: ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf), + + // --- 1. IMAGEM + FAVORITO --- + leading: Stack( + clipBehavior: Clip.none, + children: [ + CircleAvatar( + radius: 28 * sf, + 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: TextStyle(fontSize: 24 * sf), + ) + : null, + ), + Positioned( + left: -15 * sf, + top: -10 * sf, + child: IconButton( + icon: Icon( + team.isFavorite ? Icons.star : Icons.star_border, + color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), + size: 28 * sf, + shadows: [ + Shadow( + color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1), + blurRadius: 4 * sf, + ), + ], + ), + onPressed: onFavoriteTap, + ), + ), + ], + ), + + // --- 2. TÍTULO --- + title: Text( + team.name, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf), + overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos + ), + + // --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) --- + subtitle: Padding( + padding: EdgeInsets.only(top: 6.0 * sf), + child: Row( + children: [ + Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey), + SizedBox(width: 4 * sf), + + // 👇 STREAMBUILDER EM VEZ DE FUTUREBUILDER 👇 + StreamBuilder( + stream: controller.getPlayerCountStream(team.id), + initialData: 0, + builder: (context, snapshot) { + final count = snapshot.data ?? 0; + return Text( + "$count Jogs.", + style: TextStyle( + color: count > 0 ? Colors.green[700] : Colors.orange, + fontWeight: FontWeight.bold, + fontSize: 13 * sf, + ), + ); + }, + ), + + SizedBox(width: 8 * sf), + Expanded( + child: Text( + "| ${team.season}", + style: TextStyle(color: Colors.grey, fontSize: 13 * sf), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + + // --- 4. BOTÕES (Estatísticas e Apagar) --- + trailing: Row( + mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS + children: [ + IconButton( + tooltip: 'Ver Estatísticas', + icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TeamStatsPage(team: team), + ), + ); + }, + ), + IconButton( + tooltip: 'Eliminar Equipa', + icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf), + onPressed: () => _confirmDelete(context), + ), + ], + ), + ), + ); + } + + void _confirmDelete(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)), + content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)), + ), + TextButton( + onPressed: () { + controller.deleteTeam(team.id); + Navigator.pop(context); + }, + child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)), + ), + ], + ), + ); + } +} + +// --- DIALOG DE CRIAÇÃO --- +class CreateTeamDialog extends StatefulWidget { + final Function(String name, String season, String imageUrl) onConfirm; + final double sf; // Recebe a escala + + const CreateTeamDialog({super.key, required this.onConfirm, required this.sf}); + + @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( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)), + title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _nameController, + style: TextStyle(fontSize: 14 * widget.sf), + decoration: InputDecoration( + labelText: 'Nome da Equipa', + labelStyle: TextStyle(fontSize: 14 * widget.sf) + ), + textCapitalization: TextCapitalization.words, + ), + SizedBox(height: 15 * widget.sf), + DropdownButtonFormField( + value: _selectedSeason, + decoration: InputDecoration( + labelText: 'Temporada', + labelStyle: TextStyle(fontSize: 14 * widget.sf) + ), + style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87), + items: ['2023/24', '2024/25', '2025/26'] + .map((s) => DropdownMenuItem(value: s, child: Text(s))) + .toList(), + onChanged: (val) => setState(() => _selectedSeason = val!), + ), + SizedBox(height: 15 * widget.sf), + TextField( + controller: _imageController, + style: TextStyle(fontSize: 14 * widget.sf), + decoration: InputDecoration( + labelText: 'URL Imagem ou Emoji', + labelStyle: TextStyle(fontSize: 14 * widget.sf), + hintText: 'Ex: 🏀 ou https://...', + hintStyle: TextStyle(fontSize: 14 * widget.sf) + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf)) + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE74C3C), + padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf) + ), + onPressed: () { + if (_nameController.text.trim().isNotEmpty) { + widget.onConfirm( + _nameController.text.trim(), + _selectedSeason, + _imageController.text.trim(), + ); + Navigator.pop(context); + } + }, + child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)), + ), + ], + ); + } } \ No newline at end of file diff --git a/lib/screens/game_screen.dart b/lib/screens/game_screen.dart deleted file mode 100644 index a47f241..0000000 --- a/lib/screens/game_screen.dart +++ /dev/null @@ -1 +0,0 @@ -import 'package:flutter/material.dart'; diff --git a/lib/widgets/team_widgets.dart b/lib/widgets/team_widgets.dart index 3adf85d..c579164 100644 --- a/lib/widgets/team_widgets.dart +++ b/lib/widgets/team_widgets.dart @@ -72,15 +72,17 @@ class TeamCard extends StatelessWidget { overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos ), - // --- 3. SUBTÍTULO (Contagem + Época) --- + // --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) --- subtitle: Padding( padding: EdgeInsets.only(top: 6.0 * sf), child: Row( children: [ Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey), SizedBox(width: 4 * sf), - FutureBuilder( - future: controller.getPlayerCount(team.id), + + // 👇 A CORREÇÃO ESTÁ AQUI: StreamBuilder em vez de FutureBuilder 👇 + StreamBuilder( + stream: controller.getPlayerCountStream(team.id), initialData: 0, builder: (context, snapshot) { final count = snapshot.data ?? 0; @@ -94,6 +96,7 @@ class TeamCard extends StatelessWidget { ); }, ), + SizedBox(width: 8 * sf), Expanded( // Garante que a temporada se adapta se faltar espaço child: Text( @@ -107,7 +110,6 @@ class TeamCard extends StatelessWidget { ), // --- 4. BOTÕES (Estatísticas e Apagar) --- - // Removido o SizedBox fixo! Agora é MainAxisSize.min trailing: Row( mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS children: [