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 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 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> 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 myPlayerIds = myPlayers.map((p) => p['id'].toString()).toSet(); final List<_ShotDot> myTeamShots = []; final Map> 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> 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: >[ ['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 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> _buildTeamTableData(List players, Map> statsMap) { List> 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> 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> 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), ), ]), ); } }