pdf e exel

This commit is contained in:
2026-05-06 12:47:17 +01:00
parent c3a90f2816
commit 60656d77e8
14 changed files with 1512 additions and 951 deletions

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:playmaker/classe/theme.dart';
import 'package:cached_network_image/cached_network_image.dart'; // 👇 A MAGIA DO CACHE
import 'package:cached_network_image/cached_network_image.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../controllers/team_controller.dart';
import '../utils/size_extension.dart';
@@ -18,9 +19,55 @@ class _StatusPageState extends State<StatusPage> {
String? _selectedTeamId;
String _selectedTeamName = "Selecionar Equipa";
String? _selectedTeamLogo;
String _sortColumn = 'pts';
bool _isAscending = false;
@override
void initState() {
super.initState();
_loadSelectedTeam();
}
Future<void> _loadSelectedTeam() async {
final prefs = await SharedPreferences.getInstance();
final savedId = prefs.getString('last_team_id');
if (savedId != null && mounted) {
setState(() {
_selectedTeamId = savedId;
_selectedTeamName = prefs.getString('last_team_name') ?? "Selecionar Equipa";
_selectedTeamLogo = prefs.getString('last_team_logo');
});
}
}
Future<void> _saveSelectedTeam() async {
final prefs = await SharedPreferences.getInstance();
if (_selectedTeamId != null) {
await prefs.setString('last_team_id', _selectedTeamId!);
await prefs.setString('last_team_name', _selectedTeamName);
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
await prefs.setString('last_team_logo', _selectedTeamLogo!);
} else {
await prefs.remove('last_team_logo');
}
}
final userId = _supabase.auth.currentUser?.id;
if (userId != null && _selectedTeamId != null) {
try {
await _supabase.from('profiles').upsert({
'id': userId,
'selected_team_id': _selectedTeamId,
});
} catch (e) {
debugPrint("Erro ao guardar equipa no Supabase: $e");
}
}
}
@override
Widget build(BuildContext context) {
final bgColor = Theme.of(context).cardTheme.color ?? Colors.white;
@@ -44,7 +91,19 @@ class _StatusPageState extends State<StatusPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(children: [
Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
(_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty)
? ClipOval(
child: CachedNetworkImage(
imageUrl: _selectedTeamLogo!,
width: 24 * context.sf,
height: 24 * context.sf,
fit: BoxFit.cover,
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
),
)
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 24 * context.sf),
SizedBox(width: 10 * context.sf),
Text(_selectedTeamName, style: TextStyle(fontSize: 16 * context.sf, fontWeight: FontWeight.bold, color: textColor))
]),
@@ -99,12 +158,11 @@ class _StatusPageState extends State<StatusPage> {
);
}
// 👇 AGORA GUARDA TAMBÉM O IMAGE_URL DO MEMBRO PARA MOSTRAR NA TABELA
List<Map<String, dynamic>> _aggregateStats(List<dynamic> stats, List<dynamic> games, List<dynamic> members) {
Map<String, Map<String, dynamic>> aggregated = {};
for (var member in members) {
String name = member['name']?.toString() ?? "Desconhecido";
String? imageUrl = member['image_url']?.toString(); // 👈 CAPTURA A IMAGEM AQUI
String? imageUrl = member['image_url']?.toString();
aggregated[name] = {'name': name, 'image_url': imageUrl, 'j': 0, 'pts': 0, 'ast': 0, 'rbs': 0, 'stl': 0, 'blk': 0, 'mvp': 0, 'def': 0};
}
for (var row in stats) {
@@ -140,78 +198,84 @@ class _StatusPageState extends State<StatusPage> {
Widget _buildStatsGrid(BuildContext context, List<Map<String, dynamic>> players, Map<String, dynamic> teamTotals, Color bgColor, Color textColor) {
return Container(
color: Colors.transparent,
color: Colors.transparent, // 👇 VOLTOU A ESTAR TRANSPARENTE COMO TINHAS ANTES!
width: double.infinity,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
physics: const BouncingScrollPhysics(),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 25 * context.sf,
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
dataRowMaxHeight: 60 * context.sf,
dataRowMinHeight: 60 * context.sf,
columns: [
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
_buildSortableColumn(context, 'J', 'j', textColor),
_buildSortableColumn(context, 'PTS', 'pts', textColor),
_buildSortableColumn(context, 'AST', 'ast', textColor),
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
_buildSortableColumn(context, 'STL', 'stl', textColor),
_buildSortableColumn(context, 'BLK', 'blk', textColor),
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
],
rows: [
...players.map((player) => DataRow(cells: [
DataCell(
Row(
children: [
// 👇 FOTO DO JOGADOR NA TABELA (COM CACHE!) 👇
ClipOval(
child: Container(
width: 30 * context.sf,
height: 30 * context.sf,
color: Colors.grey.withOpacity(0.2),
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
? CachedNetworkImage(
imageUrl: player['image_url'],
fit: BoxFit.cover,
fadeInDuration: Duration.zero,
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
)
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
physics: const ClampingScrollPhysics(), // Mantém-se o Clamping para não puxar mais do que o ecrã
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
child: DataTable(
columnSpacing: 20 * context.sf,
horizontalMargin: 16 * context.sf,
headingRowColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
dataRowMaxHeight: 60 * context.sf,
dataRowMinHeight: 60 * context.sf,
columns: [
DataColumn(label: Text('JOGADOR', style: TextStyle(color: textColor))),
_buildSortableColumn(context, 'J', 'j', textColor),
_buildSortableColumn(context, 'PTS', 'pts', textColor),
_buildSortableColumn(context, 'AST', 'ast', textColor),
_buildSortableColumn(context, 'RBS', 'rbs', textColor),
_buildSortableColumn(context, 'STL', 'stl', textColor),
_buildSortableColumn(context, 'BLK', 'blk', textColor),
_buildSortableColumn(context, 'DEF 🛡️', 'def', textColor),
_buildSortableColumn(context, 'MVP 🏆', 'mvp', textColor),
],
rows: [
...players.map((player) => DataRow(cells: [
DataCell(
Row(
children: [
ClipOval(
child: Container(
width: 30 * context.sf,
height: 30 * context.sf,
color: Colors.grey.withOpacity(0.2),
child: (player['image_url'] != null && player['image_url'].toString().isNotEmpty)
? CachedNetworkImage(
imageUrl: player['image_url'],
fit: BoxFit.cover,
fadeInDuration: Duration.zero,
placeholder: (context, url) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
errorWidget: (context, url, error) => Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
)
: Icon(Icons.person, size: 18 * context.sf, color: Colors.grey),
),
),
),
SizedBox(width: 10 * context.sf),
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
]
)
),
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
_buildStatCell(context, player['ast'], textColor),
_buildStatCell(context, player['rbs'], textColor),
_buildStatCell(context, player['stl'], textColor),
_buildStatCell(context, player['blk'], textColor),
_buildStatCell(context, player['def'], textColor, isBlue: true),
_buildStatCell(context, player['mvp'], textColor, isGold: true),
])),
DataRow(
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
cells: [
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
_buildStatCell(context, teamTotals['ast'], textColor),
_buildStatCell(context, teamTotals['rbs'], textColor),
_buildStatCell(context, teamTotals['stl'], textColor),
_buildStatCell(context, teamTotals['blk'], textColor),
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
]
)
],
SizedBox(width: 10 * context.sf),
Text(player['name'], style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * context.sf, color: textColor))
]
)
),
DataCell(Center(child: Text(player['j'].toString(), style: TextStyle(color: textColor)))),
_buildStatCell(context, player['pts'], textColor, isHighlight: true),
_buildStatCell(context, player['ast'], textColor),
_buildStatCell(context, player['rbs'], textColor),
_buildStatCell(context, player['stl'], textColor),
_buildStatCell(context, player['blk'], textColor),
_buildStatCell(context, player['def'], textColor, isBlue: true),
_buildStatCell(context, player['mvp'], textColor, isGold: true),
])),
DataRow(
color: WidgetStateProperty.all(Theme.of(context).colorScheme.surface.withOpacity(0.5)),
cells: [
DataCell(Text('TOTAL EQUIPA', style: TextStyle(fontWeight: FontWeight.w900, color: textColor, fontSize: 12 * context.sf))),
DataCell(Center(child: Text(teamTotals['j'].toString(), style: TextStyle(fontWeight: FontWeight.bold, color: textColor)))),
_buildStatCell(context, teamTotals['pts'], textColor, isHighlight: true),
_buildStatCell(context, teamTotals['ast'], textColor),
_buildStatCell(context, teamTotals['rbs'], textColor),
_buildStatCell(context, teamTotals['stl'], textColor),
_buildStatCell(context, teamTotals['blk'], textColor),
_buildStatCell(context, teamTotals['def'], textColor, isBlue: true),
_buildStatCell(context, teamTotals['mvp'], textColor, isGold: true),
]
)
],
),
),
),
),
@@ -247,10 +311,40 @@ class _StatusPageState extends State<StatusPage> {
stream: _teamController.teamsStream,
builder: (context, snapshot) {
final teams = snapshot.data ?? [];
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) => ListTile(
title: Text(teams[i]['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
onTap: () { setState(() { _selectedTeamId = teams[i]['id']; _selectedTeamName = teams[i]['name']; }); Navigator.pop(context); },
));
return ListView.builder(itemCount: teams.length, itemBuilder: (context, i) {
final team = teams[i];
final logoUrl = team['image_url'];
return ListTile(
leading: ClipOval(
child: Container(
width: 36 * context.sf,
height: 36 * context.sf,
color: AppTheme.primaryRed.withOpacity(0.1),
child: (logoUrl != null && logoUrl.isNotEmpty)
? CachedNetworkImage(
imageUrl: logoUrl,
fit: BoxFit.cover,
placeholder: (context, url) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
errorWidget: (context, url, error) => Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
)
: Icon(Icons.shield, color: AppTheme.primaryRed, size: 20 * context.sf),
),
),
title: Text(team['name'], style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
onTap: () async {
setState(() {
_selectedTeamId = team['id'].toString();
_selectedTeamName = team['name'];
_selectedTeamLogo = logoUrl;
});
await _saveSelectedTeam();
if (context.mounted) Navigator.pop(context);
},
);
});
},
));
}