612 lines
23 KiB
Dart
612 lines
23 KiB
Dart
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';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../controllers/team_controller.dart';
|
|
import '../controllers/active_team.dart';
|
|
import '../utils/size_extension.dart';
|
|
|
|
class StatusPage extends StatefulWidget {
|
|
final String? initialTeamId;
|
|
final String initialTeamName;
|
|
final String? initialTeamLogo;
|
|
|
|
const StatusPage({
|
|
super.key,
|
|
this.initialTeamId,
|
|
this.initialTeamName = "Selecionar Equipa",
|
|
this.initialTeamLogo,
|
|
});
|
|
|
|
@override
|
|
State<StatusPage> createState() => _StatusPageState();
|
|
}
|
|
|
|
class _StatusPageState extends State<StatusPage> {
|
|
final TeamController _teamController = TeamController();
|
|
final _supabase = Supabase.instance.client;
|
|
|
|
late String? _selectedTeamId;
|
|
late String _selectedTeamName;
|
|
late String? _selectedTeamLogo;
|
|
|
|
String _sortColumn = 'pts';
|
|
bool _isAscending = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedTeamId = widget.initialTeamId;
|
|
_selectedTeamName = widget.initialTeamName;
|
|
_selectedTeamLogo = widget.initialTeamLogo;
|
|
|
|
// Se não vieram parâmetros da HomeScreen, tenta carregar do SharedPreferences
|
|
if (_selectedTeamId == null) {
|
|
_loadSelectedTeamFallback();
|
|
}
|
|
|
|
// Listen to global active team changes (e.g., when user marks favorite)
|
|
globalActiveTeam.addListener(_onGlobalActiveTeamChanged);
|
|
|
|
// Se já existe um globalActiveTeam no momento da abertura da página, aplica-o
|
|
final atNow = globalActiveTeam.value;
|
|
if (atNow != null) {
|
|
_selectedTeamId = atNow.id;
|
|
_selectedTeamName = atNow.name;
|
|
_selectedTeamLogo = atNow.logo;
|
|
}
|
|
}
|
|
|
|
void _onGlobalActiveTeamChanged() {
|
|
final at = globalActiveTeam.value;
|
|
if (!mounted) return;
|
|
|
|
// Atualiza sempre para a equipa ativa global (favorita). Isto força a Status
|
|
// a mostrar a equipa marcada como favorita assim que o utilizador a define.
|
|
if (at != null) {
|
|
setState(() {
|
|
_selectedTeamId = at.id;
|
|
_selectedTeamName = at.name;
|
|
_selectedTeamLogo = at.logo;
|
|
});
|
|
}
|
|
}
|
|
|
|
String _prefsKey(String key) {
|
|
final userId = _supabase.auth.currentUser?.id ?? 'guest';
|
|
return '${key}_$userId';
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(StatusPage oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
// Quando a HomeScreen muda a equipa, a StatusPage atualiza automaticamente
|
|
if (widget.initialTeamId != oldWidget.initialTeamId) {
|
|
setState(() {
|
|
_selectedTeamId = widget.initialTeamId;
|
|
_selectedTeamName = widget.initialTeamName;
|
|
_selectedTeamLogo = widget.initialTeamLogo;
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Fallback: só usado se a HomeScreen não passou nenhuma equipa ainda
|
|
Future<void> _loadSelectedTeamFallback() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final savedId = prefs.getString(_prefsKey('last_team_id'));
|
|
if (savedId != null && mounted) {
|
|
setState(() {
|
|
_selectedTeamId = savedId;
|
|
_selectedTeamName = prefs.getString(_prefsKey('last_team_name')) ?? "Selecionar Equipa";
|
|
_selectedTeamLogo = prefs.getString(_prefsKey('last_team_logo'));
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Guarda a equipa selecionada localmente (quando muda dentro da StatusPage)
|
|
Future<void> _saveSelectedTeamLocally() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (_selectedTeamId != null) {
|
|
await prefs.setString(_prefsKey('last_team_id'), _selectedTeamId!);
|
|
await prefs.setString(_prefsKey('last_team_name'), _selectedTeamName);
|
|
if (_selectedTeamLogo != null && _selectedTeamLogo!.isNotEmpty) {
|
|
await prefs.setString(_prefsKey('last_team_logo'), _selectedTeamLogo!);
|
|
} else {
|
|
await prefs.remove(_prefsKey('last_team_logo'));
|
|
}
|
|
}
|
|
|
|
// Também guarda no Supabase
|
|
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;
|
|
final textColor = Theme.of(context).colorScheme.onSurface;
|
|
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(16.0 * context.sf),
|
|
child: InkWell(
|
|
onTap: () => _showTeamSelector(context),
|
|
child: Container(
|
|
padding: EdgeInsets.all(12 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: bgColor,
|
|
borderRadius: BorderRadius.circular(15 * context.sf),
|
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(children: [
|
|
(_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)),
|
|
]),
|
|
Icon(Icons.arrow_drop_down, color: textColor),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: _selectedTeamId == null
|
|
? Center(
|
|
child: Text(
|
|
"Seleciona uma equipa acima.",
|
|
style: TextStyle(
|
|
color: Colors.grey, fontSize: 14 * context.sf),
|
|
),
|
|
)
|
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _supabase
|
|
.from('player_stats_with_names')
|
|
.stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
|
builder: (context, statsSnapshot) {
|
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _supabase
|
|
.from('games')
|
|
.stream(primaryKey: ['id']).eq('my_team', _selectedTeamName),
|
|
builder: (context, gamesSnapshot) {
|
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _supabase
|
|
.from('members')
|
|
.stream(primaryKey: ['id']).eq('team_id', _selectedTeamId!),
|
|
builder: (context, membersSnapshot) {
|
|
if (statsSnapshot.connectionState ==
|
|
ConnectionState.waiting ||
|
|
gamesSnapshot.connectionState ==
|
|
ConnectionState.waiting ||
|
|
membersSnapshot.connectionState ==
|
|
ConnectionState.waiting) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(
|
|
color: AppTheme.primaryRed));
|
|
}
|
|
|
|
final membersData = membersSnapshot.data ?? [];
|
|
if (membersData.isEmpty) {
|
|
return Center(
|
|
child: Text(
|
|
"Esta equipa não tem jogadores registados.",
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 14 * context.sf)));
|
|
}
|
|
|
|
final statsData = statsSnapshot.data ?? [];
|
|
final gamesData = gamesSnapshot.data ?? [];
|
|
final totalGamesPlayedByTeam = gamesData
|
|
.where((g) => g['status'] == 'Terminado')
|
|
.length;
|
|
|
|
final List<Map<String, dynamic>> playerTotals =
|
|
_aggregateStats(statsData, gamesData, membersData);
|
|
final teamTotals = _calculateTeamTotals(
|
|
playerTotals, totalGamesPlayedByTeam);
|
|
|
|
playerTotals.sort((a, b) {
|
|
var valA = a[_sortColumn] ?? 0;
|
|
var valB = b[_sortColumn] ?? 0;
|
|
return _isAscending
|
|
? valA.compareTo(valB)
|
|
: valB.compareTo(valA);
|
|
});
|
|
|
|
return _buildStatsGrid(
|
|
context, playerTotals, teamTotals, bgColor, textColor);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
globalActiveTeam.removeListener(_onGlobalActiveTeamChanged);
|
|
super.dispose();
|
|
}
|
|
|
|
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();
|
|
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) {
|
|
String name = row['player_name']?.toString() ?? "Desconhecido";
|
|
if (!aggregated.containsKey(name)) {
|
|
aggregated[name] = {
|
|
'name': name,
|
|
'image_url': null,
|
|
'j': 0,
|
|
'pts': 0,
|
|
'ast': 0,
|
|
'rbs': 0,
|
|
'stl': 0,
|
|
'blk': 0,
|
|
'mvp': 0,
|
|
'def': 0,
|
|
};
|
|
}
|
|
aggregated[name]!['j'] += 1;
|
|
aggregated[name]!['pts'] += (row['pts'] ?? 0);
|
|
aggregated[name]!['ast'] += (row['ast'] ?? 0);
|
|
aggregated[name]!['rbs'] += (row['rbs'] ?? 0);
|
|
aggregated[name]!['stl'] += (row['stl'] ?? 0);
|
|
aggregated[name]!['blk'] += (row['blk'] ?? 0);
|
|
}
|
|
|
|
for (var game in games) {
|
|
String? mvp = game['mvp_name'];
|
|
String? defRaw = game['top_def_name'];
|
|
if (mvp != null && aggregated.containsKey(mvp)) {
|
|
aggregated[mvp]!['mvp'] += 1;
|
|
}
|
|
if (defRaw != null) {
|
|
String defName = defRaw.split(' (')[0].trim();
|
|
if (aggregated.containsKey(defName)) {
|
|
aggregated[defName]!['def'] += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return aggregated.values.toList();
|
|
}
|
|
|
|
Map<String, dynamic> _calculateTeamTotals(
|
|
List<Map<String, dynamic>> players, int teamGames) {
|
|
int tPts = 0,
|
|
tAst = 0,
|
|
tRbs = 0,
|
|
tStl = 0,
|
|
tBlk = 0,
|
|
tMvp = 0,
|
|
tDef = 0;
|
|
for (var p in players) {
|
|
tPts += (p['pts'] as int);
|
|
tAst += (p['ast'] as int);
|
|
tRbs += (p['rbs'] as int);
|
|
tStl += (p['stl'] as int);
|
|
tBlk += (p['blk'] as int);
|
|
tMvp += (p['mvp'] as int);
|
|
tDef += (p['def'] as int);
|
|
}
|
|
return {
|
|
'name': 'TOTAL EQUIPA',
|
|
'image_url': null,
|
|
'j': teamGames,
|
|
'pts': tPts,
|
|
'ast': tAst,
|
|
'rbs': tRbs,
|
|
'stl': tStl,
|
|
'blk': tBlk,
|
|
'mvp': tMvp,
|
|
'def': tDef,
|
|
};
|
|
}
|
|
|
|
Widget _buildStatsGrid(
|
|
BuildContext context,
|
|
List<Map<String, dynamic>> players,
|
|
Map<String, dynamic> teamTotals,
|
|
Color bgColor,
|
|
Color textColor) {
|
|
return Container(
|
|
color: Colors.transparent,
|
|
width: double.infinity,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.vertical,
|
|
physics: const BouncingScrollPhysics(),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const ClampingScrollPhysics(),
|
|
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),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
DataColumn _buildSortableColumn(
|
|
BuildContext context, String title, String sortKey, Color textColor) {
|
|
return DataColumn(
|
|
label: InkWell(
|
|
onTap: () => setState(() {
|
|
if (_sortColumn == sortKey) {
|
|
_isAscending = !_isAscending;
|
|
} else {
|
|
_sortColumn = sortKey;
|
|
_isAscending = false;
|
|
}
|
|
}),
|
|
child: Row(children: [
|
|
Text(title,
|
|
style: TextStyle(
|
|
fontSize: 12 * context.sf,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor)),
|
|
if (_sortColumn == sortKey)
|
|
Icon(
|
|
_isAscending
|
|
? Icons.arrow_drop_up
|
|
: Icons.arrow_drop_down,
|
|
size: 18 * context.sf,
|
|
color: AppTheme.primaryRed),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
DataCell _buildStatCell(BuildContext context, int value, Color textColor,
|
|
{bool isHighlight = false, bool isGold = false, bool isBlue = false}) {
|
|
return DataCell(Center(
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 8 * context.sf, vertical: 4 * context.sf),
|
|
decoration: BoxDecoration(
|
|
color: isGold && value > 0
|
|
? Colors.amber.withOpacity(0.2)
|
|
: (isBlue && value > 0
|
|
? Colors.blue.withOpacity(0.1)
|
|
: Colors.transparent),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
value == 0 ? "-" : value.toString(),
|
|
style: TextStyle(
|
|
fontWeight: (isHighlight || isGold || isBlue)
|
|
? FontWeight.w900
|
|
: FontWeight.w600,
|
|
fontSize: 14 * context.sf,
|
|
color: isGold && value > 0
|
|
? Colors.orange.shade900
|
|
: (isBlue && value > 0
|
|
? Colors.blue.shade800
|
|
: (isHighlight ? AppTheme.successGreen : textColor)),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
}
|
|
|
|
void _showTeamSelector(BuildContext context) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
builder: (context) => StreamBuilder<List<Map<String, dynamic>>>(
|
|
stream: _teamController.teamsStream,
|
|
builder: (context, snapshot) {
|
|
final teams = snapshot.data ?? [];
|
|
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 _saveSelectedTeamLocally();
|
|
|
|
if (context.mounted) Navigator.pop(context);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
} |