historico de jogos
This commit is contained in:
@@ -23,6 +23,9 @@ class PlacarController {
|
|||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
bool isSaving = false;
|
bool isSaving = false;
|
||||||
|
|
||||||
|
// 👇 TRINCO DE SEGURANÇA: Evita contar vitórias duas vezes se clicares no Guardar repetidamente!
|
||||||
|
bool gameWasAlreadyFinished = false;
|
||||||
|
|
||||||
int myScore = 0;
|
int myScore = 0;
|
||||||
int opponentScore = 0;
|
int opponentScore = 0;
|
||||||
@@ -62,7 +65,6 @@ class PlacarController {
|
|||||||
try {
|
try {
|
||||||
await Future.delayed(const Duration(milliseconds: 1500));
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
|
|
||||||
// 1. Limpar estados para evitar duplicação
|
|
||||||
myCourt.clear();
|
myCourt.clear();
|
||||||
myBench.clear();
|
myBench.clear();
|
||||||
oppCourt.clear();
|
oppCourt.clear();
|
||||||
@@ -73,7 +75,6 @@ class PlacarController {
|
|||||||
myFouls = 0;
|
myFouls = 0;
|
||||||
opponentFouls = 0;
|
opponentFouls = 0;
|
||||||
|
|
||||||
// 2. Buscar dados básicos do JOGO
|
|
||||||
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
|
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
|
||||||
|
|
||||||
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
|
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
|
||||||
@@ -85,25 +86,24 @@ class PlacarController {
|
|||||||
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
|
||||||
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
|
||||||
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
|
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
|
||||||
|
|
||||||
|
// 👇 Verifica se o jogo já tinha acabado noutra sessão
|
||||||
|
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
|
||||||
|
|
||||||
// 3. Buscar os IDs das equipas
|
|
||||||
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
|
||||||
for (var t in teamsResponse) {
|
for (var t in teamsResponse) {
|
||||||
if (t['name'] == myTeam) myTeamDbId = t['id'];
|
if (t['name'] == myTeam) myTeamDbId = t['id'];
|
||||||
if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
|
if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Buscar os Jogadores
|
|
||||||
List<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : [];
|
List<dynamic> myPlayers = myTeamDbId != null ? await supabase.from('members').select().eq('team_id', myTeamDbId!).eq('type', 'Jogador') : [];
|
||||||
List<dynamic> oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : [];
|
List<dynamic> oppPlayers = oppTeamDbId != null ? await supabase.from('members').select().eq('team_id', oppTeamDbId!).eq('type', 'Jogador') : [];
|
||||||
|
|
||||||
// 5. BUSCAR ESTATÍSTICAS JÁ SALVAS
|
|
||||||
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
|
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
|
||||||
final Map<String, dynamic> savedStats = {
|
final Map<String, dynamic> savedStats = {
|
||||||
for (var item in statsResponse) item['member_id'].toString(): item
|
for (var item in statsResponse) item['member_id'].toString(): item
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. Registar a tua equipa
|
|
||||||
for (int i = 0; i < myPlayers.length; i++) {
|
for (int i = 0; i < myPlayers.length; i++) {
|
||||||
String dbId = myPlayers[i]['id'].toString();
|
String dbId = myPlayers[i]['id'].toString();
|
||||||
String name = myPlayers[i]['name'].toString();
|
String name = myPlayers[i]['name'].toString();
|
||||||
@@ -116,14 +116,13 @@ class PlacarController {
|
|||||||
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
|
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
|
||||||
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 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,
|
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
|
||||||
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, // <-- VARIÁVEIS NOVAS AQUI!
|
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
|
||||||
};
|
};
|
||||||
myFouls += (s['fls'] as int? ?? 0);
|
myFouls += (s['fls'] as int? ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_padTeam(myCourt, myBench, "Jogador", isMyTeam: true);
|
_padTeam(myCourt, myBench, "Jogador", isMyTeam: true);
|
||||||
|
|
||||||
// 7. Registar a equipa adversária
|
|
||||||
for (int i = 0; i < oppPlayers.length; i++) {
|
for (int i = 0; i < oppPlayers.length; i++) {
|
||||||
String dbId = oppPlayers[i]['id'].toString();
|
String dbId = oppPlayers[i]['id'].toString();
|
||||||
String name = oppPlayers[i]['name'].toString();
|
String name = oppPlayers[i]['name'].toString();
|
||||||
@@ -136,7 +135,7 @@ class PlacarController {
|
|||||||
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
|
"pts": s['pts'] ?? 0, "rbs": s['rbs'] ?? 0, "ast": s['ast'] ?? 0,
|
||||||
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 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,
|
"fls": s['fls'] ?? 0, "fgm": s['fgm'] ?? 0, "fga": s['fga'] ?? 0,
|
||||||
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0, // <-- VARIÁVEIS NOVAS AQUI!
|
"ftm": s['ftm'] ?? 0, "fta": s['fta'] ?? 0, "orb": s['orb'] ?? 0, "drb": s['drb'] ?? 0,
|
||||||
};
|
};
|
||||||
opponentFouls += (s['fls'] as int? ?? 0);
|
opponentFouls += (s['fls'] as int? ?? 0);
|
||||||
}
|
}
|
||||||
@@ -159,7 +158,6 @@ class PlacarController {
|
|||||||
playerNumbers[name] = number;
|
playerNumbers[name] = number;
|
||||||
if (dbId != null) playerDbIds[name] = dbId;
|
if (dbId != null) playerDbIds[name] = dbId;
|
||||||
|
|
||||||
// 👇 ADICIONEI AS 4 VARIÁVEIS NOVAS AQUI!
|
|
||||||
playerStats[name] = {
|
playerStats[name] = {
|
||||||
"pts": 0, "rbs": 0, "ast": 0, "stl": 0, "tov": 0, "blk": 0,
|
"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
|
"fls": 0, "fgm": 0, "fga": 0, "ftm": 0, "fta": 0, "orb": 0, "drb": 0
|
||||||
@@ -178,7 +176,6 @@ class PlacarController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- TEMPO E TIMEOUTS ---
|
|
||||||
void toggleTimer(BuildContext context) {
|
void toggleTimer(BuildContext context) {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
@@ -194,12 +191,11 @@ class PlacarController {
|
|||||||
duration = const Duration(minutes: 10);
|
duration = const Duration(minutes: 10);
|
||||||
myFouls = 0;
|
myFouls = 0;
|
||||||
opponentFouls = 0;
|
opponentFouls = 0;
|
||||||
// 👇 ESTAS DUAS LINHAS ZERAM OS TIMEOUTS NO NOVO PERÍODO
|
|
||||||
myTimeoutsUsed = 0;
|
myTimeoutsUsed = 0;
|
||||||
opponentTimeoutsUsed = 0;
|
opponentTimeoutsUsed = 0;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), backgroundColor: Colors.red));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onUpdate();
|
onUpdate();
|
||||||
@@ -222,7 +218,6 @@ class PlacarController {
|
|||||||
|
|
||||||
String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
String formatTime() => "${duration.inMinutes.toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
|
|
||||||
// --- LÓGICA DE JOGO & VALIDAÇÃO GEOMÉTRICA DE ZONAS ---
|
|
||||||
void handleActionDrag(BuildContext context, String action, String playerData) {
|
void handleActionDrag(BuildContext context, String action, String playerData) {
|
||||||
String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
|
||||||
final stats = playerStats[name]!;
|
final stats = playerStats[name]!;
|
||||||
@@ -268,21 +263,13 @@ class PlacarController {
|
|||||||
|
|
||||||
void registerShotLocation(BuildContext context, Offset position, Size size) {
|
void registerShotLocation(BuildContext context, Offset position, Size size) {
|
||||||
if (pendingAction == null || pendingPlayer == null) return;
|
if (pendingAction == null || pendingPlayer == null) return;
|
||||||
|
|
||||||
bool is3Pt = pendingAction!.contains("_3");
|
bool is3Pt = pendingAction!.contains("_3");
|
||||||
bool is2Pt = pendingAction!.contains("_2");
|
bool is2Pt = pendingAction!.contains("_2");
|
||||||
|
|
||||||
if (is3Pt || is2Pt) {
|
if (is3Pt || is2Pt) {
|
||||||
bool isValid = _validateShotZone(position, size, is3Pt);
|
bool isValid = _validateShotZone(position, size, is3Pt);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('🛑 Local de lançamento incompatível com a pontuação.'), backgroundColor: Colors.red, duration: Duration(seconds: 2)));
|
||||||
const SnackBar(
|
|
||||||
content: Text('🛑Local de lançamento incompatível com a pontuação.'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,31 +285,20 @@ class PlacarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _validateShotZone(Offset pos, Size size, bool is3Pt) {
|
bool _validateShotZone(Offset pos, Size size, bool is3Pt) {
|
||||||
double w = size.width;
|
double w = size.width; double h = size.height;
|
||||||
double h = size.height;
|
|
||||||
|
|
||||||
Offset leftHoop = Offset(w * 0.12, h * 0.5);
|
Offset leftHoop = Offset(w * 0.12, h * 0.5);
|
||||||
Offset rightHoop = Offset(w * 0.88, h * 0.5);
|
Offset rightHoop = Offset(w * 0.88, h * 0.5);
|
||||||
double threePointRadius = w * 0.28;
|
double threePointRadius = w * 0.28;
|
||||||
|
|
||||||
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop;
|
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop;
|
||||||
double distanceToHoop = (pos - activeHoop).distance;
|
double distanceToHoop = (pos - activeHoop).distance;
|
||||||
|
bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) && (pos.dx < w * 0.20 || pos.dx > w * 0.80);
|
||||||
|
|
||||||
bool isCorner3 = (pos.dy < h * 0.15 || pos.dy > h * 0.85) &&
|
if (is3Pt) return distanceToHoop >= threePointRadius || isCorner3;
|
||||||
(pos.dx < w * 0.20 || pos.dx > w * 0.80);
|
else return distanceToHoop < threePointRadius && !isCorner3;
|
||||||
|
|
||||||
if (is3Pt) {
|
|
||||||
return distanceToHoop >= threePointRadius || isCorner3;
|
|
||||||
} else {
|
|
||||||
return distanceToHoop < threePointRadius && !isCorner3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelShotLocation() {
|
void cancelShotLocation() {
|
||||||
isSelectingShotLocation = false;
|
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
|
||||||
pendingAction = null;
|
|
||||||
pendingPlayer = null;
|
|
||||||
onUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void commitStat(String action, String playerData) {
|
void commitStat(String action, String playerData) {
|
||||||
@@ -335,7 +311,7 @@ class PlacarController {
|
|||||||
if (isOpponent) opponentScore += pts; else myScore += pts;
|
if (isOpponent) opponentScore += pts; else myScore += pts;
|
||||||
stats["pts"] = stats["pts"]! + pts;
|
stats["pts"] = stats["pts"]! + pts;
|
||||||
if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; }
|
if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; }
|
||||||
if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; } // ADICIONADO LANCE LIVRE (FTM/FTA)
|
if (pts == 1) { stats["ftm"] = stats["ftm"]! + 1; stats["fta"] = stats["fta"]! + 1; }
|
||||||
}
|
}
|
||||||
else if (action.startsWith("sub_pts_")) {
|
else if (action.startsWith("sub_pts_")) {
|
||||||
int pts = int.parse(action.split("_").last);
|
int pts = int.parse(action.split("_").last);
|
||||||
@@ -346,15 +322,15 @@ class PlacarController {
|
|||||||
if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1;
|
if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 1;
|
||||||
if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1;
|
if (stats["fga"]! > 0) stats["fga"] = stats["fga"]! - 1;
|
||||||
}
|
}
|
||||||
if (pts == 1) { // ADICIONADO SUBTRAÇÃO LANCE LIVRE
|
if (pts == 1) {
|
||||||
if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1;
|
if (stats["ftm"]! > 0) stats["ftm"] = stats["ftm"]! - 1;
|
||||||
if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1;
|
if (stats["fta"]! > 0) stats["fta"] = stats["fta"]! - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; } // ADICIONADO BOTAO M1
|
else if (action == "miss_1") { stats["fta"] = stats["fta"]! + 1; }
|
||||||
else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; }
|
else if (action == "miss_2" || action == "miss_3") { stats["fga"] = stats["fga"]! + 1; }
|
||||||
else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } // SEPARAÇÃO ORB
|
else if (action == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; }
|
||||||
else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } // SEPARAÇÃO DRB
|
else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; }
|
||||||
else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 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_stl") { stats["stl"] = stats["stl"]! + 1; }
|
||||||
else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; }
|
else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; }
|
||||||
@@ -376,6 +352,10 @@ class PlacarController {
|
|||||||
onUpdate();
|
onUpdate();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
|
||||||
|
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
|
||||||
|
|
||||||
|
// 1. Atualizar o Jogo na BD
|
||||||
await supabase.from('games').update({
|
await supabase.from('games').update({
|
||||||
'my_score': myScore,
|
'my_score': myScore,
|
||||||
'opponent_score': opponentScore,
|
'opponent_score': opponentScore,
|
||||||
@@ -383,47 +363,69 @@ class PlacarController {
|
|||||||
'my_timeouts': myTimeoutsUsed,
|
'my_timeouts': myTimeoutsUsed,
|
||||||
'opp_timeouts': opponentTimeoutsUsed,
|
'opp_timeouts': opponentTimeoutsUsed,
|
||||||
'current_quarter': currentQuarter,
|
'current_quarter': currentQuarter,
|
||||||
'status': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado',
|
'status': newStatus,
|
||||||
}).eq('id', gameId);
|
}).eq('id', gameId);
|
||||||
|
|
||||||
List<Map<String, dynamic>> batchStats = [];
|
// 👇 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<String, dynamic> myTeamUpdate = {};
|
||||||
|
Map<String, dynamic> oppTeamUpdate = {};
|
||||||
|
|
||||||
|
for(var t in teamsData) {
|
||||||
|
if(t['id'].toString() == myTeamDbId) myTeamUpdate = Map.from(t);
|
||||||
|
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;
|
||||||
|
} else if (myScore < opponentScore) {
|
||||||
|
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!);
|
||||||
|
|
||||||
|
await supabase.from('teams').update({
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Atualizar as Estatísticas dos Jogadores
|
||||||
|
List<Map<String, dynamic>> batchStats = [];
|
||||||
playerStats.forEach((playerName, stats) {
|
playerStats.forEach((playerName, stats) {
|
||||||
String? memberDbId = playerDbIds[playerName];
|
String? memberDbId = playerDbIds[playerName];
|
||||||
|
|
||||||
if (memberDbId != null && stats.values.any((val) => val > 0)) {
|
if (memberDbId != null && stats.values.any((val) => val > 0)) {
|
||||||
bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName);
|
bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName);
|
||||||
String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!;
|
|
||||||
|
|
||||||
batchStats.add({
|
batchStats.add({
|
||||||
'game_id': gameId,
|
'game_id': gameId, 'member_id': memberDbId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
|
||||||
'member_id': memberDbId,
|
'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'],
|
||||||
'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'],
|
|
||||||
'ftm': stats['ftm'], // <-- GRAVAR NA BD
|
|
||||||
'fta': stats['fta'], // <-- GRAVAR NA BD
|
|
||||||
'orb': stats['orb'], // <-- GRAVAR NA BD
|
|
||||||
'drb': stats['drb'], // <-- GRAVAR NA BD
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await supabase.from('player_stats').delete().eq('game_id', gameId);
|
await supabase.from('player_stats').delete().eq('game_id', gameId);
|
||||||
|
|
||||||
if (batchStats.isNotEmpty) {
|
if (batchStats.isNotEmpty) {
|
||||||
await supabase.from('player_stats').insert(batchStats);
|
await supabase.from('player_stats').insert(batchStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas guardadas com Sucesso!'), backgroundColor: Colors.green));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Estatísticas e Resultados guardados com Sucesso!'), backgroundColor: Colors.green));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -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,29 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/dados_grafico.dart';
|
import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import
|
||||||
|
|
||||||
class PieChartController extends ChangeNotifier {
|
class PieChartController extends ChangeNotifier {
|
||||||
PieChartData _chartData = PieChartData(victories: 25, defeats: 10);
|
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
|
||||||
|
|
||||||
PieChartData get chartData => _chartData;
|
PieChartData get chartData => _chartData;
|
||||||
|
|
||||||
void updateData({int? victories, int? defeats, int? draws}) {
|
void updateData({int? victories, int? defeats, int? draws}) {
|
||||||
_chartData = PieChartData(
|
_chartData = PieChartData(
|
||||||
victories: victories ?? _chartData.victories,
|
victories: victories ?? _chartData.victories,
|
||||||
defeats: defeats ?? _chartData.defeats,
|
defeats: defeats ?? _chartData.defeats,
|
||||||
draws: draws ?? _chartData.draws,
|
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void incrementVictories() {
|
void reset() {
|
||||||
updateData(victories: _chartData.victories + 1);
|
updateData(victories: 0, defeats: 0, draws: 0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
void incrementDefeats() {
|
|
||||||
updateData(defeats: _chartData.defeats + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
updateData(victories: 0, defeats: 0, draws: 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
class PieChartData {
|
class PieChartData {
|
||||||
final int victories;
|
final int victories;
|
||||||
final int defeats;
|
final int defeats;
|
||||||
final int draws;
|
final int draws; // 👇 AQUI ESTÃO OS EMPATES
|
||||||
|
|
||||||
const PieChartData({
|
const PieChartData({
|
||||||
required this.victories,
|
required this.victories,
|
||||||
@@ -9,6 +9,7 @@ class PieChartData {
|
|||||||
this.draws = 0,
|
this.draws = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 👇 MATEMÁTICA ATUALIZADA 👇
|
||||||
int get total => victories + defeats + draws;
|
int get total => victories + defeats + draws;
|
||||||
|
|
||||||
double get victoryPercentage => total > 0 ? victories / total : 0;
|
double get victoryPercentage => total > 0 ? victories / total : 0;
|
||||||
@@ -22,5 +23,6 @@ class PieChartData {
|
|||||||
'total': total,
|
'total': total,
|
||||||
'victoryPercentage': victoryPercentage,
|
'victoryPercentage': victoryPercentage,
|
||||||
'defeatPercentage': defeatPercentage,
|
'defeatPercentage': defeatPercentage,
|
||||||
|
'drawPercentage': drawPercentage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart'; // Assume que o PieChartWidget está aqui
|
import 'package:playmaker/grafico%20de%20pizza/widgets/grafico_widgets.dart';
|
||||||
import 'dados_grafico.dart';
|
import 'dados_grafico.dart';
|
||||||
import 'controllers/contollers_grafico.dart';
|
|
||||||
|
|
||||||
class PieChartCard extends StatefulWidget {
|
class PieChartCard extends StatefulWidget {
|
||||||
final PieChartController? controller;
|
final int victories;
|
||||||
|
final int defeats;
|
||||||
|
final int draws;
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
// Variáveis de escala e tamanho
|
|
||||||
final double sf;
|
final double sf;
|
||||||
final double cardWidth;
|
|
||||||
final double cardHeight;
|
|
||||||
|
|
||||||
const PieChartCard({
|
const PieChartCard({
|
||||||
super.key,
|
super.key,
|
||||||
this.controller,
|
this.victories = 0,
|
||||||
|
this.defeats = 0,
|
||||||
|
this.draws = 0,
|
||||||
this.title = 'DESEMPENHO',
|
this.title = 'DESEMPENHO',
|
||||||
this.subtitle = 'Vitórias vs Derrotas',
|
this.subtitle = 'Temporada',
|
||||||
this.onTap,
|
this.onTap,
|
||||||
required this.backgroundColor,
|
required this.backgroundColor,
|
||||||
this.sf = 1.0,
|
this.sf = 1.0,
|
||||||
required this.cardWidth,
|
|
||||||
required this.cardHeight,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -32,30 +29,26 @@ class PieChartCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderStateMixin {
|
class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderStateMixin {
|
||||||
late PieChartController _controller;
|
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _animation;
|
late Animation<double> _animation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = widget.controller ?? PieChartController();
|
_animationController = AnimationController(duration: const Duration(milliseconds: 600), vsync: this);
|
||||||
|
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack));
|
||||||
_animationController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 600),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _animationController,
|
|
||||||
curve: Curves.easeOutBack,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_animationController.forward();
|
_animationController.forward();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PieChartCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.victories != widget.victories || oldWidget.defeats != widget.defeats || oldWidget.draws != widget.draws) {
|
||||||
|
_animationController.reset();
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
@@ -64,243 +57,160 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = _controller.chartData;
|
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
|
||||||
final double sf = widget.sf;
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _animation,
|
animation: _animation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Transform.scale(
|
return Transform.scale(
|
||||||
scale: 0.95 + (_animation.value * 0.05),
|
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
|
||||||
|
scale: 0.95 + (_animation.value * 0.05),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _animation.value,
|
// 👇 AQUI ESTÁ A FIX: Garante que fica entre 0 e 1
|
||||||
|
opacity: _animation.value.clamp(0.0, 1.0),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: SizedBox( // <-- Força a largura e altura exatas passadas pelo HomeScreen
|
child: Card(
|
||||||
width: widget.cardWidth,
|
margin: EdgeInsets.zero,
|
||||||
height: widget.cardHeight,
|
elevation: 4,
|
||||||
child: Card(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
elevation: 0,
|
child: InkWell(
|
||||||
shape: RoundedRectangleBorder(
|
onTap: widget.onTap,
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
child: Container(
|
||||||
child: InkWell(
|
decoration: BoxDecoration(
|
||||||
onTap: widget.onTap,
|
borderRadius: BorderRadius.circular(14),
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
|
||||||
child: Container(
|
),
|
||||||
decoration: BoxDecoration(
|
child: LayoutBuilder(
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
builder: (context, constraints) {
|
||||||
gradient: LinearGradient(
|
final double ch = constraints.maxHeight;
|
||||||
begin: Alignment.topLeft,
|
final double cw = constraints.maxWidth;
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
return Padding(
|
||||||
widget.backgroundColor.withOpacity(0.9),
|
padding: EdgeInsets.all(cw * 0.06),
|
||||||
widget.backgroundColor.withOpacity(0.7),
|
child: Column(
|
||||||
],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
),
|
// 👇 TÍTULOS UM POUCO MAIS PRESENTES
|
||||||
child: Padding(
|
FittedBox(
|
||||||
padding: EdgeInsets.all(16.0 * sf),
|
fit: BoxFit.scaleDown,
|
||||||
child: Column(
|
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
FittedBox(
|
||||||
// Cabeçalho compacto
|
fit: BoxFit.scaleDown,
|
||||||
Row(
|
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
SizedBox(height: ch * 0.03),
|
||||||
Expanded(
|
|
||||||
child: Column(
|
// MEIO (GRÁFICO + ESTATÍSTICAS)
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Expanded(
|
||||||
children: [
|
child: Row(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
widget.title,
|
children: [
|
||||||
style: TextStyle(
|
Expanded(
|
||||||
fontSize: 12 * sf,
|
flex: 1,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white.withOpacity(0.9),
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2 * sf),
|
|
||||||
Text(
|
|
||||||
widget.subtitle,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14 * sf,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(6 * sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orange.withOpacity(0.8),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.pie_chart,
|
|
||||||
size: 16 * sf,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 8 * sf),
|
|
||||||
|
|
||||||
// Conteúdo principal
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// Gráfico de pizza
|
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
|
||||||
child: Center(
|
|
||||||
child: PieChartWidget(
|
child: PieChartWidget(
|
||||||
victoryPercentage: data.victoryPercentage,
|
victoryPercentage: data.victoryPercentage,
|
||||||
defeatPercentage: data.defeatPercentage,
|
defeatPercentage: data.defeatPercentage,
|
||||||
drawPercentage: data.drawPercentage,
|
drawPercentage: data.drawPercentage,
|
||||||
size: 120, // O PieChartWidget vai multiplicar isto por `sf` internamente
|
sf: widget.sf,
|
||||||
sf: sf, // <-- Passa a escala para o gráfico
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(width: cw * 0.05),
|
||||||
|
Expanded(
|
||||||
SizedBox(width: 8 * sf),
|
flex: 1,
|
||||||
|
child: Column(
|
||||||
// Estatísticas ultra compactas e sem overflows
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
flex: 2,
|
children: [
|
||||||
child: Column(
|
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch),
|
||||||
children: [
|
_buildDynDivider(ch),
|
||||||
_buildMiniStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, sf),
|
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch),
|
||||||
_buildDivider(sf),
|
],
|
||||||
_buildMiniStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, sf),
|
|
||||||
_buildDivider(sf),
|
|
||||||
_buildMiniStatRow("TOT", data.total.toString(), "100", Colors.white, sf),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 10 * sf),
|
|
||||||
|
|
||||||
// Win rate - Com FittedBox para não estoirar
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(12 * sf),
|
|
||||||
),
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
data.victoryPercentage > 0.5
|
|
||||||
? Icons.trending_up
|
|
||||||
: Icons.trending_down,
|
|
||||||
color: data.victoryPercentage > 0.5
|
|
||||||
? Colors.green
|
|
||||||
: Colors.red,
|
|
||||||
size: 16 * sf,
|
|
||||||
),
|
|
||||||
SizedBox(width: 6 * sf),
|
|
||||||
Text(
|
|
||||||
'Win Rate: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12 * sf,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
// 👇 RODAPÉ AJUSTADO
|
||||||
),
|
SizedBox(height: ch * 0.03),
|
||||||
),
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white24, // Igual ao fundo do botão detalhes
|
||||||
|
borderRadius: BorderRadius.circular(ch * 0.03), // Borda arredondada
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down,
|
||||||
|
color: Colors.green,
|
||||||
|
size: ch * 0.09
|
||||||
|
),
|
||||||
|
SizedBox(width: cw * 0.02),
|
||||||
|
Text(
|
||||||
|
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: ch * 0.05,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
|
||||||
// Função auxiliar BLINDADA contra overflows
|
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) {
|
||||||
Widget _buildMiniStatRow(String label, String number, String percent, Color color, double sf) {
|
return Padding(
|
||||||
return Container(
|
padding: EdgeInsets.only(bottom: ch * 0.01),
|
||||||
margin: EdgeInsets.only(bottom: 4 * sf),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
// Número subiu para 0.10
|
||||||
width: 28 * sf,
|
Expanded(flex: 2, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(number, style: TextStyle(fontSize: ch * 0.10, fontWeight: FontWeight.bold, color: color, height: 1.0)))),
|
||||||
child: FittedBox(
|
SizedBox(width: ch * 0.02),
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
number,
|
|
||||||
style: TextStyle(fontSize: 22 * sf, fontWeight: FontWeight.bold, color: color, height: 1.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4 * sf),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
flex: 3,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
Row(children: [
|
||||||
children: [
|
Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||||
FittedBox(
|
// Label subiu para 0.045
|
||||||
fit: BoxFit.scaleDown,
|
Expanded(child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(label, style: TextStyle(fontSize: ch * 0.033, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600))))
|
||||||
child: Row(
|
]),
|
||||||
children: [
|
// Percentagem subiu para 0.05
|
||||||
Container(
|
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))),
|
||||||
width: 6 * sf,
|
]),
|
||||||
height: 6 * sf,
|
|
||||||
margin: EdgeInsets.only(right: 3 * sf),
|
|
||||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(fontSize: 9 * sf, color: Colors.white.withOpacity(0.8), fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'$percent%',
|
|
||||||
style: TextStyle(fontSize: 10 * sf, color: color, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDivider(double sf) {
|
Widget _buildDynDivider(double ch) {
|
||||||
return Container(
|
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01));
|
||||||
height: 0.5,
|
|
||||||
color: Colors.white.withOpacity(0.1),
|
|
||||||
margin: EdgeInsets.symmetric(vertical: 3 * sf),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,61 +5,70 @@ class PieChartWidget extends StatelessWidget {
|
|||||||
final double victoryPercentage;
|
final double victoryPercentage;
|
||||||
final double defeatPercentage;
|
final double defeatPercentage;
|
||||||
final double drawPercentage;
|
final double drawPercentage;
|
||||||
final double size;
|
final double sf;
|
||||||
final double sf; // <-- Fator de Escala
|
|
||||||
|
|
||||||
const PieChartWidget({
|
const PieChartWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.victoryPercentage,
|
required this.victoryPercentage,
|
||||||
required this.defeatPercentage,
|
required this.defeatPercentage,
|
||||||
this.drawPercentage = 0,
|
this.drawPercentage = 0,
|
||||||
this.size = 140,
|
required this.sf,
|
||||||
required this.sf, // <-- Obrigatório agora
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Aplica a escala ao tamanho total do quadrado do gráfico
|
return LayoutBuilder(
|
||||||
final double scaledSize = size * sf;
|
builder: (context, constraints) {
|
||||||
|
// 👇 MAGIA ANTI-DESAPARECIMENTO 👇
|
||||||
return SizedBox(
|
// Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir.
|
||||||
width: scaledSize,
|
final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
|
||||||
height: scaledSize,
|
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight;
|
||||||
child: CustomPaint(
|
|
||||||
painter: _PieChartPainter(
|
// Pega no menor valor para garantir que o círculo não é cortado
|
||||||
victoryPercentage: victoryPercentage,
|
final double size = math.min(w, h);
|
||||||
defeatPercentage: defeatPercentage,
|
|
||||||
drawPercentage: drawPercentage,
|
return Center(
|
||||||
sf: sf, // <-- Passar para desenhar a borda
|
child: SizedBox(
|
||||||
),
|
width: size,
|
||||||
child: _buildCenterLabels(scaledSize),
|
height: size,
|
||||||
),
|
child: CustomPaint(
|
||||||
|
painter: _PieChartPainter(
|
||||||
|
victoryPercentage: victoryPercentage,
|
||||||
|
defeatPercentage: defeatPercentage,
|
||||||
|
drawPercentage: drawPercentage,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: _buildCenterLabels(size),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCenterLabels(double scaledSize) {
|
Widget _buildCenterLabels(double size) {
|
||||||
return Center(
|
return Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
|
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: scaledSize * 0.144, // Mantém-se proporcional
|
fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 4 * sf),
|
),
|
||||||
Text(
|
SizedBox(height: size * 0.02),
|
||||||
'Vitórias',
|
Text(
|
||||||
style: TextStyle(
|
'Vitórias',
|
||||||
fontSize: scaledSize * 0.1, // Mantém-se proporcional
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.8),
|
fontSize: size * 0.10,
|
||||||
),
|
color: Colors.white.withOpacity(0.8),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,78 +77,63 @@ class _PieChartPainter extends CustomPainter {
|
|||||||
final double victoryPercentage;
|
final double victoryPercentage;
|
||||||
final double defeatPercentage;
|
final double defeatPercentage;
|
||||||
final double drawPercentage;
|
final double drawPercentage;
|
||||||
final double sf; // <-- Escala no pintor
|
|
||||||
|
|
||||||
_PieChartPainter({
|
_PieChartPainter({
|
||||||
required this.victoryPercentage,
|
required this.victoryPercentage,
|
||||||
required this.defeatPercentage,
|
required this.defeatPercentage,
|
||||||
required this.drawPercentage,
|
required this.drawPercentage,
|
||||||
required this.sf,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final center = Offset(size.width / 2, size.height / 2);
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
// Aplica a escala à margem para não cortar a linha da borda num ecrã pequeno
|
// Margem de 5% para a linha de fora não ser cortada
|
||||||
final radius = size.width / 2 - (5 * sf);
|
final radius = (size.width / 2) - (size.width * 0.05);
|
||||||
|
|
||||||
// Cores
|
|
||||||
const victoryColor = Colors.green;
|
const victoryColor = Colors.green;
|
||||||
const defeatColor = Colors.red;
|
const defeatColor = Colors.red;
|
||||||
const drawColor = Colors.yellow;
|
const drawColor = Colors.yellow;
|
||||||
const borderColor = Colors.white30;
|
const borderColor = Colors.white30;
|
||||||
|
|
||||||
double startAngle = 0;
|
double startAngle = -math.pi / 2;
|
||||||
|
|
||||||
// Vitórias (verde)
|
|
||||||
if (victoryPercentage > 0) {
|
if (victoryPercentage > 0) {
|
||||||
final sweepAngle = 2 * math.pi * victoryPercentage;
|
final sweepAngle = 2 * math.pi * victoryPercentage;
|
||||||
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor);
|
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width);
|
||||||
startAngle += sweepAngle;
|
startAngle += sweepAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empates (amarelo)
|
|
||||||
if (drawPercentage > 0) {
|
if (drawPercentage > 0) {
|
||||||
final sweepAngle = 2 * math.pi * drawPercentage;
|
final sweepAngle = 2 * math.pi * drawPercentage;
|
||||||
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor);
|
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
|
||||||
startAngle += sweepAngle;
|
startAngle += sweepAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derrotas (vermelho)
|
|
||||||
if (defeatPercentage > 0) {
|
if (defeatPercentage > 0) {
|
||||||
final sweepAngle = 2 * math.pi * defeatPercentage;
|
final sweepAngle = 2 * math.pi * defeatPercentage;
|
||||||
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor);
|
_drawSector(canvas, center, radius, startAngle, sweepAngle, defeatColor, size.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Borda
|
|
||||||
final borderPaint = Paint()
|
final borderPaint = Paint()
|
||||||
..color = borderColor
|
..color = borderColor
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 2 * sf; // <-- Escala na grossura da linha
|
..strokeWidth = size.width * 0.02;
|
||||||
|
|
||||||
canvas.drawCircle(center, radius, borderPaint);
|
canvas.drawCircle(center, radius, borderPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawSector(Canvas canvas, Offset center, double radius,
|
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) {
|
||||||
double startAngle, double sweepAngle, Color color) {
|
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = color
|
..color = color
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
canvas.drawArc(
|
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
|
||||||
Rect.fromCircle(center: center, radius: radius),
|
|
||||||
startAngle,
|
|
||||||
sweepAngle,
|
|
||||||
true,
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Linha divisória
|
|
||||||
if (sweepAngle < 2 * math.pi) {
|
if (sweepAngle < 2 * math.pi) {
|
||||||
final linePaint = Paint()
|
final linePaint = Paint()
|
||||||
..color = Colors.white.withOpacity(0.5)
|
..color = Colors.white.withOpacity(0.5)
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 1.5 * sf; // <-- Escala na grossura da linha
|
..strokeWidth = totalWidth * 0.015;
|
||||||
|
|
||||||
final lineX = center.dx + radius * math.cos(startAngle);
|
final lineX = center.dx + radius * math.cos(startAngle);
|
||||||
final lineY = center.dy + radius * math.sin(startAngle);
|
final lineY = center.dy + radius * math.sin(startAngle);
|
||||||
|
|||||||
@@ -1,320 +1,448 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/classe/home.config.dart';
|
import 'package:playmaker/classe/home.config.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
||||||
import 'package:playmaker/pages/gamePage.dart';
|
import 'package:playmaker/pages/gamePage.dart';
|
||||||
import 'package:playmaker/pages/teamPage.dart';
|
import 'package:playmaker/pages/teamPage.dart';
|
||||||
import 'package:playmaker/controllers/team_controller.dart';
|
import 'package:playmaker/controllers/team_controller.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
|
||||||
const HomeScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
class HomeScreen extends StatefulWidget {
|
||||||
State<HomeScreen> createState() => _HomeScreenState();
|
const HomeScreen({super.key});
|
||||||
}
|
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
@override
|
||||||
int _selectedIndex = 0;
|
State<HomeScreen> createState() => _HomeScreenState();
|
||||||
final TeamController _teamController = TeamController();
|
}
|
||||||
String? _selectedTeamId;
|
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
|
||||||
|
|
||||||
// Instância do Supabase para buscar as estatísticas
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
final _supabase = Supabase.instance.client;
|
int _selectedIndex = 0;
|
||||||
|
final TeamController _teamController = TeamController();
|
||||||
|
String? _selectedTeamId;
|
||||||
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
|
||||||
@override
|
int _teamWins = 0;
|
||||||
Widget build(BuildContext context) {
|
int _teamLosses = 0;
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
int _teamDraws = 0;
|
||||||
final double hScreen = MediaQuery.of(context).size.height;
|
|
||||||
final double sf = math.min(wScreen, hScreen) / 400;
|
|
||||||
|
|
||||||
final List<Widget> pages = [
|
final _supabase = Supabase.instance.client;
|
||||||
_buildHomeContent(sf, wScreen),
|
|
||||||
const GamePage(),
|
|
||||||
const TeamsPage(),
|
|
||||||
const Center(child: Text('Tela de Status')),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Scaffold(
|
@override
|
||||||
backgroundColor: Colors.white,
|
Widget build(BuildContext context) {
|
||||||
appBar: AppBar(
|
final double wScreen = MediaQuery.of(context).size.width;
|
||||||
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)),
|
final double hScreen = MediaQuery.of(context).size.height;
|
||||||
backgroundColor: HomeConfig.primaryColor,
|
final double sf = math.min(wScreen, hScreen) / 400;
|
||||||
foregroundColor: Colors.white,
|
|
||||||
leading: IconButton(
|
final List<Widget> pages = [
|
||||||
icon: Icon(Icons.person, size: 24 * sf),
|
_buildHomeContent(sf, wScreen),
|
||||||
onPressed: () {},
|
const GamePage(),
|
||||||
|
const TeamsPage(),
|
||||||
|
const Center(child: Text('Tela de Status')),
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
body: IndexedStack(
|
|
||||||
index: _selectedIndex,
|
|
||||||
children: pages,
|
|
||||||
),
|
|
||||||
|
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
height: 70 * math.min(sf, 1.2),
|
height: 70 * math.min(sf, 1.2),
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationDestination(
|
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
|
||||||
icon: Icon(Icons.home_outlined),
|
NavigationDestination(icon: Icon(Icons.sports_soccer_outlined), selectedIcon: Icon(Icons.sports_soccer), label: 'Jogo'),
|
||||||
selectedIcon: Icon(Icons.home_filled),
|
NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Equipas'),
|
||||||
label: 'Home',
|
NavigationDestination(icon: Icon(Icons.insights_outlined), selectedIcon: Icon(Icons.insights), label: 'Status'),
|
||||||
),
|
],
|
||||||
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',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- POPUP DE SELEÇÃO DE EQUIPA ---
|
void _showTeamSelector(BuildContext context, double sf) {
|
||||||
void _showTeamSelector(BuildContext context, double sf) {
|
showModalBottomSheet(
|
||||||
showModalBottomSheet(
|
context: context,
|
||||||
context: context,
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))),
|
||||||
shape: RoundedRectangleBorder(
|
builder: (context) {
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf)),
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
),
|
stream: _teamController.teamsStream,
|
||||||
builder: (context) {
|
builder: (context, snapshot) {
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
||||||
stream: _teamController.teamsStream,
|
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * sf, child: const Center(child: Text("Nenhuma equipa criada.")));
|
||||||
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: Center(child: Text("Nenhuma equipa criada.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
final teams = snapshot.data!;
|
final teams = snapshot.data!;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: teams.length,
|
itemCount: teams.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = teams[index];
|
final team = teams[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(team['name']),
|
title: Text(team['name']),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedTeamId = team['id'];
|
_selectedTeamId = team['id'];
|
||||||
_selectedTeamName = team['name'];
|
_selectedTeamName = team['name'];
|
||||||
});
|
_teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0;
|
||||||
Navigator.pop(context);
|
_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) {
|
Widget _buildHomeContent(double sf, double wScreen) {
|
||||||
final double cardWidth = (wScreen - (40 * sf) - (20 * sf)) / 2;
|
final double cardHeight = (wScreen / 2) * 1.0;
|
||||||
final double cardHeight = cardWidth * 1.4; // Ajustado para não cortar conteúdo
|
|
||||||
|
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
// Buscar estatísticas de todos os jogadores da equipa selecionada
|
stream: _selectedTeamId != null
|
||||||
stream: _selectedTeamId != null
|
? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!)
|
||||||
? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!)
|
: const Stream.empty(),
|
||||||
: const Stream.empty(),
|
builder: (context, snapshot) {
|
||||||
builder: (context, snapshot) {
|
Map<String, dynamic> leaders = _calculateLeaders(snapshot.data ?? []);
|
||||||
// Lógica de cálculo de líderes
|
|
||||||
Map<String, dynamic> leaders = _calculateLeaders(snapshot.data ?? []);
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(20.0 * sf),
|
padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Seletor de Equipa
|
InkWell(
|
||||||
InkWell(
|
onTap: () => _showTeamSelector(context, sf),
|
||||||
onTap: () => _showTeamSelector(context, sf),
|
child: Container(
|
||||||
child: Container(
|
padding: EdgeInsets.all(12 * sf),
|
||||||
padding: EdgeInsets.all(12 * sf),
|
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)),
|
||||||
decoration: BoxDecoration(
|
child: Row(
|
||||||
color: Colors.grey.shade100,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
borderRadius: BorderRadius.circular(15 * sf),
|
children: [
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
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),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
height: cardHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
|
||||||
children: [
|
SizedBox(width: 12 * sf),
|
||||||
Icon(Icons.shield, color: HomeConfig.primaryColor, size: 24 * sf),
|
Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
|
||||||
SizedBox(width: 10 * sf),
|
|
||||||
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 12 * sf),
|
||||||
SizedBox(height: 25 * sf),
|
|
||||||
|
|
||||||
// Primeira Linha: Pontos e Assistências
|
SizedBox(
|
||||||
Row(
|
height: cardHeight,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildStatCard(
|
Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
|
||||||
title: 'Mais Pontos',
|
SizedBox(width: 12 * sf),
|
||||||
playerName: leaders['pts_name'],
|
Expanded(
|
||||||
statValue: leaders['pts_val'].toString(),
|
child: PieChartCard(
|
||||||
statLabel: 'TOTAL',
|
victories: _teamWins,
|
||||||
color: const Color(0xFF1565C0),
|
defeats: _teamLosses,
|
||||||
icon: Icons.bolt,
|
draws: _teamDraws,
|
||||||
isHighlighted: true,
|
title: 'DESEMPENHO',
|
||||||
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
|
subtitle: 'Temporada',
|
||||||
|
backgroundColor: const Color(0xFFC62828),
|
||||||
|
sf: sf
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 20 * sf),
|
),
|
||||||
_buildStatCard(
|
SizedBox(height: 40 * sf),
|
||||||
title: 'Assistências',
|
|
||||||
playerName: leaders['ast_name'],
|
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
|
||||||
statValue: leaders['ast_val'].toString(),
|
SizedBox(height: 16 * sf),
|
||||||
statLabel: 'TOTAL',
|
|
||||||
color: const Color(0xFF2E7D32),
|
// 👇 MAGIA ACONTECE AQUI: Ligação à Base de Dados para os Jogos 👇
|
||||||
icon: Icons.star,
|
_selectedTeamId == null
|
||||||
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
|
? 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)),
|
||||||
SizedBox(height: 20 * sf),
|
)
|
||||||
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
// ⚠️ 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());
|
||||||
|
}
|
||||||
|
|
||||||
// Segunda Linha: Rebotes e Gráfico
|
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
|
||||||
Row(
|
return Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: EdgeInsets.all(20 * sf),
|
||||||
children: [
|
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
|
||||||
_buildStatCard(
|
alignment: Alignment.center,
|
||||||
title: 'Rebotes',
|
child: Column(
|
||||||
playerName: leaders['rbs_name'],
|
children: [
|
||||||
statValue: leaders['rbs_val'].toString(),
|
Icon(Icons.sports_basketball, size: 40 * sf, color: Colors.grey.shade300),
|
||||||
statLabel: 'TOTAL',
|
SizedBox(height: 10 * sf),
|
||||||
color: const Color(0xFF6A1B9A),
|
Text("Ainda não há jogos registados.", style: TextStyle(color: Colors.grey.shade600, fontSize: 14 * sf, fontWeight: FontWeight.bold)),
|
||||||
icon: Icons.trending_up,
|
],
|
||||||
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
|
),
|
||||||
),
|
);
|
||||||
SizedBox(width: 20 * sf),
|
}
|
||||||
PieChartCard(
|
|
||||||
title: 'DESEMPENHO',
|
final gamesList = gameSnapshot.data!;
|
||||||
subtitle: 'Temporada',
|
|
||||||
backgroundColor: const Color(0xFFC62828),
|
return Column(
|
||||||
onTap: () {},
|
children: gamesList.map((game) {
|
||||||
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
|
// ⚠️ 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';
|
||||||
SizedBox(height: 40 * sf),
|
int myScore = game['my_score'] != null ? int.tryParse(game['my_score'].toString()) ?? 0 : 0;
|
||||||
Text('Histórico de Jogos', style: TextStyle(fontSize: 24 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
||||||
Map<String, int> ptsMap = {};
|
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
|
||||||
Map<String, int> astMap = {};
|
for (var row in data) {
|
||||||
Map<String, int> rbsMap = {};
|
String pid = row['member_id'].toString();
|
||||||
Map<String, String> namesMap = {}; // Aqui vamos guardar o nome real
|
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<String, int> map) { var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key; return namesMap[bestId]!; }
|
||||||
|
int getBestVal(Map<String, int> 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)};
|
||||||
|
}
|
||||||
|
|
||||||
for (var row in data) {
|
Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
|
||||||
String pid = row['member_id'].toString();
|
return Card(
|
||||||
|
elevation: 4, margin: EdgeInsets.zero,
|
||||||
// 👇 BUSCA O NOME QUE VEM DA VIEW 👇
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se não houver dados, namesMap estará vazio e o reduce daria erro.
|
|
||||||
// Por isso, se estiver vazio, retornamos logo "---".
|
|
||||||
if (ptsMap.isEmpty) {
|
|
||||||
return {
|
|
||||||
'pts_name': '---', 'pts_val': 0,
|
|
||||||
'ast_name': '---', 'ast_val': 0,
|
|
||||||
'rbs_name': '---', 'rbs_val': 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
String getBest(Map<String, int> map) {
|
|
||||||
var bestId = map.entries.reduce((a, b) => a.value > b.value ? a : b).key;
|
|
||||||
return namesMap[bestId]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getBestVal(Map<String, int> 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, required IconData icon,
|
|
||||||
bool isHighlighted = false, required double sf, required double cardWidth, required double cardHeight,
|
|
||||||
}) {
|
|
||||||
return SizedBox(
|
|
||||||
width: cardWidth, height: cardHeight,
|
|
||||||
child: Card(
|
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
|
||||||
side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none,
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
||||||
borderRadius: BorderRadius.circular(20 * sf),
|
child: LayoutBuilder(
|
||||||
gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color]),
|
builder: (context, constraints) {
|
||||||
),
|
final double ch = constraints.maxHeight;
|
||||||
child: Padding(
|
final double cw = constraints.maxWidth;
|
||||||
padding: EdgeInsets.all(16.0 * sf),
|
|
||||||
child: Column(
|
return Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: EdgeInsets.all(cw * 0.06),
|
||||||
children: [
|
child: Column(
|
||||||
Text(title.toUpperCase(), style: TextStyle(fontSize: 10 * sf, fontWeight: FontWeight.bold, color: Colors.white70)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text(playerName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: Colors.white), maxLines: 1, overflow: TextOverflow.ellipsis),
|
children: [
|
||||||
const Spacer(),
|
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
Center(child: Text(statValue, style: TextStyle(fontSize: 32 * sf, fontWeight: FontWeight.bold, color: Colors.white))),
|
SizedBox(height: ch * 0.011),
|
||||||
Center(child: Text(statLabel, style: TextStyle(fontSize: 10 * sf, color: Colors.white70))),
|
SizedBox(
|
||||||
const Spacer(),
|
width: double.infinity,
|
||||||
Container(
|
child: FittedBox(
|
||||||
width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 6),
|
fit: BoxFit.scaleDown,
|
||||||
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(10)),
|
alignment: Alignment.centerLeft,
|
||||||
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: 10 * sf))),
|
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: [
|
||||||
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user