sabado
This commit is contained in:
@@ -4,51 +4,44 @@ import '../models/game_model.dart';
|
||||
class GameController {
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// 👇 Atalho para apanhar o ID do utilizador logado
|
||||
String get myUserId => _supabase.auth.currentUser?.id ?? '';
|
||||
|
||||
// 1. LER JOGOS (Stream em Tempo Real da tabela original)
|
||||
// LER JOGOS
|
||||
Stream<List<Game>> get gamesStream {
|
||||
return _supabase
|
||||
.from('games')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('user_id', myUserId) // 🔒 SEGURANÇA: Ouve apenas os jogos deste utilizador
|
||||
.eq('user_id', myUserId)
|
||||
.asyncMap((event) async {
|
||||
// Lê diretamente da tabela "games" e já não da "games_with_logos"
|
||||
final data = await _supabase
|
||||
.from('games')
|
||||
.select()
|
||||
.eq('user_id', myUserId) // 🔒 SEGURANÇA
|
||||
.eq('user_id', myUserId)
|
||||
.order('game_date', ascending: false);
|
||||
|
||||
// O Game.fromMap agora faz o trabalho sujo todo!
|
||||
return data.map((json) => Game.fromMap(json)).toList();
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 👇 LER JOGOS COM FILTROS DE EQUIPA E TEMPORADA
|
||||
// =========================================================================
|
||||
// LER JOGOS COM FILTROS
|
||||
Stream<List<Game>> getFilteredGames({required String teamFilter, required String seasonFilter}) {
|
||||
return _supabase
|
||||
.from('games')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('user_id', myUserId) // 🔒 SEGURANÇA
|
||||
.eq('user_id', myUserId)
|
||||
.asyncMap((event) async {
|
||||
|
||||
// 1. Começamos a query na tabela principal "games"
|
||||
var query = _supabase.from('games').select().eq('user_id', myUserId); // 🔒 SEGURANÇA
|
||||
var query = _supabase.from('games').select().eq('user_id', myUserId);
|
||||
|
||||
// 2. Se a temporada não for "Todas", aplicamos o filtro AQUI
|
||||
if (seasonFilter != 'Todas') {
|
||||
query = query.eq('season', seasonFilter);
|
||||
}
|
||||
|
||||
// 3. Executamos a query e ordenamos pela data
|
||||
final data = await query.order('game_date', ascending: false);
|
||||
|
||||
List<Game> games = data.map((json) => Game.fromMap(json)).toList();
|
||||
|
||||
// 4. Filtramos a equipa em memória
|
||||
if (teamFilter != 'Todas') {
|
||||
games = games.where((g) => g.myTeam == teamFilter || g.opponentTeam == teamFilter).toList();
|
||||
}
|
||||
@@ -57,11 +50,11 @@ class GameController {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. CRIAR JOGO
|
||||
// CRIAR JOGO
|
||||
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
||||
try {
|
||||
final response = await _supabase.from('games').insert({
|
||||
'user_id': myUserId, // 🔒 CARIMBA O JOGO COM O ID DO TREINADOR
|
||||
'user_id': myUserId,
|
||||
'my_team': myTeam,
|
||||
'opponent_team': opponent,
|
||||
'season': season,
|
||||
@@ -69,16 +62,24 @@ class GameController {
|
||||
'opponent_score': 0,
|
||||
'status': 'Decorrer',
|
||||
'game_date': DateTime.now().toIso8601String(),
|
||||
// 👇 Preenchemos logo com os valores iniciais da tua Base de Dados
|
||||
'remaining_seconds': 600, // Assume 10 minutos (600s)
|
||||
'my_timeouts': 0,
|
||||
'opp_timeouts': 0,
|
||||
'current_quarter': 1,
|
||||
'top_pts_name': '---',
|
||||
'top_ast_name': '---',
|
||||
'top_rbs_name': '---',
|
||||
'top_def_name': '---',
|
||||
'mvp_name': '---',
|
||||
}).select().single();
|
||||
|
||||
return response['id'];
|
||||
return response['id']?.toString();
|
||||
} catch (e) {
|
||||
print("Erro ao criar jogo: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
// Não é necessário fechar streams do Supabase manualmente aqui
|
||||
}
|
||||
void dispose() {}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ShotRecord {
|
||||
final double relativeX;
|
||||
final double relativeY;
|
||||
final bool isMake;
|
||||
final String playerId;
|
||||
final String playerName;
|
||||
final String? zone;
|
||||
final int? points;
|
||||
@@ -15,28 +18,39 @@ class ShotRecord {
|
||||
required this.relativeX,
|
||||
required this.relativeY,
|
||||
required this.isMake,
|
||||
required this.playerId,
|
||||
required this.playerName,
|
||||
this.zone,
|
||||
this.points,
|
||||
});
|
||||
|
||||
// 👇 Para o Auto-Save converter em Texto
|
||||
Map<String, dynamic> toJson() => {
|
||||
'relativeX': relativeX, 'relativeY': relativeY, 'isMake': isMake,
|
||||
'playerId': playerId, 'playerName': playerName, 'zone': zone, 'points': points,
|
||||
};
|
||||
|
||||
// 👇 Para o Auto-Save ler do Texto
|
||||
factory ShotRecord.fromJson(Map<String, dynamic> json) => ShotRecord(
|
||||
relativeX: json['relativeX'], relativeY: json['relativeY'], isMake: json['isMake'],
|
||||
playerId: json['playerId'], playerName: json['playerName'], zone: json['zone'], points: json['points'],
|
||||
);
|
||||
}
|
||||
|
||||
class PlacarController {
|
||||
// 👇 AGORA É UM CHANGENOTIFIER (Gestor de Estado Profissional) 👇
|
||||
class PlacarController extends ChangeNotifier {
|
||||
final String gameId;
|
||||
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;
|
||||
|
||||
bool gameWasAlreadyFinished = false;
|
||||
|
||||
int myScore = 0;
|
||||
@@ -55,19 +69,20 @@ class PlacarController {
|
||||
List<String> oppCourt = [];
|
||||
List<String> oppBench = [];
|
||||
|
||||
Map<String, String> playerNames = {};
|
||||
Map<String, String> playerNumbers = {};
|
||||
Map<String, Map<String, int>> playerStats = {};
|
||||
Map<String, String> playerDbIds = {};
|
||||
|
||||
bool showMyBench = false;
|
||||
bool showOppBench = false;
|
||||
|
||||
bool isSelectingShotLocation = false;
|
||||
String? pendingAction;
|
||||
String? pendingPlayer;
|
||||
String? pendingPlayerId;
|
||||
List<ShotRecord> matchShots = [];
|
||||
|
||||
Duration duration = const Duration(minutes: 10);
|
||||
// 👇 O CRONÓMETRO AGORA TEM VIDA PRÓPRIA (ValueNotifier) PARA NÃO ENCRAVAR A APP 👇
|
||||
ValueNotifier<Duration> durationNotifier = ValueNotifier(const Duration(minutes: 10));
|
||||
Timer? timer;
|
||||
bool isRunning = false;
|
||||
|
||||
@@ -81,16 +96,9 @@ class PlacarController {
|
||||
try {
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
|
||||
myCourt.clear();
|
||||
myBench.clear();
|
||||
oppCourt.clear();
|
||||
oppBench.clear();
|
||||
playerStats.clear();
|
||||
playerNumbers.clear();
|
||||
playerDbIds.clear();
|
||||
matchShots.clear(); // Limpa as bolas do último jogo
|
||||
myFouls = 0;
|
||||
opponentFouls = 0;
|
||||
myCourt.clear(); myBench.clear(); oppCourt.clear(); oppBench.clear();
|
||||
playerNames.clear(); playerStats.clear(); playerNumbers.clear();
|
||||
matchShots.clear(); myFouls = 0; opponentFouls = 0;
|
||||
|
||||
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
|
||||
|
||||
@@ -98,7 +106,7 @@ class PlacarController {
|
||||
opponentScore = int.tryParse(gameResponse['opponent_score']?.toString() ?? '0') ?? 0;
|
||||
|
||||
int totalSeconds = int.tryParse(gameResponse['remaining_seconds']?.toString() ?? '600') ?? 600;
|
||||
duration = Duration(seconds: totalSeconds);
|
||||
durationNotifier.value = Duration(seconds: totalSeconds);
|
||||
|
||||
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
||||
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
||||
@@ -128,7 +136,7 @@ class PlacarController {
|
||||
|
||||
if (savedStats.containsKey(dbId)) {
|
||||
var s = savedStats[dbId];
|
||||
playerStats[name] = {
|
||||
playerStats[dbId] = {
|
||||
"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,
|
||||
@@ -147,7 +155,7 @@ class PlacarController {
|
||||
|
||||
if (savedStats.containsKey(dbId)) {
|
||||
var s = savedStats[dbId];
|
||||
playerStats[name] = {
|
||||
playerStats[dbId] = {
|
||||
"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,
|
||||
@@ -158,44 +166,46 @@ class PlacarController {
|
||||
}
|
||||
_padTeam(oppCourt, oppBench, "Adversário", isMyTeam: false);
|
||||
|
||||
// 👇 CARREGA AS BOLINHAS ANTIGAS (MAPA DE CALOR DO JOGO ATUAL) 👇
|
||||
final shotsResponse = await supabase.from('shot_locations').select().eq('game_id', gameId);
|
||||
for (var shotData in shotsResponse) {
|
||||
matchShots.add(ShotRecord(
|
||||
relativeX: double.parse(shotData['relative_x'].toString()),
|
||||
relativeY: double.parse(shotData['relative_y'].toString()),
|
||||
isMake: shotData['is_make'] == true,
|
||||
playerId: shotData['member_id'].toString(),
|
||||
playerName: shotData['player_name'].toString(),
|
||||
zone: shotData['zone']?.toString(),
|
||||
points: shotData['points'] != null ? int.parse(shotData['points'].toString()) : null,
|
||||
));
|
||||
}
|
||||
|
||||
// 👇 AUTO-SAVE: SE O JOGO FOI ABAIXO A MEIO, RECUPERA TUDO AQUI! 👇
|
||||
await _loadLocalBackup();
|
||||
|
||||
isLoading = false;
|
||||
onUpdate();
|
||||
notifyListeners(); // Substitui o antigo onUpdate!
|
||||
} catch (e) {
|
||||
debugPrint("Erro ao retomar jogo: $e");
|
||||
_padTeam(myCourt, myBench, "Falha", isMyTeam: true);
|
||||
_padTeam(oppCourt, oppBench, "Falha Opp", isMyTeam: false);
|
||||
isLoading = false;
|
||||
onUpdate();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
String id = dbId ?? "fake_${DateTime.now().millisecondsSinceEpoch}_${math.Random().nextInt(9999)}";
|
||||
|
||||
playerStats[name] = {
|
||||
playerNames[id] = name;
|
||||
playerNumbers[id] = number;
|
||||
|
||||
playerStats[id] = {
|
||||
"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0,
|
||||
"fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0
|
||||
};
|
||||
|
||||
if (isMyTeam) {
|
||||
if (isCourt) myCourt.add(name); else myBench.add(name);
|
||||
if (isCourt) myCourt.add(id); else myBench.add(id);
|
||||
} else {
|
||||
if (isCourt) oppCourt.add(name); else oppBench.add(name);
|
||||
if (isCourt) oppCourt.add(id); else oppBench.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,33 +215,80 @@ class PlacarController {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 👇 AS DUAS FUNÇÕES MÁGICAS DO AUTO-SAVE 👇
|
||||
// =========================================================================
|
||||
Future<void> _saveLocalBackup() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final backupData = {
|
||||
'myScore': myScore, 'opponentScore': opponentScore,
|
||||
'myFouls': myFouls, 'opponentFouls': opponentFouls,
|
||||
'currentQuarter': currentQuarter, 'duration': durationNotifier.value.inSeconds,
|
||||
'myTimeoutsUsed': myTimeoutsUsed, 'opponentTimeoutsUsed': opponentTimeoutsUsed,
|
||||
'playerStats': playerStats,
|
||||
'myCourt': myCourt, 'myBench': myBench, 'oppCourt': oppCourt, 'oppBench': oppBench,
|
||||
'matchShots': matchShots.map((s) => s.toJson()).toList(),
|
||||
};
|
||||
await prefs.setString('backup_$gameId', jsonEncode(backupData));
|
||||
} catch (e) {
|
||||
debugPrint("Erro no Auto-Save: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLocalBackup() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String? backupString = prefs.getString('backup_$gameId');
|
||||
|
||||
if (backupString != null) {
|
||||
final data = jsonDecode(backupString);
|
||||
|
||||
myScore = data['myScore']; opponentScore = data['opponentScore'];
|
||||
myFouls = data['myFouls']; opponentFouls = data['opponentFouls'];
|
||||
currentQuarter = data['currentQuarter']; durationNotifier.value = Duration(seconds: data['duration']);
|
||||
myTimeoutsUsed = data['myTimeoutsUsed']; opponentTimeoutsUsed = data['opponentTimeoutsUsed'];
|
||||
|
||||
myCourt = List<String>.from(data['myCourt']); myBench = List<String>.from(data['myBench']);
|
||||
oppCourt = List<String>.from(data['oppCourt']); oppBench = List<String>.from(data['oppBench']);
|
||||
|
||||
Map<String, dynamic> decodedStats = data['playerStats'];
|
||||
playerStats = decodedStats.map((k, v) => MapEntry(k, Map<String, int>.from(v)));
|
||||
|
||||
List<dynamic> decodedShots = data['matchShots'];
|
||||
matchShots = decodedShots.map((s) => ShotRecord.fromJson(s)).toList();
|
||||
|
||||
debugPrint("🔄 AUTO-SAVE RECUPERADO COM SUCESSO!");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Erro ao carregar Auto-Save: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void toggleTimer(BuildContext context) {
|
||||
if (isRunning) {
|
||||
timer?.cancel();
|
||||
_saveLocalBackup(); // Grava no telemóvel quando pausa!
|
||||
} else {
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (duration.inSeconds > 0) {
|
||||
duration -= const Duration(seconds: 1);
|
||||
if (durationNotifier.value.inSeconds > 0) {
|
||||
durationNotifier.value -= const Duration(seconds: 1); // 👈 Só o relógio atualiza, a app não pisca!
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
durationNotifier.value = const Duration(minutes: 10);
|
||||
myFouls = 0; opponentFouls = 0;
|
||||
myTimeoutsUsed = 0; opponentTimeoutsUsed = 0;
|
||||
_saveLocalBackup(); // Grava mudança de período
|
||||
}
|
||||
notifyListeners(); // Aqui sim, redesenhamos o ecrã para mudar o Quarto
|
||||
}
|
||||
onUpdate();
|
||||
});
|
||||
}
|
||||
isRunning = !isRunning;
|
||||
onUpdate();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void useTimeout(bool isOpponent) {
|
||||
@@ -242,14 +299,14 @@ class PlacarController {
|
||||
}
|
||||
isRunning = false;
|
||||
timer?.cancel();
|
||||
onUpdate();
|
||||
_saveLocalBackup();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
|
||||
void handleActionDrag(BuildContext context, String action, String playerData) {
|
||||
String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
final stats = playerStats[name]!;
|
||||
String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
final stats = playerStats[playerId]!;
|
||||
final name = playerNames[playerId]!;
|
||||
|
||||
if (stats["fls"]! >= 5 && action != "sub_foul") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('🛑 $name atingiu 5 faltas e está expulso!'), backgroundColor: Colors.red));
|
||||
@@ -258,91 +315,61 @@ class PlacarController {
|
||||
|
||||
if (action == "add_pts_2" || action == "add_pts_3" || action == "miss_2" || action == "miss_3") {
|
||||
pendingAction = action;
|
||||
pendingPlayer = playerData;
|
||||
pendingPlayerId = playerData;
|
||||
isSelectingShotLocation = true;
|
||||
} else {
|
||||
commitStat(action, playerData);
|
||||
}
|
||||
onUpdate();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void handleSubbing(BuildContext context, String action, String courtPlayerName, bool isOpponent) {
|
||||
void handleSubbing(BuildContext context, String action, String courtPlayerId, 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;
|
||||
String benchPlayerId = action.replaceAll("bench_my_", "");
|
||||
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
|
||||
int courtIndex = myCourt.indexOf(courtPlayerId);
|
||||
int benchIndex = myBench.indexOf(benchPlayerId);
|
||||
myCourt[courtIndex] = benchPlayerId;
|
||||
myBench[benchIndex] = courtPlayerId;
|
||||
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;
|
||||
String benchPlayerId = action.replaceAll("bench_opp_", "");
|
||||
if (playerStats[benchPlayerId]!["fls"]! >= 5) return;
|
||||
int courtIndex = oppCourt.indexOf(courtPlayerId);
|
||||
int benchIndex = oppBench.indexOf(benchPlayerId);
|
||||
oppCourt[courtIndex] = benchPlayerId;
|
||||
oppBench[benchIndex] = courtPlayerId;
|
||||
showOppBench = false;
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $courtPlayerName, Entra $benchPlayer')));
|
||||
}
|
||||
onUpdate();
|
||||
_saveLocalBackup();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 👇 REGISTA PONTOS VINDO DO POP-UP AMARELO (E MARCA A BOLINHA)
|
||||
// =========================================================================
|
||||
void registerShotFromPopup(BuildContext context, String action, String targetPlayer, String zone, int points, double relativeX, double relativeY) {
|
||||
// 💡 AVISO AMIGÁVEL REMOVIDO. Agora podes marcar pontos mesmo com o tempo parado!
|
||||
|
||||
String name = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
String playerId = targetPlayer.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
bool isMyTeam = targetPlayer.startsWith("player_my_");
|
||||
bool isMake = action.startsWith("add_");
|
||||
String name = playerNames[playerId]!;
|
||||
|
||||
// 1. ATUALIZA A ESTATÍSTICA DO JOGADOR
|
||||
if (playerStats.containsKey(name)) {
|
||||
playerStats[name]!['fga'] = playerStats[name]!['fga']! + 1;
|
||||
if (playerStats.containsKey(playerId)) {
|
||||
playerStats[playerId]!['fga'] = playerStats[playerId]!['fga']! + 1;
|
||||
|
||||
if (isMake) {
|
||||
playerStats[name]!['fgm'] = playerStats[name]!['fgm']! + 1;
|
||||
playerStats[name]!['pts'] = playerStats[name]!['pts']! + points;
|
||||
|
||||
// 2. ATUALIZA O PLACAR DA EQUIPA
|
||||
if (isMyTeam) {
|
||||
myScore += points;
|
||||
} else {
|
||||
opponentScore += points;
|
||||
}
|
||||
playerStats[playerId]!['fgm'] = playerStats[playerId]!['fgm']! + 1;
|
||||
playerStats[playerId]!['pts'] = playerStats[playerId]!['pts']! + points;
|
||||
if (isMyTeam) myScore += points; else opponentScore += points;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. CRIA A BOLINHA PARA APARECER NO CAMPO
|
||||
matchShots.add(ShotRecord(
|
||||
relativeX: relativeX,
|
||||
relativeY: relativeY,
|
||||
isMake: isMake,
|
||||
playerName: name,
|
||||
zone: zone,
|
||||
points: points,
|
||||
));
|
||||
matchShots.add(ShotRecord(relativeX: relativeX, relativeY: relativeY, isMake: isMake, playerId: playerId, playerName: name, zone: zone, points: points));
|
||||
|
||||
// 4. MANDA UMA MENSAGEM NO ECRÃ
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(isMake ? '🔥 $name MARCOU de $zone!' : '❌ $name FALHOU de $zone!'),
|
||||
backgroundColor: isMake ? Colors.green : Colors.red,
|
||||
duration: const Duration(seconds: 2),
|
||||
)
|
||||
);
|
||||
|
||||
// 5. ATUALIZA O ECRÃ
|
||||
onUpdate();
|
||||
_saveLocalBackup(); // 👈 Grava logo para não perder o cesto!
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// MANTIDO PARA CASO USES A MARCAÇÃO CLÁSSICA DIRETAMENTE NO CAMPO ESCURO
|
||||
void registerShotLocation(BuildContext context, Offset position, Size size) {
|
||||
if (pendingAction == null || pendingPlayer == null) return;
|
||||
if (pendingAction == null || pendingPlayerId == null) return;
|
||||
|
||||
bool is3Pt = pendingAction!.contains("_3");
|
||||
bool is2Pt = pendingAction!.contains("_2");
|
||||
@@ -355,21 +382,15 @@ class PlacarController {
|
||||
bool isMake = pendingAction!.startsWith("add_pts_");
|
||||
double relX = position.dx / size.width;
|
||||
double relY = position.dy / size.height;
|
||||
String name = pendingPlayer!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
String pId = pendingPlayerId!.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
|
||||
matchShots.add(ShotRecord(
|
||||
relativeX: relX,
|
||||
relativeY: relY,
|
||||
isMake: isMake,
|
||||
playerName: name
|
||||
));
|
||||
matchShots.add(ShotRecord(relativeX: relX, relativeY: relY, isMake: isMake, playerId: pId, playerName: playerNames[pId]!));
|
||||
|
||||
commitStat(pendingAction!, pendingPlayer!);
|
||||
commitStat(pendingAction!, pendingPlayerId!);
|
||||
|
||||
isSelectingShotLocation = false;
|
||||
pendingAction = null;
|
||||
pendingPlayer = null;
|
||||
onUpdate();
|
||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null;
|
||||
_saveLocalBackup(); // 👈 Grava logo
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _validateShotZone(Offset position, Size size, bool is3Pt) {
|
||||
@@ -400,13 +421,13 @@ class PlacarController {
|
||||
}
|
||||
|
||||
void cancelShotLocation() {
|
||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
|
||||
isSelectingShotLocation = false; pendingAction = null; pendingPlayerId = null; notifyListeners();
|
||||
}
|
||||
|
||||
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]!;
|
||||
String playerId = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||
final stats = playerStats[playerId]!;
|
||||
|
||||
if (action.startsWith("add_pts_")) {
|
||||
int pts = int.parse(action.split("_").last);
|
||||
@@ -445,15 +466,16 @@ class PlacarController {
|
||||
if (stats["fls"]! > 0) stats["fls"] = stats["fls"]! - 1;
|
||||
if (isOpponent) { if (opponentFouls > 0) opponentFouls--; } else { if (myFouls > 0) myFouls--; }
|
||||
}
|
||||
_saveLocalBackup(); // 👈 Grava na memória!
|
||||
}
|
||||
|
||||
Future<void> saveGameStats(BuildContext context) async {
|
||||
final supabase = Supabase.instance.client;
|
||||
isSaving = true;
|
||||
onUpdate();
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
|
||||
bool isGameFinishedNow = currentQuarter >= 4 && durationNotifier.value.inSeconds == 0;
|
||||
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
|
||||
|
||||
String topPtsName = '---'; int maxPts = -1;
|
||||
@@ -462,7 +484,7 @@ class PlacarController {
|
||||
String topDefName = '---'; int maxDef = -1;
|
||||
String mvpName = '---'; int maxMvpScore = -1;
|
||||
|
||||
playerStats.forEach((playerName, stats) {
|
||||
playerStats.forEach((playerId, stats) {
|
||||
int pts = stats['pts'] ?? 0;
|
||||
int ast = stats['ast'] ?? 0;
|
||||
int rbs = stats['rbs'] ?? 0;
|
||||
@@ -471,18 +493,20 @@ class PlacarController {
|
||||
|
||||
int defScore = stl + blk;
|
||||
int mvpScore = pts + ast + rbs + defScore;
|
||||
|
||||
String pName = playerNames[playerId] ?? '---';
|
||||
|
||||
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; }
|
||||
if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$pName ($pts)'; }
|
||||
if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$pName ($ast)'; }
|
||||
if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$pName ($rbs)'; }
|
||||
if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$pName ($defScore)'; }
|
||||
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = pName; }
|
||||
});
|
||||
|
||||
await supabase.from('games').update({
|
||||
'my_score': myScore,
|
||||
'opponent_score': opponentScore,
|
||||
'remaining_seconds': duration.inSeconds,
|
||||
'remaining_seconds': durationNotifier.value.inSeconds,
|
||||
'my_timeouts': myTimeoutsUsed,
|
||||
'opp_timeouts': opponentTimeoutsUsed,
|
||||
'current_quarter': currentQuarter,
|
||||
@@ -495,9 +519,7 @@ class PlacarController {
|
||||
}).eq('id', gameId);
|
||||
|
||||
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {
|
||||
|
||||
final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]);
|
||||
|
||||
Map<String, dynamic> myTeamUpdate = {};
|
||||
Map<String, dynamic> oppTeamUpdate = {};
|
||||
|
||||
@@ -507,84 +529,65 @@ class PlacarController {
|
||||
}
|
||||
|
||||
if (myScore > opponentScore) {
|
||||
myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1;
|
||||
oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1;
|
||||
myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1; oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1;
|
||||
} else if (myScore < opponentScore) {
|
||||
myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1;
|
||||
oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1;
|
||||
myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1; oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1;
|
||||
} else {
|
||||
myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1;
|
||||
oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1;
|
||||
myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1; oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1;
|
||||
}
|
||||
|
||||
await supabase.from('teams').update({
|
||||
'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws']
|
||||
}).eq('id', myTeamDbId!);
|
||||
|
||||
await supabase.from('teams').update({
|
||||
'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws']
|
||||
}).eq('id', oppTeamDbId!);
|
||||
await supabase.from('teams').update({'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws']}).eq('id', myTeamDbId!);
|
||||
await supabase.from('teams').update({'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws']}).eq('id', oppTeamDbId!);
|
||||
|
||||
gameWasAlreadyFinished = true;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> batchStats = [];
|
||||
playerStats.forEach((playerName, stats) {
|
||||
String? memberDbId = playerDbIds[playerName];
|
||||
if (memberDbId != null && stats.values.any((val) => val > 0)) {
|
||||
bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName);
|
||||
playerStats.forEach((playerId, stats) {
|
||||
if (!playerId.startsWith("fake_") && stats.values.any((val) => val > 0)) {
|
||||
bool isMyTeamPlayer = myCourt.contains(playerId) || myBench.contains(playerId);
|
||||
batchStats.add({
|
||||
'game_id': gameId, 'member_id': memberDbId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
|
||||
'game_id': gameId, 'member_id': playerId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
|
||||
'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'], 'ftm': stats['ftm'], 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await supabase.from('player_stats').delete().eq('game_id', gameId);
|
||||
if (batchStats.isNotEmpty) {
|
||||
await supabase.from('player_stats').insert(batchStats);
|
||||
}
|
||||
if (batchStats.isNotEmpty) await supabase.from('player_stats').insert(batchStats);
|
||||
|
||||
// 👇 6. GUARDA AS BOLINHAS (MAPA DE CALOR) NO SUPABASE 👇
|
||||
List<Map<String, dynamic>> batchShots = [];
|
||||
for (var shot in matchShots) {
|
||||
String? memberDbId = playerDbIds[shot.playerName];
|
||||
if (memberDbId != null) {
|
||||
if (!shot.playerId.startsWith("fake_")) {
|
||||
batchShots.add({
|
||||
'game_id': gameId,
|
||||
'member_id': memberDbId,
|
||||
'player_name': shot.playerName,
|
||||
'relative_x': shot.relativeX,
|
||||
'relative_y': shot.relativeY,
|
||||
'is_make': shot.isMake,
|
||||
'zone': shot.zone ?? 'Desconhecida',
|
||||
'points': shot.points ?? (shot.isMake ? 2 : 0),
|
||||
'game_id': gameId, 'member_id': shot.playerId, 'player_name': shot.playerName,
|
||||
'relative_x': shot.relativeX, 'relative_y': shot.relativeY, 'is_make': shot.isMake,
|
||||
'zone': shot.zone ?? 'Desconhecida', 'points': shot.points ?? (shot.isMake ? 2 : 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apaga os antigos (para não duplicar) e guarda os novos!
|
||||
await supabase.from('shot_locations').delete().eq('game_id', gameId);
|
||||
if (batchShots.isNotEmpty) {
|
||||
await supabase.from('shot_locations').insert(batchShots);
|
||||
}
|
||||
if (batchShots.isNotEmpty) await supabase.from('shot_locations').insert(batchShots);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas, Mapa de Calor e Resultados guardados com Sucesso!'), backgroundColor: Colors.green));
|
||||
}
|
||||
// 👇 SE O SUPABASE GUARDOU COM SUCESSO, LIMPA A MEMÓRIA DO TELEMÓVEL! 👇
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('backup_$gameId');
|
||||
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas, Mapa de Calor e Resultados guardados 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));
|
||||
}
|
||||
if (context.mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Erro ao guardar: $e'), backgroundColor: Colors.red));
|
||||
} finally {
|
||||
isSaving = false;
|
||||
onUpdate();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/person_model.dart';
|
||||
|
||||
class StatsController {
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
|
||||
// 1. LER
|
||||
Stream<List<Person>> getMembers(String teamId) {
|
||||
return _supabase
|
||||
.from('members')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('team_id', teamId)
|
||||
.order('name', ascending: true)
|
||||
.map((data) => data.map((json) => Person.fromMap(json)).toList());
|
||||
}
|
||||
|
||||
// 2. APAGAR
|
||||
Future<void> deletePerson(String personId) async {
|
||||
try {
|
||||
await _supabase.from('members').delete().eq('id', personId);
|
||||
} catch (e) {
|
||||
debugPrint("Erro ao eliminar: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. DIÁLOGOS
|
||||
void showAddPersonDialog(BuildContext context, String teamId) {
|
||||
_showForm(context, teamId: teamId);
|
||||
}
|
||||
|
||||
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
|
||||
_showForm(context, teamId: teamId, person: person);
|
||||
}
|
||||
|
||||
// --- O POPUP ESTÁ AQUI ---
|
||||
void _showForm(BuildContext context, {required String teamId, Person? person}) {
|
||||
final isEdit = person != null;
|
||||
final nameCtrl = TextEditingController(text: person?.name ?? '');
|
||||
final numCtrl = TextEditingController(text: person?.number ?? '');
|
||||
|
||||
// Define o valor inicial
|
||||
String selectedType = person?.type ?? 'Jogador';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setState) => AlertDialog(
|
||||
title: Text(isEdit ? "Editar" : "Adicionar"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// NOME
|
||||
TextField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(labelText: "Nome"),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// FUNÇÃO
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedType,
|
||||
decoration: const InputDecoration(labelText: "Função"),
|
||||
items: ["Jogador", "Treinador"]
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
if (v != null) setState(() => selectedType = v);
|
||||
},
|
||||
),
|
||||
|
||||
// NÚMERO (Só aparece se for Jogador)
|
||||
if (selectedType == "Jogador") ...[
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: numCtrl,
|
||||
decoration: const InputDecoration(labelText: "Número da Camisola"),
|
||||
keyboardType: TextInputType.text, // Aceita texto para evitar erros
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text("Cancelar")
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
||||
onPressed: () async {
|
||||
print("--- 1. CLICOU EM GUARDAR ---");
|
||||
|
||||
// Validação Simples
|
||||
if (nameCtrl.text.trim().isEmpty) {
|
||||
print("ERRO: Nome vazio");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lógica do Número:
|
||||
// Se for Treinador -> envia NULL
|
||||
// Se for Jogador e estiver vazio -> envia NULL
|
||||
// Se tiver texto -> envia o Texto
|
||||
String? numeroFinal;
|
||||
if (selectedType == "Treinador") {
|
||||
numeroFinal = null;
|
||||
} else {
|
||||
numeroFinal = numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim();
|
||||
}
|
||||
|
||||
print("--- 2. DADOS A ENVIAR ---");
|
||||
print("Nome: ${nameCtrl.text}");
|
||||
print("Tipo: $selectedType");
|
||||
print("Número: $numeroFinal");
|
||||
|
||||
try {
|
||||
if (isEdit) {
|
||||
await _supabase.from('members').update({
|
||||
'name': nameCtrl.text.trim(),
|
||||
'type': selectedType,
|
||||
'number': numeroFinal,
|
||||
}).eq('id', person!.id);
|
||||
} else {
|
||||
await _supabase.from('members').insert({
|
||||
'team_id': teamId, // Verifica se este teamId é válido!
|
||||
'name': nameCtrl.text.trim(),
|
||||
'type': selectedType,
|
||||
'number': numeroFinal,
|
||||
});
|
||||
}
|
||||
|
||||
print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---");
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
|
||||
} catch (e) {
|
||||
print("--- X. ERRO AO GUARDAR ---");
|
||||
print(e.toString());
|
||||
|
||||
// MOSTRA O ERRO NO TELEMÓVEL
|
||||
if (ctx.mounted) {
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Erro: $e"),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}*/
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class TeamController {
|
||||
// Instância do cliente Supabase
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// 1. STREAM (Realtime)
|
||||
@@ -13,18 +13,39 @@ class TeamController {
|
||||
.map((data) => List<Map<String, dynamic>>.from(data));
|
||||
}
|
||||
|
||||
// 2. CRIAR
|
||||
Future<void> createTeam(String name, String season, String? imageUrl) async {
|
||||
// 2. CRIAR (Agora aceita um File e faz o Upload!)
|
||||
Future<void> createTeam(String name, String season, File? imageFile) async {
|
||||
try {
|
||||
String? uploadedImageUrl;
|
||||
|
||||
// Se o utilizador escolheu uma imagem, fazemos o upload primeiro
|
||||
if (imageFile != null) {
|
||||
// Criar um nome único para o ficheiro (ex: id do user + timestamp)
|
||||
final userId = _supabase.auth.currentUser?.id ?? 'default';
|
||||
final fileName = '${userId}_${DateTime.now().millisecondsSinceEpoch}.png';
|
||||
final storagePath = 'teams/$fileName';
|
||||
|
||||
// Faz o upload para o bucket 'avatars' (podes usar o mesmo ou criar um chamado 'teams_logos')
|
||||
await _supabase.storage.from('avatars').upload(
|
||||
storagePath,
|
||||
imageFile,
|
||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true)
|
||||
);
|
||||
|
||||
// Vai buscar o URL público
|
||||
uploadedImageUrl = _supabase.storage.from('avatars').getPublicUrl(storagePath);
|
||||
}
|
||||
|
||||
// Agora insere a equipa na base de dados (com ou sem URL)
|
||||
await _supabase.from('teams').insert({
|
||||
'name': name,
|
||||
'season': season,
|
||||
'image_url': imageUrl,
|
||||
'image_url': uploadedImageUrl ?? '', // Se não houver foto, guarda vazio
|
||||
'is_favorite': false,
|
||||
});
|
||||
print("✅ Equipa guardada no Supabase!");
|
||||
} catch (e) {
|
||||
print("❌ Erro ao criar: $e");
|
||||
print("❌ Erro ao criar equipa: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +63,7 @@ class TeamController {
|
||||
try {
|
||||
await _supabase
|
||||
.from('teams')
|
||||
.update({'is_favorite': !currentStatus}) // Inverte o valor
|
||||
.update({'is_favorite': !currentStatus})
|
||||
.eq('id', teamId);
|
||||
} catch (e) {
|
||||
print("❌ Erro ao favoritar: $e");
|
||||
@@ -52,28 +73,20 @@ class TeamController {
|
||||
// 5. CONTAR JOGADORES (LEITURA ÚNICA)
|
||||
Future<int> getPlayerCount(String teamId) async {
|
||||
try {
|
||||
final count = await _supabase
|
||||
.from('members')
|
||||
.count()
|
||||
.eq('team_id', teamId);
|
||||
final count = await _supabase.from('members').count().eq('team_id', teamId);
|
||||
return count;
|
||||
} catch (e) {
|
||||
print("Erro ao contar jogadores: $e");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 👇 6. A FUNÇÃO QUE RESOLVE O ERRO (EM TEMPO REAL) 👇
|
||||
Stream<int> getPlayerCountStream(String teamId) {
|
||||
return _supabase
|
||||
.from('members')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('team_id', teamId)
|
||||
.map((membros) => membros
|
||||
.where((membro) => membro['type'] == 'Jogador')
|
||||
.length);
|
||||
// 6. CONTAR JOGADORES (STREAM EM TEMPO REAL)
|
||||
Future<List<Map<String, dynamic>>> getTeamsWithStats() async {
|
||||
final data = await _supabase
|
||||
.from('teams_with_stats') // Lemos da View que criámos!
|
||||
.select('*')
|
||||
.order('name', ascending: true);
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
}
|
||||
|
||||
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
|
||||
void dispose() {}
|
||||
}
|
||||
Reference in New Issue
Block a user