Conflitos resolvidos: mantido o código local

This commit is contained in:
2026-03-16 22:28:41 +00:00
20 changed files with 1244 additions and 1171 deletions

View File

@@ -4,34 +4,25 @@ import '../models/game_model.dart';
class GameController {
final _supabase = Supabase.instance.client;
// 1. LER JOGOS (Com Filtros Opcionais)
Stream<List<Game>> getFilteredGames({String? teamFilter, String? seasonFilter}) {
// 1. LER JOGOS (Stream em Tempo Real)
Stream<List<Game>> get gamesStream {
return _supabase
.from('games')
.from('games') // 1. Fica à escuta da tabela original (Garante o Tempo Real!)
.stream(primaryKey: ['id'])
.asyncMap((event) async {
// 👇 A CORREÇÃO ESTÁ AQUI: Lê diretamente da tabela 'games'
var query = _supabase.from('games').select();
// Aplica o filtro de Temporada
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);
// 2. Sempre que a tabela 'games' mudar (novo jogo, alteração de resultado),
// vamos buscar os dados já misturados com as imagens à nossa View.
final viewData = await _supabase
.from('games_with_logos')
.select()
.order('game_date', ascending: false);
// 3. Convertemos para a nossa lista de objetos Game
return viewData.map((json) => Game.fromMap(json)).toList();
});
}
// 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 {
try {
final response = await _supabase.from('games').insert({
@@ -40,16 +31,18 @@ class GameController {
'season': season,
'my_score': 0,
'opponent_score': 0,
'status': 'Decorrer',
'status': 'Decorrer', // Começa como "Decorrer"
'game_date': DateTime.now().toIso8601String(),
}).select().single();
}).select().single(); // .select().single() retorna o objeto criado
return response['id'];
return response['id']; // Retorna o UUID gerado pelo Supabase
} catch (e) {
print("Erro ao criar jogo: $e");
return null;
}
}
void dispose() {}
void dispose() {
// Não é necessário fechar streams do Supabase manualmente aqui
}
}

View File

@@ -0,0 +1,158 @@
/*import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/person_model.dart';
class StatsController {
final SupabaseClient _supabase = Supabase.instance.client;
// 1. LER
Stream<List<Person>> getMembers(String teamId) {
return _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.order('name', ascending: true)
.map((data) => data.map((json) => Person.fromMap(json)).toList());
}
// 2. APAGAR
Future<void> deletePerson(String personId) async {
try {
await _supabase.from('members').delete().eq('id', personId);
} catch (e) {
debugPrint("Erro ao eliminar: $e");
}
}
// 3. DIÁLOGOS
void showAddPersonDialog(BuildContext context, String teamId) {
_showForm(context, teamId: teamId);
}
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
_showForm(context, teamId: teamId, person: person);
}
// --- O POPUP ESTÁ AQUI ---
void _showForm(BuildContext context, {required String teamId, Person? person}) {
final isEdit = person != null;
final nameCtrl = TextEditingController(text: person?.name ?? '');
final numCtrl = TextEditingController(text: person?.number ?? '');
// Define o valor inicial
String selectedType = person?.type ?? 'Jogador';
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) => AlertDialog(
title: Text(isEdit ? "Editar" : "Adicionar"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// NOME
TextField(
controller: nameCtrl,
decoration: const InputDecoration(labelText: "Nome"),
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 10),
// FUNÇÃO
DropdownButtonFormField<String>(
value: selectedType,
decoration: const InputDecoration(labelText: "Função"),
items: ["Jogador", "Treinador"]
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (v) {
if (v != null) setState(() => selectedType = v);
},
),
// NÚMERO (Só aparece se for Jogador)
if (selectedType == "Jogador") ...[
const SizedBox(height: 10),
TextField(
controller: numCtrl,
decoration: const InputDecoration(labelText: "Número da Camisola"),
keyboardType: TextInputType.text, // Aceita texto para evitar erros
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("Cancelar")
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
onPressed: () async {
print("--- 1. CLICOU EM GUARDAR ---");
// Validação Simples
if (nameCtrl.text.trim().isEmpty) {
print("ERRO: Nome vazio");
return;
}
// Lógica do Número:
// Se for Treinador -> envia NULL
// Se for Jogador e estiver vazio -> envia NULL
// Se tiver texto -> envia o Texto
String? numeroFinal;
if (selectedType == "Treinador") {
numeroFinal = null;
} else {
numeroFinal = numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim();
}
print("--- 2. DADOS A ENVIAR ---");
print("Nome: ${nameCtrl.text}");
print("Tipo: $selectedType");
print("Número: $numeroFinal");
try {
if (isEdit) {
await _supabase.from('members').update({
'name': nameCtrl.text.trim(),
'type': selectedType,
'number': numeroFinal,
}).eq('id', person!.id);
} else {
await _supabase.from('members').insert({
'team_id': teamId, // Verifica se este teamId é válido!
'name': nameCtrl.text.trim(),
'type': selectedType,
'number': numeroFinal,
});
}
print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---");
if (ctx.mounted) Navigator.pop(ctx);
} catch (e) {
print("--- X. ERRO AO GUARDAR ---");
print(e.toString());
// MOSTRA O ERRO NO TELEMÓVEL
if (ctx.mounted) {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text("Erro: $e"),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
}
},
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
)
],
),
),
);
}
}*/

