diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index a15f571..aeb37fb 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -79,7 +79,19 @@ class GameController { print("Erro ao criar jogo: $e"); return null; } + + } +// ELIMINAR JOGO + Future deleteGame(String gameId) async { + try { + await _supabase.from('games').delete().eq('id', gameId); + // Como o Supabase tem Cascade Delete (se configurado), vai apagar também + // as stats e shot_locations associadas a este game_id automaticamente. + return true; + } catch (e) { + print("Erro ao eliminar jogo: $e"); + return false; + } } - void dispose() {} } \ No newline at end of file diff --git a/lib/controllers/placar_controller.dart b/lib/controllers/placar_controller.dart index c7b96df..317d9f8 100644 --- a/lib/controllers/placar_controller.dart +++ b/lib/controllers/placar_controller.dart @@ -78,7 +78,7 @@ class PlacarController extends ChangeNotifier { String? pendingPlayerId; List matchShots = []; - // 👇 LISTA PARA O HISTÓRICO (PLAY-BY-PLAY) + // Lista para o Histórico de Jogadas List playByPlay = []; ValueNotifier durationNotifier = ValueNotifier(const Duration(minutes: 10)); @@ -113,6 +113,13 @@ class PlacarController extends ChangeNotifier { gameWasAlreadyFinished = gameResponse['status'] == 'Terminado'; + // CARREGAR HISTÓRICO DA BASE DE DADOS + if (gameResponse['play_by_play'] != null) { + playByPlay = List.from(gameResponse['play_by_play']); + } else { + playByPlay = []; + } + final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]); for (var t in teamsResponse) { if (t['name'] == myTeam) myTeamDbId = t['id']; @@ -224,7 +231,7 @@ class PlacarController extends ChangeNotifier { 'playerStats': playerStats, 'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench, 'matchShots': matchShots.map((s) => s.toJson()).toList(), - 'playByPlay': playByPlay, // 👇 Guarda o histórico + 'playByPlay': playByPlay, // Guarda o histórico no telemóvel }; await prefs.setString('backup_$gameId', jsonEncode(backupData)); } catch (e) { @@ -254,7 +261,7 @@ class PlacarController extends ChangeNotifier { List decodedShots = data['matchShots']; matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList(); - playByPlay = List.from(data['playByPlay'] ?? []); // 👇 Carrega o histórico + playByPlay = List.from(data['playByPlay'] ?? []); debugPrint("🔄 AUTO-SAVE RECUPERADO COM SUCESSO!"); } @@ -346,23 +353,22 @@ class PlacarController extends ChangeNotifier { void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) { String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", ""); - bool isMyTeam = targetPlayer.startsWith("player_my_"); bool isMake = action.startsWith("add_"); - String name = playerNames[playerId]!; + String name = playerNames[playerId] ?? "Jogador"; - if (playerStats.containsKey(playerId)) { - playerStats[playerId]!['fga'] = playerStats[playerId]!['fga']! + 1; + matchShots.add(ShotRecord( + relativeX: relativeX, + relativeY: relativeY, + isMake: isMake, + playerId: playerId, + playerName: name, + zone: zone, + points: points + )); - if (isMake) { - playerStats[playerId]!['fgm'] = playerStats[playerId]!['fgm']! + 1; - playerStats[playerId]!['pts'] = playerStats[playerId]!['pts']! + points; - if (isMyTeam) myScore += points; else opponentScore += points; - } - } + String finalAction = isMake ? "add_pts_$points" : "miss_$points"; + commitStat(finalAction, targetPlayer); - matchShots.add(ShotRecord(relativeX: relativeX, relativeY: relativeY, isMake: isMake, playerId: playerId, playerName: name, zone: zone, points: points)); - - _saveLocalBackup(); notifyListeners(); } @@ -457,7 +463,18 @@ class PlacarController extends ChangeNotifier { else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; logText = "falhou lançamento ❌"; } else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto ofensivo 🔄"; } else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; logText = "ganhou ressalto defensivo 🛡️"; } - else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; logText = "fez uma assistência 🤝"; } + else if (action == "add_ast") { + stats["ast"] = stats["ast"]! + 1; + + if (playByPlay.isNotEmpty && playByPlay[0].contains("marcou") && !playByPlay[0].contains("Assistência")) { + playByPlay[0] = "${playByPlay[0]} (Assistência: $name 🤝)"; + _saveLocalBackup(); + notifyListeners(); + return; + } else { + logText = "fez uma assistência 🤝"; + } + } else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; logText = "roubou a bola 🥷"; } else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; logText = "perdeu a bola (turnover) 🤦"; } else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; logText = "fez um desarme (bloco) ✋"; } @@ -514,6 +531,7 @@ class PlacarController extends ChangeNotifier { if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = pName; } }); + // ATUALIZA O JOGO COM OS NOVOS ESTADOS E COM O HISTÓRICO DE JOGADAS! await supabase.from('games').update({ 'my_score': myScore, 'opponent_score': opponentScore, @@ -527,6 +545,7 @@ class PlacarController extends ChangeNotifier { 'top_rbs_name': topRbsName, 'top_def_name': topDefName, 'mvp_name': mvpName, + 'play_by_play': playByPlay, // Envia o histórico para a base de dados }).eq('id', gameId); if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) { diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 5cd6398..71097ee 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:playmaker/pages/PlacarPage.dart'; import 'package:playmaker/classe/theme.dart'; -import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE AQUI +import 'package:cached_network_image/cached_network_image.dart'; import '../controllers/team_controller.dart'; import '../controllers/game_controller.dart'; import '../models/game_model.dart'; @@ -12,16 +12,17 @@ class GameResultCard extends StatelessWidget { final String gameId, myTeam, opponentTeam, myScore, opponentScore, status, season; final String? myTeamLogo, opponentTeamLogo; final double sf; + final VoidCallback onDelete; 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, + this.myTeamLogo, this.opponentTeamLogo, required this.sf, required this.onDelete, }); @override Widget build(BuildContext context) { - final bgColor = Theme.of(context).cardTheme.color; + final bgColor = Theme.of(context).cardTheme.color ?? Theme.of(context).colorScheme.surface; final textColor = Theme.of(context).colorScheme.onSurface; return Container( @@ -33,20 +34,57 @@ class GameResultCard extends StatelessWidget { boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)], border: Border.all(color: Colors.grey.withOpacity(0.1)), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Stack( children: [ - Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), - _buildScoreCenter(context, gameId, sf, textColor), - Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: _buildTeamInfo(myTeam, AppTheme.primaryRed, myTeamLogo, sf, textColor)), + _buildScoreCenter(context, gameId, sf, textColor), + Expanded(child: _buildTeamInfo(opponentTeam, Colors.grey.shade600, opponentTeamLogo, sf, textColor)), + ], + ), + Positioned( + top: -10 * sf, + right: -10 * sf, + child: IconButton( + icon: Icon(Icons.delete_outline, color: Colors.grey.shade400, size: 22 * sf), + splashRadius: 20 * sf, + onPressed: () => _showDeleteConfirmation(context), + ), + ), ], ), ); } - // 👇 AVATAR OTIMIZADO COM CACHE 👇 + void _showDeleteConfirmation(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)), + title: Text('Eliminar Jogo', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf, color: Theme.of(context).colorScheme.onSurface)), + content: Text('Tem a certeza que deseja eliminar este jogo? Esta ação apagará todas as estatísticas associadas e não pode ser desfeita.', style: TextStyle(fontSize: 14 * sf, color: Theme.of(context).colorScheme.onSurface)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('CANCELAR', style: TextStyle(color: Colors.grey, fontSize: 14 * sf)) + ), + TextButton( + onPressed: () { + Navigator.pop(ctx); + onDelete(); + }, + child: Text('ELIMINAR', style: TextStyle(color: AppTheme.primaryRed, fontWeight: FontWeight.bold, fontSize: 14 * sf)) + ), + ], + ) + ); + } + Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf, Color textColor) { - final double avatarSize = 48 * sf; // 2 * radius (24) + final double avatarSize = 48 * sf; return Column( children: [ @@ -54,18 +92,14 @@ class GameResultCard extends StatelessWidget { child: Container( width: avatarSize, height: avatarSize, - color: color.withOpacity(0.1), // Fundo suave para não ser agressivo + color: color.withOpacity(0.1), child: (logoUrl != null && logoUrl.isNotEmpty) ? CachedNetworkImage( imageUrl: logoUrl, fit: BoxFit.cover, - fadeInDuration: Duration.zero, // Fica instantâneo - placeholder: (context, url) => Center( - child: Icon(Icons.shield, color: color, size: 24 * sf) - ), - errorWidget: (context, url, error) => Center( - child: Icon(Icons.shield, color: color, size: 24 * sf) - ), + fadeInDuration: Duration.zero, + placeholder: (context, url) => Center(child: Icon(Icons.shield, color: color, size: 24 * sf)), + errorWidget: (context, url, error) => Center(child: Icon(Icons.shield, color: color, size: 24 * sf)), ) : Center(child: Icon(Icons.shield, color: color, size: 24 * sf)), ), @@ -179,7 +213,6 @@ class _CreateGameDialogManualState extends State { ); } - // 👇 PESQUISA COM CACHE 👇 Widget _buildSearch(BuildContext context, String label, TextEditingController controller) { return StreamBuilder>>( stream: widget.teamController.teamsStream, @@ -304,9 +337,33 @@ class _GamePageState extends State { if (team['name'] == game.opponentTeam) oppLogo = team['image_url']; } return GameResultCard( - gameId: game.id, myTeam: game.myTeam, opponentTeam: game.opponentTeam, myScore: game.myScore, - opponentScore: game.opponentScore, status: game.status, season: game.season, myTeamLogo: myLogo, opponentTeamLogo: oppLogo, + gameId: game.id, + myTeam: game.myTeam, + opponentTeam: game.opponentTeam, + myScore: game.myScore, + opponentScore: game.opponentScore, + status: game.status, + season: game.season, + myTeamLogo: myLogo, + opponentTeamLogo: oppLogo, sf: context.sf, + onDelete: () async { + bool success = await gameController.deleteGame(game.id); + if (context.mounted) { + if (success) { + // 👇 ISTO FORÇA A LISTA A ATUALIZAR IMEDIATAMENTE 👇 + setState(() {}); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Jogo eliminado com sucesso!'), backgroundColor: Colors.green) + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Erro ao eliminar o jogo.'), backgroundColor: Colors.red) + ); + } + } + }, ); }, ); diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 4acbe00..ac21498 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -31,7 +31,7 @@ class _HomeScreenState extends State { final _supabase = Supabase.instance.client; String? _avatarUrl; - bool _isMemoryLoaded = false; // 👈 A variável mágica que impede o "piscar" inicial + bool _isMemoryLoaded = false; // A variável mágica que impede o "piscar" inicial @override void initState() { @@ -39,7 +39,7 @@ class _HomeScreenState extends State { _loadUserAvatar(); } - // 👇 FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background + // FUNÇÃO OTIMIZADA: Carrega da memória instantaneamente e atualiza em background Future _loadUserAvatar() async { // 1. LÊ DA MEMÓRIA RÁPIDA PRIMEIRO final prefs = await SharedPreferences.getInstance(); @@ -84,7 +84,7 @@ class _HomeScreenState extends State { final List pages = [ _buildHomeContent(context), const GamePage(), - const TeamsPage(), + const TeamsPage(), const StatusPage(), ]; @@ -107,7 +107,7 @@ class _HomeScreenState extends State { ); _loadUserAvatar(); }, - // 👇 SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA + // SÓ MOSTRA A IMAGEM OU O BONECO DEPOIS DE LER A MEMÓRIA child: !_isMemoryLoaded // Nos primeiros 0.05 segs, mostra só o círculo de fundo (sem boneco) ? CircleAvatar(backgroundColor: Colors.white.withOpacity(0.2)) @@ -167,8 +167,13 @@ class _HomeScreenState extends State { 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 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)))); + // Correção: Verifica hasData para evitar piscar tela de loading + if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())); + } + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return SizedBox(height: 200 * context.sf, child: Center(child: Text("Nenhuma equipa criada.", style: TextStyle(color: Theme.of(context).colorScheme.onSurface)))); + } final teams = snapshot.data!; return ListView.builder( @@ -178,11 +183,11 @@ class _HomeScreenState extends State { final team = teams[index]; return ListTile( leading: const Icon(Icons.shield, color: AppTheme.primaryRed), - title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), + title: Text(team['name'] ?? 'Sem Nome', style: TextStyle(color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold)), onTap: () { setState(() { _selectedTeamId = team['id'].toString(); - _selectedTeamName = team['name']; + _selectedTeamName = team['name'] ?? 'Desconhecido'; _teamWins = int.tryParse(team['wins']?.toString() ?? '0') ?? 0; _teamLosses = int.tryParse(team['losses']?.toString() ?? '0') ?? 0; _teamDraws = int.tryParse(team['draws']?.toString() ?? '0') ?? 0; @@ -325,7 +330,11 @@ class _HomeScreenState extends State { stream: _supabase.from('games').stream(primaryKey: ['id']).order('game_date', ascending: false), builder: (context, gameSnapshot) { if (gameSnapshot.hasError) return Text("Erro: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red)); - if (gameSnapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); + + // Correção: Verifica hasData em vez de ConnectionState para manter a lista na tela enquanto atualiza em plano de fundo + if (!gameSnapshot.hasData && gameSnapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } final todosOsJogos = gameSnapshot.data ?? []; final gamesList = todosOsJogos.where((game) { @@ -349,8 +358,8 @@ class _HomeScreenState extends State { children: gamesList.map((game) { 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; + int dbMyScore = int.tryParse(game['my_score']?.toString() ?? '0') ?? 0; + int dbOppScore = int.tryParse(game['opponent_score']?.toString() ?? '0') ?? 0; String opponent; int myScore; int oppScore; @@ -389,16 +398,33 @@ class _HomeScreenState extends State { Map _calculateLeaders(List> data) { Map ptsMap = {}; Map astMap = {}; Map rbsMap = {}; Map namesMap = {}; for (var row in data) { - String pid = row['member_id'].toString(); + String pid = row['member_id']?.toString() ?? "unknown"; namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido"; ptsMap[pid] = (ptsMap[pid] ?? 0) + (int.tryParse(row['pts']?.toString() ?? '0') ?? 0); astMap[pid] = (astMap[pid] ?? 0) + (int.tryParse(row['ast']?.toString() ?? '0') ?? 0); rbsMap[pid] = (rbsMap[pid] ?? 0) + (int.tryParse(row['rbs']?.toString() ?? '0') ?? 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)}; + + if (ptsMap.isEmpty) { + return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0}; + } + + String getBest(Map map) { + if (map.isEmpty) return '---'; + var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; + return namesMap[bestId] ?? '---'; + } + + int getBestVal(Map map) { + if (map.isEmpty) return 0; + return 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 BuildContext context, required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) { 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';