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

@@ -24,6 +24,9 @@ class PlacarController {
bool isLoading = true;
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 opponentScore = 0;
int myFouls = 0;
@@ -62,7 +65,6 @@ class PlacarController {
try {
await Future.delayed(const Duration(milliseconds: 1500));
// 1. Limpar estados para evitar duplicação
myCourt.clear();
myBench.clear();
oppCourt.clear();
@@ -73,7 +75,6 @@ class PlacarController {
myFouls = 0;
opponentFouls = 0;
// 2. Buscar dados básicos do JOGO
final gameResponse = await supabase.from('games').select().eq('id', gameId).single();
myScore = int.tryParse(gameResponse['my_score']?.toString() ?? '0') ?? 0;
@@ -86,24 +87,23 @@ class PlacarController {
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
currentQuarter = int.tryParse(gameResponse['current_quarter']?.toString() ?? '1') ?? 1;
// 3. Buscar os IDs das equipas
// 👇 Verifica se o jogo já tinha acabado noutra sessão
gameWasAlreadyFinished = gameResponse['status'] == 'Terminado';
final teamsResponse = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
for (var t in teamsResponse) {
if (t['name'] == myTeam) myTeamDbId = t['id'];
if (t['name'] == opponentTeam) oppTeamDbId = t['id'];
}
// 4. Buscar os Jogadores
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') : [];
// 5. BUSCAR ESTATÍSTICAS JÁ SALVAS
final statsResponse = await supabase.from('player_stats').select().eq('game_id', gameId);
final Map<String, dynamic> savedStats = {
for (var item in statsResponse) item['member_id'].toString(): item
};
// 6. Registar a tua equipa
for (int i = 0; i < myPlayers.length; i++) {
String dbId = myPlayers[i]['id'].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,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 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);
}
}
_padTeam(myCourt, myBench, "Jogador", isMyTeam: true);
// 7. Registar a equipa adversária
for (int i = 0; i < oppPlayers.length; i++) {
String dbId = oppPlayers[i]['id'].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,
"stl": s['stl'] ?? 0, "tov": s['tov'] ?? 0, "blk": s['blk'] ?? 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);
}
@@ -159,7 +158,6 @@ class PlacarController {
playerNumbers[name] = number;
if (dbId != null) playerDbIds[name] = dbId;
// 👇 ADICIONEI AS 4 VARIÁVEIS NOVAS AQUI!
playerStats[name] = {
"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
@@ -178,7 +176,6 @@ class PlacarController {
}
}
// --- TEMPO E TIMEOUTS ---
void toggleTimer(BuildContext context) {
if (isRunning) {
timer?.cancel();
@@ -194,12 +191,11 @@ class PlacarController {
duration = const Duration(minutes: 10);
myFouls = 0;
opponentFouls = 0;
// 👇 ESTAS DUAS LINHAS ZERAM OS TIMEOUTS NO NOVO PERÍODO
myTimeoutsUsed = 0;
opponentTimeoutsUsed = 0;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO!'), 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();
@@ -222,7 +218,6 @@ class PlacarController {
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) {
String name = playerData.replaceAll("player_my_", "").replaceAll("player_opp_", "");
final stats = playerStats[name]!;
@@ -268,21 +263,13 @@ class PlacarController {
void registerShotLocation(BuildContext context, Offset position, Size size) {
if (pendingAction == null || pendingPlayer == null) return;
bool is3Pt = pendingAction!.contains("_3");
bool is2Pt = pendingAction!.contains("_2");
if (is3Pt || is2Pt) {
bool isValid = _validateShotZone(position, size, is3Pt);
if (!isValid) {
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),
)
);
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)));
return;
}
}
@@ -298,31 +285,20 @@ class PlacarController {
}
bool _validateShotZone(Offset pos, Size size, bool is3Pt) {
double w = size.width;
double h = size.height;
double w = size.width; double h = size.height;
Offset leftHoop = Offset(w * 0.12, h * 0.5);
Offset rightHoop = Offset(w * 0.88, h * 0.5);
double threePointRadius = w * 0.28;
Offset activeHoop = pos.dx < w / 2 ? leftHoop : rightHoop;
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) &&
(pos.dx < w * 0.20 || pos.dx > w * 0.80);
if (is3Pt) {
return distanceToHoop >= threePointRadius || isCorner3;
} else {
return distanceToHoop < threePointRadius && !isCorner3;
}
if (is3Pt) return distanceToHoop >= threePointRadius || isCorner3;
else return distanceToHoop < threePointRadius && !isCorner3;
}
void cancelShotLocation() {
isSelectingShotLocation = false;
pendingAction = null;
pendingPlayer = null;
onUpdate();
isSelectingShotLocation = false; pendingAction = null; pendingPlayer = null; onUpdate();
}
void commitStat(String action, String playerData) {
@@ -335,7 +311,7 @@ class PlacarController {
if (isOpponent) opponentScore += pts; else myScore += pts;
stats["pts"] = stats["pts"]! + pts;
if (pts == 2 || pts == 3) { stats["fgm"] = stats["fgm"]! + 1; stats["fga"] = stats["fga"]! + 1; }
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_")) {
int pts = int.parse(action.split("_").last);
@@ -346,15 +322,15 @@ class PlacarController {
if (stats["fgm"]! > 0) stats["fgm"] = stats["fgm"]! - 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["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 == "add_orb") { stats["orb"] = stats["orb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } // SEPARAÇÃO ORB
else if (action == "add_drb") { stats["drb"] = stats["drb"]! + 1; stats["rbs"] = stats["rbs"]! + 1; } // SEPARAÇÃO DRB
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; }
else if (action == "add_ast") { stats["ast"] = stats["ast"]! + 1; }
else if (action == "add_stl") { stats["stl"] = stats["stl"]! + 1; }
else if (action == "add_tov") { stats["tov"] = stats["tov"]! + 1; }
@@ -376,6 +352,10 @@ class PlacarController {
onUpdate();
try {
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
// 1. Atualizar o Jogo na BD
await supabase.from('games').update({
'my_score': myScore,
'opponent_score': opponentScore,
@@ -383,47 +363,69 @@ class PlacarController {
'my_timeouts': myTimeoutsUsed,
'opp_timeouts': opponentTimeoutsUsed,
'current_quarter': currentQuarter,
'status': currentQuarter >= 4 && duration.inSeconds == 0 ? 'Terminado' : 'Pausado',
'status': newStatus,
}).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) {
String? memberDbId = playerDbIds[playerName];
if (memberDbId != null && stats.values.any((val) => val > 0)) {
bool isMyTeamPlayer = myCourt.contains(playerName) || myBench.contains(playerName);
String teamId = isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!;
batchStats.add({
'game_id': gameId,
'member_id': memberDbId,
'team_id': teamId,
'pts': stats['pts'],
'rbs': stats['rbs'],
'ast': stats['ast'],
'stl': stats['stl'],
'blk': stats['blk'],
'tov': stats['tov'],
'fls': stats['fls'],
'fgm': stats['fgm'],
'fga': stats['fga'],
'ftm': stats['ftm'], // <-- GRAVAR NA BD
'fta': stats['fta'], // <-- GRAVAR NA BD
'orb': stats['orb'], // <-- GRAVAR NA BD
'drb': stats['drb'], // <-- GRAVAR NA BD
'game_id': gameId, 'member_id': memberDbId, 'team_id': isMyTeamPlayer ? myTeamDbId! : oppTeamDbId!,
'pts': stats['pts'], 'rbs': stats['rbs'], 'ast': stats['ast'], 'stl': stats['stl'], 'blk': stats['blk'], 'tov': stats['tov'], 'fls': stats['fls'], 'fgm': stats['fgm'], 'fga': stats['fga'], 'ftm': stats['ftm'], 'fta': stats['fta'], 'orb': stats['orb'], 'drb': stats['drb'],
});
}
});
await supabase.from('player_stats').delete().eq('game_id', gameId);
if (batchStats.isNotEmpty) {
await supabase.from('player_stats').insert(batchStats);
}
if (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) {

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,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:playmaker/grafico%20de%20pizza/dados_grafico.dart';
import 'package:flutter/material.dart';
import '../dados_grafico.dart'; // Ajusta o caminho se der erro de import
class PieChartController extends ChangeNotifier {
PieChartData _chartData = PieChartData(victories: 25, defeats: 10);
class PieChartController extends ChangeNotifier {
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
PieChartData get chartData => _chartData;
@@ -10,20 +10,12 @@
_chartData = PieChartData(
victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws,
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES
);
notifyListeners();
}
void incrementVictories() {
updateData(victories: _chartData.victories + 1);
}
void incrementDefeats() {
updateData(defeats: _chartData.defeats + 1);
}
void reset() {
updateData(victories: 0, defeats: 0, draws: 0);
}
}
}

View File

@@ -1,7 +1,7 @@
class PieChartData {
final int victories;
final int defeats;
final int draws;
final int draws; // 👇 AQUI ESTÃO OS EMPATES
const PieChartData({
required this.victories,
@@ -9,6 +9,7 @@ class PieChartData {
this.draws = 0,
});
// 👇 MATEMÁTICA ATUALIZADA 👇
int get total => victories + defeats + draws;
double get victoryPercentage => total > 0 ? victories / total : 0;
@@ -22,5 +23,6 @@ class PieChartData {
'total': total,
'victoryPercentage': victoryPercentage,
'defeatPercentage': defeatPercentage,
'drawPercentage': drawPercentage,
};
}

View File

@@ -1,30 +1,27 @@
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 'controllers/contollers_grafico.dart';
class PieChartCard extends StatefulWidget {
final PieChartController? controller;
final int victories;
final int defeats;
final int draws;
final String title;
final String subtitle;
final Color backgroundColor;
final VoidCallback? onTap;
// Variáveis de escala e tamanho
final double sf;
final double cardWidth;
final double cardHeight;
const PieChartCard({
super.key,
this.controller,
this.victories = 0,
this.defeats = 0,
this.draws = 0,
this.title = 'DESEMPENHO',
this.subtitle = 'Vitórias vs Derrotas',
this.subtitle = 'Temporada',
this.onTap,
required this.backgroundColor,
this.sf = 1.0,
required this.cardWidth,
required this.cardHeight,
});
@override
@@ -32,30 +29,26 @@ class PieChartCard extends StatefulWidget {
}
class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderStateMixin {
late PieChartController _controller;
late AnimationController _animationController;
late Animation<double> _animation;
@override
void 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();
}
@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
void dispose() {
_animationController.dispose();
@@ -64,129 +57,81 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
@override
Widget build(BuildContext context) {
final data = _controller.chartData;
final double sf = widget.sf;
final data = PieChartData(victories: widget.victories, defeats: widget.defeats, draws: widget.draws);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
// O scale pode passar de 1.0 (efeito back), mas a opacidade NÃO
scale: 0.95 + (_animation.value * 0.05),
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: SizedBox( // <-- Força a largura e altura exatas passadas pelo HomeScreen
width: widget.cardWidth,
height: widget.cardHeight,
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20 * sf),
),
margin: EdgeInsets.zero,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(20 * sf),
borderRadius: BorderRadius.circular(14),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * sf),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
widget.backgroundColor.withOpacity(0.9),
widget.backgroundColor.withOpacity(0.7),
],
borderRadius: BorderRadius.circular(14),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
),
),
child: Padding(
padding: EdgeInsets.all(16.0 * sf),
child: LayoutBuilder(
builder: (context, constraints) {
final double ch = constraints.maxHeight;
final double cw = constraints.maxWidth;
return Padding(
padding: EdgeInsets.all(cw * 0.06),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Cabeçalho compacto
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: TextStyle(
fontSize: 12 * sf,
fontWeight: FontWeight.bold,
color: Colors.white.withOpacity(0.9),
letterSpacing: 1.5,
// 👇 TÍTULOS UM POUCO MAIS PRESENTES
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), letterSpacing: 1.0)),
),
),
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,
),
),
],
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)),
),
SizedBox(height: 8 * sf),
SizedBox(height: ch * 0.03),
// Conteúdo principal
// MEIO (GRÁFICO + ESTATÍSTICAS)
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Gráfico de pizza
Expanded(
flex: 3,
child: Center(
flex: 1,
child: PieChartWidget(
victoryPercentage: data.victoryPercentage,
defeatPercentage: data.defeatPercentage,
drawPercentage: data.drawPercentage,
size: 120, // O PieChartWidget vai multiplicar isto por `sf` internamente
sf: sf, // <-- Passa a escala para o gráfico
sf: widget.sf,
),
),
),
SizedBox(width: 8 * sf),
// Estatísticas ultra compactas e sem overflows
SizedBox(width: cw * 0.05),
Expanded(
flex: 2,
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMiniStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, sf),
_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),
_buildDynStatRow("VIT", data.victories.toString(), (data.victoryPercentage * 100).toStringAsFixed(0), Colors.green, ch),
_buildDynStatRow("EMP", data.draws.toString(), (data.drawPercentage * 100).toStringAsFixed(0), Colors.yellow, ch),
_buildDynStatRow("DER", data.defeats.toString(), (data.defeatPercentage * 100).toStringAsFixed(0), Colors.red, ch),
_buildDynDivider(ch),
_buildDynStatRow("TOT", data.total.toString(), "100", Colors.white, ch),
],
),
),
@@ -194,113 +139,78 @@ class _PieChartCardState extends State<PieChartCard> with SingleTickerProviderSt
),
),
SizedBox(height: 10 * sf),
// Win rate - Com FittedBox para não estoirar
// 👇 RODAPÉ AJUSTADO
SizedBox(height: ch * 0.03),
Container(
padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 6 * sf),
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: ch * 0.035),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12 * sf),
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: data.victoryPercentage > 0.5
? Colors.green
: Colors.red,
size: 16 * sf,
data.victoryPercentage >= 0.5 ? Icons.trending_up : Icons.trending_down,
color: Colors.green,
size: ch * 0.09
),
SizedBox(width: 6 * sf),
SizedBox(width: cw * 0.02),
Text(
'Win Rate: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
'WIN RATE: ${(data.victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 12 * sf,
fontSize: ch * 0.05,
fontWeight: FontWeight.bold,
color: Colors.white,
),
color: Colors.white
)
),
],
),
),
),
),
],
),
),
);
}
),
),
),
),
);
}
// Função auxiliar BLINDADA contra overflows
Widget _buildMiniStatRow(String label, String number, String percent, Color color, double sf) {
return Container(
margin: EdgeInsets.only(bottom: 4 * sf),
// 👇 PERCENTAGENS SUBIDAS LIGEIRAMENTE (0.10 e 0.045)
Widget _buildDynStatRow(String label, String number, String percent, Color color, double ch) {
return Padding(
padding: EdgeInsets.only(bottom: ch * 0.01),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 28 * sf,
child: FittedBox(
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),
// Número subiu para 0.10
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)))),
SizedBox(width: ch * 0.02),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Row(
children: [
Container(
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),
),
),
],
),
flex: 3,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Row(children: [
Container(width: ch * 0.018, height: ch * 0.018, margin: EdgeInsets.only(right: ch * 0.015), decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
// Label subiu para 0.045
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))))
]),
// Percentagem subiu para 0.05
FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text('$percent%', style: TextStyle(fontSize: ch * 0.04, color: color, fontWeight: FontWeight.bold))),
]),
),
],
),
);
}
Widget _buildDivider(double sf) {
return Container(
height: 0.5,
color: Colors.white.withOpacity(0.1),
margin: EdgeInsets.symmetric(vertical: 3 * sf),
);
Widget _buildDynDivider(double ch) {
return Container(height: 0.5, color: Colors.white.withOpacity(0.1), margin: EdgeInsets.symmetric(vertical: ch * 0.01));
}
}

