bora para o relatorio
This commit is contained in:
@@ -139,6 +139,36 @@ class PlacarController extends ChangeNotifier {
|
|||||||
_setTimerRunning(shouldRun, emitSync: false);
|
_setTimerRunning(shouldRun, emitSync: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void applyRemoteAddShot(Map<String, dynamic> shotJson) {
|
||||||
|
try {
|
||||||
|
matchShots.add(ShotRecord.fromJson(shotJson));
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erro ao aplicar shot remoto: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistShotRemote(Map<String, dynamic> shotJson) async {
|
||||||
|
try {
|
||||||
|
final supabase = Supabase.instance.client;
|
||||||
|
final row = {
|
||||||
|
'game_id': gameId,
|
||||||
|
'member_id': shotJson['playerId'] ?? shotJson['player_id'],
|
||||||
|
'player_name': shotJson['playerName'] ?? shotJson['player_name'],
|
||||||
|
'relative_x': shotJson['relativeX'] ?? shotJson['relative_x'],
|
||||||
|
'relative_y': shotJson['relativeY'] ?? shotJson['relative_y'],
|
||||||
|
'is_make': shotJson['isMake'] ?? shotJson['is_make'],
|
||||||
|
'zone': shotJson['zone'],
|
||||||
|
'points': shotJson['points'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await supabase.from('shot_locations').insert(row);
|
||||||
|
debugPrint('✅ Shot persisted remotely');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Erro ao persistir shot remoto: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
bool isSaving = false;
|
bool isSaving = false;
|
||||||
bool gameWasAlreadyFinished = false;
|
bool gameWasAlreadyFinished = false;
|
||||||
@@ -666,6 +696,14 @@ class PlacarController extends ChangeNotifier {
|
|||||||
|
|
||||||
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
String finalAction = isMake ? "add_pts_$points" : "miss_$points";
|
||||||
commitStat(finalAction, targetPlayer);
|
commitStat(finalAction, targetPlayer);
|
||||||
|
// Emitir evento de shot para parceiros remotos
|
||||||
|
try {
|
||||||
|
final shotJson = matchShots.last.toJson();
|
||||||
|
_dispatchSyncAction('add_shot', {'shot': shotJson});
|
||||||
|
// Persist shot immediately on server (fire-and-forget)
|
||||||
|
_persistShotRemote(shotJson);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,6 +735,14 @@ class PlacarController extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Emitir evento de shot para parceiros remotos
|
||||||
|
try {
|
||||||
|
final shotJson = matchShots.last.toJson();
|
||||||
|
_dispatchSyncAction('add_shot', {'shot': shotJson});
|
||||||
|
// Persist shot immediately on server (fire-and-forget)
|
||||||
|
_persistShotRemote(shotJson);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
commitStat(pendingAction!, pendingPlayerId!);
|
commitStat(pendingAction!, pendingPlayerId!);
|
||||||
|
|
||||||
isSelectingShotLocation = false;
|
isSelectingShotLocation = false;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
StreamSubscription? _syncSubscription;
|
StreamSubscription? _syncSubscription;
|
||||||
bool _isApplyingRemoteSync = false;
|
bool _isApplyingRemoteSync = false;
|
||||||
final Set<String> _appliedSyncEventIds = {};
|
final Set<String> _appliedSyncEventIds = {};
|
||||||
|
final Map<String, DateTime> _lastAppliedActionAt = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -169,7 +170,7 @@
|
|||||||
_sharedWithName = await _resolveUserName(sharedWith);
|
_sharedWithName = await _resolveUserName(sharedWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupSyncListener();
|
await _setupSyncListener();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +190,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupSyncListener() {
|
Future<void> _setupSyncListener() async {
|
||||||
if (_sessionId == null) return;
|
if (_sessionId == null) return;
|
||||||
_syncSubscription?.cancel();
|
_syncSubscription?.cancel();
|
||||||
_appliedSyncEventIds.clear();
|
_appliedSyncEventIds.clear();
|
||||||
|
await _seedAppliedSyncEventIds();
|
||||||
_syncSubscription = _sharingController
|
_syncSubscription = _sharingController
|
||||||
.listenToGameSyncOthers(_sessionId!)
|
.listenToGameSyncOthers(_sessionId!)
|
||||||
.listen(
|
.listen(
|
||||||
@@ -233,17 +235,39 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (final record in rows) {
|
for (final record in rows) {
|
||||||
final recordId = record['id']?.toString();
|
final recordId = record['id']?.toString();
|
||||||
if (recordId == null || _appliedSyncEventIds.contains(recordId)) {
|
if (recordId == null || _appliedSyncEventIds.contains(recordId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if this event is older or equal to the last applied event of the same type
|
||||||
|
final actionType = record['action_type']?.toString();
|
||||||
|
DateTime? recordTime;
|
||||||
|
try {
|
||||||
|
final created = record['created_at']?.toString();
|
||||||
|
if (created != null) recordTime = DateTime.parse(created);
|
||||||
|
} catch (_) {
|
||||||
|
recordTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionType != null && recordTime != null) {
|
||||||
|
final last = _lastAppliedActionAt[actionType];
|
||||||
|
if (last != null && !recordTime.isAfter(last)) {
|
||||||
|
_appliedSyncEventIds.add(recordId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_appliedSyncEventIds.add(recordId);
|
|
||||||
print(
|
|
||||||
"🔄 Evento remoto recebido: ${record['action_type']} - ${record['action_data']}",
|
|
||||||
);
|
|
||||||
_applyRemoteSyncEvent(record);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_appliedSyncEventIds.add(recordId);
|
||||||
|
if (actionType != null && recordTime != null) {
|
||||||
|
_lastAppliedActionAt[actionType] = recordTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
print(
|
||||||
|
"🔄 Evento remoto recebido: ${record['action_type']} - ${record['action_data']}",
|
||||||
|
);
|
||||||
|
_applyRemoteSyncEvent(record);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
print("⚠️ Erro no stream de sync: $error");
|
print("⚠️ Erro no stream de sync: $error");
|
||||||
@@ -251,6 +275,29 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _seedAppliedSyncEventIds() async {
|
||||||
|
if (_sessionId == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await Supabase.instance.client
|
||||||
|
.from('game_sync_events')
|
||||||
|
.select('id')
|
||||||
|
.eq('session_id', _sessionId!)
|
||||||
|
.order('created_at', ascending: true);
|
||||||
|
|
||||||
|
if (response is List) {
|
||||||
|
for (final item in response) {
|
||||||
|
final id = item['id']?.toString();
|
||||||
|
if (id != null) {
|
||||||
|
_appliedSyncEventIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('⚠️ Erro ao semear eventos históricos de sync: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _handleSyncRecords(Map<String, dynamic> record) {
|
void _handleSyncRecords(Map<String, dynamic> record) {
|
||||||
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
// Mantido apenas como fallback, mas a escuta principal usa listenToGameSyncOthers.
|
||||||
_applyRemoteSyncEvent(record);
|
_applyRemoteSyncEvent(record);
|
||||||
@@ -317,7 +364,6 @@
|
|||||||
if (remoteIsRunning != _controller.isRunning) {
|
if (remoteIsRunning != _controller.isRunning) {
|
||||||
_isApplyingRemoteSync = true;
|
_isApplyingRemoteSync = true;
|
||||||
_controller.applyRemoteTimerState(remoteIsRunning);
|
_controller.applyRemoteTimerState(remoteIsRunning);
|
||||||
_controller.notifyListeners();
|
|
||||||
_isApplyingRemoteSync = false;
|
_isApplyingRemoteSync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +408,13 @@
|
|||||||
_isApplyingRemoteSync = true;
|
_isApplyingRemoteSync = true;
|
||||||
_controller.useTimeout(isOpponent);
|
_controller.useTimeout(isOpponent);
|
||||||
_isApplyingRemoteSync = false;
|
_isApplyingRemoteSync = false;
|
||||||
|
} else if (actionType == 'add_shot') {
|
||||||
|
final shot = actionData['shot'] as Map<String, dynamic>?;
|
||||||
|
if (shot != null) {
|
||||||
|
_isApplyingRemoteSync = true;
|
||||||
|
_controller.applyRemoteAddShot(Map<String, dynamic>.from(shot));
|
||||||
|
_isApplyingRemoteSync = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +436,7 @@
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
_sessionId = result['session_id']?.toString();
|
_sessionId = result['session_id']?.toString();
|
||||||
_shareCode = result['share_code']?.toString();
|
_shareCode = result['share_code']?.toString();
|
||||||
_setupSyncListener();
|
await _setupSyncListener();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +451,7 @@
|
|||||||
_sessionId = result['session_id']?.toString();
|
_sessionId = result['session_id']?.toString();
|
||||||
_shareCode = result['share_code']?.toString();
|
_shareCode = result['share_code']?.toString();
|
||||||
_sharedWithName = result['creator_name']?.toString() ?? '';
|
_sharedWithName = result['creator_name']?.toString() ?? '';
|
||||||
_setupSyncListener();
|
await _setupSyncListener();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,293 +404,336 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 22.0 * context.sf,
|
horizontal: 22.0 * context.sf,
|
||||||
vertical: 16.0 * context.sf),
|
vertical: 16.0 * context.sf),
|
||||||
child: Column(
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, constraints) {
|
||||||
children: [
|
final bool isWide = constraints.maxWidth >= 1100;
|
||||||
InkWell(
|
final double effectiveCardHeight =
|
||||||
onTap: () => _showTeamSelector(context),
|
isWide ? 280 * context.sf : cardHeight;
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(12 * context.sf),
|
Widget statsSection = Column(
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: Theme.of(context).cardTheme.color,
|
SizedBox(
|
||||||
borderRadius:
|
height: effectiveCardHeight,
|
||||||
BorderRadius.circular(15 * context.sf),
|
child: Row(
|
||||||
border:
|
children: [
|
||||||
Border.all(color: Colors.grey.withOpacity(0.2)),
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
context: context,
|
||||||
|
title: 'Mais Pontos',
|
||||||
|
playerName: leaders['pts_name'],
|
||||||
|
statValue: leaders['pts_val'].toString(),
|
||||||
|
statLabel: 'TOTAL',
|
||||||
|
color: AppTheme.statPtsBg,
|
||||||
|
isHighlighted: true)),
|
||||||
|
SizedBox(width: 12 * context.sf),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
context: context,
|
||||||
|
title: 'Assistências',
|
||||||
|
playerName: leaders['ast_name'],
|
||||||
|
statValue: leaders['ast_val'].toString(),
|
||||||
|
statLabel: 'TOTAL',
|
||||||
|
color: AppTheme.statAstBg)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
SizedBox(height: 12 * context.sf),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
SizedBox(
|
||||||
children: [
|
height: effectiveCardHeight,
|
||||||
Row(children: [
|
child: Row(
|
||||||
(_selectedTeamLogo != null &&
|
children: [
|
||||||
_selectedTeamLogo!.isNotEmpty)
|
Expanded(
|
||||||
? ClipOval(
|
child: _buildStatCard(
|
||||||
child: CachedNetworkImage(
|
context: context,
|
||||||
imageUrl: _selectedTeamLogo!,
|
title: 'Rebotes',
|
||||||
width: 24 * context.sf,
|
playerName: leaders['rbs_name'],
|
||||||
height: 24 * context.sf,
|
statValue: leaders['rbs_val'].toString(),
|
||||||
fit: BoxFit.cover,
|
statLabel: 'TOTAL',
|
||||||
placeholder: (context, url) => Icon(
|
color: AppTheme.statRebBg)),
|
||||||
Icons.shield,
|
SizedBox(width: 12 * context.sf),
|
||||||
color: AppTheme.primaryRed,
|
Expanded(
|
||||||
size: 24 * context.sf),
|
child: PieChartCard(
|
||||||
errorWidget:
|
victories: _teamWins,
|
||||||
(context, url, error) => Icon(
|
defeats: _teamLosses,
|
||||||
|
draws: _teamDraws,
|
||||||
|
title: 'DESEMPENHO',
|
||||||
|
subtitle: 'Temporada',
|
||||||
|
backgroundColor: AppTheme.statPieBg,
|
||||||
|
sf: context.sf)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget historySection = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Histórico de Jogos',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)),
|
||||||
|
SizedBox(height: 16 * context.sf),
|
||||||
|
_selectedTeamName == "Selecionar Equipa"
|
||||||
|
? Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(24.0 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardTheme.color ??
|
||||||
|
Colors.white,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(16 * context.sf),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey.withOpacity(0.1)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(18 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryRed
|
||||||
|
.withOpacity(0.08),
|
||||||
|
shape: BoxShape.circle),
|
||||||
|
child: Icon(Icons.shield_outlined,
|
||||||
|
color: AppTheme.primaryRed,
|
||||||
|
size: 42 * context.sf),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20 * context.sf),
|
||||||
|
Text("Nenhuma Equipa Ativa",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor)),
|
||||||
|
SizedBox(height: 8 * context.sf),
|
||||||
|
Text(
|
||||||
|
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13 * context.sf,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
height: 1.4),
|
||||||
|
),
|
||||||
|
SizedBox(height: 24 * context.sf),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48 * context.sf,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _showTeamSelector(context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.primaryRed,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
10 * context.sf)),
|
||||||
|
),
|
||||||
|
icon: Icon(Icons.touch_app,
|
||||||
|
size: 20 * context.sf),
|
||||||
|
label: Text("Selecionar Agora",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15 * context.sf,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: _supabase
|
||||||
|
.from('games')
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.order('game_date', ascending: false),
|
||||||
|
builder: (context, gameSnapshot) {
|
||||||
|
if (gameSnapshot.hasError) {
|
||||||
|
return Text(
|
||||||
|
"Erro: ${gameSnapshot.error}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red));
|
||||||
|
}
|
||||||
|
if (!gameSnapshot.hasData &&
|
||||||
|
gameSnapshot.connectionState ==
|
||||||
|
ConnectionState.waiting) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final todosOsJogos = gameSnapshot.data ?? [];
|
||||||
|
final gamesList = todosOsJogos.where((game) {
|
||||||
|
String myT =
|
||||||
|
game['my_team']?.toString() ?? '';
|
||||||
|
String oppT =
|
||||||
|
game['opponent_team']?.toString() ?? '';
|
||||||
|
String status =
|
||||||
|
game['status']?.toString() ?? '';
|
||||||
|
return (myT == _selectedTeamName ||
|
||||||
|
oppT == _selectedTeamName) &&
|
||||||
|
status == 'Terminado';
|
||||||
|
}).take(3).toList();
|
||||||
|
|
||||||
|
if (gamesList.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(20 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardTheme.color,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Text(
|
||||||
|
"Ainda não há jogos terminados.",
|
||||||
|
style: TextStyle(color: Colors.grey)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: gamesList.map((game) {
|
||||||
|
String dbMyTeam =
|
||||||
|
game['my_team']?.toString() ?? '';
|
||||||
|
String dbOppTeam =
|
||||||
|
game['opponent_team']?.toString() ?? '';
|
||||||
|
int dbMyScore = int.tryParse(
|
||||||
|
game['my_score']?.toString() ??
|
||||||
|
'0') ??
|
||||||
|
0;
|
||||||
|
int dbOppScore = int.tryParse(
|
||||||
|
game['opponent_score']
|
||||||
|
?.toString() ??
|
||||||
|
'0') ??
|
||||||
|
0;
|
||||||
|
String opponent;
|
||||||
|
int myScore;
|
||||||
|
int oppScore;
|
||||||
|
|
||||||
|
if (dbMyTeam == _selectedTeamName) {
|
||||||
|
opponent = dbOppTeam;
|
||||||
|
myScore = dbMyScore;
|
||||||
|
oppScore = dbOppScore;
|
||||||
|
} else {
|
||||||
|
opponent = dbMyTeam;
|
||||||
|
myScore = dbOppScore;
|
||||||
|
oppScore = dbMyScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
String rawDate =
|
||||||
|
game['game_date']?.toString() ?? '---';
|
||||||
|
String date = rawDate.length >= 10
|
||||||
|
? rawDate.substring(0, 10)
|
||||||
|
: rawDate;
|
||||||
|
String result = myScore > oppScore
|
||||||
|
? 'V'
|
||||||
|
: (myScore < oppScore ? 'D' : 'E');
|
||||||
|
|
||||||
|
return _buildGameHistoryCard(
|
||||||
|
context: context,
|
||||||
|
opponent: opponent,
|
||||||
|
result: result,
|
||||||
|
myScore: myScore,
|
||||||
|
oppScore: oppScore,
|
||||||
|
date: date,
|
||||||
|
topPts:
|
||||||
|
game['top_pts_name'] ?? '---',
|
||||||
|
topAst:
|
||||||
|
game['top_ast_name'] ?? '---',
|
||||||
|
topRbs:
|
||||||
|
game['top_rbs_name'] ?? '---',
|
||||||
|
topDef:
|
||||||
|
game['top_def_name'] ?? '---',
|
||||||
|
mvp: game['mvp_name'] ?? '---',
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget mainContent = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _showTeamSelector(context),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(12 * context.sf),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardTheme.color,
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(15 * context.sf),
|
||||||
|
border:
|
||||||
|
Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
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,
|
Icons.shield,
|
||||||
color: AppTheme.primaryRed,
|
color: AppTheme.primaryRed,
|
||||||
size: 24 * context.sf),
|
size: 24 * context.sf),
|
||||||
),
|
errorWidget:
|
||||||
)
|
(context, url, error) => Icon(
|
||||||
: Icon(Icons.shield,
|
Icons.shield,
|
||||||
color: AppTheme.primaryRed,
|
color: AppTheme.primaryRed,
|
||||||
size: 24 * context.sf),
|
size: 24 * context.sf),
|
||||||
SizedBox(width: 10 * context.sf),
|
),
|
||||||
Text(_selectedTeamName,
|
)
|
||||||
style: TextStyle(
|
: Icon(Icons.shield,
|
||||||
fontSize: 16 * context.sf,
|
color: AppTheme.primaryRed,
|
||||||
fontWeight: FontWeight.bold,
|
size: 24 * context.sf),
|
||||||
color: textColor)),
|
SizedBox(width: 10 * context.sf),
|
||||||
]),
|
Text(_selectedTeamName,
|
||||||
Icon(Icons.arrow_drop_down, color: textColor),
|
style: TextStyle(
|
||||||
],
|
fontSize: 16 * context.sf,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: textColor)),
|
||||||
),
|
]),
|
||||||
SizedBox(height: 20 * context.sf),
|
Icon(Icons.arrow_drop_down, color: textColor),
|
||||||
|
|
||||||
SizedBox(
|
|
||||||
height: cardHeight,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
context: context,
|
|
||||||
title: 'Mais Pontos',
|
|
||||||
playerName: leaders['pts_name'],
|
|
||||||
statValue: leaders['pts_val'].toString(),
|
|
||||||
statLabel: 'TOTAL',
|
|
||||||
color: AppTheme.statPtsBg,
|
|
||||||
isHighlighted: true)),
|
|
||||||
SizedBox(width: 12 * context.sf),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
context: context,
|
|
||||||
title: 'Assistências',
|
|
||||||
playerName: leaders['ast_name'],
|
|
||||||
statValue: leaders['ast_val'].toString(),
|
|
||||||
statLabel: 'TOTAL',
|
|
||||||
color: AppTheme.statAstBg)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 12 * context.sf),
|
|
||||||
|
|
||||||
SizedBox(
|
|
||||||
height: cardHeight,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
context: context,
|
|
||||||
title: 'Rebotes',
|
|
||||||
playerName: leaders['rbs_name'],
|
|
||||||
statValue: leaders['rbs_val'].toString(),
|
|
||||||
statLabel: 'TOTAL',
|
|
||||||
color: AppTheme.statRebBg)),
|
|
||||||
SizedBox(width: 12 * context.sf),
|
|
||||||
Expanded(
|
|
||||||
child: PieChartCard(
|
|
||||||
victories: _teamWins,
|
|
||||||
defeats: _teamLosses,
|
|
||||||
draws: _teamDraws,
|
|
||||||
title: 'DESEMPENHO',
|
|
||||||
subtitle: 'Temporada',
|
|
||||||
backgroundColor: AppTheme.statPieBg,
|
|
||||||
sf: context.sf)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 40 * context.sf),
|
|
||||||
|
|
||||||
Text('Histórico de Jogos',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20 * context.sf,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: textColor)),
|
|
||||||
SizedBox(height: 16 * context.sf),
|
|
||||||
|
|
||||||
_selectedTeamName == "Selecionar Equipa"
|
|
||||||
? Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.all(24.0 * context.sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardTheme.color ??
|
|
||||||
Colors.white,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(16 * context.sf),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey.withOpacity(0.1)),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.04),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.all(18 * context.sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.primaryRed.withOpacity(0.08),
|
|
||||||
shape: BoxShape.circle),
|
|
||||||
child: Icon(Icons.shield_outlined,
|
|
||||||
color: AppTheme.primaryRed,
|
|
||||||
size: 42 * context.sf),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20 * context.sf),
|
|
||||||
Text("Nenhuma Equipa Ativa",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18 * context.sf,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: textColor)),
|
|
||||||
SizedBox(height: 8 * context.sf),
|
|
||||||
Text(
|
|
||||||
"Escolha uma equipa no seletor acima para ver as estatísticas e o histórico.",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13 * context.sf,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
height: 1.4),
|
|
||||||
),
|
|
||||||
SizedBox(height: 24 * context.sf),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48 * context.sf,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => _showTeamSelector(context),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppTheme.primaryRed,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
10 * context.sf)),
|
|
||||||
),
|
|
||||||
icon: Icon(Icons.touch_app,
|
|
||||||
size: 20 * context.sf),
|
|
||||||
label: Text("Selecionar Agora",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15 * context.sf,
|
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: StreamBuilder<List<Map<String, dynamic>>>(
|
|
||||||
stream: _supabase
|
|
||||||
.from('games')
|
|
||||||
.stream(primaryKey: ['id']).order('game_date',
|
|
||||||
ascending: false),
|
|
||||||
builder: (context, gameSnapshot) {
|
|
||||||
if (gameSnapshot.hasError) {
|
|
||||||
return Text("Erro: ${gameSnapshot.error}",
|
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.red));
|
|
||||||
}
|
|
||||||
if (!gameSnapshot.hasData &&
|
|
||||||
gameSnapshot.connectionState ==
|
|
||||||
ConnectionState.waiting) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
final todosOsJogos = gameSnapshot.data ?? [];
|
|
||||||
final gamesList = todosOsJogos.where((game) {
|
|
||||||
String myT =
|
|
||||||
game['my_team']?.toString() ?? '';
|
|
||||||
String oppT =
|
|
||||||
game['opponent_team']?.toString() ?? '';
|
|
||||||
String status =
|
|
||||||
game['status']?.toString() ?? '';
|
|
||||||
return (myT == _selectedTeamName ||
|
|
||||||
oppT == _selectedTeamName) &&
|
|
||||||
status == 'Terminado';
|
|
||||||
}).take(3).toList();
|
|
||||||
|
|
||||||
if (gamesList.isEmpty) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: EdgeInsets.all(20 * context.sf),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: const Text(
|
|
||||||
"Ainda não há jogos terminados.",
|
|
||||||
style: TextStyle(color: Colors.grey)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: gamesList.map((game) {
|
|
||||||
String dbMyTeam =
|
|
||||||
game['my_team']?.toString() ?? '';
|
|
||||||
String dbOppTeam =
|
|
||||||
game['opponent_team']?.toString() ?? '';
|
|
||||||
int dbMyScore = int.tryParse(
|
|
||||||
game['my_score']?.toString() ??
|
|
||||||
'0') ??
|
|
||||||
0;
|
|
||||||
int dbOppScore = int.tryParse(
|
|
||||||
game['opponent_score']
|
|
||||||
?.toString() ??
|
|
||||||
'0') ??
|
|
||||||
0;
|
|
||||||
String opponent;
|
|
||||||
int myScore;
|
|
||||||
int oppScore;
|
|
||||||
|
|
||||||
if (dbMyTeam == _selectedTeamName) {
|
|
||||||
opponent = dbOppTeam;
|
|
||||||
myScore = dbMyScore;
|
|
||||||
oppScore = dbOppScore;
|
|
||||||
} else {
|
|
||||||
opponent = dbMyTeam;
|
|
||||||
myScore = dbOppScore;
|
|
||||||
oppScore = dbMyScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
String rawDate =
|
|
||||||
game['game_date']?.toString() ?? '---';
|
|
||||||
String date = rawDate.length >= 10
|
|
||||||
? rawDate.substring(0, 10)
|
|
||||||
: rawDate;
|
|
||||||
String result = myScore > oppScore
|
|
||||||
? 'V'
|
|
||||||
: (myScore < oppScore ? 'D' : 'E');
|
|
||||||
|
|
||||||
return _buildGameHistoryCard(
|
|
||||||
context: context,
|
|
||||||
opponent: opponent,
|
|
||||||
result: result,
|
|
||||||
myScore: myScore,
|
|
||||||
oppScore: oppScore,
|
|
||||||
date: date,
|
|
||||||
topPts: game['top_pts_name'] ?? '---',
|
|
||||||
topAst: game['top_ast_name'] ?? '---',
|
|
||||||
topRbs: game['top_rbs_name'] ?? '---',
|
|
||||||
topDef: game['top_def_name'] ?? '---',
|
|
||||||
mvp: game['mvp_name'] ?? '---',
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 20 * context.sf),
|
),
|
||||||
],
|
SizedBox(height: 20 * context.sf),
|
||||||
|
if (!isWide) ...[
|
||||||
|
statsSection,
|
||||||
|
SizedBox(height: 40 * context.sf),
|
||||||
|
historySection,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
mainContent = Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(flex: 5, child: statsSection),
|
||||||
|
SizedBox(width: 20 * context.sf),
|
||||||
|
Expanded(flex: 6, child: historySection),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
mainContent,
|
||||||
|
SizedBox(height: 20 * context.sf),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user