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

@@ -4,7 +4,6 @@ import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
// Modelo local para os tiros
class _ShotDot {
final double relX;
final double relY;
@@ -13,29 +12,22 @@ class _ShotDot {
}
class PdfExportService {
// ════════════════════════════════════════════════════════════════════════════
// ENTRY POINT
// ════════════════════════════════════════════════════════════════════════════
static Future<void> generateAndPrintBoxScore({
required String gameId,
required String myTeam,
required String opponentTeam,
required String myScore,
required String opponentScore,
required String season,
required String season,
required String targetTeam,
}) async {
final supabase = Supabase.instance.client;
// ── Jogo ────────────────────────────────────────────────────────────────
final gameData =
await supabase.from('games').select().eq('id', gameId).single();
final gameData = await supabase.from('games').select().eq('id', gameId).single();
// ── Equipas ─────────────────────────────────────────────────────────────
final teamsData = await supabase
.from('teams')
.select('id, name')
.inFilter('name', [myTeam, opponentTeam]);
final teamsData = await supabase.from('teams').select('id, name').inFilter('name', [myTeam, opponentTeam]);
String? myTeamId;
for (var t in teamsData) {
@@ -44,32 +36,19 @@ class PdfExportService {
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
List<dynamic> myPlayers = myTeamId != null
? await supabase
.from('members')
.select()
.eq('team_id', myTeamId)
.eq('type', 'Jogador')
? await supabase.from('members').select().eq('team_id', myTeamId).eq('type', 'Jogador')
: [];
// ── Estatísticas ─────────────────────────────────────────────────────────
final statsData =
await supabase.from('player_stats').select().eq('game_id', gameId);
final statsData = await supabase.from('player_stats').select().eq('game_id', gameId);
Map<String, Map<String, dynamic>> statsMap = {};
for (var s in statsData) {
statsMap[s['member_id'].toString()] = s;
}
// ── Tiros (para o mapa de calor da minha equipa) ──────────────────────
final shotsData = await supabase
.from('shot_locations')
.select()
.eq('game_id', gameId);
// IDs da minha equipa
final Set<String> myPlayerIds =
myPlayers.map((p) => p['id'].toString()).toSet();
// Separa os tiros: todos da minha equipa, depois por jogador
// ── Tiros ──────────────────────
final shotsData = await supabase.from('shot_locations').select().eq('game_id', gameId);
final Set<String> myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet();
final List<_ShotDot> myTeamShots = [];
final Map<String, List<_ShotDot>> shotsByPlayer = {};
@@ -86,16 +65,14 @@ class PdfExportService {
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
}
// ── Tabela de estatísticas (Apenas a minha equipa) ────────────────────
List<List<String>> myTeamTable =
_buildTeamTableData(myPlayers, statsMap);
// ── Tabela de estatísticas ────────────────────
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
// ════════════════════════════════════════════════════════════════════════
// CONSTRUÇÃO DO PDF
// ════════════════════════════════════════════════════════════════════════
final pdf = pw.Document();
// ── PÁGINA 1: Box Score ──────────────────────────────────────────────
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4.landscape,
@@ -110,71 +87,81 @@ class PdfExportService {
children: [
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Relatório Estatístico',
style: pw.TextStyle(
fontSize: 22,
fontWeight: pw.FontWeight.bold)),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Relatório Estatístico', style: pw.TextStyle(fontSize: 22, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 10),
pw.Text('Equipa: $myTeam', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold, color: const PdfColor.fromInt(0xFFA00000))),
]
),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text('$myTeam vs $opponentTeam',
style: pw.TextStyle(
fontSize: 15,
fontWeight: pw.FontWeight.bold)),
pw.Text('Resultado: $myScore$opponentScore',
style: const pw.TextStyle(fontSize: 13)),
pw.Text('Época: $season',
style: const pw.TextStyle(fontSize: 11)),
pw.Text('$myTeam vs $opponentTeam', style: pw.TextStyle(fontSize: 15, fontWeight: pw.FontWeight.bold)),
pw.Text('Resultado: $myScore$opponentScore', style: const pw.TextStyle(fontSize: 13)),
pw.Text('Época: $season', style: const pw.TextStyle(fontSize: 11)),
pw.SizedBox(height: 10),
// 👇 NOVA TABELA: PONTUAÇÃO POR PERÍODO 👇
pw.Table.fromTextArray(
context: context,
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 8),
cellStyle: const pw.TextStyle(fontSize: 8),
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey200),
cellAlignment: pw.Alignment.center,
data: <List<String>>[
['Equipa', '1ºQ', '2ºQ', '3ºQ', '4ºQ', 'F'],
[
myTeam,
gameData['my_q1']?.toString() ?? '-',
gameData['my_q2']?.toString() ?? '-',
gameData['my_q3']?.toString() ?? '-',
gameData['my_q4']?.toString() ?? '-',
myScore
],
[
opponentTeam,
gameData['opp_q1']?.toString() ?? '-',
gameData['opp_q2']?.toString() ?? '-',
gameData['opp_q3']?.toString() ?? '-',
gameData['opp_q4']?.toString() ?? '-',
opponentScore
],
],
)
],
),
],
),
pw.SizedBox(height: 12),
pw.Text('Equipa: $myTeam',
style: pw.TextStyle(
fontSize: 14,
fontWeight: pw.FontWeight.bold,
color: const PdfColor.fromInt(0xFFA00000))),
pw.SizedBox(height: 8),
pw.Text('Pontos e Lançamentos',
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey700)),
pw.Text('Pontos e Lançamentos', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
pw.SizedBox(height: 2),
_buildPdfTablePart1(
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
_buildPdfTablePart1(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
pw.SizedBox(height: 14),
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)',
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey700)),
pw.Text('Outras Estatísticas (Ressaltos, Faltas, Turnovers, etc.)', style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold, color: PdfColors.grey700)),
pw.SizedBox(height: 2),
_buildPdfTablePart2(
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
_buildPdfTablePart2(myTeamTable, const PdfColor.fromInt(0xFFA00000)),
pw.SizedBox(height: 16),
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryBox('Melhor Marcador',
gameData['top_pts_name'] ?? '---'),
_buildSummaryBox('Melhor Marcador', gameData['top_pts_name'] ?? '---'),
pw.SizedBox(width: 10),
_buildSummaryBox('Melhor Ressaltador',
gameData['top_rbs_name'] ?? '---'),
_buildSummaryBox('Melhor Ressaltador', gameData['top_rbs_name'] ?? '---'),
pw.SizedBox(width: 10),
_buildSummaryBox('Melhor Passador',
gameData['top_ast_name'] ?? '---'),
_buildSummaryBox('Melhor Passador', gameData['top_ast_name'] ?? '---'),
pw.SizedBox(width: 10),
_buildSummaryBox(
'MVP', gameData['mvp_name'] ?? '---'),
_buildSummaryBox('MVP', gameData['mvp_name'] ?? '---'),
],
),
],
@@ -195,15 +182,13 @@ class PdfExportService {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)',
const PdfColor.fromInt(0xFFA00000)),
_heatmapPageHeader('MAPA DE CALOR — $myTeam (Equipa Completa)', const PdfColor.fromInt(0xFFA00000)),
pw.SizedBox(height: 12),
pw.Expanded(
child: pw.Center(
child: pw.CustomPaint(
size: const PdfPoint(360, 360),
painter: (canvas, size) =>
_paintCourt(canvas, size, myTeamShots),
painter: (canvas, size) => _paintCourt(canvas, size, myTeamShots),
),
),
),
@@ -217,7 +202,6 @@ class PdfExportService {
}
// ── PÁGINAS 3+: Mapa de Calor por Jogador (4 por folha) ──────────────
// 👇 FILTRO ATIVO: Só entra aqui quem tiver tiros na lista "shotsByPlayer"!
final activePlayers = myPlayers.where((p) {
final pid = p['id'].toString();
return shotsByPlayer[pid] != null && shotsByPlayer[pid]!.isNotEmpty;
@@ -280,9 +264,6 @@ class PdfExportService {
);
}
// ════════════════════════════════════════════════════════════════════════════
// WIDGET DO MAPA DE CALOR INDIVIDUAL
// ════════════════════════════════════════════════════════════════════════════
static pw.Widget _buildPlayerHeatmap(dynamic player, List<_ShotDot> shots, Map<String, dynamic> stats) {
final String playerName = player['name']?.toString() ?? 'Jogador';
final String playerNumber = player['number']?.toString() ?? '0';
@@ -320,16 +301,11 @@ class PdfExportService {
);
}
// ════════════════════════════════════════════════════════════════════════════
// CORREÇÃO: DESENHO DO CAMPO E LINHAS ADAPTADAS
// ════════════════════════════════════════════════════════════════════════════
static void _paintCourt(PdfGraphics canvas, PdfPoint size, List<_ShotDot> shots) {
final double w = size.x;
final double h = size.y;
final double basketX = w / 2;
// Fundo Amarelo (Toda a área)
canvas
..setFillColor(const PdfColor.fromInt(0xFFDFAB00))
..drawRect(0, 0, w, h)
@@ -341,7 +317,6 @@ class PdfExportService {
final double alturaDoArco = larguraDoArco * 0.30;
final double totalArcoHeight = alturaDoArco * 4;
// ── 1. LINHAS BRANCAS ───────────────────────────────────────────────
canvas.setStrokeColor(PdfColors.white);
canvas.setLineWidth(2.0);
@@ -350,7 +325,6 @@ class PdfExportService {
_drawLine(canvas, h, 0, length, margin, length);
_drawLine(canvas, h, w - margin, length, w, length);
// Arco 3pts
_drawEllipseArc(canvas, h, basketX, length, larguraDoArco, totalArcoHeight / 2, 0, math.pi);
double sXL = basketX + (larguraDoArco * math.cos(math.pi * 0.75));
@@ -361,46 +335,34 @@ class PdfExportService {
_drawLine(canvas, h, sXL, sYL, 0, h * 0.85);
_drawLine(canvas, h, sXR, sYR, w, h * 0.85);
// ── 2. LINHAS PRETAS ─────────────────────────────────────────────────
canvas.setStrokeColor(PdfColors.black);
canvas.setLineWidth(1.5);
final double pW = w * 0.28;
final double pH = h * 0.38;
// Garrafão
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
// Círculo Lances Livres
final double ftR = pW / 2;
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
// Tracejado
for (int i = 0; i < 10; i++) {
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
}
// Linhas oblíquas do garrafão
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
// Meio Campo
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
// Cesto e Tabela
_drawCircle(canvas, h, basketX, h * 0.12, w * 0.02);
_drawLine(canvas, h, basketX - w * 0.08, h * 0.12 - 5, basketX + w * 0.08, h * 0.12 - 5);
// ── 3. TIROS ─────────────────────────────────────────────────────────
for (final shot in shots) {
final double px = shot.relX * w;
final double py = shot.relY * h;
final PdfColor dotColor = shot.isMake ? PdfColors.green600 : PdfColors.red600;
// Desenha Círculo Colorido
_fillCircle(canvas, h, px, py, 6, dotColor);
// Símbolos
canvas.setStrokeColor(PdfColors.white);
canvas.setLineWidth(1.2);
if (shot.isMake) {
@@ -413,19 +375,12 @@ class PdfExportService {
}
}
// ── Helpers com inversão automática do Eixo Y para casar com Flutter ──
static void _drawLine(PdfGraphics c, double canvasH, double x1, double y1, double x2, double y2) {
c.moveTo(x1, canvasH - y1);
c.lineTo(x2, canvasH - y2);
c.strokePath();
}
static void _lineRaw(PdfGraphics c, double x1, double y1, double x2, double y2) {
c.moveTo(x1, y1);
c.lineTo(x2, y2);
c.strokePath();
}
static void _drawRect(PdfGraphics c, double canvasH, double x, double y, double width, double height) {
c.drawRect(x, canvasH - (y + height), width, height);
c.strokePath();
@@ -460,12 +415,7 @@ class PdfExportService {
c.strokePath();
}
// ════════════════════════════════════════════════════════════════════════════
// TABELAS DE ESTATÍSTICAS
// ════════════════════════════════════════════════════════════════════════════
static List<List<String>> _buildTeamTableData(
List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
static List<List<String>> _buildTeamTableData(List<dynamic> players, Map<String, Map<String, dynamic>> statsMap) {
List<List<String>> tableData = [];
int tPts = 0, tFgm = 0, tFga = 0, tFtm = 0, tFta = 0, tFls = 0;
@@ -485,27 +435,16 @@ class PdfExportService {
var s = statsMap[id] ?? {};
int pts = s['pts'] ?? 0;
int fgm = s['fgm'] ?? 0;
int fga = s['fga'] ?? 0;
int ftm = s['ftm'] ?? 0;
int fta = s['fta'] ?? 0;
int p2m = s['p2m'] ?? 0;
int p2a = s['p2a'] ?? 0;
int p3m = s['p3m'] ?? 0;
int p3a = s['p3a'] ?? 0;
int fgm = s['fgm'] ?? 0; int fga = s['fga'] ?? 0;
int ftm = s['ftm'] ?? 0; int fta = s['fta'] ?? 0;
int p2m = s['p2m'] ?? 0; int p2a = s['p2a'] ?? 0;
int p3m = s['p3m'] ?? 0; int p3a = s['p3a'] ?? 0;
int fls = s['fls'] ?? 0;
int orb = s['orb'] ?? 0;
int drb = s['drb'] ?? 0;
int stl = s['stl'] ?? 0;
int ast = s['ast'] ?? 0;
int tov = s['tov'] ?? 0;
int blk = s['blk'] ?? 0;
int so = s['so'] ?? 0;
int il = s['il'] ?? 0;
int li = s['li'] ?? 0;
int pa = s['pa'] ?? 0;
int tresS = s['tres_seg'] ?? 0;
int dr = s['dr'] ?? 0;
int orb = s['orb'] ?? 0; int drb = s['drb'] ?? 0;
int stl = s['stl'] ?? 0; int ast = s['ast'] ?? 0;
int tov = s['tov'] ?? 0; int blk = s['blk'] ?? 0;
int so = s['so'] ?? 0; int il = s['il'] ?? 0; int li = s['li'] ?? 0;
int pa = s['pa'] ?? 0; int tresS = s['tres_seg'] ?? 0; int dr = s['dr'] ?? 0;
int sec = s['minutos_jogados'] ?? 0;
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
@@ -525,8 +464,7 @@ class PdfExportService {
tableData.add([
p['number']?.toString() ?? '-',
p['name']?.toString() ?? '?',
minStr,
pts.toString(),
minStr, pts.toString(),
p2m.toString(), p2a.toString(), p2Pct,
p3m.toString(), p3a.toString(), p3Pct,
fgm.toString(), fga.toString(), fgPct,
@@ -717,8 +655,7 @@ class PdfExportService {
);
}
static pw.Widget _groupHeader(
String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
static pw.Widget _groupHeader(String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
return pw.Column(
children: [
pw.Container(
@@ -726,54 +663,28 @@ class PdfExportService {
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
decoration: const pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.white, width: 0.5)),
),
child: pw.Text(title, style: hStyle),
),
pw.Row(children: [
pw.Expanded(
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Text('C', style: sStyle))),
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('C', style: sStyle))),
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
pw.Expanded(
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Text('T', style: sStyle))),
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('T', style: sStyle))),
pw.Container(width: 0.5, height: 10, color: PdfColors.white),
pw.Expanded(
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Text('%', style: sStyle))),
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 2), child: pw.Text('%', style: sStyle))),
]),
],
);
}
static pw.Widget _groupData(
String c, String t, String pct, pw.TextStyle style) {
static pw.Widget _groupData(String c, String t, String pct, pw.TextStyle style) {
return pw.Row(children: [
pw.Expanded(
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 4),
child: pw.Text(c, style: style))),
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(c, style: style))),
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
pw.Expanded(
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 4),
child: pw.Text(t, style: style))),
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(t, style: style))),
pw.Container(width: 0.5, height: 12, color: PdfColors.grey400),
pw.Expanded(
child: pw.Container(
alignment: pw.Alignment.center,
padding: const pw.EdgeInsets.symmetric(vertical: 4),
child: pw.Text(pct, style: style))),
pw.Expanded(child: pw.Container(alignment: pw.Alignment.center, padding: const pw.EdgeInsets.symmetric(vertical: 4), child: pw.Text(pct, style: style))),
]);
}
@@ -781,17 +692,8 @@ class PdfExportService {
return pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: pw.BoxDecoration(
color: color,
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
),
child: pw.Text(
title,
style: pw.TextStyle(
color: PdfColors.white,
fontSize: 14,
fontWeight: pw.FontWeight.bold),
),
decoration: pw.BoxDecoration(color: color, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6))),
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 14, fontWeight: pw.FontWeight.bold)),
);
}
@@ -799,15 +701,11 @@ class PdfExportService {
return pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.center,
children: [
pw.Container(width: 12, height: 12,
decoration: const pw.BoxDecoration(
color: PdfColors.green600, shape: pw.BoxShape.circle)),
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.green600, shape: pw.BoxShape.circle)),
pw.SizedBox(width: 4),
pw.Text('Cesto marcado', style: pw.TextStyle(fontSize: 10)),
pw.SizedBox(width: 20),
pw.Container(width: 12, height: 12,
decoration: const pw.BoxDecoration(
color: PdfColors.red600, shape: pw.BoxShape.circle)),
pw.Container(width: 12, height: 12, decoration: const pw.BoxDecoration(color: PdfColors.red600, shape: pw.BoxShape.circle)),
pw.SizedBox(width: 4),
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
],
@@ -817,28 +715,15 @@ class PdfExportService {
static pw.Widget _buildSummaryBox(String title, String value) {
return pw.Container(
width: 120,
decoration: pw.BoxDecoration(
border: pw.TableBorder.all(color: PdfColors.black, width: 1),
),
decoration: pw.BoxDecoration(border: pw.TableBorder.all(color: PdfColors.black, width: 1)),
child: pw.Column(children: [
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(6),
color: const PdfColor.fromInt(0xFFA00000),
child: pw.Text(title,
style: pw.TextStyle(
color: PdfColors.white,
fontSize: 9,
fontWeight: pw.FontWeight.bold),
textAlign: pw.TextAlign.center),
width: double.infinity, padding: const pw.EdgeInsets.all(6), color: const PdfColor.fromInt(0xFFA00000),
child: pw.Text(title, style: pw.TextStyle(color: PdfColors.white, fontSize: 9, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
),
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(8),
child: pw.Text(value,
style: pw.TextStyle(
fontSize: 10, fontWeight: pw.FontWeight.bold),
textAlign: pw.TextAlign.center),
width: double.infinity, padding: const pw.EdgeInsets.all(8),
child: pw.Text(value, style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center),
),
]),
);