From 7232c5a493d4742f21b5aa3a01bde2f2d1386a54 Mon Sep 17 00:00:00 2001 From: 230404 <230404@epvc.pt> Date: Tue, 3 Mar 2026 17:18:26 +0000 Subject: [PATCH] agora as jogadrres vao para o supabase --- lib/controllers/stats_controller.dart | 4 +- lib/pages/teamPage.dart | 1 + lib/screens/team_stats_page.dart | 263 +++++++++++++------------- 3 files changed, 131 insertions(+), 137 deletions(-) diff --git a/lib/controllers/stats_controller.dart b/lib/controllers/stats_controller.dart index 634f114..aa3a955 100644 --- a/lib/controllers/stats_controller.dart +++ b/lib/controllers/stats_controller.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +/*import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/person_model.dart'; @@ -155,4 +155,4 @@ class StatsController { ), ); } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/lib/pages/teamPage.dart b/lib/pages/teamPage.dart index e1e4036..97ac52d 100644 --- a/lib/pages/teamPage.dart +++ b/lib/pages/teamPage.dart @@ -247,6 +247,7 @@ class _TeamsPageState extends State { context: context, builder: (context) => CreateTeamDialog( onConfirm: (name, season, imageUrl) => controller.createTeam(name, season, imageUrl), + ), ); } diff --git a/lib/screens/team_stats_page.dart b/lib/screens/team_stats_page.dart index c720111..f8f9f35 100644 --- a/lib/screens/team_stats_page.dart +++ b/lib/screens/team_stats_page.dart @@ -1,11 +1,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/team_model.dart'; import '../models/person_model.dart'; -// Se tiveres os widgets noutro ficheiro, importa-os. -// Caso contrário, podes colar as classes StatsHeader, etc. no fundo deste ficheiro. import '../widgets/team_widgets.dart'; -import '../widgets/stats_widgets.dart'; // Assumindo que criaste este ficheiro anteriormente +import '../widgets/stats_widgets.dart'; class TeamStatsPage extends StatefulWidget { final Team team; @@ -17,74 +16,77 @@ class TeamStatsPage extends StatefulWidget { } class _TeamStatsPageState extends State { - // Instancia o controlador local final StatsController _controller = StatsController(); - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F7FA), body: Column( children: [ - // Header (Widget que criámos antes) + // Cabeçalho com informações da equipa StatsHeader(team: widget.team), Expanded( child: StreamBuilder>( - // LÊ DA LISTA LOCAL + // O StreamBuilder reconstrói a UI automaticamente sempre que o Supabase envia novos dados stream: _controller.getMembers(widget.team.id), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } + if (snapshot.hasError) { + return Center(child: Text("Erro ao carregar: ${snapshot.error}")); + } + final members = snapshot.data ?? []; + + // Filtros para organizar a lista final coaches = members.where((m) => m.type == 'Treinador').toList(); final players = members.where((m) => m.type == 'Jogador').toList(); - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Resumo - StatsSummaryCard(total: members.length), - const SizedBox(height: 30), - - // Secção Treinadores - if (coaches.isNotEmpty) ...[ - const StatsSectionTitle(title: "Treinadores"), - ...coaches.map((c) => PersonCard( - person: c, - isCoach: true, - onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c), - onDelete: () => _confirmDelete(context, c), - )), + return RefreshIndicator( + onRefresh: () async => setState(() {}), // Pull to refresh como backup + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StatsSummaryCard(total: members.length), const SizedBox(height: 30), - ], - // Secção Jogadores - const StatsSectionTitle(title: "Jogadores"), - if (players.isEmpty) - const Padding( - padding: EdgeInsets.only(top: 20), - child: Text("Nenhum jogador adicionado.", style: TextStyle(color: Colors.grey)), - ) - else - ...players.map((p) => PersonCard( - person: p, - isCoach: false, - onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p), - onDelete: () => _confirmDelete(context, p), - )), - const SizedBox(height: 80), - ], + // SECÇÃO TREINADORES + if (coaches.isNotEmpty) ...[ + const StatsSectionTitle(title: "Treinadores"), + ...coaches.map((c) => PersonCard( + person: c, + isCoach: true, + onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c), + onDelete: () => _confirmDelete(context, c), + )), + const SizedBox(height: 30), + ], + + // SECÇÃO JOGADORES + const StatsSectionTitle(title: "Jogadores"), + if (players.isEmpty) + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("Nenhum jogador nesta equipa.", + style: TextStyle(color: Colors.grey, fontSize: 16)), + ) + else + ...players.map((p) => PersonCard( + person: p, + isCoach: false, + onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p), + onDelete: () => _confirmDelete(context, p), + )), + const SizedBox(height: 80), // Espaço para o FAB não tapar o último card + ], + ), ), ); }, @@ -93,7 +95,8 @@ class _TeamStatsPageState extends State { ], ), floatingActionButton: FloatingActionButton( - heroTag: 'player_fab', + // Hero tag única para evitar o erro de tags duplicadas + heroTag: 'fab_team_${widget.team.id}', onPressed: () => _controller.showAddPersonDialog(context, widget.team.id), backgroundColor: const Color(0xFF00C853), child: const Icon(Icons.add, color: Colors.white), @@ -104,15 +107,15 @@ class _TeamStatsPageState extends State { void _confirmDelete(BuildContext context, Person person) { showDialog( context: context, - builder: (context) => AlertDialog( - title: const Text("Eliminar?"), - content: Text("Remover ${person.name}?"), + builder: (ctx) => AlertDialog( + title: const Text("Eliminar Membro?"), + content: Text("Tens a certeza que queres remover ${person.name}?"), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancelar")), + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Cancelar")), TextButton( - onPressed: () { - _controller.deletePerson(widget.team.id, person.id); - Navigator.pop(context); + onPressed: () async { + await _controller.deletePerson(person.id); + if (ctx.mounted) Navigator.pop(ctx); }, child: const Text("Eliminar", style: TextStyle(color: Colors.red)), ), @@ -122,42 +125,28 @@ class _TeamStatsPageState extends State { } } -// --- CONTROLLER LOCAL (SEM SUPABASE) --- +// --- CONTROLLER SUPABASE --- class StatsController { - // Base de dados simulada na memória (Estática para não perder dados ao mudar de ecrã) - static final List> _mockMembers = [ - // Podes deixar vazio ou meter dados de teste - // {'id': '1', 'team_id': 'exemplo', 'name': 'Mourinho', 'type': 'Treinador', 'number': null}, - ]; + final _supabase = Supabase.instance.client; - // Stream para atualizar a UI automaticamente - final StreamController> _streamController = StreamController>.broadcast(); - - // 1. LER (Filtra a lista local) + // 1. LER (A escuta em tempo real) Stream> getMembers(String teamId) { - // Pequeno delay para garantir que o Stream ouve a primeira emissão - Future.delayed(Duration.zero, () => _emitMembers(teamId)); - return _streamController.stream; + return _supabase + .from('members') + .stream(primaryKey: ['id']) // Garante que a PK na tabela é 'id' + .eq('team_id', teamId) + .order('name', ascending: true) + .map((data) => data.map((json) => Person.fromMap(json)).toList()); } - // Função auxiliar para atualizar quem está a ouvir - void _emitMembers(String teamId) { - final filtered = _mockMembers - .where((m) => m['team_id'] == teamId) - .map((m) => Person.fromMap(m)) - .toList(); - - // Ordenar por nome - filtered.sort((a, b) => a.name.compareTo(b.name)); - - _streamController.add(filtered); - } - - // 2. APAGAR LOCALMENTE - void deletePerson(String teamId, String personId) { - _mockMembers.removeWhere((m) => m['id'] == personId); - _emitMembers(teamId); // Atualiza o ecrã + // 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 @@ -179,33 +168,38 @@ class StatsController { context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setState) => AlertDialog( - title: Text(isEdit ? "Editar" : "Adicionar"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameCtrl, - decoration: const InputDecoration(labelText: "Nome"), - textCapitalization: TextCapitalization.sentences, - ), - const SizedBox(height: 10), - 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); - }, - ), - if (selectedType == "Jogador") ...[ - const SizedBox(height: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + title: Text(isEdit ? "Editar Membro" : "Novo Membro"), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ TextField( - controller: numCtrl, - decoration: const InputDecoration(labelText: "Número da Camisola"), - keyboardType: TextInputType.text, + controller: nameCtrl, + decoration: const InputDecoration(labelText: "Nome Completo"), + textCapitalization: TextCapitalization.words, ), - ] - ], + const SizedBox(height: 15), + 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); + }, + ), + if (selectedType == "Jogador") ...[ + const SizedBox(height: 15), + TextField( + controller: numCtrl, + decoration: const InputDecoration(labelText: "Número da Camisola"), + keyboardType: TextInputType.number, + ), + ] + ], + ), ), actions: [ TextButton( @@ -213,40 +207,41 @@ class StatsController { child: const Text("Cancelar") ), ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)), - onPressed: () { + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF00C853), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)) + ), + onPressed: () async { if (nameCtrl.text.trim().isEmpty) return; String? numeroFinal = (selectedType == "Treinador") ? null : (numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim()); - if (isEdit) { - // ATUALIZAR NA LISTA LOCAL - final index = _mockMembers.indexWhere((m) => m['id'] == person!.id); - if (index != -1) { - _mockMembers[index] = { - 'id': person!.id, + 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, 'name': nameCtrl.text.trim(), 'type': selectedType, 'number': numeroFinal, - }; + }); + } + if (ctx.mounted) Navigator.pop(ctx); + } catch (e) { + debugPrint("Erro Supabase: $e"); + if (ctx.mounted) { + ScaffoldMessenger.of(ctx).showSnackBar( + SnackBar(content: Text("Erro ao guardar: $e"), backgroundColor: Colors.red) + ); } - } else { - // CRIAR NA LISTA LOCAL (Gera ID com a data atual) - _mockMembers.add({ - 'id': DateTime.now().millisecondsSinceEpoch.toString(), - 'team_id': teamId, - 'name': nameCtrl.text.trim(), - 'type': selectedType, - 'number': numeroFinal, - }); } - - // Atualiza a UI - _emitMembers(teamId); - Navigator.pop(ctx); }, child: const Text("Guardar", style: TextStyle(color: Colors.white)), ) @@ -256,7 +251,5 @@ class StatsController { ); } - void dispose() { - _streamController.close(); - } + void dispose() {} } \ No newline at end of file