View File

@@ -1,24 +1,21 @@
import 'package:supabase_flutter/supabase_flutter.dart';
class TeamController {
// Instância do cliente Supabase
final _supabase = Supabase.instance.client;
// 1. Variável fixa para guardar o Stream principal
late final Stream<List<Map<String, dynamic>>> teamsStream;
// 2. Dicionário (Cache) para não recriar Streams de contagem repetidos
final Map<String, Stream<int>> _playerCountStreams = {};
TeamController() {
// INICIALIZAÇÃO: O stream é criado APENAS UMA VEZ quando abres a página!
teamsStream = _supabase
// 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 {
return _supabase
.from('teams')
.stream(primaryKey: ['id'])
.order('name', ascending: true)
.map((data) => List<Map<String, dynamic>>.from(data));
}
// 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 {
try {
await _supabase.from('teams').insert({
@@ -33,50 +30,42 @@ class TeamController {
}
}
// ELIMINAR
// 3. ELIMINAR
Future<void> deleteTeam(String id) async {
try {
await _supabase.from('teams').delete().eq('id', id);
// Limpa o cache deste teamId se a equipa for apagada
_playerCountStreams.remove(id);
} catch (e) {
print("❌ Erro ao eliminar: $e");
}
}
// FAVORITAR
// 4. FAVORITAR
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
try {
await _supabase
.from('teams')
.update({'is_favorite': !currentStatus})
.update({'is_favorite': !currentStatus}) // Inverte o valor
.eq('id', teamId);
} catch (e) {
print("❌ Erro ao favoritar: $e");
}
}
// CONTAR JOGADORES (AGORA COM CACHE DE MEMÓRIA!)
Stream<int> getPlayerCountStream(String teamId) {
// Se já criámos um "Tubo de ligação" para esta equipa, REUTILIZA-O!
if (_playerCountStreams.containsKey(teamId)) {
return _playerCountStreams[teamId]!;
// 5. CONTAR JOGADORES
// CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
Future<int> getPlayerCount(String teamId) async {
try {
final count = await _supabase
.from('members')
.count() // Retorna diretamente o número inteiro
.eq('team_id', teamId);
return count;
} catch (e) {
print("Erro ao contar jogadores: $e");
return 0;
}
// Se é a primeira vez que pede esta equipa, cria a ligação e guarda na memória
final newStream = _supabase
.from('members')
.stream(primaryKey: ['id'])
.eq('team_id', teamId)
.map((data) => data.length);
_playerCountStreams[teamId] = newStream; // Guarda no dicionário
return newStream;
}
// LIMPEZA FINAL QUANDO SAÍMOS DA PÁGINA
void dispose() {
// Limpamos o dicionário de streams para libertar memória RAM
_playerCountStreams.clear();
}
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
void dispose() {}
}