846 lines
34 KiB
Dart
846 lines
34 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:pdf/pdf.dart';
|
|
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;
|
|
final bool isMake;
|
|
_ShotDot({required this.relX, required this.relY, required this.isMake});
|
|
}
|
|
|
|
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,
|
|
}) async {
|
|
final supabase = Supabase.instance.client;
|
|
|
|
// ── Jogo ────────────────────────────────────────────────────────────────
|
|
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]);
|
|
|
|
String? myTeamId;
|
|
for (var t in teamsData) {
|
|
if (t['name'] == myTeam) myTeamId = t['id'].toString();
|
|
}
|
|
|
|
// ── Jogadores (Apenas a minha equipa) ───────────────────────────────────
|
|
List<dynamic> myPlayers = myTeamId != null
|
|
? 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);
|
|
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
|
|
final List<_ShotDot> myTeamShots = [];
|
|
final Map<String, List<_ShotDot>> shotsByPlayer = {};
|
|
|
|
for (var shot in shotsData) {
|
|
final memberId = shot['member_id'].toString();
|
|
if (!myPlayerIds.contains(memberId)) continue;
|
|
|
|
final dot = _ShotDot(
|
|
relX: double.tryParse(shot['relative_x'].toString()) ?? 0.5,
|
|
relY: double.tryParse(shot['relative_y'].toString()) ?? 0.5,
|
|
isMake: shot['is_make'] == true,
|
|
);
|
|
myTeamShots.add(dot);
|
|
shotsByPlayer.putIfAbsent(memberId, () => []).add(dot);
|
|
}
|
|
|
|
// ── Tabela de estatísticas (Apenas a minha equipa) ────────────────────
|
|
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,
|
|
margin: const pw.EdgeInsets.all(14),
|
|
build: (pw.Context context) {
|
|
return pw.FittedBox(
|
|
fit: pw.BoxFit.scaleDown,
|
|
child: pw.Container(
|
|
width: PdfPageFormat.a4.landscape.availableWidth,
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text('Relatório Estatístico',
|
|
style: pw.TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: pw.FontWeight.bold)),
|
|
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.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.SizedBox(height: 2),
|
|
_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.SizedBox(height: 2),
|
|
_buildPdfTablePart2(
|
|
myTeamTable, const PdfColor.fromInt(0xFFA00000)),
|
|
|
|
pw.SizedBox(height: 16),
|
|
|
|
pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSummaryBox('Melhor Marcador',
|
|
gameData['top_pts_name'] ?? '---'),
|
|
pw.SizedBox(width: 10),
|
|
_buildSummaryBox('Melhor Ressaltador',
|
|
gameData['top_rbs_name'] ?? '---'),
|
|
pw.SizedBox(width: 10),
|
|
_buildSummaryBox('Melhor Passador',
|
|
gameData['top_ast_name'] ?? '---'),
|
|
pw.SizedBox(width: 10),
|
|
_buildSummaryBox(
|
|
'MVP', gameData['mvp_name'] ?? '---'),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// ── PÁGINA 2: Mapa de Calor — Equipa completa ────────────────────────
|
|
if (myTeamShots.isNotEmpty) {
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4.landscape,
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (pw.Context context) {
|
|
return pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
_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),
|
|
),
|
|
),
|
|
),
|
|
pw.SizedBox(height: 10),
|
|
_heatmapLegend(),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── 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;
|
|
}).toList();
|
|
|
|
for (int i = 0; i < activePlayers.length; i += 4) {
|
|
final chunk = activePlayers.sublist(i, math.min(i + 4, activePlayers.length));
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4.landscape,
|
|
margin: const pw.EdgeInsets.all(20),
|
|
build: (pw.Context context) {
|
|
return pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
_heatmapPageHeader('MAPAS DE CALOR INDIVIDUAIS', const PdfColor.fromInt(0xFFA00000)),
|
|
pw.SizedBox(height: 12),
|
|
pw.Expanded(
|
|
child: pw.Column(
|
|
children: [
|
|
if (chunk.isNotEmpty)
|
|
pw.Expanded(
|
|
child: pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
children: [
|
|
_buildPlayerHeatmap(chunk[0], shotsByPlayer[chunk[0]['id'].toString()]!, statsMap[chunk[0]['id'].toString()] ?? {}),
|
|
pw.SizedBox(width: 40),
|
|
chunk.length > 1 ? _buildPlayerHeatmap(chunk[1], shotsByPlayer[chunk[1]['id'].toString()]!, statsMap[chunk[1]['id'].toString()] ?? {}) : pw.Container(),
|
|
],
|
|
),
|
|
),
|
|
pw.SizedBox(height: 12),
|
|
if (chunk.length > 2)
|
|
pw.Expanded(
|
|
child: pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.center,
|
|
children: [
|
|
_buildPlayerHeatmap(chunk[2], shotsByPlayer[chunk[2]['id'].toString()]!, statsMap[chunk[2]['id'].toString()] ?? {}),
|
|
pw.SizedBox(width: 40),
|
|
chunk.length > 3 ? _buildPlayerHeatmap(chunk[3], shotsByPlayer[chunk[3]['id'].toString()]!, statsMap[chunk[3]['id'].toString()] ?? {}) : pw.Container(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
pw.SizedBox(height: 10),
|
|
_heatmapLegend(),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
await Printing.layoutPdf(
|
|
onLayout: (PdfPageFormat format) async => pdf.save(),
|
|
name: 'BoxScore_$myTeam.pdf',
|
|
);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
// 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';
|
|
|
|
final int pts = stats['pts'] ?? 0;
|
|
final int fgm = stats['fgm'] ?? 0;
|
|
final int fga = stats['fga'] ?? 0;
|
|
final String fgPct = fga > 0 ? '${((fgm / fga) * 100).toStringAsFixed(0)}%' : '0%';
|
|
|
|
return pw.Container(
|
|
width: 250,
|
|
padding: const pw.EdgeInsets.all(8),
|
|
decoration: pw.BoxDecoration(
|
|
color: PdfColors.white,
|
|
border: pw.Border.all(color: PdfColors.grey300),
|
|
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)),
|
|
),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Text('#$playerNumber $playerName', style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 11, color: const PdfColor.fromInt(0xFFA00000))),
|
|
pw.SizedBox(height: 4),
|
|
pw.Text('PTS: $pts | FG: $fgm/$fga ($fgPct)', style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700)),
|
|
pw.SizedBox(height: 8),
|
|
pw.Expanded(
|
|
child: pw.Center(
|
|
child: pw.CustomPaint(
|
|
size: const PdfPoint(180, 180),
|
|
painter: (canvas, size) => _paintCourt(canvas, size, shots),
|
|
)
|
|
)
|
|
)
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
// 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)
|
|
..fillPath();
|
|
|
|
final double margin = w * 0.10;
|
|
final double length = h * 0.35;
|
|
final double larguraDoArco = (w / 2) - margin;
|
|
final double alturaDoArco = larguraDoArco * 0.30;
|
|
final double totalArcoHeight = alturaDoArco * 4;
|
|
|
|
// ── 1. LINHAS BRANCAS ───────────────────────────────────────────────
|
|
canvas.setStrokeColor(PdfColors.white);
|
|
canvas.setLineWidth(2.0);
|
|
|
|
_drawLine(canvas, h, margin, 0, margin, length);
|
|
_drawLine(canvas, h, w - margin, 0, w - margin, length);
|
|
_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));
|
|
double sYL = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.75));
|
|
double sXR = basketX + (larguraDoArco * math.cos(math.pi * 0.25));
|
|
double sYR = length + ((totalArcoHeight / 2) * math.sin(math.pi * 0.25));
|
|
|
|
_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) {
|
|
_drawLine(canvas, h, px - 3, py + 1.5, px - 0.5, py - 3);
|
|
_drawLine(canvas, h, px - 0.5, py - 3, px + 4.0, py + 3.5);
|
|
} else {
|
|
_drawLine(canvas, h, px - 3, py - 3, px + 3, py + 3);
|
|
_drawLine(canvas, h, px + 3, py - 3, px - 3, py + 3);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 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();
|
|
}
|
|
|
|
static void _drawCircle(PdfGraphics c, double canvasH, double cx, double cy, double r) {
|
|
c.drawEllipse(cx, canvasH - cy, r, r);
|
|
c.strokePath();
|
|
}
|
|
|
|
static void _fillCircle(PdfGraphics c, double canvasH, double cx, double cy, double r, PdfColor color) {
|
|
c.setFillColor(color);
|
|
c.drawEllipse(cx, canvasH - cy, r, r);
|
|
c.fillPath();
|
|
}
|
|
|
|
static void _drawEllipseArc(PdfGraphics c, double canvasH, double cx, double cy, double rx, double ry, double startAngle, double sweepAngle) {
|
|
const int steps = 30;
|
|
final double step = sweepAngle / steps;
|
|
double angle = startAngle;
|
|
|
|
double fx = cx + rx * math.cos(angle);
|
|
double fy = cy + ry * math.sin(angle);
|
|
c.moveTo(fx, canvasH - fy);
|
|
|
|
for (int i = 1; i <= steps; i++) {
|
|
angle += step;
|
|
fx = cx + rx * math.cos(angle);
|
|
fy = cy + ry * math.sin(angle);
|
|
c.lineTo(fx, canvasH - fy);
|
|
}
|
|
c.strokePath();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
// TABELAS DE ESTATÍSTICAS
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
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;
|
|
int tOrb = 0, tDrb = 0, tStl = 0, tAst = 0, tTov = 0, tBlk = 0;
|
|
int tP3m = 0, tP2m = 0, tP3a = 0, tP2a = 0;
|
|
int tSo = 0, tIl = 0, tLi = 0, tPa = 0, tTresS = 0, tDr = 0;
|
|
int tSec = 0;
|
|
|
|
players.sort((a, b) {
|
|
int numA = int.tryParse(a['number']?.toString() ?? '0') ?? 0;
|
|
int numB = int.tryParse(b['number']?.toString() ?? '0') ?? 0;
|
|
return numA.compareTo(numB);
|
|
});
|
|
|
|
for (var p in players) {
|
|
String id = p['id'].toString();
|
|
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 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 sec = s['minutos_jogados'] ?? 0;
|
|
|
|
tPts += pts; tFgm += fgm; tFga += fga; tFtm += ftm; tFta += fta;
|
|
tFls += fls; tOrb += orb; tDrb += drb; tStl += stl;
|
|
tAst += ast; tTov += tov; tBlk += blk;
|
|
tP3m += p3m; tP2m += p2m; tP3a += p3a; tP2a += p2a;
|
|
tSo += so; tIl += il; tLi += li; tPa += pa;
|
|
tTresS += tresS; tDr += dr; tSec += sec;
|
|
|
|
String p2Pct = p2a > 0 ? '${((p2m / p2a) * 100).toStringAsFixed(0)}%' : '0%';
|
|
String p3Pct = p3a > 0 ? '${((p3m / p3a) * 100).toStringAsFixed(0)}%' : '0%';
|
|
String fgPct = fga > 0 ? '${((fgm / fga) * 100).toStringAsFixed(0)}%' : '0%';
|
|
String ftPct = fta > 0 ? '${((ftm / fta) * 100).toStringAsFixed(0)}%' : '0%';
|
|
|
|
String minStr = _secToMin(sec);
|
|
|
|
tableData.add([
|
|
p['number']?.toString() ?? '-',
|
|
p['name']?.toString() ?? '?',
|
|
minStr,
|
|
pts.toString(),
|
|
p2m.toString(), p2a.toString(), p2Pct,
|
|
p3m.toString(), p3a.toString(), p3Pct,
|
|
fgm.toString(), fga.toString(), fgPct,
|
|
ftm.toString(), fta.toString(), ftPct,
|
|
orb.toString(), drb.toString(), (orb + drb).toString(),
|
|
stl.toString(), ast.toString(), tov.toString(), blk.toString(), fls.toString(),
|
|
so.toString(), il.toString(), li.toString(),
|
|
pa.toString(), tresS.toString(), dr.toString(),
|
|
]);
|
|
}
|
|
|
|
if (tableData.isEmpty) {
|
|
tableData.add(List.filled(30, '0')..[0] = '-'..[1] = 'Sem jogadores');
|
|
}
|
|
|
|
String tP2Pct = tP2a > 0 ? '${((tP2m / tP2a) * 100).toStringAsFixed(0)}%' : '0%';
|
|
String tP3Pct = tP3a > 0 ? '${((tP3m / tP3a) * 100).toStringAsFixed(0)}%' : '0%';
|
|
String tFgPct = tFga > 0 ? '${((tFgm / tFga) * 100).toStringAsFixed(0)}%' : '0%';
|
|
String tFtPct = tFta > 0 ? '${((tFtm / tFta) * 100).toStringAsFixed(0)}%' : '0%';
|
|
|
|
tableData.add([
|
|
'', 'TOTAIS', _secToMin(tSec), tPts.toString(),
|
|
tP2m.toString(), tP2a.toString(), tP2Pct,
|
|
tP3m.toString(), tP3a.toString(), tP3Pct,
|
|
tFgm.toString(), tFga.toString(), tFgPct,
|
|
tFtm.toString(), tFta.toString(), tFtPct,
|
|
tOrb.toString(), tDrb.toString(), (tOrb + tDrb).toString(),
|
|
tStl.toString(), tAst.toString(), tTov.toString(), tBlk.toString(), tFls.toString(),
|
|
tSo.toString(), tIl.toString(), tLi.toString(),
|
|
tPa.toString(), tTresS.toString(), tDr.toString(),
|
|
]);
|
|
|
|
return tableData;
|
|
}
|
|
|
|
static String _secToMin(int sec) {
|
|
final m = sec ~/ 60;
|
|
final s = sec % 60;
|
|
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
static pw.Widget _buildPdfTablePart1(List<List<String>> data, PdfColor headerColor) {
|
|
final hBold = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 9);
|
|
final hSub = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 8);
|
|
final cell = const pw.TextStyle(fontSize: 9);
|
|
final cellBold = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
|
|
|
return pw.Table(
|
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
|
columnWidths: {
|
|
0: const pw.FlexColumnWidth(1.2),
|
|
1: const pw.FlexColumnWidth(4.5),
|
|
2: const pw.FlexColumnWidth(2.0),
|
|
3: const pw.FlexColumnWidth(1.5),
|
|
4: const pw.FlexColumnWidth(4.5),
|
|
5: const pw.FlexColumnWidth(4.5),
|
|
6: const pw.FlexColumnWidth(4.5),
|
|
7: const pw.FlexColumnWidth(4.5),
|
|
},
|
|
children: [
|
|
pw.TableRow(
|
|
decoration: pw.BoxDecoration(color: headerColor),
|
|
children: [
|
|
_sh('Nº', hSub),
|
|
_sh('NOME', hSub, left: true),
|
|
_sh('MIN', hSub),
|
|
_sh('PTS', hSub),
|
|
_groupHeader('2 PONTOS', hBold, hSub),
|
|
_groupHeader('3 PONTOS', hBold, hSub),
|
|
_groupHeader('GLOBAL', hBold, hSub),
|
|
_groupHeader('L. LIVRES', hBold, hSub),
|
|
],
|
|
),
|
|
...data.map((row) {
|
|
bool isTotais = row[1] == 'TOTAIS';
|
|
var s = isTotais ? cellBold : cell;
|
|
PdfColor? bg = isTotais ? PdfColors.grey200 : null;
|
|
|
|
return pw.TableRow(
|
|
decoration: pw.BoxDecoration(color: bg),
|
|
children: [
|
|
_sd(row[0], s),
|
|
_sd(row[1], s, left: true),
|
|
_sd(row[2], s),
|
|
_sd(row[3], s),
|
|
_groupData(row[4], row[5], row[6], s),
|
|
_groupData(row[7], row[8], row[9], s),
|
|
_groupData(row[10], row[11], row[12], s),
|
|
_groupData(row[13], row[14], row[15], s),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
static pw.Widget _buildPdfTablePart2(List<List<String>> data, PdfColor headerColor) {
|
|
final hSub = pw.TextStyle(color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 8);
|
|
final cell = const pw.TextStyle(fontSize: 9);
|
|
final cellBold = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
|
|
|
return pw.Table(
|
|
border: pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
|
columnWidths: {
|
|
0: const pw.FlexColumnWidth(1.2),
|
|
1: const pw.FlexColumnWidth(4.5),
|
|
2: const pw.FlexColumnWidth(1.5),
|
|
3: const pw.FlexColumnWidth(1.5),
|
|
4: const pw.FlexColumnWidth(1.5),
|
|
5: const pw.FlexColumnWidth(1.5),
|
|
6: const pw.FlexColumnWidth(1.5),
|
|
7: const pw.FlexColumnWidth(1.5),
|
|
8: const pw.FlexColumnWidth(1.5),
|
|
9: const pw.FlexColumnWidth(1.5),
|
|
10: const pw.FlexColumnWidth(1.5),
|
|
11: const pw.FlexColumnWidth(1.5),
|
|
12: const pw.FlexColumnWidth(1.5),
|
|
13: const pw.FlexColumnWidth(1.5),
|
|
14: const pw.FlexColumnWidth(1.5),
|
|
15: const pw.FlexColumnWidth(1.5),
|
|
},
|
|
children: [
|
|
pw.TableRow(
|
|
decoration: pw.BoxDecoration(color: headerColor),
|
|
children: [
|
|
_sh('Nº', hSub),
|
|
_sh('NOME', hSub, left: true),
|
|
_sh('RO', hSub),
|
|
_sh('RD', hSub),
|
|
_sh('TR', hSub),
|
|
_sh('BR', hSub),
|
|
_sh('AS', hSub),
|
|
_sh('BP', hSub),
|
|
_sh('BLK', hSub),
|
|
_sh('FLS', hSub),
|
|
_sh('SO', hSub),
|
|
_sh('IL', hSub),
|
|
_sh('LI', hSub),
|
|
_sh('PA', hSub),
|
|
_sh('3S', hSub),
|
|
_sh('DR', hSub),
|
|
],
|
|
),
|
|
...data.map((row) {
|
|
bool isTotais = row[1] == 'TOTAIS';
|
|
var s = isTotais ? cellBold : cell;
|
|
PdfColor? bg = isTotais ? PdfColors.grey200 : null;
|
|
|
|
return pw.TableRow(
|
|
decoration: pw.BoxDecoration(color: bg),
|
|
children: [
|
|
_sd(row[0], s),
|
|
_sd(row[1], s, left: true),
|
|
_sd(row[16], s),
|
|
_sd(row[17], s),
|
|
_sd(row[18], s),
|
|
_sd(row[19], s),
|
|
_sd(row[20], s),
|
|
_sd(row[21], s),
|
|
_sd(row[22], s),
|
|
_sd(row[23], s),
|
|
_sd(row[24], s),
|
|
_sd(row[25], s),
|
|
_sd(row[26], s),
|
|
_sd(row[27], s),
|
|
_sd(row[28], s),
|
|
_sd(row[29], s),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
static pw.Widget _sh(String text, pw.TextStyle style, {bool left = false}) {
|
|
return pw.Container(
|
|
alignment: left ? pw.Alignment.centerLeft : pw.Alignment.center,
|
|
padding: const pw.EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
child: pw.Text(text, style: style),
|
|
);
|
|
}
|
|
|
|
static pw.Widget _sd(String text, pw.TextStyle style, {bool left = false}) {
|
|
return pw.Container(
|
|
alignment: left ? pw.Alignment.centerLeft : pw.Alignment.center,
|
|
padding: const pw.EdgeInsets.symmetric(vertical: 4, horizontal: 4),
|
|
child: pw.Text(text, style: style),
|
|
);
|
|
}
|
|
|
|
static pw.Widget _groupHeader(
|
|
String title, pw.TextStyle hStyle, pw.TextStyle sStyle) {
|
|
return pw.Column(
|
|
children: [
|
|
pw.Container(
|
|
width: double.infinity,
|
|
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)),
|
|
),
|
|
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.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.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))),
|
|
]),
|
|
],
|
|
);
|
|
}
|
|
|
|
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.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.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))),
|
|
]);
|
|
}
|
|
|
|
static pw.Widget _heatmapPageHeader(String title, PdfColor color) {
|
|
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),
|
|
),
|
|
);
|
|
}
|
|
|
|
static pw.Widget _heatmapLegend() {
|
|
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.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.SizedBox(width: 4),
|
|
pw.Text('Cesto falhado', style: pw.TextStyle(fontSize: 10)),
|
|
],
|
|
);
|
|
}
|
|
|
|
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),
|
|
),
|
|
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),
|
|
),
|
|
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),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
} |