Files
PlayMaker/lib/pages/pdf_export_service.dart
2026-05-06 12:47:17 +01:00

731 lines
30 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';
class _ShotDot {
final double relX;
final double relY;
final bool isMake;
_ShotDot({required this.relX, required this.relY, required this.isMake});
}
class PdfExportService {
static Future<void> generateAndPrintBoxScore({
required String gameId,
required String myTeam,
required String opponentTeam,
required String myScore,
required String opponentScore,
required String season,
required String targetTeam,
}) 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 ──────────────────────
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 = {};
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 ────────────────────
List<List<String>> myTeamTable = _buildTeamTableData(myPlayers, statsMap);
// ════════════════════════════════════════════════════════════════════════
// CONSTRUÇÃO DO PDF
// ════════════════════════════════════════════════════════════════════════
final pdf = pw.Document();
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,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
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.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: 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) ──────────────
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',
);
}
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),
)
)
)
]
)
);
}
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;
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;
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);
_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);
canvas.setStrokeColor(PdfColors.black);
canvas.setLineWidth(1.5);
final double pW = w * 0.28;
final double pH = h * 0.38;
_drawRect(canvas, h, basketX - pW / 2, 0, pW, pH);
final double ftR = pW / 2;
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, 0, math.pi);
for (int i = 0; i < 10; i++) {
_drawEllipseArc(canvas, h, basketX, pH, ftR, ftR, math.pi + (i * 2 * (math.pi / 20)), math.pi / 20);
}
_drawLine(canvas, h, basketX - pW / 2, pH, sXL, sYL);
_drawLine(canvas, h, basketX + pW / 2, pH, sXR, sYR);
_drawEllipseArc(canvas, h, basketX, h, w * 0.12, w * 0.12, math.pi, math.pi);
_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);
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;
_fillCircle(canvas, h, px, py, 6, dotColor);
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);
}
}
}
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 _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();
}
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('', 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('', 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),
),
]),
);
}
}