historico de jogos

This commit is contained in:
2026-03-11 12:45:34 +00:00
parent 49bb371ef4
commit 5be578a64e
7 changed files with 707 additions and 837 deletions

View File

@@ -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) {

View File

@@ -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)),
)
],
),
),
);
}
}*/

View File

@@ -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);
}
}

View File

@@ -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,
}; };
} }

View File

@@ -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),
);
} }
} }

View File

@@ -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);

View File

@@ -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
)
),
],
);
}
}