esta melhor des que comecei
This commit is contained in:
@@ -4,25 +4,34 @@ import '../models/game_model.dart';
|
|||||||
class GameController {
|
class GameController {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// 1. LER JOGOS (Stream em Tempo Real)
|
// 1. LER JOGOS (Com Filtros Opcionais)
|
||||||
Stream<List<Game>> get gamesStream {
|
Stream<List<Game>> getFilteredGames({String? teamFilter, String? seasonFilter}) {
|
||||||
return _supabase
|
return _supabase
|
||||||
.from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
|
.from('games')
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
.asyncMap((event) async {
|
.asyncMap((event) async {
|
||||||
// 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado),
|
|
||||||
// vamos buscar os dados já misturados com as imagens à nossa View.
|
// 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games'
|
||||||
final viewData = await _supabase
|
var query = _supabase.from('games').select();
|
||||||
.from('games_with_logos')
|
|
||||||
.select()
|
// Aplica o filtro de Temporada
|
||||||
.order('game_date', ascending: false);
|
if (seasonFilter != null && seasonFilter.isNotEmpty && seasonFilter != 'Todas') {
|
||||||
|
query = query.eq('season', seasonFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica o filtro de Equipa (Procura em casa ou fora)
|
||||||
|
if (teamFilter != null && teamFilter.isNotEmpty && teamFilter != 'Todas') {
|
||||||
|
query = query.or('my_team.eq.$teamFilter,opponent_team.eq.$teamFilter');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executa a query com a ordenação por data
|
||||||
|
final viewData = await query.order('game_date', ascending: false);
|
||||||
|
|
||||||
// 3. Convertemos para a nossa lista de objetos Game
|
|
||||||
return viewData.map((json) => Game.fromMap(json)).toList();
|
return viewData.map((json) => Game.fromMap(json)).toList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CRIAR JOGO
|
// 2. CRIAR JOGO
|
||||||
// Retorna o ID do jogo criado para podermos navegar para o placar
|
|
||||||
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
Future<String?> createGame(String myTeam, String opponent, String season) async {
|
||||||
try {
|
try {
|
||||||
final response = await _supabase.from('games').insert({
|
final response = await _supabase.from('games').insert({
|
||||||
@@ -31,18 +40,16 @@ Stream<List<Game>> get gamesStream {
|
|||||||
'season': season,
|
'season': season,
|
||||||
'my_score': 0,
|
'my_score': 0,
|
||||||
'opponent_score': 0,
|
'opponent_score': 0,
|
||||||
'status': 'Decorrer', // Começa como "Decorrer"
|
'status': 'Decorrer',
|
||||||
'game_date': DateTime.now().toIso8601String(),
|
'game_date': DateTime.now().toIso8601String(),
|
||||||
}).select().single(); // .select().single() retorna o objeto criado
|
}).select().single();
|
||||||
|
|
||||||
return response['id']; // Retorna o UUID gerado pelo Supabase
|
return response['id'];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Erro ao criar jogo: $e");
|
print("Erro ao criar jogo: $e");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {}
|
||||||
// Não é necessário fechar streams do Supabase manualmente aqui
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -187,16 +187,16 @@ class PlacarController {
|
|||||||
timer.cancel();
|
timer.cancel();
|
||||||
isRunning = false;
|
isRunning = false;
|
||||||
if (currentQuarter < 4) {
|
if (currentQuarter < 4) {
|
||||||
currentQuarter++;
|
currentQuarter++;
|
||||||
duration = const Duration(minutes: 10);
|
duration = const Duration(minutes: 10);
|
||||||
myFouls = 0;
|
myFouls = 0;
|
||||||
opponentFouls = 0;
|
opponentFouls = 0;
|
||||||
myTimeoutsUsed = 0;
|
myTimeoutsUsed = 0;
|
||||||
opponentTimeoutsUsed = 0;
|
opponentTimeoutsUsed = 0;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Período $currentQuarter iniciado. Faltas e Timeouts resetados!'), backgroundColor: Colors.blue));
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('FIM DO JOGO! Clica em Guardar para fechar a partida.'), backgroundColor: Colors.red));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onUpdate();
|
onUpdate();
|
||||||
});
|
});
|
||||||
@@ -355,7 +355,34 @@ class PlacarController {
|
|||||||
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
|
bool isGameFinishedNow = currentQuarter >= 4 && duration.inSeconds == 0;
|
||||||
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
|
String newStatus = isGameFinishedNow ? 'Terminado' : 'Pausado';
|
||||||
|
|
||||||
// 1. Atualizar o Jogo na BD
|
// 👇👇👇 0. CÉREBRO: CALCULAR OS LÍDERES E MVP DO JOGO 👇👇👇
|
||||||
|
String topPtsName = '---'; int maxPts = -1;
|
||||||
|
String topAstName = '---'; int maxAst = -1;
|
||||||
|
String topRbsName = '---'; int maxRbs = -1;
|
||||||
|
String topDefName = '---'; int maxDef = -1;
|
||||||
|
String mvpName = '---'; int maxMvpScore = -1;
|
||||||
|
|
||||||
|
// Passa por todos os jogadores e calcula a matemática
|
||||||
|
playerStats.forEach((playerName, stats) {
|
||||||
|
int pts = stats['pts'] ?? 0;
|
||||||
|
int ast = stats['ast'] ?? 0;
|
||||||
|
int rbs = stats['rbs'] ?? 0;
|
||||||
|
int stl = stats['stl'] ?? 0;
|
||||||
|
int blk = stats['blk'] ?? 0;
|
||||||
|
|
||||||
|
int defScore = stl + blk; // Defesa: Roubos + Cortes
|
||||||
|
int mvpScore = pts + ast + rbs + defScore; // Impacto Total (MVP)
|
||||||
|
|
||||||
|
// Compara com o máximo atual e substitui se for maior
|
||||||
|
if (pts > maxPts && pts > 0) { maxPts = pts; topPtsName = '$playerName ($pts)'; }
|
||||||
|
if (ast > maxAst && ast > 0) { maxAst = ast; topAstName = '$playerName ($ast)'; }
|
||||||
|
if (rbs > maxRbs && rbs > 0) { maxRbs = rbs; topRbsName = '$playerName ($rbs)'; }
|
||||||
|
if (defScore > maxDef && defScore > 0) { maxDef = defScore; topDefName = '$playerName ($defScore)'; }
|
||||||
|
if (mvpScore > maxMvpScore && mvpScore > 0) { maxMvpScore = mvpScore; mvpName = playerName; } // MVP não leva nº à frente, fica mais limpo
|
||||||
|
});
|
||||||
|
// 👆👆👆 FIM DO CÉREBRO 👆👆👆
|
||||||
|
|
||||||
|
// 1. Atualizar o Jogo na BD (Agora inclui os Reis da partida!)
|
||||||
await supabase.from('games').update({
|
await supabase.from('games').update({
|
||||||
'my_score': myScore,
|
'my_score': myScore,
|
||||||
'opponent_score': opponentScore,
|
'opponent_score': opponentScore,
|
||||||
@@ -364,12 +391,18 @@ class PlacarController {
|
|||||||
'opp_timeouts': opponentTimeoutsUsed,
|
'opp_timeouts': opponentTimeoutsUsed,
|
||||||
'current_quarter': currentQuarter,
|
'current_quarter': currentQuarter,
|
||||||
'status': newStatus,
|
'status': newStatus,
|
||||||
|
|
||||||
|
// ENVIA A MATEMÁTICA PARA A TUA BASE DE DADOS
|
||||||
|
'top_pts_name': topPtsName,
|
||||||
|
'top_ast_name': topAstName,
|
||||||
|
'top_rbs_name': topRbsName,
|
||||||
|
'top_def_name': topDefName,
|
||||||
|
'mvp_name': mvpName,
|
||||||
}).eq('id', gameId);
|
}).eq('id', gameId);
|
||||||
|
|
||||||
// 👇 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES 👇
|
// 2. LÓGICA DE VITÓRIAS, DERROTAS E EMPATES
|
||||||
if (isGameFinishedNow && !gameWasAlreadyFinished && myTeamDbId != null && oppTeamDbId != null) {
|
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]);
|
final teamsData = await supabase.from('teams').select('id, wins, losses, draws').inFilter('id', [myTeamDbId, oppTeamDbId]);
|
||||||
|
|
||||||
Map<String, dynamic> myTeamUpdate = {};
|
Map<String, dynamic> myTeamUpdate = {};
|
||||||
@@ -380,7 +413,6 @@ class PlacarController {
|
|||||||
if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t);
|
if(t['id'].toString() == oppTeamDbId) oppTeamUpdate = Map.from(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcula os resultados
|
|
||||||
if (myScore > opponentScore) {
|
if (myScore > opponentScore) {
|
||||||
myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1;
|
myTeamUpdate['wins'] = (myTeamUpdate['wins'] ?? 0) + 1;
|
||||||
oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1;
|
oppTeamUpdate['losses'] = (oppTeamUpdate['losses'] ?? 0) + 1;
|
||||||
@@ -388,12 +420,10 @@ class PlacarController {
|
|||||||
myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1;
|
myTeamUpdate['losses'] = (myTeamUpdate['losses'] ?? 0) + 1;
|
||||||
oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1;
|
oppTeamUpdate['wins'] = (oppTeamUpdate['wins'] ?? 0) + 1;
|
||||||
} else {
|
} else {
|
||||||
// Empate
|
|
||||||
myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1;
|
myTeamUpdate['draws'] = (myTeamUpdate['draws'] ?? 0) + 1;
|
||||||
oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1;
|
oppTeamUpdate['draws'] = (oppTeamUpdate['draws'] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Envia as atualizações para a tabela 'teams'
|
|
||||||
await supabase.from('teams').update({
|
await supabase.from('teams').update({
|
||||||
'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws']
|
'wins': myTeamUpdate['wins'], 'losses': myTeamUpdate['losses'], 'draws': myTeamUpdate['draws']
|
||||||
}).eq('id', myTeamDbId!);
|
}).eq('id', myTeamDbId!);
|
||||||
@@ -402,7 +432,6 @@ class PlacarController {
|
|||||||
'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws']
|
'wins': oppTeamUpdate['wins'], 'losses': oppTeamUpdate['losses'], 'draws': oppTeamUpdate['draws']
|
||||||
}).eq('id', oppTeamDbId!);
|
}).eq('id', oppTeamDbId!);
|
||||||
|
|
||||||
// Bloqueia o trinco para não contar 2 vezes se o utilizador clicar "Guardar" outra vez
|
|
||||||
gameWasAlreadyFinished = true;
|
gameWasAlreadyFinished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ class TeamController {
|
|||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// 1. STREAM (Realtime)
|
// 1. STREAM (Realtime)
|
||||||
// Adicionei o .map() no final para garantir que o Dart entende que é uma List<Map>
|
|
||||||
Stream<List<Map<String, dynamic>>> get teamsStream {
|
Stream<List<Map<String, dynamic>>> get teamsStream {
|
||||||
return _supabase
|
return _supabase
|
||||||
.from('teams')
|
.from('teams')
|
||||||
@@ -15,7 +14,6 @@ class TeamController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. CRIAR
|
// 2. CRIAR
|
||||||
// Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem
|
|
||||||
Future<void> createTeam(String name, String season, String? imageUrl) async {
|
Future<void> createTeam(String name, String season, String? imageUrl) async {
|
||||||
try {
|
try {
|
||||||
await _supabase.from('teams').insert({
|
await _supabase.from('teams').insert({
|
||||||
@@ -51,21 +49,14 @@ class TeamController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. CONTAR JOGADORES
|
// 5. CONTAR JOGADORES (AGORA EM TEMPO REAL COM STREAM!)
|
||||||
// CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
|
Stream<int> getPlayerCountStream(String teamId) {
|
||||||
Future<int> getPlayerCount(String teamId) async {
|
return _supabase
|
||||||
try {
|
.from('members')
|
||||||
final count = await _supabase
|
.stream(primaryKey: ['id'])
|
||||||
.from('members')
|
.eq('team_id', teamId)
|
||||||
.count() // Retorna diretamente o número inteiro
|
.map((data) => data.length); // O tamanho da lista é o número de jogadores
|
||||||
.eq('team_id', teamId);
|
|
||||||
return count;
|
|
||||||
} catch (e) {
|
|
||||||
print("Erro ao contar jogadores: $e");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,6 @@ class Game {
|
|||||||
final String id;
|
final String id;
|
||||||
final String myTeam;
|
final String myTeam;
|
||||||
final String opponentTeam;
|
final String opponentTeam;
|
||||||
final String? myTeamLogo; // URL da imagem
|
|
||||||
final String? opponentTeamLogo; // URL da imagem
|
|
||||||
final String myScore;
|
final String myScore;
|
||||||
final String opponentScore;
|
final String opponentScore;
|
||||||
final String status;
|
final String status;
|
||||||
@@ -13,26 +11,22 @@ class Game {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.myTeam,
|
required this.myTeam,
|
||||||
required this.opponentTeam,
|
required this.opponentTeam,
|
||||||
this.myTeamLogo,
|
|
||||||
this.opponentTeamLogo,
|
|
||||||
required this.myScore,
|
required this.myScore,
|
||||||
required this.opponentScore,
|
required this.opponentScore,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.season,
|
required this.season,
|
||||||
});
|
});
|
||||||
|
|
||||||
// No seu factory, certifique-se de mapear os campos da tabela (ou de um JOIN)
|
|
||||||
factory Game.fromMap(Map<String, dynamic> map) {
|
factory Game.fromMap(Map<String, dynamic> map) {
|
||||||
return Game(
|
return Game(
|
||||||
id: map['id'],
|
// O "?." converte para texto com segurança, e o "?? '...'" diz o que mostrar se for nulo (vazio)
|
||||||
myTeam: map['my_team_name'],
|
id: map['id']?.toString() ?? '',
|
||||||
opponentTeam: map['opponent_team_name'],
|
myTeam: map['my_team']?.toString() ?? 'Desconhecida',
|
||||||
myTeamLogo: map['my_team_logo'], // Certifique-se que o Supabase retorna isto
|
opponentTeam: map['opponent_team']?.toString() ?? 'Adversário',
|
||||||
opponentTeamLogo: map['opponent_team_logo'],
|
myScore: map['my_score']?.toString() ?? '0',
|
||||||
myScore: map['my_score'].toString(),
|
opponentScore: map['opponent_score']?.toString() ?? '0',
|
||||||
opponentScore: map['opponent_score'].toString(),
|
status: map['status']?.toString() ?? 'Terminado',
|
||||||
status: map['status'],
|
season: map['season']?.toString() ?? 'Sem Época',
|
||||||
season: map['season'],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,230 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/pages/PlacarPage.dart';
|
||||||
import '../controllers/game_controller.dart';
|
import '../controllers/game_controller.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../models/game_model.dart';
|
import '../models/game_model.dart';
|
||||||
import '../widgets/game_widgets.dart';
|
import 'dart:math' as math;
|
||||||
import 'dart:math' as math; // <-- IMPORTANTE: Para o cálculo da escala
|
|
||||||
|
|
||||||
|
// --- CARD DE EXIBIÇÃO DO JOGO ---
|
||||||
|
class GameResultCard extends StatelessWidget {
|
||||||
|
final String gameId;
|
||||||
|
final String myTeam, opponentTeam, myScore, opponentScore, status, season;
|
||||||
|
final String? myTeamLogo;
|
||||||
|
final String? opponentTeamLogo;
|
||||||
|
final double sf;
|
||||||
|
|
||||||
|
const GameResultCard({
|
||||||
|
super.key,
|
||||||
|
required this.gameId,
|
||||||
|
required this.myTeam,
|
||||||
|
required this.opponentTeam,
|
||||||
|
required this.myScore,
|
||||||
|
required this.opponentScore,
|
||||||
|
required this.status,
|
||||||
|
required this.season,
|
||||||
|
this.myTeamLogo,
|
||||||
|
this.opponentTeamLogo,
|
||||||
|
required this.sf,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 16 * sf),
|
||||||
|
padding: EdgeInsets.all(16 * sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20 * sf),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10 * sf)],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C), myTeamLogo, sf)),
|
||||||
|
_buildScoreCenter(context, gameId, sf),
|
||||||
|
Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87, opponentTeamLogo, sf)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTeamInfo(String name, Color color, String? logoUrl, double sf) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 24 * sf,
|
||||||
|
backgroundColor: color,
|
||||||
|
backgroundImage: (logoUrl != null && logoUrl.isNotEmpty) ? NetworkImage(logoUrl) : null,
|
||||||
|
child: (logoUrl == null || logoUrl.isEmpty) ? Icon(Icons.shield, color: Colors.white, size: 24 * sf) : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 6 * sf),
|
||||||
|
Text(name,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf),
|
||||||
|
textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScoreCenter(BuildContext context, String id, double sf) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_scoreBox(myScore, Colors.green, sf),
|
||||||
|
Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22 * sf)),
|
||||||
|
_scoreBox(opponentScore, Colors.grey, sf),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 10 * sf),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: id, myTeam: myTeam, opponentTeam: opponentTeam)));
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.play_circle_fill, size: 18 * sf, color: const Color(0xFFE74C3C)),
|
||||||
|
label: Text("RETORNAR", style: TextStyle(fontSize: 11 * sf, color: const Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 14 * sf, vertical: 8 * sf),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 6 * sf),
|
||||||
|
Text(status, style: TextStyle(fontSize: 12 * sf, color: Colors.blue, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _scoreBox(String pts, Color c, double sf) => Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12 * sf, vertical: 6 * sf),
|
||||||
|
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8 * sf)),
|
||||||
|
child: Text(pts, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16 * sf)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- POPUP DE CRIAÇÃO ---
|
||||||
|
class CreateGameDialogManual extends StatefulWidget {
|
||||||
|
final TeamController teamController;
|
||||||
|
final GameController gameController;
|
||||||
|
final double sf;
|
||||||
|
|
||||||
|
const CreateGameDialogManual({super.key, required this.teamController, required this.gameController, required this.sf});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
||||||
|
late TextEditingController _seasonController;
|
||||||
|
final TextEditingController _myTeamController = TextEditingController();
|
||||||
|
final TextEditingController _opponentController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_seasonController = TextEditingController(text: _calculateSeason());
|
||||||
|
}
|
||||||
|
|
||||||
|
String _calculateSeason() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return now.month >= 7 ? "${now.year}/${(now.year + 1).toString().substring(2)}" : "${now.year - 1}/${now.year.toString().substring(2)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * widget.sf)),
|
||||||
|
title: Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * widget.sf)),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _seasonController, style: TextStyle(fontSize: 14 * widget.sf),
|
||||||
|
decoration: InputDecoration(labelText: 'Temporada', labelStyle: TextStyle(fontSize: 14 * widget.sf), border: const OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today, size: 20 * widget.sf)),
|
||||||
|
),
|
||||||
|
SizedBox(height: 15 * widget.sf),
|
||||||
|
_buildSearch(label: "Minha Equipa", controller: _myTeamController, sf: widget.sf),
|
||||||
|
Padding(padding: EdgeInsets.symmetric(vertical: 10 * widget.sf), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey, fontSize: 16 * widget.sf))),
|
||||||
|
_buildSearch(label: "Adversário", controller: _opponentController, sf: widget.sf),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: Text('CANCELAR', style: TextStyle(fontSize: 14 * widget.sf))),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * widget.sf)), padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)),
|
||||||
|
onPressed: _isLoading ? null : () async {
|
||||||
|
if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
String? newGameId = await widget.gameController.createGame(_myTeamController.text, _opponentController.text, _seasonController.text);
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
if (newGameId != null && context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => PlacarPage(gameId: newGameId, myTeam: _myTeamController.text, opponentTeam: _opponentController.text)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: _isLoading ? SizedBox(width: 20 * widget.sf, height: 20 * widget.sf, child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14 * widget.sf)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearch({required String label, required TextEditingController controller, required double sf}) {
|
||||||
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: widget.teamController.teamsStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
List<Map<String, dynamic>> teamList = snapshot.hasData ? snapshot.data! : [];
|
||||||
|
return Autocomplete<Map<String, dynamic>>(
|
||||||
|
displayStringForOption: (Map<String, dynamic> option) => option['name'].toString(),
|
||||||
|
optionsBuilder: (TextEditingValue val) {
|
||||||
|
if (val.text.isEmpty) return const Iterable<Map<String, dynamic>>.empty();
|
||||||
|
return teamList.where((t) => t['name'].toString().toLowerCase().contains(val.text.toLowerCase()));
|
||||||
|
},
|
||||||
|
onSelected: (Map<String, dynamic> selection) { controller.text = selection['name'].toString(); },
|
||||||
|
optionsViewBuilder: (context, onSelected, options) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0, borderRadius: BorderRadius.circular(8 * sf),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: 250 * sf, maxWidth: MediaQuery.of(context).size.width * 0.7),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final option = options.elementAt(index);
|
||||||
|
final String name = option['name'].toString();
|
||||||
|
final String? imageUrl = option['image_url'];
|
||||||
|
return ListTile(
|
||||||
|
leading: CircleAvatar(radius: 20 * sf, backgroundColor: Colors.grey.shade200, backgroundImage: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : null, child: (imageUrl == null || imageUrl.isEmpty) ? Icon(Icons.shield, color: Colors.grey, size: 20 * sf) : null),
|
||||||
|
title: Text(name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14 * sf)),
|
||||||
|
onTap: () { onSelected(option); },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (ctx, txtCtrl, node, submit) {
|
||||||
|
if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { txtCtrl.text = controller.text; }
|
||||||
|
txtCtrl.addListener(() { controller.text = txtCtrl.text; });
|
||||||
|
return TextField(
|
||||||
|
controller: txtCtrl, focusNode: node, style: TextStyle(fontSize: 14 * sf),
|
||||||
|
decoration: InputDecoration(labelText: label, labelStyle: TextStyle(fontSize: 14 * sf), prefixIcon: Icon(Icons.search, size: 20 * sf), border: const OutlineInputBorder()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PÁGINA PRINCIPAL DOS JOGOS COM FILTROS ---
|
||||||
class GamePage extends StatefulWidget {
|
class GamePage extends StatefulWidget {
|
||||||
const GamePage({super.key});
|
const GamePage({super.key});
|
||||||
|
|
||||||
@@ -16,40 +236,68 @@ class _GamePageState extends State<GamePage> {
|
|||||||
final GameController gameController = GameController();
|
final GameController gameController = GameController();
|
||||||
final TeamController teamController = TeamController();
|
final TeamController teamController = TeamController();
|
||||||
|
|
||||||
|
// Variáveis para os filtros
|
||||||
|
String selectedSeason = 'Todas';
|
||||||
|
String selectedTeam = 'Todas';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇
|
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
final double wScreen = MediaQuery.of(context).size.width;
|
||||||
final double hScreen = MediaQuery.of(context).size.height;
|
final double hScreen = MediaQuery.of(context).size.height;
|
||||||
final double sf = math.min(wScreen, hScreen) / 400;
|
final double sf = math.min(wScreen, hScreen) / 400;
|
||||||
|
|
||||||
|
// Verifica se algum filtro está ativo para mudar a cor do ícone
|
||||||
|
bool isFilterActive = selectedSeason != 'Todas' || selectedTeam != 'Todas';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: const Color(0xFFF5F7FA),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * sf)),
|
title: Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20 * sf)),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
// 👇 BOTÃO DE FILTRO NA APP BAR 👇
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8.0 * sf),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isFilterActive ? Icons.filter_list_alt : Icons.filter_list,
|
||||||
|
color: isFilterActive ? const Color(0xFFE74C3C) : Colors.black87,
|
||||||
|
size: 26 * sf,
|
||||||
|
),
|
||||||
|
onPressed: () => _showFilterPopup(context, sf),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
// 1º STREAM: Lemos as equipas para ter as imagens
|
|
||||||
body: StreamBuilder<List<Map<String, dynamic>>>(
|
body: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: teamController.teamsStream,
|
stream: teamController.teamsStream,
|
||||||
builder: (context, teamSnapshot) {
|
builder: (context, teamSnapshot) {
|
||||||
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
|
final List<Map<String, dynamic>> teamsList = teamSnapshot.data ?? [];
|
||||||
|
|
||||||
// 2º STREAM: Lemos os jogos
|
|
||||||
return StreamBuilder<List<Game>>(
|
return StreamBuilder<List<Game>>(
|
||||||
stream: gameController.gamesStream,
|
stream: gameController.getFilteredGames(teamFilter: selectedTeam, seasonFilter: selectedSeason),
|
||||||
builder: (context, gameSnapshot) {
|
builder: (context, gameSnapshot) {
|
||||||
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) {
|
if (gameSnapshot.connectionState == ConnectionState.waiting && teamsList.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameSnapshot.hasError) {
|
if (gameSnapshot.hasError) {
|
||||||
return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf)));
|
return Center(child: Text("Erro: ${gameSnapshot.error}", style: TextStyle(fontSize: 14 * sf)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
|
if (!gameSnapshot.hasData || gameSnapshot.data!.isEmpty) {
|
||||||
return Center(child: Text("Nenhum jogo registado.", style: TextStyle(fontSize: 16 * sf)));
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.search_off, size: 48 * sf, color: Colors.grey.shade300),
|
||||||
|
SizedBox(height: 10 * sf),
|
||||||
|
Text("Nenhum jogo encontrado para este filtro.", style: TextStyle(fontSize: 14 * sf, color: Colors.grey.shade600)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@@ -58,17 +306,12 @@ class _GamePageState extends State<GamePage> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final game = gameSnapshot.data![index];
|
final game = gameSnapshot.data![index];
|
||||||
|
|
||||||
// --- LÓGICA PARA ENCONTRAR A IMAGEM PELO NOME ---
|
|
||||||
String? myLogo;
|
String? myLogo;
|
||||||
String? oppLogo;
|
String? oppLogo;
|
||||||
|
|
||||||
for (var team in teamsList) {
|
for (var team in teamsList) {
|
||||||
if (team['name'] == game.myTeam) {
|
if (team['name'] == game.myTeam) { myLogo = team['image_url']; }
|
||||||
myLogo = team['image_url'];
|
if (team['name'] == game.opponentTeam) { oppLogo = team['image_url']; }
|
||||||
}
|
|
||||||
if (team['name'] == game.opponentTeam) {
|
|
||||||
oppLogo = team['image_url'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return GameResultCard(
|
return GameResultCard(
|
||||||
@@ -81,7 +324,7 @@ class _GamePageState extends State<GamePage> {
|
|||||||
season: game.season,
|
season: game.season,
|
||||||
myTeamLogo: myLogo,
|
myTeamLogo: myLogo,
|
||||||
opponentTeamLogo: oppLogo,
|
opponentTeamLogo: oppLogo,
|
||||||
sf: sf, // <-- Passamos a escala para o Cartão
|
sf: sf,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -97,13 +340,135 @@ class _GamePageState extends State<GamePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 👇 O POPUP DE FILTROS 👇
|
||||||
|
void _showFilterPopup(BuildContext context, double sf) {
|
||||||
|
// Variáveis temporárias para o Popup (para não atualizar a lista antes de clicar em "Aplicar")
|
||||||
|
String tempSeason = selectedSeason;
|
||||||
|
String tempTeam = selectedTeam;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
// StatefulBuilder permite atualizar a interface APENAS dentro do Popup
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setPopupState) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20 * sf)),
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('Filtrar Jogos', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18 * sf)),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.grey),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 1. Filtro de Temporada
|
||||||
|
Text("Temporada", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 6 * sf),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: tempSeason,
|
||||||
|
style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold),
|
||||||
|
items: ['Todas', '2024/25', '2025/26'].map((String value) {
|
||||||
|
return DropdownMenuItem<String>(value: value, child: Text(value));
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setPopupState(() => tempSeason = newValue!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 20 * sf),
|
||||||
|
|
||||||
|
// 2. Filtro de Equipa
|
||||||
|
Text("Equipa", style: TextStyle(fontSize: 12 * sf, color: Colors.grey.shade600, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 6 * sf),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10 * sf)),
|
||||||
|
child: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: teamController.teamsStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
List<String> teamNames = ['Todas'];
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
teamNames.addAll(snapshot.data!.map((t) => t['name'].toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teamNames.contains(tempTeam)) tempTeam = 'Todas';
|
||||||
|
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: tempTeam,
|
||||||
|
style: TextStyle(fontSize: 14 * sf, color: Colors.black87, fontWeight: FontWeight.bold),
|
||||||
|
items: teamNames.map((String value) {
|
||||||
|
return DropdownMenuItem<String>(value: value, child: Text(value, overflow: TextOverflow.ellipsis));
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
setPopupState(() => tempTeam = newValue!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Limpar Filtros
|
||||||
|
setState(() {
|
||||||
|
selectedSeason = 'Todas';
|
||||||
|
selectedTeam = 'Todas';
|
||||||
|
});
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('LIMPAR', style: TextStyle(fontSize: 12 * sf, color: Colors.grey))
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFE74C3C),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10 * sf)),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// Aplicar Filtros (atualiza a página principal)
|
||||||
|
setState(() {
|
||||||
|
selectedSeason = tempSeason;
|
||||||
|
selectedTeam = tempTeam;
|
||||||
|
});
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('APLICAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * sf)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showCreateDialog(BuildContext context, double sf) {
|
void _showCreateDialog(BuildContext context, double sf) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => CreateGameDialogManual(
|
builder: (context) => CreateGameDialogManual(
|
||||||
teamController: teamController,
|
teamController: teamController,
|
||||||
gameController: gameController,
|
gameController: gameController,
|
||||||
sf: sf, // <-- Passamos a escala para o Pop-up
|
sf: sf,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,448 +1,473 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:playmaker/classe/home.config.dart';
|
import 'package:playmaker/classe/home.config.dart';
|
||||||
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
import 'package:playmaker/grafico%20de%20pizza/grafico.dart';
|
||||||
import 'package:playmaker/pages/gamePage.dart';
|
import 'package:playmaker/pages/gamePage.dart';
|
||||||
import 'package:playmaker/pages/teamPage.dart';
|
import 'package:playmaker/pages/teamPage.dart';
|
||||||
import 'package:playmaker/controllers/team_controller.dart';
|
import 'package:playmaker/controllers/team_controller.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'dart:math' as math;
|
import 'package:playmaker/pages/status_page.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
|
import 'package:playmaker/grafico%20de%20pizza/controllers/contollers_grafico.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HomeScreen> createState() => _HomeScreenState();
|
State<HomeScreen> createState() => _HomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
int _selectedIndex = 0;
|
||||||
|
final TeamController _teamController = TeamController();
|
||||||
|
String? _selectedTeamId;
|
||||||
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
|
||||||
|
int _teamWins = 0;
|
||||||
|
int _teamLosses = 0;
|
||||||
|
int _teamDraws = 0;
|
||||||
|
|
||||||
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
@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 StatusPage(),
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
|
||||||
|
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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
void _showTeamSelector(BuildContext context, double sf) {
|
||||||
int _selectedIndex = 0;
|
showModalBottomSheet(
|
||||||
final TeamController _teamController = TeamController();
|
context: context,
|
||||||
String? _selectedTeamId;
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))),
|
||||||
String _selectedTeamName = "Selecionar Equipa";
|
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.")));
|
||||||
|
|
||||||
int _teamWins = 0;
|
final teams = snapshot.data!;
|
||||||
int _teamLosses = 0;
|
return ListView.builder(
|
||||||
int _teamDraws = 0;
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final _supabase = Supabase.instance.client;
|
Widget _buildHomeContent(double sf, double wScreen) {
|
||||||
|
final double cardHeight = (wScreen / 2) * 1.0;
|
||||||
|
|
||||||
@override
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
Widget build(BuildContext context) {
|
stream: _selectedTeamId != null
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
? _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!)
|
||||||
final double hScreen = MediaQuery.of(context).size.height;
|
: const Stream.empty(),
|
||||||
final double sf = math.min(wScreen, hScreen) / 400;
|
builder: (context, snapshot) {
|
||||||
|
Map<String, dynamic> leaders = _calculateLeaders(snapshot.data ?? []);
|
||||||
|
|
||||||
final List<Widget> pages = [
|
return SingleChildScrollView(
|
||||||
_buildHomeContent(sf, wScreen),
|
child: Padding(
|
||||||
const GamePage(),
|
padding: EdgeInsets.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf),
|
||||||
const TeamsPage(),
|
child: Column(
|
||||||
const Center(child: Text('Tela de Status')),
|
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),
|
||||||
|
|
||||||
return Scaffold(
|
SizedBox(
|
||||||
backgroundColor: Colors.white,
|
height: cardHeight,
|
||||||
appBar: AppBar(
|
child: Row(
|
||||||
title: Text('PlayMaker', style: TextStyle(fontSize: 20 * sf)),
|
children: [
|
||||||
backgroundColor: HomeConfig.primaryColor,
|
Expanded(child: _buildStatCard(title: 'Mais Pontos', playerName: leaders['pts_name'], statValue: leaders['pts_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF1565C0), isHighlighted: true)),
|
||||||
foregroundColor: Colors.white,
|
SizedBox(width: 12 * sf),
|
||||||
leading: IconButton(
|
Expanded(child: _buildStatCard(title: 'Assistências', playerName: leaders['ast_name'], statValue: leaders['ast_val'].toString(), statLabel: 'TOTAL', color: const Color(0xFF2E7D32))),
|
||||||
icon: Icon(Icons.person, size: 24 * sf),
|
],
|
||||||
onPressed: () {},
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
sf: 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),
|
||||||
|
|
||||||
|
// 👇 HISTÓRICO LIGADO À BASE DE DADOS (COM AS COLUNAS CORRIGIDAS) 👇
|
||||||
|
// 👇 LIGAÇÃO CORRIGIDA: Agora usa a coluna 'nome' como pediste 👇
|
||||||
|
// 👇 HISTÓRICO DINÂMICO (Qualquer equipa) 👇
|
||||||
|
_selectedTeamName == "Selecionar Equipa"
|
||||||
|
? Container(
|
||||||
|
padding: EdgeInsets.all(20 * sf),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text("Seleciona uma equipa no topo.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)),
|
||||||
|
)
|
||||||
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
// Pede os jogos ordenados pela data (sem filtros rígidos aqui)
|
||||||
|
stream: _supabase.from('games').stream(primaryKey: ['id'])
|
||||||
|
.order('game_date', ascending: false),
|
||||||
|
builder: (context, gameSnapshot) {
|
||||||
|
|
||||||
|
if (gameSnapshot.hasError) {
|
||||||
|
return Text("Erro ao carregar jogos: ${gameSnapshot.error}", style: const TextStyle(color: Colors.red));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameSnapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 O CÉREBRO DA APP: Filtro inteligente no Flutter 👇
|
||||||
|
final todosOsJogos = gameSnapshot.data ?? [];
|
||||||
|
|
||||||
|
final gamesList = todosOsJogos.where((game) {
|
||||||
|
String myT = game['my_team']?.toString() ?? '';
|
||||||
|
String oppT = game['opponent_team']?.toString() ?? '';
|
||||||
|
String status = game['status']?.toString() ?? '';
|
||||||
|
|
||||||
|
// O jogo tem de envolver a equipa selecionada E estar Terminado
|
||||||
|
bool isPlaying = (myT == _selectedTeamName || oppT == _selectedTeamName);
|
||||||
|
bool isFinished = status == 'Terminado';
|
||||||
|
|
||||||
|
return isPlaying && isFinished;
|
||||||
|
}).take(3).toList(); // Pega apenas nos 3 mais recentes
|
||||||
|
|
||||||
|
if (gamesList.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(20 * sf),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(14)),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text("Ainda não há jogos terminados para $_selectedTeamName.", style: TextStyle(color: Colors.grey)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: gamesList.map((game) {
|
||||||
|
|
||||||
|
// Lê os dados brutos da base de dados
|
||||||
|
String dbMyTeam = game['my_team']?.toString() ?? '';
|
||||||
|
String dbOppTeam = game['opponent_team']?.toString() ?? '';
|
||||||
|
int dbMyScore = int.tryParse(game['my_score'].toString()) ?? 0;
|
||||||
|
int dbOppScore = int.tryParse(game['opponent_score'].toString()) ?? 0;
|
||||||
|
|
||||||
|
String opponent;
|
||||||
|
int myScore;
|
||||||
|
int oppScore;
|
||||||
|
|
||||||
|
// 🔄 MAGIA DA INVERSÃO DE RESULTADOS 🔄
|
||||||
|
// Garante que os pontos da equipa selecionada aparecem sempre do lado esquerdo
|
||||||
|
if (dbMyTeam == _selectedTeamName) {
|
||||||
|
// A equipa que escolhemos está guardada no 'my_team'
|
||||||
|
opponent = dbOppTeam;
|
||||||
|
myScore = dbMyScore;
|
||||||
|
oppScore = dbOppScore;
|
||||||
|
} else {
|
||||||
|
// A equipa que escolhemos está guardada no 'opponent_team'
|
||||||
|
opponent = dbMyTeam;
|
||||||
|
myScore = dbOppScore;
|
||||||
|
oppScore = dbMyScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpa a data (Remove as horas e deixa só YYYY-MM-DD)
|
||||||
|
String rawDate = game['game_date']?.toString() ?? '---';
|
||||||
|
String date = rawDate.length >= 10 ? rawDate.substring(0, 10) : rawDate;
|
||||||
|
|
||||||
|
// Calcula Vitória, Empate ou Derrota para a equipa selecionada
|
||||||
|
String result = 'E';
|
||||||
|
if (myScore > oppScore) result = 'V';
|
||||||
|
if (myScore < oppScore) result = 'D';
|
||||||
|
|
||||||
|
return _buildGameHistoryCard(
|
||||||
|
opponent: opponent,
|
||||||
|
result: result,
|
||||||
|
myScore: myScore,
|
||||||
|
oppScore: oppScore,
|
||||||
|
date: date,
|
||||||
|
sf: sf,
|
||||||
|
topPts: game['top_pts_name'] ?? '---',
|
||||||
|
topAst: game['top_ast_name'] ?? '---',
|
||||||
|
topRbs: game['top_rbs_name'] ?? '---',
|
||||||
|
topDef: game['top_def_name'] ?? '---',
|
||||||
|
mvp: game['mvp_name'] ?? '---',
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 20 * sf),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
body: IndexedStack(
|
);
|
||||||
index: _selectedIndex,
|
}
|
||||||
children: pages,
|
|
||||||
),
|
|
||||||
|
|
||||||
bottomNavigationBar: NavigationBar(
|
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
||||||
selectedIndex: _selectedIndex,
|
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
|
||||||
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
for (var row in data) {
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
String pid = row['member_id'].toString();
|
||||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
||||||
elevation: 1,
|
ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0);
|
||||||
height: 70 * math.min(sf, 1.2),
|
astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0);
|
||||||
destinations: const [
|
rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0);
|
||||||
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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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)};
|
||||||
|
}
|
||||||
|
|
||||||
void _showTeamSelector(BuildContext context, double sf) {
|
Widget _buildStatCard({required String title, required String playerName, required String statValue, required String statLabel, required Color color, bool isHighlighted = false}) {
|
||||||
showModalBottomSheet(
|
return Card(
|
||||||
context: context,
|
elevation: 4, margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20 * sf))),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14), side: isHighlighted ? const BorderSide(color: Colors.amber, width: 2) : BorderSide.none),
|
||||||
builder: (context) {
|
child: Container(
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.9), color])),
|
||||||
stream: _teamController.teamsStream,
|
child: LayoutBuilder(
|
||||||
builder: (context, snapshot) {
|
builder: (context, constraints) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
|
final double ch = constraints.maxHeight;
|
||||||
if (!snapshot.hasData || snapshot.data!.isEmpty) return SizedBox(height: 200 * sf, child: const Center(child: Text("Nenhuma equipa criada.")));
|
final double cw = constraints.maxWidth;
|
||||||
|
|
||||||
final teams = snapshot.data!;
|
return Padding(
|
||||||
return ListView.builder(
|
padding: EdgeInsets.all(cw * 0.06),
|
||||||
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 cardHeight = (wScreen / 2) * 1.0;
|
|
||||||
|
|
||||||
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.symmetric(horizontal: 22.0 * sf, vertical: 16.0 * sf),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
onTap: () => _showTeamSelector(context, sf),
|
SizedBox(height: ch * 0.011),
|
||||||
child: Container(
|
SizedBox(
|
||||||
padding: EdgeInsets.all(12 * sf),
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(15 * sf), border: Border.all(color: Colors.grey.shade300)),
|
child: FittedBox(
|
||||||
child: Row(
|
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,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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))]),
|
Expanded(child: Text(_selectedTeamName == "Selecionar Equipa" ? "Minha Equipa" : _selectedTeamName, style: TextStyle(fontSize: 14 * sf, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
const Icon(Icons.arrow_drop_down),
|
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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 20 * sf),
|
),
|
||||||
|
],
|
||||||
SizedBox(
|
|
||||||
height: cardHeight,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
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: 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),
|
|
||||||
sf: 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
Divider(height: 1, color: Colors.grey.shade100, thickness: 1.5),
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _calculateLeaders(List<Map<String, dynamic>> data) {
|
Container(
|
||||||
Map<String, int> ptsMap = {}; Map<String, int> astMap = {}; Map<String, int> rbsMap = {}; Map<String, String> namesMap = {};
|
width: double.infinity,
|
||||||
for (var row in data) {
|
padding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 12 * sf),
|
||||||
String pid = row['member_id'].toString();
|
decoration: BoxDecoration(
|
||||||
namesMap[pid] = row['player_name']?.toString() ?? "Desconhecido";
|
color: Colors.grey.shade50,
|
||||||
ptsMap[pid] = (ptsMap[pid] ?? 0) + (row['pts'] as int? ?? 0);
|
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)),
|
||||||
astMap[pid] = (astMap[pid] ?? 0) + (row['ast'] as int? ?? 0);
|
),
|
||||||
rbsMap[pid] = (rbsMap[pid] ?? 0) + (row['rbs'] as int? ?? 0);
|
child: Column(
|
||||||
}
|
children: [
|
||||||
if (ptsMap.isEmpty) return {'pts_name': '---', 'pts_val': 0, 'ast_name': '---', 'ast_val': 0, 'rbs_name': '---', 'rbs_val': 0};
|
Row(
|
||||||
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, 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(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: [
|
children: [
|
||||||
Text(title.toUpperCase(), style: TextStyle(fontSize: ch * 0.06, fontWeight: FontWeight.bold, color: Colors.white70), maxLines: 1, overflow: TextOverflow.ellipsis),
|
Expanded(child: _buildGridStatRow(Icons.workspace_premium, Colors.amber.shade700, "MVP", mvp, sf, isMvp: true)),
|
||||||
SizedBox(height: ch * 0.011),
|
Expanded(child: _buildGridStatRow(Icons.shield, Colors.deepOrange.shade700, "Defesa", topDef, sf)),
|
||||||
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)))
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
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)),
|
||||||
}
|
],
|
||||||
|
),
|
||||||
Widget _buildGameHistoryCard({
|
SizedBox(height: 8 * sf),
|
||||||
required String opponent, required String result, required int myScore, required int oppScore, required String date, required double sf,
|
Row(
|
||||||
required String topPts, required String topAst, required String topRbs, required String topDef, required String mvp
|
children: [
|
||||||
}) {
|
Expanded(child: _buildGridStatRow(Icons.star, Colors.green.shade700, "Assists", topAst, sf)),
|
||||||
bool isWin = result == 'V';
|
const Expanded(child: SizedBox()),
|
||||||
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
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
282
lib/pages/status_page.dart
Normal file
282
lib/pages/status_page.dart
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import '../controllers/team_controller.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class StatusPage extends StatefulWidget {
|
||||||
|
const StatusPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatusPage> createState() => _StatusPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusPageState extends State<StatusPage> {
|
||||||
|
final TeamController _teamController = TeamController();
|
||||||
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
String? _selectedTeamId;
|
||||||
|
String _selectedTeamName = "Selecionar Equipa";
|
||||||
|
String _sortColumn = 'pts';
|
||||||
|
bool _isAscending = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double sf = math.min(MediaQuery.of(context).size.width, MediaQuery.of(context).size.height) / 400;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// --- SELETOR DE EQUIPA ---
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(16.0 * sf),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showTeamSelector(context, sf),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(12 * sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15 * sf),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)]
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
Icon(Icons.shield, color: const Color(0xFFE74C3C), size: 24 * sf),
|
||||||
|
SizedBox(width: 10 * sf),
|
||||||
|
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * sf, fontWeight: FontWeight.bold))
|
||||||
|
]),
|
||||||
|
const Icon(Icons.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- TABELA DE ESTATÍSTICAS (AGORA EM TEMPO REAL) ---
|
||||||
|
Expanded(
|
||||||
|
child: _selectedTeamId == null
|
||||||
|
? Center(child: Text("Seleciona uma equipa acima.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)))
|
||||||
|
|
||||||
|
// 👇 STREAM 1: LÊ AS ESTATÍSTICAS 👇
|
||||||
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _supabase.from('player_stats_with_names').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
||||||
|
builder: (context, statsSnapshot) {
|
||||||
|
|
||||||
|
// 👇 STREAM 2: LÊ OS JOGOS (Para os MVPs e contagem de jogos da equipa) 👇
|
||||||
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _supabase.from('games').stream(primaryKey: ['id']).eq('my_team', _selectedTeamName),
|
||||||
|
builder: (context, gamesSnapshot) {
|
||||||
|
|
||||||
|
// 👇 STREAM 3: LÊ TODOS OS MEMBROS DO PLANTEL 👇
|
||||||
|
// 👇 A CORREÇÃO ESTÁ AQUI: Remover o .eq('type', 'Jogador')
|
||||||
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _supabase.from('members').stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
||||||
|
builder: (context, membersSnapshot) {
|
||||||
|
|
||||||
|
// Verifica se ALGUM dos 3 streams ainda está a carregar
|
||||||
|
if (statsSnapshot.connectionState == ConnectionState.waiting ||
|
||||||
|
gamesSnapshot.connectionState == ConnectionState.waiting ||
|
||||||
|
membersSnapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator(color: Color(0xFFE74C3C)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final membersData = membersSnapshot.data ?? [];
|
||||||
|
|
||||||
|
if (membersData.isEmpty) {
|
||||||
|
return Center(child: Text("Esta equipa não tem jogadores registados.", style: TextStyle(color: Colors.grey, fontSize: 14 * sf)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final statsData = statsSnapshot.data ?? [];
|
||||||
|
final gamesData = gamesSnapshot.data ?? [];
|
||||||
|
|
||||||
|
// Conta o total de jogos terminados da equipa
|
||||||
|
final totalGamesPlayedByTeam = gamesData.where((g) => g['status'] == 'Terminado').length;
|
||||||
|
|
||||||
|
// Agrega os dados
|
||||||
|
final List<Map<String, dynamic>> playerTotals = _aggregateStats(statsData, gamesData, membersData);
|
||||||
|
|
||||||
|
// Calcula os Totais da Equipa
|
||||||
|
final teamTotals = _calculateTeamTotals(playerTotals, totalGamesPlayedByTeam);
|
||||||
|
|
||||||
|
// Ordenação
|
||||||
|
playerTotals.sort((a, b) {
|
||||||
|
var valA = a[_sortColumn] ?? 0;
|
||||||
|
var valB = b[_sortColumn] ?? 0;
|
||||||
|
return _isAscending ? valA.compareTo(valB) : valB.compareTo(valA);
|
||||||
|
});
|
||||||
|
|
||||||
|
return _buildStatsGrid(playerTotals, teamTotals, sf);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CÉREBRO CORRIGIDO ---
|
||||||
|
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
|
||||||
|
Map<String, Map<String, dynamic>> aggregated = {};
|
||||||
|
|
||||||
|
// 1. Mete a malta toda do plantel com ZERO JOGOS e ZERO STATS
|
||||||
|
for (var member in members) {
|
||||||
|
String name = member['name']?.toString() ?? "Desconhecido";
|
||||||
|
aggregated[name] = {
|
||||||
|
'name': name,
|
||||||
|
'j': 0,
|
||||||
|
'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Se o jogador tiver linha nas estatísticas, soma +1 Jogo e os pontos dele
|
||||||
|
for (var row in stats) {
|
||||||
|
String name = row['player_name']?.toString() ?? "Desconhecido";
|
||||||
|
|
||||||
|
if (!aggregated.containsKey(name)) {
|
||||||
|
aggregated[name] = {
|
||||||
|
'name': name, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregated[name]!['j'] += 1;
|
||||||
|
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
||||||
|
aggregated[name]!['ast'] += (row['ast'] ?? 0);
|
||||||
|
aggregated[name]!['rbs'] += (row['rbs'] ?? 0);
|
||||||
|
aggregated[name]!['stl'] += (row['stl'] ?? 0);
|
||||||
|
aggregated[name]!['blk'] += (row['blk'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Conta os troféus
|
||||||
|
for (var game in games) {
|
||||||
|
String? mvp = game['mvp_name'];
|
||||||
|
String? defRaw = game['top_def_name'];
|
||||||
|
|
||||||
|
if (mvp != null && aggregated.containsKey(mvp)) {
|
||||||
|
aggregated[mvp]!['mvp'] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defRaw != null) {
|
||||||
|
String defName = defRaw.split(' (')[0].trim();
|
||||||
|
if (aggregated.containsKey(defName)) {
|
||||||
|
aggregated[defName]!['def'] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggregated.values.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _calculateTeamTotals(List<Map<String, dynamic>> players, int teamGames) {
|
||||||
|
int tPts = 0, tAst = 0, tRbs = 0, tStl = 0, tBlk = 0, tMvp = 0, tDef = 0;
|
||||||
|
for (var p in players) {
|
||||||
|
tPts += (p['pts'] as int); tAst += (p['ast'] as int); tRbs += (p['rbs'] as int);
|
||||||
|
tStl += (p['stl'] as int); tBlk += (p['blk'] as int); tMvp += (p['mvp'] as int);
|
||||||
|
tDef += (p['def'] as int);
|
||||||
|
}
|
||||||
|
return {'name': 'TOTAL EQUIPA', 'j': teamGames, 'pts': tPts, 'ast': tAst, 'rbs': tRbs, 'stl': tStl, 'blk': tBlk, 'mvp': tMvp, 'def': tDef};
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsGrid(List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, double sf) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.white,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: DataTable(
|
||||||
|
columnSpacing: 25 * sf,
|
||||||
|
headingRowColor: MaterialStateProperty.all(Colors.grey.shade100),
|
||||||
|
dataRowHeight: 60 * sf,
|
||||||
|
columns: [
|
||||||
|
DataColumn(label: const Text('JOGADOR')),
|
||||||
|
_buildSortableColumn('J', 'j', sf),
|
||||||
|
_buildSortableColumn('PTS', 'pts', sf),
|
||||||
|
_buildSortableColumn('AST', 'ast', sf),
|
||||||
|
_buildSortableColumn('RBS', 'rbs', sf),
|
||||||
|
_buildSortableColumn('STL', 'stl', sf),
|
||||||
|
_buildSortableColumn('BLK', 'blk', sf),
|
||||||
|
_buildSortableColumn('DEF 🛡️', 'def', sf),
|
||||||
|
_buildSortableColumn('MVP 🏆', 'mvp', sf),
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
...players.map((player) => DataRow(cells: [
|
||||||
|
DataCell(Row(children: [
|
||||||
|
CircleAvatar(radius: 15 * sf, backgroundColor: Colors.grey.shade200, child: Icon(Icons.person, size: 18 * sf)),
|
||||||
|
SizedBox(width: 10 * sf),
|
||||||
|
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * sf)),
|
||||||
|
])),
|
||||||
|
DataCell(Center(child: Text(player['j'].toString()))),
|
||||||
|
_buildStatCell(player['pts'], sf, isHighlight: true),
|
||||||
|
_buildStatCell(player['ast'], sf),
|
||||||
|
_buildStatCell(player['rbs'], sf),
|
||||||
|
_buildStatCell(player['stl'], sf),
|
||||||
|
_buildStatCell(player['blk'], sf),
|
||||||
|
_buildStatCell(player['def'], sf, isBlue: true),
|
||||||
|
_buildStatCell(player['mvp'], sf, isGold: true),
|
||||||
|
])),
|
||||||
|
|
||||||
|
DataRow(
|
||||||
|
color: MaterialStateProperty.all(Colors.grey.shade50),
|
||||||
|
cells: [
|
||||||
|
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.black, fontSize: 12 * sf))),
|
||||||
|
DataCell(Center(child: Text(teamTotals['j'].toString(), style: const TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
_buildStatCell(teamTotals['pts'], sf, isHighlight: true),
|
||||||
|
_buildStatCell(teamTotals['ast'], sf),
|
||||||
|
_buildStatCell(teamTotals['rbs'], sf),
|
||||||
|
_buildStatCell(teamTotals['stl'], sf),
|
||||||
|
_buildStatCell(teamTotals['blk'], sf),
|
||||||
|
_buildStatCell(teamTotals['def'], sf, isBlue: true),
|
||||||
|
_buildStatCell(teamTotals['mvp'], sf, isGold: true),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataColumn _buildSortableColumn(String title, String sortKey, double sf) {
|
||||||
|
return DataColumn(label: InkWell(
|
||||||
|
onTap: () => setState(() {
|
||||||
|
if (_sortColumn == sortKey) _isAscending = !_isAscending;
|
||||||
|
else { _sortColumn = sortKey; _isAscending = false; }
|
||||||
|
}),
|
||||||
|
child: Row(children: [
|
||||||
|
Text(title, style: TextStyle(fontSize: 12 * sf, fontWeight: FontWeight.bold)),
|
||||||
|
if (_sortColumn == sortKey) Icon(_isAscending ? Icons.arrow_drop_up : Icons.arrow_drop_down, size: 18 * sf, color: const Color(0xFFE74C3C)),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
DataCell _buildStatCell(int value, double sf, {bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
|
||||||
|
return DataCell(Center(child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8 * sf, vertical: 4 * sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isGold && value > 0 ? Colors.amber.withOpacity(0.2) : (isBlue && value > 0 ? Colors.blue.withOpacity(0.1) : Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(value == 0 ? "-" : value.toString(), style: TextStyle(
|
||||||
|
fontWeight: (isHighlight || isGold || isBlue) ? FontWeight.w900 : FontWeight.w600,
|
||||||
|
fontSize: 14 * sf,
|
||||||
|
color: isGold && value > 0 ? Colors.orange.shade900 : (isBlue && value > 0 ? Colors.blue.shade800 : (isHighlight ? Colors.green.shade700 : Colors.black87))
|
||||||
|
)),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTeamSelector(BuildContext context, double sf) {
|
||||||
|
showModalBottomSheet(context: context, builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _teamController.teamsStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final teams = snapshot.data ?? [];
|
||||||
|
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
|
||||||
|
title: Text(teams[i]['name']),
|
||||||
|
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
|
||||||
|
));
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:playmaker/screens/team_stats_page.dart';
|
import 'package:playmaker/screens/team_stats_page.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../widgets/team_widgets.dart';
|
|
||||||
import 'dart:math' as math; // <-- IMPORTANTE: Adicionar para o cálculo
|
import 'dart:math' as math; // <-- IMPORTANTE: Adicionar para o cálculo
|
||||||
|
|
||||||
class TeamsPage extends StatefulWidget {
|
class TeamsPage extends StatefulWidget {
|
||||||
@@ -139,7 +138,8 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
// 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇
|
// 👇 CÁLCULO DA ESCALA (sf) PARA SE ADAPTAR A QUALQUER ECRÃ 👇
|
||||||
final double wScreen = MediaQuery.of(context).size.width;
|
final double wScreen = MediaQuery.of(context).size.width;
|
||||||
final double hScreen = MediaQuery.of(context).size.height;
|
final double hScreen = MediaQuery.of(context).size.height;
|
||||||
final double sf = math.min(wScreen, hScreen) / 400;
|
final double sf = math.min(wScreen, hScreen) / 400;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: const Color(0xFFF5F7FA),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -256,4 +256,250 @@ final double sf = math.min(wScreen, hScreen) / 400;
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TEAM CARD ---
|
||||||
|
class TeamCard extends StatelessWidget {
|
||||||
|
final Team team;
|
||||||
|
final TeamController controller;
|
||||||
|
final VoidCallback onFavoriteTap;
|
||||||
|
final double sf; // <-- Variável de escala
|
||||||
|
|
||||||
|
const TeamCard({
|
||||||
|
super.key,
|
||||||
|
required this.team,
|
||||||
|
required this.controller,
|
||||||
|
required this.onFavoriteTap,
|
||||||
|
required this.sf,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: Colors.white,
|
||||||
|
elevation: 3,
|
||||||
|
margin: EdgeInsets.only(bottom: 12 * sf),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * sf)),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 16 * sf, vertical: 8 * sf),
|
||||||
|
|
||||||
|
// --- 1. IMAGEM + FAVORITO ---
|
||||||
|
leading: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28 * sf,
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
||||||
|
? NetworkImage(team.imageUrl)
|
||||||
|
: null,
|
||||||
|
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
||||||
|
? Text(
|
||||||
|
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
||||||
|
style: TextStyle(fontSize: 24 * sf),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: -15 * sf,
|
||||||
|
top: -10 * sf,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
team.isFavorite ? Icons.star : Icons.star_border,
|
||||||
|
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
|
||||||
|
size: 28 * sf,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: Colors.black.withOpacity(team.isFavorite ? 0.3 : 0.1),
|
||||||
|
blurRadius: 4 * sf,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: onFavoriteTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- 2. TÍTULO ---
|
||||||
|
title: Text(
|
||||||
|
team.name,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16 * sf),
|
||||||
|
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 6.0 * sf),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
||||||
|
SizedBox(width: 4 * sf),
|
||||||
|
|
||||||
|
// 👇 STREAMBUILDER EM VEZ DE FUTUREBUILDER 👇
|
||||||
|
StreamBuilder<int>(
|
||||||
|
stream: controller.getPlayerCountStream(team.id),
|
||||||
|
initialData: 0,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final count = snapshot.data ?? 0;
|
||||||
|
return Text(
|
||||||
|
"$count Jogs.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: count > 0 ? Colors.green[700] : Colors.orange,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13 * sf,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(width: 8 * sf),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"| ${team.season}",
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 13 * sf),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- 4. BOTÕES (Estatísticas e Apagar) ---
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Ver Estatísticas',
|
||||||
|
icon: Icon(Icons.bar_chart_rounded, color: Colors.blue, size: 24 * sf),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TeamStatsPage(team: team),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Eliminar Equipa',
|
||||||
|
icon: Icon(Icons.delete_outline, color: const Color(0xFFE74C3C), size: 24 * sf),
|
||||||
|
onPressed: () => _confirmDelete(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmDelete(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Eliminar Equipa?', style: TextStyle(fontSize: 18 * sf, fontWeight: FontWeight.bold)),
|
||||||
|
content: Text('Tens a certeza que queres eliminar "${team.name}"?', style: TextStyle(fontSize: 14 * sf)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text('Cancelar', style: TextStyle(fontSize: 14 * sf)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.deleteTeam(team.id);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('Eliminar', style: TextStyle(color: Colors.red, fontSize: 14 * sf)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DIALOG DE CRIAÇÃO ---
|
||||||
|
class CreateTeamDialog extends StatefulWidget {
|
||||||
|
final Function(String name, String season, String imageUrl) onConfirm;
|
||||||
|
final double sf; // Recebe a escala
|
||||||
|
|
||||||
|
const CreateTeamDialog({super.key, required this.onConfirm, required this.sf});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateTeamDialog> createState() => _CreateTeamDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _imageController = TextEditingController();
|
||||||
|
String _selectedSeason = '2024/25';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15 * widget.sf)),
|
||||||
|
title: Text('Nova Equipa', style: TextStyle(fontSize: 18 * widget.sf, fontWeight: FontWeight.bold)),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
style: TextStyle(fontSize: 14 * widget.sf),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Nome da Equipa',
|
||||||
|
labelStyle: TextStyle(fontSize: 14 * widget.sf)
|
||||||
|
),
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
|
),
|
||||||
|
SizedBox(height: 15 * widget.sf),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _selectedSeason,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Temporada',
|
||||||
|
labelStyle: TextStyle(fontSize: 14 * widget.sf)
|
||||||
|
),
|
||||||
|
style: TextStyle(fontSize: 14 * widget.sf, color: Colors.black87),
|
||||||
|
items: ['2023/24', '2024/25', '2025/26']
|
||||||
|
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (val) => setState(() => _selectedSeason = val!),
|
||||||
|
),
|
||||||
|
SizedBox(height: 15 * widget.sf),
|
||||||
|
TextField(
|
||||||
|
controller: _imageController,
|
||||||
|
style: TextStyle(fontSize: 14 * widget.sf),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'URL Imagem ou Emoji',
|
||||||
|
labelStyle: TextStyle(fontSize: 14 * widget.sf),
|
||||||
|
hintText: 'Ex: 🏀 ou https://...',
|
||||||
|
hintStyle: TextStyle(fontSize: 14 * widget.sf)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text('Cancelar', style: TextStyle(fontSize: 14 * widget.sf))
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFE74C3C),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16 * widget.sf, vertical: 10 * widget.sf)
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (_nameController.text.trim().isNotEmpty) {
|
||||||
|
widget.onConfirm(
|
||||||
|
_nameController.text.trim(),
|
||||||
|
_selectedSeason,
|
||||||
|
_imageController.text.trim(),
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Criar', style: TextStyle(color: Colors.white, fontSize: 14 * widget.sf)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
@@ -72,15 +72,17 @@ class TeamCard extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
|
overflow: TextOverflow.ellipsis, // Previne overflows em nomes longos
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- 3. SUBTÍTULO (Contagem + Época) ---
|
// --- 3. SUBTÍTULO (Contagem + Época em TEMPO REAL) ---
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: EdgeInsets.only(top: 6.0 * sf),
|
padding: EdgeInsets.only(top: 6.0 * sf),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
Icon(Icons.groups_outlined, size: 16 * sf, color: Colors.grey),
|
||||||
SizedBox(width: 4 * sf),
|
SizedBox(width: 4 * sf),
|
||||||
FutureBuilder<int>(
|
|
||||||
future: controller.getPlayerCount(team.id),
|
// 👇 A CORREÇÃO ESTÁ AQUI: StreamBuilder em vez de FutureBuilder 👇
|
||||||
|
StreamBuilder<int>(
|
||||||
|
stream: controller.getPlayerCountStream(team.id),
|
||||||
initialData: 0,
|
initialData: 0,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final count = snapshot.data ?? 0;
|
final count = snapshot.data ?? 0;
|
||||||
@@ -94,6 +96,7 @@ class TeamCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(width: 8 * sf),
|
SizedBox(width: 8 * sf),
|
||||||
Expanded( // Garante que a temporada se adapta se faltar espaço
|
Expanded( // Garante que a temporada se adapta se faltar espaço
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -107,7 +110,6 @@ class TeamCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// --- 4. BOTÕES (Estatísticas e Apagar) ---
|
// --- 4. BOTÕES (Estatísticas e Apagar) ---
|
||||||
// Removido o SizedBox fixo! Agora é MainAxisSize.min
|
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
|
mainAxisSize: MainAxisSize.min, // <-- ISTO RESOLVE O OVERFLOW DAS RISCAS AMARELAS
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
Reference in New Issue
Block a user