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