View File

@@ -5,61 +5,70 @@ class PieChartWidget extends StatelessWidget {
final double victoryPercentage;
final double defeatPercentage;
final double drawPercentage;
final double size;
final double sf; // <-- Fator de Escala
final double sf;
const PieChartWidget({
super.key,
required this.victoryPercentage,
required this.defeatPercentage,
this.drawPercentage = 0,
this.size = 140,
required this.sf, // <-- Obrigatório agora
required this.sf,
});
@override
Widget build(BuildContext context) {
// Aplica a escala ao tamanho total do quadrado do gráfico
final double scaledSize = size * sf;
return LayoutBuilder(
builder: (context, constraints) {
// 👇 MAGIA ANTI-DESAPARECIMENTO 👇
// Vê o espaço real. Se por algum motivo for infinito, assume 100 para não sumir.
final double w = constraints.maxWidth.isInfinite ? 100.0 : constraints.maxWidth;
final double h = constraints.maxHeight.isInfinite ? 100.0 : constraints.maxHeight;
return SizedBox(
width: scaledSize,
height: scaledSize,
// Pega no menor valor para garantir que o círculo não é cortado
final double size = math.min(w, h);
return Center(
child: SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _PieChartPainter(
victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage,
sf: sf, // <-- Passar para desenhar a borda
),
child: _buildCenterLabels(scaledSize),
child: Center(
child: _buildCenterLabels(size),
),
),
),
);
},
);
}
Widget _buildCenterLabels(double scaledSize) {
return Center(
child: Column(
Widget _buildCenterLabels(double size) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
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,
color: Colors.white,
),
),
SizedBox(height: 4 * sf),
SizedBox(height: size * 0.02),
Text(
'Vitórias',
style: TextStyle(
fontSize: scaledSize * 0.1, // Mantém-se proporcional
fontSize: size * 0.10,
color: Colors.white.withOpacity(0.8),
),
),
],
),
);
}
}
@@ -68,78 +77,63 @@ class _PieChartPainter extends CustomPainter {
final double victoryPercentage;
final double defeatPercentage;
final double drawPercentage;
final double sf; // <-- Escala no pintor
_PieChartPainter({
required this.victoryPercentage,
required this.defeatPercentage,
required this.drawPercentage,
required this.sf,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// Aplica a escala à margem para não cortar a linha da borda num ecrã pequeno
final radius = size.width / 2 - (5 * sf);
// Margem de 5% para a linha de fora não ser cortada
final radius = (size.width / 2) - (size.width * 0.05);
// Cores
const victoryColor = Colors.green;
const defeatColor = Colors.red;
const drawColor = Colors.yellow;
const borderColor = Colors.white30;
double startAngle = 0;
double startAngle = -math.pi / 2;
// Vitórias (verde)
if (victoryPercentage > 0) {
final sweepAngle = 2 * math.pi * victoryPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor);
_drawSector(canvas, center, radius, startAngle, sweepAngle, victoryColor, size.width);
startAngle += sweepAngle;
}
// Empates (amarelo)
if (drawPercentage > 0) {
final sweepAngle = 2 * math.pi * drawPercentage;
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor);
_drawSector(canvas, center, radius, startAngle, sweepAngle, drawColor, size.width);
startAngle += sweepAngle;
}
// Derrotas (vermelho)
if (defeatPercentage > 0) {
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()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = 2 * sf; // <-- Escala na grossura da linha
..strokeWidth = size.width * 0.02;
canvas.drawCircle(center, radius, borderPaint);
}
void _drawSector(Canvas canvas, Offset center, double radius,
double startAngle, double sweepAngle, Color color) {
void _drawSector(Canvas canvas, Offset center, double radius, double startAngle, double sweepAngle, Color color, double totalWidth) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
true,
paint,
);
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint);
// Linha divisória
if (sweepAngle < 2 * math.pi) {
final linePaint = Paint()
..color = Colors.white.withOpacity(0.5)
..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 lineY = center.dy + radius * math.sin(startAngle);

View File

@@ -1,26 +1,31 @@
import 'package:flutter/material.dart';
import 'package:playmaker/classe/home.config.dart';
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
import 'package:playmaker/pages/gamePage.dart';
import 'package:playmaker/pages/teamPage.dart';
import 'package:playmaker/controllers/team_controller.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:playmaker/classe/home.config.dart';
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
import 'package:playmaker/pages/gamePage.dart';
import 'package:playmaker/pages/teamPage.dart';
import 'package:playmaker/controllers/team_controller.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:math' as math;
class HomeScreen extends StatefulWidget {
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
}
class _HomeScreenState extends State<HomeScreen> {
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final TeamController _teamController = TeamController();
String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa";
// Instância do Supabase para buscar as estatísticas
int _teamWins = 0;
int _teamLosses = 0;
int _teamDraws = 0;
final _supabase = Supabase.instance.client;
@override
@@ -61,48 +66,25 @@ class _HomeScreenState extends State<HomeScreen> {
elevation: 1,
height: 70 * math.min(sf, 1.2),
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home_filled),
label: 'Home',
),
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',
),
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home_filled), label: 'Home'),
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) {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf)),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))),
builder: (context) {
return StreamBuilder<List<Map<String, dynamic>>>(
stream: _teamController.teamsStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return SizedBox(height: 200 * sf, child: Center(child: Text("Nenhuma equipa criada.")));
}
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: const Center(child: Text("Nenhuma equipa criada.")));
final teams = snapshot.data!;
return ListView.builder(
@@ -116,6 +98,9 @@ class _HomeScreenState extends State<HomeScreen> {
setState(() {
_selectedTeamId = team['id'];
_selectedTeamName = team['name'];
_teamWins = team['wins'] != null ? int.tryParse(team['wins'].toString()) ?? 0 : 0;
_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);
},
@@ -129,104 +114,152 @@ class _HomeScreenState extends State<HomeScreen> {
}
Widget _buildHomeContent(double sf, double wScreen) {
final double cardWidth = (wScreen - (40 * sf) - (20 * sf)) / 2;
final double cardHeight = cardWidth * 1.4; // Ajustado para não cortar conteúdo
final double cardHeight = (wScreen / 2) * 1.0;
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!)
: const Stream.empty(),
builder: (context, snapshot) {
// Lógica de cálculo de líderes
Map<String, dynamic> leaders = _calculateLeaders(snapshot.data ?? []);
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20.0 * sf),
padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Seletor de Equipa
InkWell(
onTap: () => _showTeamSelector(context, sf),
child: Container(
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(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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)),
],
),
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: 25 * sf),
// Primeira Linha: Pontos e Assistências
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCard(
title: 'Mais Pontos',
playerName: leaders['pts_name'],
statValue: leaders['pts_val'].toString(),
statLabel: 'TOTAL',
color: const Color(0xFF1565C0),
icon: Icons.bolt,
isHighlighted: true,
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
),
SizedBox(width: 20 * sf),
_buildStatCard(
title: 'Assistências',
playerName: leaders['ast_name'],
statValue: leaders['ast_val'].toString(),
statLabel: 'TOTAL',
color: const Color(0xFF2E7D32),
icon: Icons.star,
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
),
],
),
SizedBox(height: 20 * sf),
// Segunda Linha: Rebotes e Gráfico
Row(
mainAxisAlignment: MainAxisAlignment.center,
SizedBox(
height: cardHeight,
child: Row(
children: [
_buildStatCard(
title: 'Rebotes',
playerName: leaders['rbs_name'],
statValue: leaders['rbs_val'].toString(),
statLabel: 'TOTAL',
color: const Color(0xFF6A1B9A),
icon: Icons.trending_up,
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
SizedBox(width: 12 * sf),
Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
],
),
SizedBox(width: 20 * sf),
PieChartCard(
),
SizedBox(height: 12 * sf),
SizedBox(
height: cardHeight,
child: Row(
children: [
Expanded(child: _buildStatCard(title: 'Rebotes', playerName: leaders['rbs_name'], statValue: leaders['rbs_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF6A1B9A))),
SizedBox(width: 12 * sf),
Expanded(
child: PieChartCard(
victories: _teamWins,
defeats: _teamLosses,
draws: _teamDraws,
title: 'DESEMPENHO',
subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828),
onTap: () {},
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
sf: sf
),
),
],
),
),
SizedBox(height: 40 * sf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 24 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
Text('Histórico de Jogos', style: TextStyle(fontSize: 20 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
SizedBox(height: 16 * sf),
// 👇 MAGIA ACONTECE AQUI: Ligação à Base de Dados para os Jogos 👇
_selectedTeamId == null
? 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)),
)
: 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());
}
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
return Container(
padding: EdgeInsets.all(20 * sf),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
alignment: Alignment.center,
child: Column(
children: [
Icon(Icons.sports_basketball, size: 40 * sf, color: Colors.grey.shade300),
SizedBox(height: 10 * sf),
Text("Ainda não há jogos registados.", style: TextStyle(color: Colors.grey.shade600, fontSize: 14 * sf, fontWeight: FontWeight.bold)),
],
),
);
}
final gamesList = gameSnapshot.data!;
return Column(
children: gamesList.map((game) {
// ⚠️ 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';
int myScore = game['my_score'] != null ? int.tryParse(game['my_score'].toString()) ?? 0 : 0;
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),
],
),
),
@@ -235,86 +268,181 @@ stream: _selectedTeamId != null
);
}
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
Map<String, int> ptsMap = {};
Map<String, int> astMap = {};
Map<String, int> rbsMap = {};
Map<String, String> namesMap = {}; // Aqui vamos guardar o nome real
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
for (var row in data) {
String pid = row['member_id'].toString();
// 👇 BUSCA O NOME QUE VEM DA VIEW 👇
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]!;
}
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)};
}
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,
),
Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
return Card(
elevation: 4, margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * sf),
gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color]),
),
child: Padding(
padding: EdgeInsets.all(16.0 * sf),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
child: LayoutBuilder(
builder: (context, constraints) {
final double ch = constraints.maxHeight;
final double cw = constraints.maxWidth;
return Padding(
padding: EdgeInsets.all(cw * 0.06),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title.toUpperCase(), style: TextStyle(fontSize: 10 * sf, fontWeight: FontWeight.bold, color: Colors.white70)),
Text(playerName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold, color: Colors.white), maxLines: 1, overflow: TextOverflow.ellipsis),
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
SizedBox(height: ch * 0.011),
SizedBox(
width: double.infinity,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(playerName, style: TextStyle(fontSize: ch * 0.08, fontWeight: FontWeight.bold, color: Colors.white)),
),
),
const Spacer(),
Center(child: Text(statValue, style: TextStyle(fontSize: 32 * sf, fontWeight: FontWeight.bold, color: Colors.white))),
Center(child: Text(statLabel, style: TextStyle(fontSize: 10 * sf, color: Colors.white70))),
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: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(10)),
child: Center(child: Text('DETALHES', style: TextStyle(color: Colors.white, fontSize: 10 * sf))),
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
)
),
],
);
}
}