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 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;
@@ -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;
@@ -85,25 +86,24 @@ class PlacarController {
myTimeoutsUsed = int.tryParse(gameResponse['my_timeouts']?.toString() ?? '0') ?? 0;
opponentTimeoutsUsed = int.tryParse(gameResponse['opp_timeouts']?.toString() ?? '0') ?? 0;
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]);
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,29 +1,21 @@
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);
PieChartData get chartData => _chartData;
void updateData({int? victories, int? defeats, int? draws}) {
_chartData = PieChartData(
victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws,
);
notifyListeners();
}
void incrementVictories() {
updateData(victories: _chartData.victories + 1);
}
void incrementDefeats() {
updateData(defeats: _chartData.defeats + 1);
}
void reset() {
updateData(victories: 0, defeats: 0, draws: 0);
}
}
class PieChartController extends ChangeNotifier {
PieChartData _chartData = const PieChartData(victories: 0, defeats: 0, draws: 0);
PieChartData get chartData => _chartData;
void updateData({int? victories, int? defeats, int? draws}) {
_chartData = PieChartData(
victories: victories ?? _chartData.victories,
defeats: defeats ?? _chartData.defeats,
draws: draws ?? _chartData.draws, // 👇 AGORA ELE ACEITA OS EMPATES
);
notifyListeners();
}
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,243 +57,160 @@ 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(
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
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(
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),
),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(20 * sf),
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),
],
),
),
child: Padding(
padding: EdgeInsets.all(16.0 * sf),
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,
),
),
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: Card(
margin: EdgeInsets.zero,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [widget.backgroundColor.withOpacity(0.9), widget.backgroundColor.withOpacity(0.7)]),
),
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: [
// 👇 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)),
),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.subtitle, style: TextStyle(fontSize: ch * 0.07, fontWeight: FontWeight.bold, color: Colors.white)),
),
SizedBox(height: ch * 0.03),
// MEIO (GRÁFICO + ESTATÍSTICAS)
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
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
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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),
],
),
),
],
),
),
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,
SizedBox(width: cw * 0.05),
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_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),
],
),
),
],
),
),
),
],
),
),
// 👇 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
)
),
],
),
),
),
),
],
),
);
}
),
),
),
),
);
}
// 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 SizedBox(
width: scaledSize,
height: scaledSize,
child: CustomPaint(
painter: _PieChartPainter(
victoryPercentage: victoryPercentage,
defeatPercentage: defeatPercentage,
drawPercentage: drawPercentage,
sf: sf, // <-- Passar para desenhar a borda
),
child: _buildCenterLabels(scaledSize),
),
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;
// 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,
),
child: Center(
child: _buildCenterLabels(size),
),
),
),
);
},
);
}
Widget _buildCenterLabels(double scaledSize) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: scaledSize * 0.144, // Mantém-se proporcional
fontWeight: FontWeight.bold,
color: Colors.white,
),
Widget _buildCenterLabels(double size) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${(victoryPercentage * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: size * 0.18, // O texto cresce ou encolhe com o círculo
fontWeight: FontWeight.bold,
color: Colors.white,
),
SizedBox(height: 4 * sf),
Text(
'Vitórias',
style: TextStyle(
fontSize: scaledSize * 0.1, // Mantém-se proporcional
color: Colors.white.withOpacity(0.8),
),
),
SizedBox(height: size * 0.02),
Text(
'Vitórias',
style: TextStyle(
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,320 +1,448 @@
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 {
const HomeScreen({super.key});
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final TeamController _teamController = TeamController();
String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa";
@override
State<HomeScreen> createState() => _HomeScreenState();
}
// Instância do Supabase para buscar as estatísticas
final _supabase = Supabase.instance.client;
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
final TeamController _teamController = TeamController();
String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa";
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen, hScreen) / 400;
int _teamWins = 0;
int _teamLosses = 0;
int _teamDraws = 0;
final List<Widget> pages = [
_buildHomeContent(sf, wScreen),
const GamePage(),
const TeamsPage(),
const Center(child: Text('Tela de Status')),
];
final _supabase = Supabase.instance.client;
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: () {},
@override
Widget build(BuildContext context) {
final double wScreen = MediaQuery.of(context).size.width;
final double hScreen = MediaQuery.of(context).size.height;
final double sf = math.min(wScreen, hScreen) / 400;
final List<Widget> pages = [
_buildHomeContent(sf, wScreen),
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(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
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',
),
],
),
);
}
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
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'),
],
),
);
}
// --- 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)),
),
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.")));
}
void _showTeamSelector(BuildContext context, double sf) {
showModalBottomSheet(
context: context,
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: const Center(child: Text("Nenhuma equipa criada.")));
final teams = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
itemCount: teams.length,
itemBuilder: (context, index) {
final team = teams[index];
return ListTile(
title: Text(team['name']),
onTap: () {
setState(() {
_selectedTeamId = team['id'];
_selectedTeamName = team['name'];
});
Navigator.pop(context);
},
);
},
);
},
);
},
);
}
final teams = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
itemCount: teams.length,
itemBuilder: (context, index) {
final team = teams[index];
return ListTile(
title: Text(team['name']),
onTap: () {
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);
},
);
},
);
},
);
},
);
}
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
Widget _buildHomeContent(double sf, double wScreen) {
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
? _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 StreamBuilder<List<Map<String, dynamic>>>(
stream: _selectedTeamId != null
? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!)
: const Stream.empty(),
builder: (context, snapshot) {
Map<String, dynamic> leaders = _calculateLeaders(snapshot.data ?? []);
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(20.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),
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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)),
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))]),
const Icon(Icons.arrow_drop_down),
],
),
),
),
SizedBox(height: 20 * sf),
SizedBox(
height: cardHeight,
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)),
],
),
const Icon(Icons.arrow_drop_down),
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(height: 25 * sf),
SizedBox(height: 12 * 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(
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),
sf: sf
),
),
],
),
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),
),
SizedBox(height: 40 * sf),
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());
}
// Segunda Linha: Rebotes e Gráfico
Row(
mainAxisAlignment: MainAxisAlignment.center,
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,
),
SizedBox(width: 20 * sf),
PieChartCard(
title: 'DESEMPENHO',
subtitle: 'Temporada',
backgroundColor: const Color(0xFFC62828),
onTap: () {},
sf: sf, cardWidth: cardWidth, cardHeight: cardHeight,
),
],
),
SizedBox(height: 40 * sf),
Text('Histórico de Jogos', style: TextStyle(fontSize: 24 * sf, fontWeight: FontWeight.bold, color: Colors.grey[800])),
],
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),
],
),
),
),
);
},
);
}
);
},
);
}
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();
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) {
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]!;
}
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,
),
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),
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),
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))),
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))),
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: 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: 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
)
),
],
);
}
}