import 'dart:async'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class ShotRecord { final Offset position; final bool isMake; ShotRecord(this.position, this.isMake); } class PlacarController { final String gameId; // O ID real do jogo na base de dados final String myTeam; final String opponentTeam; final VoidCallback onUpdate; PlacarController({ required this.gameId, required this.myTeam, required this.opponentTeam, required this.onUpdate }); bool isLoading = true; bool isSaving = false; // Para mostrar o ícone de loading a guardar int myScore = 0; int opponentScore = 0; int myFouls = 0; int opponentFouls = 0; int currentQuarter = 1; int myTimeoutsUsed = 0; int opponentTimeoutsUsed = 0; String? myTeamDbId; // ID da tua equipa na BD String? oppTeamDbId; // ID da equipa adversária na BD List myCourt = []; List myBench = []; List oppCourt = []; List oppBench = []; Map playerNumbers = {}; Map> playerStats = {}; Map playerDbIds = {}; // NOVO: Mapeia o Nome do jogador -> UUID na base de dados bool showMyBench = false; bool showOppBench = false; bool isSelectingShotLocation = false; String? pendingAction; String? pendingPlayer; List matchShots = []; Duration duration = const Duration(minutes: 10); Timer? timer; bool isRunning = false; Future loadPlayers() async { final supabase = Supabase.instance.client; try { // 1. Limpeza de segurança para evitar duplicados em "Hot Reload" myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear(); playerStats.clear(); playerNumbers.clear(); playerDbIds.clear(); myFouls = 0; opponentFouls = 0; await Future.delayed(const Duration(milliseconds: 1500)); // 2. Buscar dados do JOGO (Placar) final gameResponse = await supabase.from('games').select().eq('id', gameId).single(); myScore = gameResponse['my_score'] ?? 0; opponentScore = gameResponse['opponent_score'] ?? 0; // 3. Buscar IDs das Equipas 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']; if (t['name'] == opponentTeam) oppTeamDbId = t['id']; } // 4. Buscar Membros e ESTATÍSTICAS existentes final myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : []; final oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : []; // Buscar todas as stats deste jogo de uma vez final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId); final Map savedStatsMap = { for (var s in statsResponse) s['member_id'].toString(): s }; // 5. Processar Minha Equipa for (int i = 0; i < myPlayers.length; i++) { String dbId = myPlayers[i]['id'].toString(); String name = myPlayers[i]['name'].toString(); _registerPlayer(name: name, number: myPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: true, isCourt: i < 5); if (savedStatsMap.containsKey(dbId)) { _injectStats(name, savedStatsMap[dbId], true); } } _padTeam(myCourt, myBench, "Jogador", isMyTeam: true); // 6. Processar Adversário for (int i = 0; i < oppPlayers.length; i++) { String dbId = oppPlayers[i]['id'].toString(); String name = oppPlayers[i]['name'].toString(); _registerPlayer(name: name, number: oppPlayers[i]['number']?.toString() ?? "0", dbId: dbId, isMyTeam: false, isCourt: i < 5); if (savedStatsMap.containsKey(dbId)) { _injectStats(name, savedStatsMap[dbId], false); } } _padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false); isLoading = false; onUpdate(); } catch (e) { debugPrint("Erro ao carregar: $e"); isLoading = false; onUpdate(); } } // Função auxiliar para injetar os dados salvos void _injectStats(String playerName, Map s, bool isMyTeam) { playerStats[playerName] = { "pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0, "stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 0, "fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0, }; if (isMyTeam) myFouls += (s['fls'] as int? ?? 0); else opponentFouls += (s['fls'] as int? ?? 0); } void _registerPlayer({required String name, required String number, String? dbId, required bool isMyTeam, required bool isCourt}) { if (playerNumbers.containsKey(name)) name = "$name (Opp)"; playerNumbers[name] = number; if (dbId != null) playerDbIds[name] = dbId; // Só guarda na lista de IDs se for um jogador real da BD playerStats[name] = {"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0, "fls": 0, "fgm": 0, "fga": 0}; if (isMyTeam) { if (isCourt) myCourt.add(name); else myBench.add(name); } else { if (isCourt) oppCourt.add(name); else oppBench.add(name); } } void _padTeam(List court, List bench, String prefix, {required bool isMyTeam}) { while (court.length < 5) { _registerPlayer(name: "Sem $prefix ${court.length + 1}", number: "0", dbId: null, isMyTeam: isMyTeam, isCourt: true); } } // --- TEMPO E TIMEOUTS --- // (Mantive o teu código original igualzinho aqui) void toggleTimer(BuildContext context) { if (isRunning) { timer?.cancel(); } else { timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (duration.inSeconds > 0) { duration -= const Duration(seconds: 1); } else { timer.cancel(); isRunning = false; if (currentQuarter < 4) { currentQuarter++; duration = const Duration(minutes: 10); myFouls = 0; opponentFouls = 0; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas resetadas!'), backgroundColor: Colors.blue)); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red)); } } onUpdate(); }); } isRunning = !isRunning; onUpdate(); } void useTimeout(bool isOpponent) { if (isOpponent) { if (opponentTimeoutsUsed < 3) opponentTimeoutsUsed++; } else { if (myTimeoutsUsed < 3) myTimeoutsUsed++; } isRunning = false; timer?.cancel(); onUpdate(); } String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; // --- LÓGICA DE JOGO --- void handleActionDrag(BuildContext context, String action, String playerData) { String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[name]!; if (stats["fls"]! >= 5 && action != "sub_foul") { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red)); return; } if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") { pendingAction = action; pendingPlayer = playerData; isSelectingShotLocation = true; } else { commitStat(action, playerData); } onUpdate(); } void handleSubbing(BuildContext context, String action, String courtPlayerName, bool isOpponent) { if (action.startsWith("bench_my_") && !isOpponent) { String benchPlayer = action.replaceAll("bench_my_", ""); if (playerStats[benchPlayer]!["fls"]! >= 5) return; int courtIndex = myCourt.indexOf(courtPlayerName); int benchIndex = myBench.indexOf(benchPlayer); myCourt[courtIndex] = benchPlayer; myBench[benchIndex] = courtPlayerName; showMyBench = false; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer'))); } if (action.startsWith("bench_opp_") && isOpponent) { String benchPlayer = action.replaceAll("bench_opp_", ""); if (playerStats[benchPlayer]!["fls"]! >= 5) return; int courtIndex = oppCourt.indexOf(courtPlayerName); int benchIndex = oppBench.indexOf(benchPlayer); oppCourt[courtIndex] = benchPlayer; oppBench[benchIndex] = courtPlayerName; showOppBench = false; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer'))); } onUpdate(); } void registerShotLocation(Offset position) { bool isMake = pendingAction!.startsWith("add_pts_"); matchShots.add(ShotRecord(position, isMake)); commitStat(pendingAction!, pendingPlayer!); isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } void cancelShotLocation() { isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate(); } void commitStat(String action, String playerData) { bool isOpponent = playerData.startsWith("player_opp_"); String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", ""); final stats = playerStats[name]!; if (action.startsWith("add_pts_")) { int pts = int.parse(action.split("_").last); if (isOpponent) opponentScore += pts; else myScore += pts; stats["pts"] = stats["pts"]! + pts; if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; } } else if (action.startsWith("sub_pts_")) { int pts = int.parse(action.split("_").last); if (isOpponent) { opponentScore = (opponentScore - pts < 0) ? 0 : opponentScore - pts; } else { myScore = (myScore - pts < 0) ? 0 : myScore - pts; } stats["pts"] = (stats["pts"]! - pts < 0) ? 0 : stats["pts"]! - pts; if (pts == 2 || pts == 3) { if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1; if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1; } } else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; } else if (action == "add_rbs") { stats["rbs"] = stats["rbs"]! + 1; } else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; } else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; } else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; } else if (action == "add_blk") { stats["blk"] = stats["blk"]! + 1; } else if (action == "add_foul") { stats["fls"] = stats["fls"]! + 1; if (isOpponent) { opponentFouls++; } else { myFouls++; } } else if (action == "sub_foul") { if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1; if (isOpponent) { if (opponentFouls > 0) opponentFouls--; } else { if (myFouls > 0) myFouls--; } } } // --- 💾 FUNÇÃO PARA GUARDAR DADOS NA BD --- Future saveGameStats(BuildContext context) async { final supabase = Supabase.instance.client; isSaving = true; onUpdate(); try { // 1. Atualizar o resultado final na tabela 'games' await supabase.from('games').update({ 'my_score': myScore, 'opponent_score': opponentScore, 'status': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado', }).eq('id', gameId); // 2. Preparar a lista de estatísticas individuais List> batchStats = []; playerStats.forEach((playerName, stats) { String? memberDbId = playerDbIds[playerName]; // Vai buscar o UUID real do jogador // Só guarda se for um jogador real (com ID) e se tiver feito ALGUMA coisa (pontos, faltas, etc) if (memberDbId != null && stats.values.any((val) => val > 0)) { bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName); String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!; batchStats.add({ 'game_id': gameId, 'member_id': memberDbId, 'team_id': teamId, 'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], }); } }); // 3. Apagar stats antigas deste jogo para não haver duplicados caso cliques no botão "Guardar" 2 vezes await supabase.from('player_stats').delete().eq('game_id', gameId); // 4. Inserir as novas estatísticas de todos os jogadores de uma vez if (batchStats.isNotEmpty) { await supabase.from('player_stats').insert(batchStats); } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas guardadas com Sucesso!'), backgroundColor: Colors.green)); } } catch (e) { debugPrint("Erro ao gravar estatísticas: $e"); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red)); } } finally { isSaving = false; onUpdate(); } } void dispose() { timer?.cancel(); } }