nao repetir o numero e mais algo tou com sono

This commit is contained in:
Diogo
2026-03-04 00:46:21 +00:00
parent 7232c5a493
commit af765fc5ab
3 changed files with 195 additions and 32 deletions

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -424,8 +423,9 @@ class _PlacarPageState extends State<PlacarPage> {
}
List<Widget> _buildBenchPlayers(List<String> bench, bool isOpponent) {
final teamColor = isOpponent ? const Color(0xFF8B1A1A) : const Color(0xFF1E5BB2);
final prefix = isOpponent ? "sub_opp_" : "sub_my_";
final teamColor = isOpponent ? const Color(0xFFD92C2C) : const Color(0xFF1E5BB2);
// CORREÇÃO: Utilização do prefixo 'bench_' em vez de 'sub_'
final prefix = isOpponent ? "bench_opp_" : "bench_my_";
return bench.map((playerName) {
final num = _playerNumbers[playerName]!;
@@ -489,12 +489,13 @@ class _PlacarPageState extends State<PlacarPage> {
final action = details.data;
if (action.startsWith("add_") || action.startsWith("sub_") || action.startsWith("miss_")) {
_handleActionDrag(action, "$prefix$name"); // CHAMA A NOVA LÓGICA DE INTERCEÇÃO
_handleActionDrag(action, "$prefix$name");
}
else {
// CORREÇÃO: Nova lógica que processa apenas ações que comecem por 'bench_' para substituições
else if (action.startsWith("bench_")) {
setState(() {
if (action.startsWith("sub_my_") && !isOpponent) {
String benchPlayer = action.replaceAll("sub_my_", "");
if (action.startsWith("bench_my_") && !isOpponent) {
String benchPlayer = action.replaceAll("bench_my_", "");
if (_playerStats[benchPlayer]!["fls"]! >= 5) return;
int courtIndex = _myCourt.indexOf(name);
@@ -504,8 +505,8 @@ class _PlacarPageState extends State<PlacarPage> {
_showMyBench = false;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sai $name, Entra $benchPlayer')));
}
if (action.startsWith("sub_opp_") && isOpponent) {
String benchPlayer = action.replaceAll("sub_opp_", "");
if (action.startsWith("bench_opp_") && isOpponent) {
String benchPlayer = action.replaceAll("bench_opp_", "");
if (_playerStats[benchPlayer]!["fls"]! >= 5) return;
int courtIndex = _oppCourt.indexOf(name);
@@ -519,7 +520,8 @@ class _PlacarPageState extends State<PlacarPage> {
}
},
builder: (context, candidateData, rejectedData) {
bool isSubbing = candidateData.any((data) => data != null && data.startsWith("sub_my_") || data != null && data.startsWith("sub_opp_"));
// CORREÇÃO: Atualização da verificação de hover com base no novo prefixo
bool isSubbing = candidateData.any((data) => data != null && (data.startsWith("bench_my_") || data.startsWith("bench_opp_")));
bool isActionHover = candidateData.any((data) => data != null && (data.startsWith("add_") || data.startsWith("sub_") || data.startsWith("miss_")));
return _playerCardUI(number, name, stats, teamColor, isSubbing, isActionHover);
},
@@ -870,4 +872,4 @@ Widget _circle(String label, Color color, IconData? icon, bool isFeed, {double f
],
);
}
}
}

View File

@@ -3,8 +3,171 @@ import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/team_model.dart';
import '../models/person_model.dart';
import '../widgets/team_widgets.dart';
import '../widgets/stats_widgets.dart';
// ==========================================
// 1. WIDGETS
// ==========================================
// --- CABEÇALHO ---
class StatsHeader extends StatelessWidget {
final Team team;
const StatsHeader({super.key, required this.team});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20),
decoration: const BoxDecoration(
color: Color(0xFF2C3E50),
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)),
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 10),
// IMAGEM OU EMOJI DA EQUIPA AQUI!
CircleAvatar(
radius: 24,
backgroundColor: Colors.white24,
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: const TextStyle(fontSize: 20),
)
: null,
),
const SizedBox(width: 15),
Expanded( // Expanded evita overflow se o nome for muito longo
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis),
Text(team.season, style: const TextStyle(color: Colors.white70, fontSize: 14)),
],
),
),
],
),
);
}
}
// --- CARD DE RESUMO ---
class StatsSummaryCard extends StatelessWidget {
final int total;
const StatsSummaryCard({super.key, required this.total});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(colors: [Colors.blue.shade700, Colors.blue.shade400]),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Total de Membros", style: TextStyle(color: Colors.white, fontSize: 16)),
Text("$total", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
],
),
),
);
}
}
// --- TÍTULO DE SECÇÃO ---
class StatsSectionTitle extends StatelessWidget {
final String title;
const StatsSectionTitle({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
const Divider(),
],
);
}
}
// --- CARD DA PESSOA (JOGADOR/TREINADOR) ---
class PersonCard extends StatelessWidget {
final Person person;
final bool isCoach;
final VoidCallback onEdit;
final VoidCallback onDelete;
const PersonCard({
super.key,
required this.person,
required this.isCoach,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(top: 12),
elevation: 2,
color: isCoach ? const Color(0xFFFFF9C4) : Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: ListTile(
leading: isCoach
? const CircleAvatar(backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white))
: Container(
width: 45,
height: 45,
alignment: Alignment.center,
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
child: Text(person.number ?? "J", style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)),
),
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)),
// --- CANTO DIREITO (Trailing) ---
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// IMAGEM DA EQUIPA NO CARD DO JOGADOR
const SizedBox(width: 5), // Espaço
IconButton(
icon: const Icon(Icons.edit_outlined, color: Colors.blue),
onPressed: onEdit,
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: onDelete,
),
],
),
),
);
}
}
// ==========================================
// 2. PÁGINA PRINCIPAL
// ==========================================
class TeamStatsPage extends StatefulWidget {
final Team team;
@@ -24,12 +187,11 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
backgroundColor: const Color(0xFFF5F7FA),
body: Column(
children: [
// Cabeçalho com informações da equipa
// Cabeçalho
StatsHeader(team: widget.team),
Expanded(
child: StreamBuilder<List<Person>>(
// 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) {
@@ -42,12 +204,11 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
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 RefreshIndicator(
onRefresh: () async => setState(() {}), // Pull to refresh como backup
onRefresh: () async => setState(() {}),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
@@ -57,25 +218,25 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
StatsSummaryCard(total: members.length),
const SizedBox(height: 30),
// SECÇÃO TREINADORES
// 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
// 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)),
child: Text("Nenhum jogador nesta equipa.", style: TextStyle(color: Colors.grey, fontSize: 16)),
)
else
...players.map((p) => PersonCard(
@@ -84,7 +245,7 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
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
const SizedBox(height: 80),
],
),
),
@@ -95,7 +256,6 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
],
),
floatingActionButton: FloatingActionButton(
// 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),
@@ -125,22 +285,22 @@ class _TeamStatsPageState extends State<TeamStatsPage> {
}
}
// --- CONTROLLER SUPABASE ---
// ==========================================
// 3. CONTROLLER
// ==========================================
class StatsController {
final _supabase = Supabase.instance.client;
// 1. LER (A escuta em tempo real)
Stream<List<Person>> getMembers(String teamId) {
return _supabase
.from('members')
.stream(primaryKey: ['id']) // Garante que a PK na tabela é 'id'
.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);
@@ -149,7 +309,6 @@ class StatsController {
}
}
// 3. DIÁLOGOS
void showAddPersonDialog(BuildContext context, String teamId) {
_showForm(context, teamId: teamId);
}
@@ -237,8 +396,12 @@ class StatsController {
} catch (e) {
debugPrint("Erro Supabase: $e");
if (ctx.mounted) {
String errorMsg = "Erro ao guardar: $e";
if (e.toString().contains('unique')) {
errorMsg = "Já existe um membro com este nome na equipa.";
}
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text("Erro ao guardar: $e"), backgroundColor: Colors.red)
SnackBar(content: Text(errorMsg), backgroundColor: Colors.red)
);
}
}
@@ -250,6 +413,4 @@ class StatsController {
),
);
}
void dispose() {}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:playmaker/pages/PlacarPage.dart'; // Garante que o import está correto
import '../controllers/team_controller.dart';
import '../controllers/game_controller.dart'; // Import necessário
import '../controllers/game_controller.dart';
// --- CARD DE EXIBIÇÃO DO JOGO (Mantém-se quase igual) ---
class GameResultCard extends StatelessWidget {